diff --git a/crates/trawler/assets/icons/minus.svg b/crates/trawler/assets/icons/minus.svg new file mode 100644 index 0000000..5d6501f --- /dev/null +++ b/crates/trawler/assets/icons/minus.svg @@ -0,0 +1,19 @@ + + + + diff --git a/crates/trawler/assets/icons/square.svg b/crates/trawler/assets/icons/square.svg new file mode 100644 index 0000000..a7efd5a --- /dev/null +++ b/crates/trawler/assets/icons/square.svg @@ -0,0 +1,19 @@ + + + + diff --git a/crates/trawler/assets/icons/squares.svg b/crates/trawler/assets/icons/squares.svg new file mode 100644 index 0000000..539c987 --- /dev/null +++ b/crates/trawler/assets/icons/squares.svg @@ -0,0 +1,20 @@ + + + + + diff --git a/crates/trawler/assets/icons/x.svg b/crates/trawler/assets/icons/x.svg new file mode 100644 index 0000000..40a873c --- /dev/null +++ b/crates/trawler/assets/icons/x.svg @@ -0,0 +1,20 @@ + + + + + diff --git a/crates/trawler/src/assets.rs b/crates/trawler/src/assets.rs index 3c0e1da..0fcfec4 100644 --- a/crates/trawler/src/assets.rs +++ b/crates/trawler/src/assets.rs @@ -43,6 +43,20 @@ const ICONS: &[(&str, &[u8])] = &[ "icons/layout-sidebar-right-expand.svg", include_bytes!("../assets/icons/layout-sidebar-right-expand.svg"), ), + // Window caption glyphs (design D1): minimize, maximize, restore, close. + ( + "icons/minus.svg", + include_bytes!("../assets/icons/minus.svg"), + ), + ( + "icons/square.svg", + include_bytes!("../assets/icons/square.svg"), + ), + ( + "icons/squares.svg", + include_bytes!("../assets/icons/squares.svg"), + ), + ("icons/x.svg", include_bytes!("../assets/icons/x.svg")), ]; /// The application's [`AssetSource`], registered via diff --git a/crates/trawler/src/main.rs b/crates/trawler/src/main.rs index 464afce..15a1b57 100644 --- a/crates/trawler/src/main.rs +++ b/crates/trawler/src/main.rs @@ -51,7 +51,7 @@ use gpui::{ Application, Bounds, ClickEvent, Context, Entity, FocusHandle, Focusable as _, FontWeight, InteractiveElement as _, IntoElement, KeyBinding, KeyDownEvent, ListAlignment, ListState, MouseButton, ParentElement, Render, StatefulInteractiveElement as _, Styled, Subscription, - Window, WindowBounds, WindowOptions, + TitlebarOptions, Window, WindowBounds, WindowControlArea, WindowOptions, }; use loro::TreeID; use markdown::{Block, Inline}; @@ -2405,6 +2405,28 @@ struct RowSnapshot { query_outcome: Option, } +/// A window caption button (min/max/close) for the client-drawn titlebar. +/// The `WindowControlArea` maps its bounds to the platform's native +/// non-client hit-test (HTMINBUTTON etc. on Windows), so hover flyouts +/// (Snap Layouts) and clicks are OS-handled there; the `on_click` attached +/// at the call site covers paths that deliver plain clicks instead (e.g. +/// the test platform). +fn caption_button( + id: &'static str, + icon_path: &'static str, + area: WindowControlArea, +) -> gpui::Stateful { + div() + .id(id) + .window_control_area(area) + .w(px(46.0)) + .h_full() + .flex() + .items_center() + .justify_center() + .child(assets::icon(icon_path).size(px(14.0))) +} + impl Render for TrawlerApp { fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { self.check_journal_rollover(cx); @@ -2745,8 +2767,17 @@ impl Render for TrawlerApp { // `MONO_FONT` code/Scheme spans. .font_family(UI_FONT) .child( + // The titlebar row (openspec change ui-polish, design D1): + // with the native titlebar hidden (`appears_transparent`), + // this row is the window caption — nav controls on the + // left, a drag region through the middle, caption buttons + // flush to the right edge. The platform maps the control + // areas to native hit-test codes, so drag, double-click + // maximize, Snap Layouts, and caption clicks behave + // natively on Windows. div() - .p_2() + .h(px(34.0)) + .pl_2() .border_b_1() .border_color(rgb(0x3a3a3a)) .flex() @@ -2813,11 +2844,52 @@ impl Render for TrawlerApp { ) .children(calendar_overlay), ) - .child(div().flex_1().child(format!( - "trawler — {title} — {} ({} blocks)", - self.graph_dir.display(), - row_count - ))), + .child( + div() + .id("titlebar-drag") + .window_control_area(WindowControlArea::Drag) + .flex_1() + .h_full() + .flex() + .items_center() + .overflow_hidden() + .child(format!( + "trawler — {title} — {} ({} blocks)", + self.graph_dir.display(), + row_count + )), + ) + .child( + caption_button("caption-min", "icons/minus.svg", WindowControlArea::Min) + .hover(|d| d.bg(rgb(0x2a2a3a))) + .on_click(cx.listener(|_, _: &ClickEvent, window, _| { + window.minimize_window(); + })), + ) + .child( + caption_button( + "caption-max", + if window.is_maximized() { + "icons/squares.svg" + } else { + "icons/square.svg" + }, + WindowControlArea::Max, + ) + .hover(|d| d.bg(rgb(0x2a2a3a))) + .on_click(cx.listener( + |_, _: &ClickEvent, window, _| { + window.zoom_window(); + }, + )), + ) + .child( + caption_button("caption-close", "icons/x.svg", WindowControlArea::Close) + .hover(|d| d.bg(rgb(0xc42b1c)).text_color(rgb(0xffffff))) + .on_click(cx.listener(|_, _: &ClickEvent, window, _| { + window.remove_window(); + })), + ), ) .children(view_header) .children(breadcrumb_bar) @@ -3095,6 +3167,15 @@ fn main() { .open_window( WindowOptions { window_bounds: Some(WindowBounds::Windowed(bounds)), + // Client-drawn chrome (openspec change ui-polish, + // design D1): hide the native titlebar; the app's + // header row doubles as the caption via + // WindowControlArea hit-test regions. + titlebar: Some(TitlebarOptions { + title: Some("trawler".into()), + appears_transparent: true, + traffic_light_position: None, + }), ..Default::default() }, move |window, cx| cx.new(|cx| TrawlerApp::new(open_dir.clone(), window, cx)), diff --git a/openspec/changes/ui-polish/tasks.md b/openspec/changes/ui-polish/tasks.md index c2d1713..e379302 100644 --- a/openspec/changes/ui-polish/tasks.md +++ b/openspec/changes/ui-polish/tasks.md @@ -5,8 +5,8 @@ ## 2. Window chrome (design D1) -- [ ] 2.1 Switch `WindowOptions` to a transparent/client titlebar (verify exact gpui 0.2 API; Zed's Windows titlebar is the reference); render the titlebar row: drag region, caption controls, existing header controls folded in -- [ ] 2.2 Verify drag-to-move, double-click maximize/restore, minimize/close on Windows via dev-loop; if gpui's caption handling is incomplete, fall back to stock titlebar and record the finding in this file +- [x] 2.1 Switch `WindowOptions` to a transparent/client titlebar (`TitlebarOptions::appears_transparent`); the header row became the titlebar — nav/calendar controls left, title text as the drag region (`WindowControlArea::Drag`), caption buttons (min / max-restore with icon swap via `is_maximized` / close with Windows-red hover) flush right, 34px row +- [x] 2.2 Verified on Windows: gpui 0.2.2's `WindowControlArea` hitboxes map to native NCHITTEST codes (HTCAPTION/HTMINBUTTON/HTMAXBUTTON/HTCLOSE), so drag, double-click maximize/restore, Snap Layouts flyout, and caption clicks are OS-handled — **no fallback needed**. Caption buttons also carry plain `on_click` handlers for client-click paths (test platform). User-verified interactively (drag, snap, caption behavior) plus dev-loop screenshot ## 3. Sidebar (design D3)