🐛 fix(gui): resolve step header layout overlaps and improve scaling
- Fix 6 overlapping widget bugs in StepHeader (title/desc, btn/icon, run/rerun) with proper 5-column grid layout - Merge run/rerun buttons into single _action_btn with state-driven text - Fix DARK_CARD NameError (undefined import → STEP_CARD_BG) - Replace fragile vertical connector lines with full-width dividers - Add panel.grid_columnconfigure(0, weight=1) for horizontal scaling - Unify button text across all states (▶ pending/disabled, ↻ retry) - Tighten padding and reduce whitespace between steps
This commit is contained in:
+11
-12
@@ -42,7 +42,6 @@ from gui.styles import (
|
|||||||
DANGER_COLOR,
|
DANGER_COLOR,
|
||||||
INFO_COLOR,
|
INFO_COLOR,
|
||||||
CONNECTOR_COLOR,
|
CONNECTOR_COLOR,
|
||||||
STEP_ICON_SIZE,
|
|
||||||
get_theme,
|
get_theme,
|
||||||
)
|
)
|
||||||
from gui.widgets import (
|
from gui.widgets import (
|
||||||
@@ -189,9 +188,10 @@ class MainWindow(ctk.CTk):
|
|||||||
panel = ctk.CTkFrame(parent, corner_radius=CORNER_RADIUS)
|
panel = ctk.CTkFrame(parent, corner_radius=CORNER_RADIUS)
|
||||||
panel.grid(
|
panel.grid(
|
||||||
row=0, column=0, sticky="nsew",
|
row=0, column=0, sticky="nsew",
|
||||||
padx=PADDING, pady=PADDING,
|
padx=PADDING, pady=(PADDING, 0),
|
||||||
)
|
)
|
||||||
panel.grid_rowconfigure(10, weight=1)
|
panel.grid_rowconfigure(10, weight=1)
|
||||||
|
panel.grid_columnconfigure(0, weight=1)
|
||||||
|
|
||||||
# ── Title ──
|
# ── Title ──
|
||||||
title = ctk.CTkLabel(
|
title = ctk.CTkLabel(
|
||||||
@@ -199,7 +199,7 @@ class MainWindow(ctk.CTk):
|
|||||||
text="Workflow",
|
text="Workflow",
|
||||||
font=FONT_HEADING,
|
font=FONT_HEADING,
|
||||||
)
|
)
|
||||||
title.grid(row=0, column=0, padx=PADDING, pady=(PADDING, PADDING_SMALL))
|
title.grid(row=0, column=0, padx=PADDING_SMALL, pady=(PADDING_SMALL, PADDING_SMALL))
|
||||||
|
|
||||||
# ── Steps ──
|
# ── Steps ──
|
||||||
step_defs = [
|
step_defs = [
|
||||||
@@ -223,19 +223,18 @@ class MainWindow(ctk.CTk):
|
|||||||
self._steps.append(step)
|
self._steps.append(step)
|
||||||
self._step_statuses[i] = "pending"
|
self._step_statuses[i] = "pending"
|
||||||
|
|
||||||
# Connector line between steps
|
# Divider line between steps
|
||||||
if i > 0:
|
if i > 0:
|
||||||
connector = ctk.CTkFrame(
|
divider = ctk.CTkFrame(
|
||||||
panel,
|
panel,
|
||||||
width=3,
|
height=1,
|
||||||
height=12,
|
|
||||||
fg_color=CONNECTOR_COLOR,
|
fg_color=CONNECTOR_COLOR,
|
||||||
corner_radius=2,
|
|
||||||
)
|
)
|
||||||
connector.grid(
|
divider.grid(
|
||||||
row=i * 2, column=0,
|
row=i * 2, column=0,
|
||||||
sticky="n",
|
sticky="ew",
|
||||||
padx=(PADDING_SMALL + STEP_ICON_SIZE + PADDING_SMALL, PADDING_SMALL),
|
padx=PADDING,
|
||||||
|
pady=(2, 0),
|
||||||
)
|
)
|
||||||
|
|
||||||
# Position step
|
# Position step
|
||||||
@@ -243,7 +242,7 @@ class MainWindow(ctk.CTk):
|
|||||||
row=i * 2 + 1, column=0,
|
row=i * 2 + 1, column=0,
|
||||||
sticky="nsew",
|
sticky="nsew",
|
||||||
padx=PADDING_SMALL,
|
padx=PADDING_SMALL,
|
||||||
pady=PADDING_SMALL,
|
pady=(0, 2),
|
||||||
)
|
)
|
||||||
|
|
||||||
# ── Progress indicator ──
|
# ── Progress indicator ──
|
||||||
|
|||||||
+90
-149
@@ -131,136 +131,83 @@ class StepHeader(ctk.CTkFrame):
|
|||||||
self._set_status(status)
|
self._set_status(status)
|
||||||
|
|
||||||
def _create_widgets(self) -> None:
|
def _create_widgets(self) -> None:
|
||||||
"""Create the step header UI components."""
|
"""Create the step header UI components.
|
||||||
self.grid_rowconfigure(0, weight=1)
|
|
||||||
self.grid_columnconfigure((0, 1, 2, 3), weight=0, minsize=PADDING_SMALL)
|
Grid layout (no overlaps):
|
||||||
self.grid_columnconfigure(1, weight=1)
|
|
||||||
|
col0 col1 col2 (expand) col3 col4
|
||||||
|
┌────────┬──────┬───────────────────────┬──────┬──────┐
|
||||||
|
│ accent │ ① │ Title │ ▶Run │ ✓ │ row 0
|
||||||
|
│ bar │ │ Description │ │ │ row 1
|
||||||
|
└────────┴──────┴───────────────────────┴──────┴──────┘
|
||||||
|
"""
|
||||||
|
self.grid_rowconfigure((0, 1), weight=0)
|
||||||
|
self.grid_columnconfigure(0, weight=0, minsize=4) # accent bar
|
||||||
|
self.grid_columnconfigure(1, weight=0) # number circle
|
||||||
|
self.grid_columnconfigure(2, weight=1) # content (expands)
|
||||||
|
self.grid_columnconfigure(3, weight=0) # action button
|
||||||
|
self.grid_columnconfigure(4, weight=0) # status icon
|
||||||
|
|
||||||
# Accent bar (left edge)
|
# ── Accent bar (spans both rows) ──
|
||||||
self._accent_bar = ctk.CTkFrame(
|
self._accent_bar = ctk.CTkFrame(
|
||||||
self,
|
self, width=4, height=44, corner_radius=2,
|
||||||
width=5,
|
|
||||||
height=48,
|
|
||||||
corner_radius=3,
|
|
||||||
fg_color=ACCENT_PENDING,
|
fg_color=ACCENT_PENDING,
|
||||||
)
|
)
|
||||||
self._accent_bar.grid(
|
self._accent_bar.grid(row=0, column=0, rowspan=2, sticky="ns")
|
||||||
row=0, column=0,
|
|
||||||
sticky="n",
|
|
||||||
pady=PADDING,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Step number circle
|
# ── Number circle (spans both rows, vertically centered) ──
|
||||||
|
circle_size = 28
|
||||||
self._num_label = ctk.CTkLabel(
|
self._num_label = ctk.CTkLabel(
|
||||||
self,
|
self, text=str(self._step_number),
|
||||||
text=str(self._step_number),
|
font=_font(13, "bold"),
|
||||||
font=_font(14, "bold"),
|
width=circle_size, height=circle_size,
|
||||||
width=STEP_ICON_SIZE,
|
corner_radius=circle_size // 2,
|
||||||
height=STEP_ICON_SIZE,
|
|
||||||
corner_radius=STEP_ICON_SIZE // 2,
|
|
||||||
fg_color=CIRCLE_PENDING,
|
fg_color=CIRCLE_PENDING,
|
||||||
text_color="white",
|
text_color="white",
|
||||||
)
|
)
|
||||||
self._num_label.grid(
|
self._num_label.grid(row=0, column=1, rowspan=2, padx=(8, 8), pady=4)
|
||||||
row=0, column=1,
|
|
||||||
sticky="n",
|
|
||||||
pady=PADDING,
|
|
||||||
)
|
|
||||||
|
|
||||||
# Content area (title + description)
|
|
||||||
content_frame = ctk.CTkFrame(self, fg_color="transparent")
|
|
||||||
content_frame.grid(
|
|
||||||
row=0, column=2,
|
|
||||||
sticky="nsew",
|
|
||||||
pady=PADDING,
|
|
||||||
padx=PADDING_SMALL,
|
|
||||||
)
|
|
||||||
content_frame.grid_rowconfigure(0, weight=1)
|
|
||||||
content_frame.grid_columnconfigure(0, weight=1)
|
|
||||||
|
|
||||||
|
# ── Title (top row of content) ──
|
||||||
self._title_label = ctk.CTkLabel(
|
self._title_label = ctk.CTkLabel(
|
||||||
content_frame,
|
self, text=self._title,
|
||||||
text=self._title,
|
|
||||||
font=_font(13, "bold"),
|
font=_font(13, "bold"),
|
||||||
anchor="w",
|
anchor="w",
|
||||||
text_color=DARK_FG if _is_dark() else LIGHT_FG,
|
text_color=DARK_FG if _is_dark() else LIGHT_FG,
|
||||||
)
|
)
|
||||||
self._title_label.grid(
|
self._title_label.grid(row=0, column=2, sticky="sw", padx=(0, 6), pady=(5, 0))
|
||||||
row=0, column=0,
|
|
||||||
sticky="nw",
|
|
||||||
pady=(0, 3),
|
|
||||||
)
|
|
||||||
|
|
||||||
|
# ── Description (bottom row of content) ──
|
||||||
self._desc_label = ctk.CTkLabel(
|
self._desc_label = ctk.CTkLabel(
|
||||||
content_frame,
|
self, text=self._description,
|
||||||
text=self._description,
|
|
||||||
font=FONT_SMALL,
|
font=FONT_SMALL,
|
||||||
anchor="w",
|
anchor="w",
|
||||||
text_color=DESC_TEXT_COLOR if _is_dark() else DESC_TEXT_COLOR_LIGHT,
|
text_color=DESC_TEXT_COLOR if _is_dark() else DESC_TEXT_COLOR_LIGHT,
|
||||||
wraplength=400,
|
|
||||||
)
|
|
||||||
self._desc_label.grid(
|
|
||||||
row=0, column=0,
|
|
||||||
sticky="nw",
|
|
||||||
pady=(0, 0),
|
|
||||||
)
|
)
|
||||||
|
self._desc_label.grid(row=1, column=2, sticky="nw", padx=(0, 6), pady=(0, 5))
|
||||||
|
|
||||||
# Action buttons area
|
# ── Action button (spans both rows) ──
|
||||||
btn_frame = ctk.CTkFrame(self, fg_color="transparent")
|
self._action_btn = ctk.CTkButton(
|
||||||
btn_frame.grid(
|
self, text="▶",
|
||||||
row=0, column=3,
|
font=_font(12),
|
||||||
sticky="n",
|
width=56, height=28,
|
||||||
pady=PADDING,
|
corner_radius=6,
|
||||||
)
|
|
||||||
btn_frame.grid_rowconfigure(0, weight=1)
|
|
||||||
btn_frame.grid_columnconfigure(0, weight=1)
|
|
||||||
|
|
||||||
# Run button (▶)
|
|
||||||
self._run_btn = ctk.CTkButton(
|
|
||||||
btn_frame,
|
|
||||||
text="▶",
|
|
||||||
font=_font(14, "bold"),
|
|
||||||
width=STEP_BUTTON_SIZE,
|
|
||||||
height=STEP_BUTTON_SIZE,
|
|
||||||
corner_radius=8,
|
|
||||||
fg_color=STEP_RUN_BG,
|
fg_color=STEP_RUN_BG,
|
||||||
hover_color=STEP_RUN_BG_HOVER,
|
hover_color=STEP_RUN_BG_HOVER,
|
||||||
text_color="white",
|
text_color="white",
|
||||||
command=self._on_run,
|
command=self._on_run,
|
||||||
state="disabled",
|
|
||||||
)
|
)
|
||||||
self._run_btn.grid(row=0, column=0, padx=(0, PADDING_SMALL))
|
self._action_btn.grid(row=0, column=3, rowspan=2, padx=(4, 4), pady=4)
|
||||||
|
|
||||||
# Re-run button (↻) — hidden by default
|
# ── Status icon (spans both rows, rightmost) ──
|
||||||
self._rerun_btn = ctk.CTkButton(
|
|
||||||
btn_frame,
|
|
||||||
text="↻",
|
|
||||||
font=_font(14, "bold"),
|
|
||||||
width=STEP_BUTTON_SIZE,
|
|
||||||
height=STEP_BUTTON_SIZE,
|
|
||||||
corner_radius=8,
|
|
||||||
fg_color=STEP_RUNNER_BG,
|
|
||||||
hover_color=STEP_RUNNER_BG_HOVER,
|
|
||||||
text_color="white",
|
|
||||||
command=self._on_rerun,
|
|
||||||
state="hidden",
|
|
||||||
)
|
|
||||||
self._rerun_btn.grid(row=0, column=0, padx=(0, PADDING_SMALL))
|
|
||||||
|
|
||||||
# Status icon — separate from buttons
|
|
||||||
self._status_label = ctk.CTkLabel(
|
self._status_label = ctk.CTkLabel(
|
||||||
self,
|
self, text="",
|
||||||
text="",
|
font=_font(14),
|
||||||
font=_font(16),
|
anchor="center",
|
||||||
anchor="w",
|
width=20,
|
||||||
)
|
|
||||||
self._status_label.grid(
|
|
||||||
row=0, column=3,
|
|
||||||
sticky="e",
|
|
||||||
padx=PADDING_SMALL,
|
|
||||||
)
|
)
|
||||||
|
self._status_label.grid(row=0, column=4, rowspan=2, padx=(0, 4), pady=4)
|
||||||
|
|
||||||
# Hover effect bindings
|
# ── Hover effect ──
|
||||||
self.bind("<Enter>", self._on_hover_enter)
|
self.bind("<Enter>", self._on_hover_enter)
|
||||||
self.bind("<Leave>", self._on_hover_leave)
|
self.bind("<Leave>", self._on_hover_leave)
|
||||||
|
|
||||||
@@ -279,11 +226,6 @@ class StepHeader(ctk.CTkFrame):
|
|||||||
if self._callback:
|
if self._callback:
|
||||||
self._callback()
|
self._callback()
|
||||||
|
|
||||||
def _on_rerun(self) -> None:
|
|
||||||
"""Handle re-run button click."""
|
|
||||||
if self._callback:
|
|
||||||
self._callback()
|
|
||||||
|
|
||||||
def _set_status(self, status: str) -> None:
|
def _set_status(self, status: str) -> None:
|
||||||
"""Set the step status with appropriate colors and icons.
|
"""Set the step status with appropriate colors and icons.
|
||||||
|
|
||||||
@@ -292,81 +234,80 @@ class StepHeader(ctk.CTkFrame):
|
|||||||
"""
|
"""
|
||||||
self._status = status
|
self._status = status
|
||||||
|
|
||||||
# Accent bar color
|
# ── Accent bar color ──
|
||||||
accent_colors = {
|
accent_colors = {
|
||||||
"pending": ACCENT_PENDING,
|
"pending": ACCENT_PENDING,
|
||||||
"running": RUNNING_FADE_LIGHT,
|
"running": RUNNING_FADE_LIGHT,
|
||||||
"complete": ACCENT_COMPLETE,
|
"complete": ACCENT_COMPLETE,
|
||||||
"error": ACCENT_ERROR,
|
"error": ACCENT_ERROR,
|
||||||
"disabled": ACCENT_DISABLED,
|
"disabled": ACCENT_DISABLED,
|
||||||
}
|
}
|
||||||
self._accent_bar.configure(fg_color=accent_colors.get(status, ACCENT_PENDING))
|
self._accent_bar.configure(fg_color=accent_colors.get(status, ACCENT_PENDING))
|
||||||
|
|
||||||
# Number circle
|
# ── Number circle color ──
|
||||||
circle_colors = {
|
circle_colors = {
|
||||||
"pending": CIRCLE_PENDING,
|
"pending": CIRCLE_PENDING,
|
||||||
"running": CIRCLE_RUNNING,
|
"running": CIRCLE_RUNNING,
|
||||||
"complete": CIRCLE_COMPLETE,
|
"complete": CIRCLE_COMPLETE,
|
||||||
"error": CIRCLE_ERROR,
|
"error": CIRCLE_ERROR,
|
||||||
"disabled": CIRCLE_DISABLED,
|
"disabled": CIRCLE_DISABLED,
|
||||||
}
|
}
|
||||||
circle_color = circle_colors.get(status, CIRCLE_PENDING)
|
self._num_label.configure(fg_color=circle_colors.get(status, CIRCLE_PENDING))
|
||||||
self._num_label.configure(fg_color=circle_color)
|
|
||||||
|
|
||||||
# Status icon — separate from buttons
|
# ── Status icon (rightmost column) ──
|
||||||
icons = {
|
icons = {"running": "◌", "complete": "✓", "error": "✗"}
|
||||||
"pending": "",
|
|
||||||
"running": "◌",
|
|
||||||
"complete": "✓",
|
|
||||||
"error": "✗",
|
|
||||||
"disabled": "",
|
|
||||||
}
|
|
||||||
icon = icons.get(status, "")
|
|
||||||
self._status_label.configure(text=icon)
|
|
||||||
icon_colors = {
|
icon_colors = {
|
||||||
"pending": "#666666",
|
"running": RUNNING_FADE_LIGHT,
|
||||||
"running": RUNNING_FADE_LIGHT,
|
|
||||||
"complete": ACCENT_COMPLETE,
|
"complete": ACCENT_COMPLETE,
|
||||||
"error": ACCENT_ERROR,
|
"error": ACCENT_ERROR,
|
||||||
"disabled": ACCENT_DISABLED,
|
|
||||||
}
|
}
|
||||||
self._status_label.configure(text_color=icon_colors.get(status, "#666666"))
|
self._status_label.configure(
|
||||||
|
text=icons.get(status, ""),
|
||||||
|
text_color=icon_colors.get(status, "#666666"),
|
||||||
|
)
|
||||||
|
|
||||||
# Buttons visibility
|
# ── Action button (single slot, text changes per state) ──
|
||||||
if status == "running":
|
if status == "running":
|
||||||
self._run_btn.configure(state="hidden")
|
self._action_btn.configure(state="disabled", text="…")
|
||||||
self._rerun_btn.configure(state="hidden")
|
|
||||||
self._start_pulse()
|
self._start_pulse()
|
||||||
elif status == "disabled":
|
elif status == "disabled":
|
||||||
self._run_btn.configure(state="hidden")
|
self._action_btn.configure(state="disabled", text="▶")
|
||||||
self._rerun_btn.configure(state="hidden")
|
|
||||||
self._stop_pulse()
|
self._stop_pulse()
|
||||||
self.configure(fg_color=DARK_CARD)
|
self.configure(fg_color=STEP_CARD_BG)
|
||||||
self._num_label.configure(fg_color=CIRCLE_DISABLED)
|
self._num_label.configure(fg_color=CIRCLE_DISABLED)
|
||||||
self._title_label.configure(text_color=DISABLED_TITLE_COLOR)
|
self._title_label.configure(text_color=DISABLED_TITLE_COLOR)
|
||||||
self._desc_label.configure(text_color=DISABLED_DESC_COLOR)
|
self._desc_label.configure(text_color=DISABLED_DESC_COLOR)
|
||||||
elif status == "complete":
|
elif status == "complete":
|
||||||
self._run_btn.configure(state="hidden")
|
self._action_btn.configure(
|
||||||
self._rerun_btn.configure(state="normal")
|
state="normal" if self._can_run else "disabled",
|
||||||
|
text="↻",
|
||||||
|
fg_color=STEP_RUNNER_BG,
|
||||||
|
hover_color=STEP_RUNNER_BG_HOVER,
|
||||||
|
)
|
||||||
self._stop_pulse()
|
self._stop_pulse()
|
||||||
elif status == "error":
|
elif status == "error":
|
||||||
self._run_btn.configure(state="hidden")
|
self._action_btn.configure(
|
||||||
self._rerun_btn.configure(state="normal")
|
state="normal" if self._can_run else "disabled",
|
||||||
|
text="↻",
|
||||||
|
fg_color=STEP_RUNNER_BG,
|
||||||
|
hover_color=STEP_RUNNER_BG_HOVER,
|
||||||
|
)
|
||||||
self._stop_pulse()
|
self._stop_pulse()
|
||||||
else: # pending
|
else: # pending
|
||||||
if self._can_run:
|
self._action_btn.configure(
|
||||||
self._run_btn.configure(state="normal")
|
state="normal" if self._can_run else "disabled",
|
||||||
else:
|
text="▶",
|
||||||
self._run_btn.configure(state="disabled")
|
fg_color=STEP_RUN_BG,
|
||||||
self._rerun_btn.configure(state="hidden")
|
hover_color=STEP_RUN_BG_HOVER,
|
||||||
|
)
|
||||||
self._stop_pulse()
|
self._stop_pulse()
|
||||||
|
|
||||||
# Update description — concise status text
|
# ── Description text ──
|
||||||
desc_map = {
|
desc_map = {
|
||||||
"pending": self._description or "Not started",
|
"pending": self._description or "Not started",
|
||||||
"running": "Running…",
|
"running": "Running…",
|
||||||
"complete": "Done",
|
"complete": "Done",
|
||||||
"error": "Failed — click ↻ to retry",
|
"error": "Failed — click ↻ to retry",
|
||||||
"disabled": "Waiting for dependencies",
|
"disabled": "Waiting for dependencies",
|
||||||
}
|
}
|
||||||
self._desc_label.configure(text=desc_map.get(status, self._description))
|
self._desc_label.configure(text=desc_map.get(status, self._description))
|
||||||
@@ -405,7 +346,7 @@ class StepHeader(ctk.CTkFrame):
|
|||||||
"""
|
"""
|
||||||
self._can_run = can_run
|
self._can_run = can_run
|
||||||
if self._status == "pending":
|
if self._status == "pending":
|
||||||
self._run_btn.configure(state="normal" if can_run else "disabled")
|
self._action_btn.configure(state="normal" if can_run else "disabled")
|
||||||
|
|
||||||
def _start_pulse(self) -> None:
|
def _start_pulse(self) -> None:
|
||||||
"""Start the pulsing animation for running state."""
|
"""Start the pulsing animation for running state."""
|
||||||
|
|||||||
Reference in New Issue
Block a user