🐛 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:
Jeremy Shen
2026-06-09 17:30:36 +08:00
parent bda64d5f46
commit 91a7843dbe
2 changed files with 101 additions and 161 deletions
+11 -12
View File
@@ -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
View File
@@ -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."""