diff --git a/src/gui/main_window.py b/src/gui/main_window.py index 837a5e8..ac5e18c 100644 --- a/src/gui/main_window.py +++ b/src/gui/main_window.py @@ -42,7 +42,6 @@ from gui.styles import ( DANGER_COLOR, INFO_COLOR, CONNECTOR_COLOR, - STEP_ICON_SIZE, get_theme, ) from gui.widgets import ( @@ -189,9 +188,10 @@ class MainWindow(ctk.CTk): panel = ctk.CTkFrame(parent, corner_radius=CORNER_RADIUS) panel.grid( row=0, column=0, sticky="nsew", - padx=PADDING, pady=PADDING, + padx=PADDING, pady=(PADDING, 0), ) panel.grid_rowconfigure(10, weight=1) + panel.grid_columnconfigure(0, weight=1) # ── Title ── title = ctk.CTkLabel( @@ -199,7 +199,7 @@ class MainWindow(ctk.CTk): text="Workflow", 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 ── step_defs = [ @@ -223,19 +223,18 @@ class MainWindow(ctk.CTk): self._steps.append(step) self._step_statuses[i] = "pending" - # Connector line between steps + # Divider line between steps if i > 0: - connector = ctk.CTkFrame( + divider = ctk.CTkFrame( panel, - width=3, - height=12, + height=1, fg_color=CONNECTOR_COLOR, - corner_radius=2, ) - connector.grid( + divider.grid( row=i * 2, column=0, - sticky="n", - padx=(PADDING_SMALL + STEP_ICON_SIZE + PADDING_SMALL, PADDING_SMALL), + sticky="ew", + padx=PADDING, + pady=(2, 0), ) # Position step @@ -243,7 +242,7 @@ class MainWindow(ctk.CTk): row=i * 2 + 1, column=0, sticky="nsew", padx=PADDING_SMALL, - pady=PADDING_SMALL, + pady=(0, 2), ) # ── Progress indicator ── diff --git a/src/gui/widgets.py b/src/gui/widgets.py index c325a94..e61ac63 100644 --- a/src/gui/widgets.py +++ b/src/gui/widgets.py @@ -131,136 +131,83 @@ class StepHeader(ctk.CTkFrame): self._set_status(status) def _create_widgets(self) -> None: - """Create the step header UI components.""" - self.grid_rowconfigure(0, weight=1) - self.grid_columnconfigure((0, 1, 2, 3), weight=0, minsize=PADDING_SMALL) - self.grid_columnconfigure(1, weight=1) + """Create the step header UI components. + + Grid layout (no overlaps): + + 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, - width=5, - height=48, - corner_radius=3, + self, width=4, height=44, corner_radius=2, fg_color=ACCENT_PENDING, ) - self._accent_bar.grid( - row=0, column=0, - sticky="n", - pady=PADDING, - ) + self._accent_bar.grid(row=0, column=0, rowspan=2, sticky="ns") - # Step number circle + # ── Number circle (spans both rows, vertically centered) ── + circle_size = 28 self._num_label = ctk.CTkLabel( - self, - text=str(self._step_number), - font=_font(14, "bold"), - width=STEP_ICON_SIZE, - height=STEP_ICON_SIZE, - corner_radius=STEP_ICON_SIZE // 2, + self, text=str(self._step_number), + font=_font(13, "bold"), + width=circle_size, height=circle_size, + corner_radius=circle_size // 2, fg_color=CIRCLE_PENDING, text_color="white", ) - self._num_label.grid( - 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) + self._num_label.grid(row=0, column=1, rowspan=2, padx=(8, 8), pady=4) + # ── Title (top row of content) ── self._title_label = ctk.CTkLabel( - content_frame, - text=self._title, + self, text=self._title, font=_font(13, "bold"), anchor="w", text_color=DARK_FG if _is_dark() else LIGHT_FG, ) - self._title_label.grid( - row=0, column=0, - sticky="nw", - pady=(0, 3), - ) + self._title_label.grid(row=0, column=2, sticky="sw", padx=(0, 6), pady=(5, 0)) + # ── Description (bottom row of content) ── self._desc_label = ctk.CTkLabel( - content_frame, - text=self._description, + self, text=self._description, font=FONT_SMALL, anchor="w", 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 - btn_frame = ctk.CTkFrame(self, fg_color="transparent") - btn_frame.grid( - row=0, column=3, - sticky="n", - pady=PADDING, - ) - 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, + # ── Action button (spans both rows) ── + self._action_btn = ctk.CTkButton( + self, text="▶", + font=_font(12), + width=56, height=28, + corner_radius=6, fg_color=STEP_RUN_BG, hover_color=STEP_RUN_BG_HOVER, text_color="white", 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 - 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 + # ── Status icon (spans both rows, rightmost) ── self._status_label = ctk.CTkLabel( - self, - text="", - font=_font(16), - anchor="w", - ) - self._status_label.grid( - row=0, column=3, - sticky="e", - padx=PADDING_SMALL, + self, text="", + font=_font(14), + anchor="center", + width=20, ) + self._status_label.grid(row=0, column=4, rowspan=2, padx=(0, 4), pady=4) - # Hover effect bindings + # ── Hover effect ── self.bind("", self._on_hover_enter) self.bind("", self._on_hover_leave) @@ -279,11 +226,6 @@ class StepHeader(ctk.CTkFrame): if 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: """Set the step status with appropriate colors and icons. @@ -292,81 +234,80 @@ class StepHeader(ctk.CTkFrame): """ self._status = status - # Accent bar color + # ── Accent bar color ── accent_colors = { - "pending": ACCENT_PENDING, - "running": RUNNING_FADE_LIGHT, + "pending": ACCENT_PENDING, + "running": RUNNING_FADE_LIGHT, "complete": ACCENT_COMPLETE, - "error": ACCENT_ERROR, + "error": ACCENT_ERROR, "disabled": ACCENT_DISABLED, } self._accent_bar.configure(fg_color=accent_colors.get(status, ACCENT_PENDING)) - # Number circle + # ── Number circle color ── circle_colors = { - "pending": CIRCLE_PENDING, - "running": CIRCLE_RUNNING, + "pending": CIRCLE_PENDING, + "running": CIRCLE_RUNNING, "complete": CIRCLE_COMPLETE, - "error": CIRCLE_ERROR, + "error": CIRCLE_ERROR, "disabled": CIRCLE_DISABLED, } - circle_color = circle_colors.get(status, CIRCLE_PENDING) - self._num_label.configure(fg_color=circle_color) + self._num_label.configure(fg_color=circle_colors.get(status, CIRCLE_PENDING)) - # Status icon — separate from buttons - icons = { - "pending": "", - "running": "◌", - "complete": "✓", - "error": "✗", - "disabled": "", - } - icon = icons.get(status, "") - self._status_label.configure(text=icon) + # ── Status icon (rightmost column) ── + icons = {"running": "◌", "complete": "✓", "error": "✗"} icon_colors = { - "pending": "#666666", - "running": RUNNING_FADE_LIGHT, + "running": RUNNING_FADE_LIGHT, "complete": ACCENT_COMPLETE, - "error": ACCENT_ERROR, - "disabled": ACCENT_DISABLED, + "error": ACCENT_ERROR, } - 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": - self._run_btn.configure(state="hidden") - self._rerun_btn.configure(state="hidden") + self._action_btn.configure(state="disabled", text="…") self._start_pulse() elif status == "disabled": - self._run_btn.configure(state="hidden") - self._rerun_btn.configure(state="hidden") + self._action_btn.configure(state="disabled", text="▶") 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._title_label.configure(text_color=DISABLED_TITLE_COLOR) self._desc_label.configure(text_color=DISABLED_DESC_COLOR) elif status == "complete": - self._run_btn.configure(state="hidden") - self._rerun_btn.configure(state="normal") + self._action_btn.configure( + state="normal" if self._can_run else "disabled", + text="↻", + fg_color=STEP_RUNNER_BG, + hover_color=STEP_RUNNER_BG_HOVER, + ) self._stop_pulse() elif status == "error": - self._run_btn.configure(state="hidden") - self._rerun_btn.configure(state="normal") + self._action_btn.configure( + state="normal" if self._can_run else "disabled", + text="↻", + fg_color=STEP_RUNNER_BG, + hover_color=STEP_RUNNER_BG_HOVER, + ) self._stop_pulse() else: # pending - if self._can_run: - self._run_btn.configure(state="normal") - else: - self._run_btn.configure(state="disabled") - self._rerun_btn.configure(state="hidden") + self._action_btn.configure( + state="normal" if self._can_run else "disabled", + text="▶", + fg_color=STEP_RUN_BG, + hover_color=STEP_RUN_BG_HOVER, + ) self._stop_pulse() - # Update description — concise status text + # ── Description text ── desc_map = { - "pending": self._description or "Not started", - "running": "Running…", + "pending": self._description or "Not started", + "running": "Running…", "complete": "Done", - "error": "Failed — click ↻ to retry", + "error": "Failed — click ↻ to retry", "disabled": "Waiting for dependencies", } self._desc_label.configure(text=desc_map.get(status, self._description)) @@ -405,7 +346,7 @@ class StepHeader(ctk.CTkFrame): """ self._can_run = can_run 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: """Start the pulsing animation for running state."""