Personal outliner built with Rust.
4

Configure Feed

Select the types of activity you want to include in your feed.

ui-polish group 2: client-drawn window chrome — transparent titlebar, header row becomes the caption with drag region and min/max/close buttons mapped to native hit-test codes; no fallback needed on gpui 0.2.2

graham.systems (Jul 15, 2026, 3:40 PM -0700) bb3814ad 1d065c01

+182 -9
+19
crates/trawler/assets/icons/minus.svg
··· 1 + <!-- 2 + tags: [subtract, less, minus, calculation, equation, remove, decrease, negative, mathematics, numeric] 3 + category: Math 4 + version: "1.0" 5 + unicode: "eaf2" 6 + --> 7 + <svg 8 + xmlns="http://www.w3.org/2000/svg" 9 + width="24" 10 + height="24" 11 + viewBox="0 0 24 24" 12 + fill="none" 13 + stroke="currentColor" 14 + stroke-width="2" 15 + stroke-linecap="round" 16 + stroke-linejoin="round" 17 + > 18 + <path d="M5 12l14 0" /> 19 + </svg>
+19
crates/trawler/assets/icons/square.svg
··· 1 + <!-- 2 + tags: [checkbox, box, shape, square, rectangular, geometry, form, figure, pattern, outline] 3 + category: Shapes 4 + version: "1.0" 5 + unicode: "eb2c" 6 + --> 7 + <svg 8 + xmlns="http://www.w3.org/2000/svg" 9 + width="24" 10 + height="24" 11 + viewBox="0 0 24 24" 12 + fill="none" 13 + stroke="currentColor" 14 + stroke-width="2" 15 + stroke-linecap="round" 16 + stroke-linejoin="round" 17 + > 18 + <path d="M3 5a2 2 0 0 1 2 -2h14a2 2 0 0 1 2 2v14a2 2 0 0 1 -2 2h-14a2 2 0 0 1 -2 -2v-14" /> 19 + </svg>
+20
crates/trawler/assets/icons/squares.svg
··· 1 + <!-- 2 + tags: [boxes, layers, squares, creative, artistic, rectangular, visual, aesthetic, style] 3 + category: Design 4 + version: "1.39" 5 + unicode: "eef6" 6 + --> 7 + <svg 8 + xmlns="http://www.w3.org/2000/svg" 9 + width="24" 10 + height="24" 11 + viewBox="0 0 24 24" 12 + fill="none" 13 + stroke="currentColor" 14 + stroke-width="2" 15 + stroke-linecap="round" 16 + stroke-linejoin="round" 17 + > 18 + <path d="M8 10a2 2 0 0 1 2 -2h9a2 2 0 0 1 2 2v9a2 2 0 0 1 -2 2h-9a2 2 0 0 1 -2 -2l0 -9" /> 19 + <path d="M16 8v-3a2 2 0 0 0 -2 -2h-9a2 2 0 0 0 -2 2v9a2 2 0 0 0 2 2h3" /> 20 + </svg>
+20
crates/trawler/assets/icons/x.svg
··· 1 + <!-- 2 + category: System 3 + tags: [cancel, remove, delete, empty, close, x] 4 + version: "1.0" 5 + unicode: "eb55" 6 + --> 7 + <svg 8 + xmlns="http://www.w3.org/2000/svg" 9 + width="24" 10 + height="24" 11 + viewBox="0 0 24 24" 12 + fill="none" 13 + stroke="currentColor" 14 + stroke-width="2" 15 + stroke-linecap="round" 16 + stroke-linejoin="round" 17 + > 18 + <path d="M18 6l-12 12" /> 19 + <path d="M6 6l12 12" /> 20 + </svg>
+14
crates/trawler/src/assets.rs
··· 43 43 "icons/layout-sidebar-right-expand.svg", 44 44 include_bytes!("../assets/icons/layout-sidebar-right-expand.svg"), 45 45 ), 46 + // Window caption glyphs (design D1): minimize, maximize, restore, close. 47 + ( 48 + "icons/minus.svg", 49 + include_bytes!("../assets/icons/minus.svg"), 50 + ), 51 + ( 52 + "icons/square.svg", 53 + include_bytes!("../assets/icons/square.svg"), 54 + ), 55 + ( 56 + "icons/squares.svg", 57 + include_bytes!("../assets/icons/squares.svg"), 58 + ), 59 + ("icons/x.svg", include_bytes!("../assets/icons/x.svg")), 46 60 ]; 47 61 48 62 /// The application's [`AssetSource`], registered via
+88 -7
crates/trawler/src/main.rs
··· 51 51 Application, Bounds, ClickEvent, Context, Entity, FocusHandle, Focusable as _, FontWeight, 52 52 InteractiveElement as _, IntoElement, KeyBinding, KeyDownEvent, ListAlignment, ListState, 53 53 MouseButton, ParentElement, Render, StatefulInteractiveElement as _, Styled, Subscription, 54 - Window, WindowBounds, WindowOptions, 54 + TitlebarOptions, Window, WindowBounds, WindowControlArea, WindowOptions, 55 55 }; 56 56 use loro::TreeID; 57 57 use markdown::{Block, Inline}; ··· 2405 2405 query_outcome: Option<QueryOutcome>, 2406 2406 } 2407 2407 2408 + /// A window caption button (min/max/close) for the client-drawn titlebar. 2409 + /// The `WindowControlArea` maps its bounds to the platform's native 2410 + /// non-client hit-test (HTMINBUTTON etc. on Windows), so hover flyouts 2411 + /// (Snap Layouts) and clicks are OS-handled there; the `on_click` attached 2412 + /// at the call site covers paths that deliver plain clicks instead (e.g. 2413 + /// the test platform). 2414 + fn caption_button( 2415 + id: &'static str, 2416 + icon_path: &'static str, 2417 + area: WindowControlArea, 2418 + ) -> gpui::Stateful<gpui::Div> { 2419 + div() 2420 + .id(id) 2421 + .window_control_area(area) 2422 + .w(px(46.0)) 2423 + .h_full() 2424 + .flex() 2425 + .items_center() 2426 + .justify_center() 2427 + .child(assets::icon(icon_path).size(px(14.0))) 2428 + } 2429 + 2408 2430 impl Render for TrawlerApp { 2409 2431 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement { 2410 2432 self.check_journal_rollover(cx); ··· 2745 2767 // `MONO_FONT` code/Scheme spans. 2746 2768 .font_family(UI_FONT) 2747 2769 .child( 2770 + // The titlebar row (openspec change ui-polish, design D1): 2771 + // with the native titlebar hidden (`appears_transparent`), 2772 + // this row is the window caption — nav controls on the 2773 + // left, a drag region through the middle, caption buttons 2774 + // flush to the right edge. The platform maps the control 2775 + // areas to native hit-test codes, so drag, double-click 2776 + // maximize, Snap Layouts, and caption clicks behave 2777 + // natively on Windows. 2748 2778 div() 2749 - .p_2() 2779 + .h(px(34.0)) 2780 + .pl_2() 2750 2781 .border_b_1() 2751 2782 .border_color(rgb(0x3a3a3a)) 2752 2783 .flex() ··· 2813 2844 ) 2814 2845 .children(calendar_overlay), 2815 2846 ) 2816 - .child(div().flex_1().child(format!( 2817 - "trawler — {title} — {} ({} blocks)", 2818 - self.graph_dir.display(), 2819 - row_count 2820 - ))), 2847 + .child( 2848 + div() 2849 + .id("titlebar-drag") 2850 + .window_control_area(WindowControlArea::Drag) 2851 + .flex_1() 2852 + .h_full() 2853 + .flex() 2854 + .items_center() 2855 + .overflow_hidden() 2856 + .child(format!( 2857 + "trawler — {title} — {} ({} blocks)", 2858 + self.graph_dir.display(), 2859 + row_count 2860 + )), 2861 + ) 2862 + .child( 2863 + caption_button("caption-min", "icons/minus.svg", WindowControlArea::Min) 2864 + .hover(|d| d.bg(rgb(0x2a2a3a))) 2865 + .on_click(cx.listener(|_, _: &ClickEvent, window, _| { 2866 + window.minimize_window(); 2867 + })), 2868 + ) 2869 + .child( 2870 + caption_button( 2871 + "caption-max", 2872 + if window.is_maximized() { 2873 + "icons/squares.svg" 2874 + } else { 2875 + "icons/square.svg" 2876 + }, 2877 + WindowControlArea::Max, 2878 + ) 2879 + .hover(|d| d.bg(rgb(0x2a2a3a))) 2880 + .on_click(cx.listener( 2881 + |_, _: &ClickEvent, window, _| { 2882 + window.zoom_window(); 2883 + }, 2884 + )), 2885 + ) 2886 + .child( 2887 + caption_button("caption-close", "icons/x.svg", WindowControlArea::Close) 2888 + .hover(|d| d.bg(rgb(0xc42b1c)).text_color(rgb(0xffffff))) 2889 + .on_click(cx.listener(|_, _: &ClickEvent, window, _| { 2890 + window.remove_window(); 2891 + })), 2892 + ), 2821 2893 ) 2822 2894 .children(view_header) 2823 2895 .children(breadcrumb_bar) ··· 3095 3167 .open_window( 3096 3168 WindowOptions { 3097 3169 window_bounds: Some(WindowBounds::Windowed(bounds)), 3170 + // Client-drawn chrome (openspec change ui-polish, 3171 + // design D1): hide the native titlebar; the app's 3172 + // header row doubles as the caption via 3173 + // WindowControlArea hit-test regions. 3174 + titlebar: Some(TitlebarOptions { 3175 + title: Some("trawler".into()), 3176 + appears_transparent: true, 3177 + traffic_light_position: None, 3178 + }), 3098 3179 ..Default::default() 3099 3180 }, 3100 3181 move |window, cx| cx.new(|cx| TrawlerApp::new(open_dir.clone(), window, cx)),
+2 -2
openspec/changes/ui-polish/tasks.md
··· 5 5 6 6 ## 2. Window chrome (design D1) 7 7 8 - - [ ] 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 9 - - [ ] 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 8 + - [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 9 + - [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 10 10 11 11 ## 3. Sidebar (design D3) 12 12