From 1e27991cf1c4a557e660ed6e18a02cb2c179871d Mon Sep 17 00:00:00 2001 From: Nathaniel Ledford Date: Thu, 8 Oct 2026 19:35:36 -0400 Subject: [PATCH 1/7] Title bar: window-gesture policy (drag, double-click zoom, full-screen exempt) Co-Authored-By: Claude Sonnet 5.5 --- crates/ui-egui/src/lib.rs | 1 + crates/ui-egui/src/titlebar.rs | 76 ++++++++++++++++++++++++++++++++++ 2 files changed, 77 insertions(+) create mode 100644 crates/ui-egui/src/titlebar.rs diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 502e31ffe..43b210f1f 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -25,6 +25,7 @@ pub mod softpaint; pub mod state; pub mod tasks; pub mod theme; +pub mod titlebar; pub mod widgets; #[cfg(test)] diff --git a/crates/ui-egui/src/titlebar.rs b/crates/ui-egui/src/titlebar.rs new file mode 100644 index 000000000..40c8975cd --- /dev/null +++ b/crates/ui-egui/src/titlebar.rs @@ -0,0 +1,76 @@ +//! The integrated title bar's window gestures (macOS: the native title bar is hidden and the top +//! bar takes its place, so dragging and double-click-to-zoom have to be done here). +//! +//! Pure policy, no egui widgets: [`command_for`] maps a gesture on the bar's empty space and the +//! window's current state to the viewport command the host should run. + +use egui::ViewportCommand; + +/// What the user did on the bar's empty space. +#[derive(Clone, Copy, Debug, PartialEq, Eq)] +pub enum Gesture { + /// A primary-button drag began: the window follows the pointer. + DragStarted, + /// A primary-button double-click: zoom the window, or restore it when already zoomed. + DoubleClicked, +} + +/// The window as the host reports it. +#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)] +pub struct WindowState { + /// Zoomed (macOS) / maximized (elsewhere). + pub maximized: bool, + /// In the OS's full-screen mode. + pub fullscreen: bool, +} + +/// The viewport command for `gesture`, if any. Full screen has no title bar to drag or zoom. +pub fn command_for(gesture: Gesture, window: WindowState) -> Option { + if window.fullscreen { + return None; + } + match gesture { + Gesture::DragStarted => Some(ViewportCommand::StartDrag), + Gesture::DoubleClicked => Some(ViewportCommand::Maximized(!window.maximized)), + } +} + +#[cfg(test)] +mod tests { + use super::*; + + const NORMAL: WindowState = WindowState { maximized: false, fullscreen: false }; + const ZOOMED: WindowState = WindowState { maximized: true, fullscreen: false }; + const FULL: WindowState = WindowState { maximized: false, fullscreen: true }; + + // Scenario: dragging the title bar moves the window + #[test] + fn given_a_normal_window_when_dragging_then_the_window_follows_the_pointer() { + assert_eq!(command_for(Gesture::DragStarted, NORMAL), Some(ViewportCommand::StartDrag)); + } + + // Scenario: a zoomed window can still be dragged (macOS lets you pull it free) + #[test] + fn given_a_zoomed_window_when_dragging_then_the_window_follows_the_pointer() { + assert_eq!(command_for(Gesture::DragStarted, ZOOMED), Some(ViewportCommand::StartDrag)); + } + + // Scenario: double-click zooms + #[test] + fn given_a_normal_window_when_double_clicking_then_it_zooms() { + assert_eq!(command_for(Gesture::DoubleClicked, NORMAL), Some(ViewportCommand::Maximized(true))); + } + + // Scenario: double-click again restores + #[test] + fn given_a_zoomed_window_when_double_clicking_then_it_is_restored() { + assert_eq!(command_for(Gesture::DoubleClicked, ZOOMED), Some(ViewportCommand::Maximized(false))); + } + + // Scenario: full screen ignores both + #[test] + fn given_a_full_screen_window_then_neither_gesture_does_anything() { + assert_eq!(command_for(Gesture::DragStarted, FULL), None); + assert_eq!(command_for(Gesture::DoubleClicked, FULL), None); + } +} From 54b037a90a4cf0808a956066358e131295f721da Mon Sep 17 00:00:00 2001 From: Nathaniel Ledford Date: Thu, 8 Oct 2026 19:37:05 -0400 Subject: [PATCH 2/7] Headless host: record window commands and report the zoomed state back Co-Authored-By: Claude Sonnet 5.5 --- crates/ui-egui/src/headless.rs | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/crates/ui-egui/src/headless.rs b/crates/ui-egui/src/headless.rs index 4411c35ac..484828e34 100644 --- a/crates/ui-egui/src/headless.rs +++ b/crates/ui-egui/src/headless.rs @@ -115,6 +115,10 @@ pub struct Headless { events: Vec, control: Sender, quit: bool, + /// The simulated window is zoomed (`Maximized(true)` seen, reported back to the app like a real host). + pub window_maximized: bool, + /// Window-management commands the app sent (`StartDrag`, `Maximized`, …), oldest first. + pub window_commands: Vec, } impl Headless { @@ -133,6 +137,8 @@ impl Headless { events: vec![], control: tx, quit: false, + window_maximized: false, + window_commands: vec![], } } @@ -148,6 +154,7 @@ impl Headless { /// Run one frame. pub fn step(&mut self) { let mut raw = HeadlessView::raw_input(self.size, self.pixels_per_point, self.time, std::mem::take(&mut self.events)); + raw.viewports.entry(ViewportId::ROOT).or_default().maximized = Some(self.window_maximized); self.app.raw_input_hook(&mut raw); let app = &mut self.app; let commands = self.view.run(raw, |ui| { @@ -164,6 +171,11 @@ impl Headless { } ViewportCommand::InnerSize(s) if s.x >= 1.0 && s.y >= 1.0 => self.size = s, ViewportCommand::Close => self.quit = true, + ViewportCommand::Maximized(on) => { + self.window_maximized = on; + self.window_commands.push(c); + } + ViewportCommand::StartDrag => self.window_commands.push(c), _ => {} } } From d01ebd79538bbe53c7246d62919310ec430a9fe1 Mon Sep 17 00:00:00 2001 From: Nathaniel Ledford Date: Thu, 8 Oct 2026 19:37:05 -0400 Subject: [PATCH 3/7] Top bar: drag to move and double-click to zoom the window (integrated title bar) Co-Authored-By: Claude Sonnet 5.5 --- crates/ui-egui/src/lib.rs | 2 + crates/ui-egui/src/panels/topbar.rs | 25 +++++++ crates/ui-egui/src/tests_titlebar.rs | 107 +++++++++++++++++++++++++++ 3 files changed, 134 insertions(+) create mode 100644 crates/ui-egui/src/tests_titlebar.rs diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index 43b210f1f..b327e2454 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -55,6 +55,8 @@ mod tests_scroll; #[cfg(test)] mod tests_switch_library; #[cfg(test)] +mod tests_titlebar; +#[cfg(test)] mod tests_unsaved; use std::sync::mpsc::{Receiver, Sender}; diff --git a/crates/ui-egui/src/panels/topbar.rs b/crates/ui-egui/src/panels/topbar.rs index e960909ea..7451c8acb 100644 --- a/crates/ui-egui/src/panels/topbar.rs +++ b/crates/ui-egui/src/panels/topbar.rs @@ -8,6 +8,28 @@ use crate::icons::{Icon, paint}; use crate::theme::Tokens; use crate::widgets::{icon_button, register}; +/// The bar doubles as the window's title bar: dragging its empty space moves the window and a +/// double-click zooms or restores it. Registered before the bar's widgets so they win the click. +fn window_handle(app: &LightcraftApp, ui: &mut egui::Ui, content: Rect, margin_left: f32, margin_right: f32) { + use crate::titlebar::{Gesture, WindowState, command_for}; + let bar = Rect::from_min_max(pos2(content.left() - margin_left, content.top()), pos2(content.right() + margin_right, content.bottom())); + register(ui.ctx(), "region:titlebar", bar); + let resp = ui.interact(bar, egui::Id::new("titlebar-handle"), Sense::click_and_drag()); + let gesture = if resp.double_clicked_by(egui::PointerButton::Primary) { + Some(Gesture::DoubleClicked) + } else if resp.drag_started_by(egui::PointerButton::Primary) { + Some(Gesture::DragStarted) + } else { + None + }; + if let Some(g) = gesture { + let window = WindowState { maximized: ui.input(|i| i.viewport().maximized).unwrap_or(false), fullscreen: app.window_is_fullscreen }; + if let Some(cmd) = command_for(g, window) { + ui.ctx().send_viewport_cmd(cmd); + } + } +} + pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { let t = Tokens::get(ui.ctx()); let left = if app.integrated_titlebar { 78 } else { 10 }; @@ -16,6 +38,9 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { .frame(egui::Frame::NONE.fill(t.chrome).inner_margin(egui::Margin { left, right: 12, top: 0, bottom: 0 })) .show(ui, |ui| { let full = ui.max_rect(); + if app.integrated_titlebar { + window_handle(app, ui, full, left as f32, 12.0); + } let mut sw = 640.0f32.min(full.width() - 460.0).max(200.0); if !app.native_menu { // leave room for the in-window menus left of the (centred) search field diff --git a/crates/ui-egui/src/tests_titlebar.rs b/crates/ui-egui/src/tests_titlebar.rs new file mode 100644 index 000000000..f1ff782ef --- /dev/null +++ b/crates/ui-egui/src/tests_titlebar.rs @@ -0,0 +1,107 @@ +//! The integrated title bar behaves like a native one: drag the empty space to move the window, +//! double-click it to zoom and again to restore. Buttons and the search field keep their clicks. + +use std::time::Duration; + +use egui::ViewportCommand; +use serde_json::json; + +use crate::headless::Headless; +use crate::{LightcraftApp, Services}; + +const T: Duration = Duration::from_secs(20); + +/// Given LightCraft with the integrated title bar (macOS) in a 1200×800 window. +fn integrated() -> Headless { + let services = Services { png: None, ..Default::default() }; + let mut app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), services); + app.integrated_titlebar = true; + let mut h = Headless::new(app, [1200.0, 800.0], 1.0); + h.settle(Duration::from_secs(120)); + h +} + +fn widget(h: &Headless, id: &str) -> egui::Rect { + h.app.widgets.iter().find(|(w, _)| w == id).map(|(_, r)| *r).unwrap_or_else(|| panic!("no widget {id}")) +} + +/// Empty top-bar space: where the traffic lights sit, left of the sidebar button. +fn empty_spot(h: &Headless) -> egui::Pos2 { + let bar = widget(h, "region:titlebar"); + egui::pos2(bar.left() + 20.0, bar.center().y) +} + +fn play(h: &mut Headless) { + for _ in 0..6 { + h.step(); + } +} + +// Scenario: double-clicking the empty top bar zooms the window, and again restores it +#[test] +fn double_click_on_the_empty_bar_zooms_then_restores() { + let mut h = integrated(); + let p = empty_spot(&h); + h.request("ui.click", json!({"x": p.x, "y": p.y, "count": 2}), T); + play(&mut h); + assert_eq!(h.window_commands, vec![ViewportCommand::Maximized(true)]); + // a pause longer than egui's multi-click window, or the second pair would count as a triple click + for _ in 0..120 { + h.step(); + } + h.request("ui.click", json!({"x": p.x, "y": p.y, "count": 2}), T); + play(&mut h); + assert_eq!(h.window_commands, vec![ViewportCommand::Maximized(true), ViewportCommand::Maximized(false)]); +} + +// Scenario: dragging the empty top bar starts a window move +#[test] +fn dragging_the_empty_bar_moves_the_window() { + let mut h = integrated(); + let p = empty_spot(&h); + h.request("ui.drag", json!({"x": p.x, "y": p.y, "toX": p.x + 120.0, "toY": p.y + 40.0}), T); + play(&mut h); + assert_eq!(h.window_commands, vec![ViewportCommand::StartDrag]); +} + +// Scenario: a single click on the empty bar does nothing +#[test] +fn single_click_on_the_empty_bar_does_nothing() { + let mut h = integrated(); + let p = empty_spot(&h); + h.request("ui.click", json!({"x": p.x, "y": p.y}), T); + play(&mut h); + assert!(h.window_commands.is_empty(), "{:?}", h.window_commands); +} + +// Scenario: double-clicking a button does not zoom the window +#[test] +fn double_click_on_a_button_does_not_zoom() { + let mut h = integrated(); + let r = widget(&h, "icon:help"); + h.request("ui.click", json!({"x": r.center().x, "y": r.center().y, "count": 2}), T); + play(&mut h); + assert!(h.window_commands.is_empty(), "{:?}", h.window_commands); +} + +// Scenario: double-clicking the search field selects a word, it does not zoom the window +#[test] +fn double_click_in_the_search_field_does_not_zoom() { + let mut h = integrated(); + let r = widget(&h, "field:search"); + h.request("ui.click", json!({"x": r.center().x, "y": r.center().y, "count": 2}), T); + play(&mut h); + assert!(h.window_commands.is_empty(), "{:?}", h.window_commands); +} + +// Scenario: a window with its native title bar (Windows, Linux) leaves the bar alone +#[test] +fn without_the_integrated_title_bar_the_bar_is_not_a_window_handle() { + let services = Services { png: None, ..Default::default() }; + let app = LightcraftApp::new(lightcraft_engine::Session::with_demo(), services); + let mut h = Headless::new(app, [1200.0, 800.0], 1.0); + h.settle(Duration::from_secs(120)); + assert!(!h.app.integrated_titlebar); + let bar = h.app.widgets.iter().find(|(w, _)| w == "region:titlebar"); + assert!(bar.is_none()); +} From 86d88de2ee6bf302d0d73a1106cb54df7f7b4982 Mon Sep 17 00:00:00 2001 From: Nathaniel Ledford Date: Thu, 8 Oct 2026 19:37:55 -0400 Subject: [PATCH 4/7] Parity: track title-bar drag and double-click zoom Co-Authored-By: Claude Sonnet 5.5 --- docs/parity.md | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/docs/parity.md b/docs/parity.md index b9c5eed5a..b1932654b 100644 --- a/docs/parity.md +++ b/docs/parity.md @@ -44,13 +44,13 @@ and prints the summary; `cargo xtask parity --write` refreshes the summary table | U. Map & location (MAP) | 0 | 1 | 1 | 0 | — | 0/1 (0%) | | V. Preferences (PREF) | 6 | 0 | 3 | 2 | 1/1 (100%) | 4/4 (100%) | | W. Cloud & AI infrastructure (CLOUD / AI) | 1 | 0 | 1 | 2 | — | — | -| X. Cross-cutting behaviours (BEHAV) | 17 | 4 | 1 | 1 | 8/8 (100%) | 6/8 (75%) | +| X. Cross-cutting behaviours (BEHAV) | 17 | 5 | 1 | 1 | 8/8 (100%) | 6/8 (75%) | | Y. Menus | 81 | 0 | 4 | 8 | 47/47 (100%) | 23/23 (100%) | | Z. Keyboard shortcuts (desktop) | 75 | 3 | 3 | 1 | 49/52 (94%) | 23/24 (96%) | | Lightroom Classic extras | 32 | 11 | 39 | 9 | — | 22/24 (92%) | -| **Total** | 392 | 35 | 86 | 36 | 193/200 (97%) | 142/152 (93%) | +| **Total** | 392 | 36 | 86 | 36 | 193/200 (97%) | 142/152 (93%) | -Weighted completion (✅ = 1, 🟡 = ½, 🚫 left out): **79.8%** of 513 in-scope rows — P0 98.2% of 200 · P1 95.7% of 152 · P2 41.6% of 160. +Weighted completion (✅ = 1, 🟡 = ½, 🚫 left out): **79.8%** of 514 in-scope rows — P0 98.2% of 200 · P1 95.7% of 152 · P2 41.6% of 161. ## Top gaps @@ -522,6 +522,7 @@ under the UI command that wraps it (a no-op error); the UI command now wins. | LR-BEHAV-LEARN | Tutorials | OOS | 🚫 | | | | LR-BEHAV-WHATSNEW | What's new | P2 | ✅ | `cmd:app.whatsNew`, `docs/whats-new.md` | Help ▸ What's New: release highlights | | LR-BEHAV-AI-EA | Early-access badges | P2 | ⬜ | | | +| LR-BEHAV-TITLEBAR | Title bar drag and double-click zoom (macOS) | P2 | 🟡 | `crates/ui-egui/src/titlebar.rs`, `crates/ui-egui/src/panels/topbar.rs`, `crates/ui-egui/src/tests_titlebar.rs` | the native title bar is hidden on macOS, so the top bar's empty space (including the traffic-light margin) moves the window and a double-click zooms or restores it; buttons and the search field keep their clicks; ignored in full screen. Zoom is winit's `Maximized`: it does not follow System Settings › “Double-click a window's title bar to” (Fill / Minimize). Not checked on a real macOS window yet | ## Y. Menus From cc8d6174597be553cc97541035261310a2199d50 Mon Sep 17 00:00:00 2001 From: Nathaniel Ledford Date: Thu, 8 Oct 2026 19:42:52 -0400 Subject: [PATCH 5/7] Top bar: a press that starts on a button or the search field never drags the window Co-Authored-By: Claude Sonnet 5.5 --- crates/ui-egui/src/panels/topbar.rs | 16 ++++++++++++++-- crates/ui-egui/src/tests_titlebar.rs | 12 +++++++++++- 2 files changed, 25 insertions(+), 3 deletions(-) diff --git a/crates/ui-egui/src/panels/topbar.rs b/crates/ui-egui/src/panels/topbar.rs index 7451c8acb..d06a75594 100644 --- a/crates/ui-egui/src/panels/topbar.rs +++ b/crates/ui-egui/src/panels/topbar.rs @@ -14,10 +14,22 @@ fn window_handle(app: &LightcraftApp, ui: &mut egui::Ui, content: Rect, margin_l use crate::titlebar::{Gesture, WindowState, command_for}; let bar = Rect::from_min_max(pos2(content.left() - margin_left, content.top()), pos2(content.right() + margin_right, content.bottom())); register(ui.ctx(), "region:titlebar", bar); - let resp = ui.interact(bar, egui::Id::new("titlebar-handle"), Sense::click_and_drag()); + let handle = egui::Id::new("titlebar-handle"); + let resp = ui.interact(bar, handle, Sense::click_and_drag()); + // a press that began on a button or the search field is theirs: dragging off it must not move the window + // (while pressing, egui hovers every widget under the pointer; once the drag starts, only the dragged one) + let ctx = ui.ctx().clone(); + let memo = egui::Id::new("titlebar-press-on-widget"); + if resp.is_pointer_button_down_on() && ctx.input(|i| i.pointer.primary_pressed()) { + let hovered = ctx.interaction_snapshot(|s| s.hovered.clone()); + let on_widget = + hovered.iter().any(|id| *id != handle && ctx.read_response(*id).is_some_and(|r| r.sense.senses_click() || r.sense.senses_drag())); + ctx.data_mut(|d| d.insert_temp(memo, on_widget)); + } + let on_widget = ctx.data(|d| d.get_temp::(memo)).unwrap_or(false); let gesture = if resp.double_clicked_by(egui::PointerButton::Primary) { Some(Gesture::DoubleClicked) - } else if resp.drag_started_by(egui::PointerButton::Primary) { + } else if resp.drag_started_by(egui::PointerButton::Primary) && !on_widget { Some(Gesture::DragStarted) } else { None diff --git a/crates/ui-egui/src/tests_titlebar.rs b/crates/ui-egui/src/tests_titlebar.rs index f1ff782ef..40eba4b13 100644 --- a/crates/ui-egui/src/tests_titlebar.rs +++ b/crates/ui-egui/src/tests_titlebar.rs @@ -46,7 +46,7 @@ fn double_click_on_the_empty_bar_zooms_then_restores() { play(&mut h); assert_eq!(h.window_commands, vec![ViewportCommand::Maximized(true)]); // a pause longer than egui's multi-click window, or the second pair would count as a triple click - for _ in 0..120 { + for _ in 0..30 { h.step(); } h.request("ui.click", json!({"x": p.x, "y": p.y, "count": 2}), T); @@ -84,6 +84,16 @@ fn double_click_on_a_button_does_not_zoom() { assert!(h.window_commands.is_empty(), "{:?}", h.window_commands); } +// Scenario: pressing a button and dragging off it does not move the window (as in a native title bar) +#[test] +fn dragging_from_a_button_does_not_move_the_window() { + let mut h = integrated(); + let r = widget(&h, "icon:help"); + h.request("ui.drag", json!({"x": r.center().x, "y": r.center().y, "toX": r.center().x - 150.0, "toY": r.center().y + 30.0}), T); + play(&mut h); + assert!(h.window_commands.is_empty(), "{:?}", h.window_commands); +} + // Scenario: double-clicking the search field selects a word, it does not zoom the window #[test] fn double_click_in_the_search_field_does_not_zoom() { From 2bd13c150068e5d55dd4ee2634a05fea831e8e1e Mon Sep 17 00:00:00 2001 From: Nathaniel Ledford Date: Thu, 8 Oct 2026 19:42:52 -0400 Subject: [PATCH 6/7] Parity: note the stale zoomed-state limitation of the title-bar toggle Co-Authored-By: Claude Sonnet 5.5 --- docs/parity.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/parity.md b/docs/parity.md index b1932654b..9e797eaef 100644 --- a/docs/parity.md +++ b/docs/parity.md @@ -522,7 +522,7 @@ under the UI command that wraps it (a no-op error); the UI command now wins. | LR-BEHAV-LEARN | Tutorials | OOS | 🚫 | | | | LR-BEHAV-WHATSNEW | What's new | P2 | ✅ | `cmd:app.whatsNew`, `docs/whats-new.md` | Help ▸ What's New: release highlights | | LR-BEHAV-AI-EA | Early-access badges | P2 | ⬜ | | | -| LR-BEHAV-TITLEBAR | Title bar drag and double-click zoom (macOS) | P2 | 🟡 | `crates/ui-egui/src/titlebar.rs`, `crates/ui-egui/src/panels/topbar.rs`, `crates/ui-egui/src/tests_titlebar.rs` | the native title bar is hidden on macOS, so the top bar's empty space (including the traffic-light margin) moves the window and a double-click zooms or restores it; buttons and the search field keep their clicks; ignored in full screen. Zoom is winit's `Maximized`: it does not follow System Settings › “Double-click a window's title bar to” (Fill / Minimize). Not checked on a real macOS window yet | +| LR-BEHAV-TITLEBAR | Title bar drag and double-click zoom (macOS) | P2 | 🟡 | `crates/ui-egui/src/titlebar.rs`, `crates/ui-egui/src/panels/topbar.rs`, `crates/ui-egui/src/tests_titlebar.rs` | the native title bar is hidden on macOS, so the top bar's empty space (including the traffic-light margin) moves the window and a double-click zooms or restores it; buttons and the search field keep their clicks; ignored in full screen. Zoom is winit's `Maximized`: it does not follow System Settings › “Double-click a window's title bar to” (Fill / Minimize). a press that starts on a button or the search field never moves the window. Known gap: egui-winit reads the zoomed state only at start-up and after our own `Maximized` command, so a window zoomed another way (green button, Window ▸ Zoom, snapping) makes the first double-click a no-op before the toggle catches up. Not checked on a real macOS window yet | ## Y. Menus From eaaf85985334e464a11402d4886c2d594ffc17e6 Mon Sep 17 00:00:00 2001 From: Nathaniel Ledford Date: Thu, 8 Oct 2026 19:44:23 -0400 Subject: [PATCH 7/7] Parity: title-bar drag and zoom confirmed on a real macOS window Co-Authored-By: Claude Sonnet 5.5 --- docs/parity.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/docs/parity.md b/docs/parity.md index 9e797eaef..6857e7a07 100644 --- a/docs/parity.md +++ b/docs/parity.md @@ -522,7 +522,7 @@ under the UI command that wraps it (a no-op error); the UI command now wins. | LR-BEHAV-LEARN | Tutorials | OOS | 🚫 | | | | LR-BEHAV-WHATSNEW | What's new | P2 | ✅ | `cmd:app.whatsNew`, `docs/whats-new.md` | Help ▸ What's New: release highlights | | LR-BEHAV-AI-EA | Early-access badges | P2 | ⬜ | | | -| LR-BEHAV-TITLEBAR | Title bar drag and double-click zoom (macOS) | P2 | 🟡 | `crates/ui-egui/src/titlebar.rs`, `crates/ui-egui/src/panels/topbar.rs`, `crates/ui-egui/src/tests_titlebar.rs` | the native title bar is hidden on macOS, so the top bar's empty space (including the traffic-light margin) moves the window and a double-click zooms or restores it; buttons and the search field keep their clicks; ignored in full screen. Zoom is winit's `Maximized`: it does not follow System Settings › “Double-click a window's title bar to” (Fill / Minimize). a press that starts on a button or the search field never moves the window. Known gap: egui-winit reads the zoomed state only at start-up and after our own `Maximized` command, so a window zoomed another way (green button, Window ▸ Zoom, snapping) makes the first double-click a no-op before the toggle catches up. Not checked on a real macOS window yet | +| LR-BEHAV-TITLEBAR | Title bar drag and double-click zoom (macOS) | P2 | 🟡 | `crates/ui-egui/src/titlebar.rs`, `crates/ui-egui/src/panels/topbar.rs`, `crates/ui-egui/src/tests_titlebar.rs` | the native title bar is hidden on macOS, so the top bar's empty space (including the traffic-light margin) moves the window and a double-click zooms or restores it; buttons and the search field keep their clicks; ignored in full screen. Zoom is winit's `Maximized`: it does not follow System Settings › “Double-click a window's title bar to” (Fill / Minimize). a press that starts on a button or the search field never moves the window. Known gap: egui-winit reads the zoomed state only at start-up and after our own `Maximized` command, so a window zoomed another way (green button, Window ▸ Zoom, snapping) makes the first double-click a no-op before the toggle catches up. Checked by hand on a real macOS window (drag, zoom, restore) | ## Y. Menus