diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 86397292e..9aebdb5ce 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -93,8 +93,11 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context, blocked: bool) { let t = Tokens::get(ctx); let screen = ctx.content_rect(); // The backdrop is an area below the dialog window (a bare `Middle` layer painter would be - // painted after every area — i.e. over the dialog too). + // painted after every area — i.e. over the dialog too). Like a macOS sheet, the dialog leaves + // the integrated title bar lit: the window buttons the OS draws over it do not dim. + let title_bar = title_bar_band(app); egui::Area::new(egui::Id::unique("dialog-dim")).order(egui::Order::Middle).fixed_pos(screen.min).interactable(false).show(ctx, |ui| { + let screen = below_title_bar(screen, title_bar); ui.painter().rect_filled(screen, 0.0, egui::Color32::from_black_alpha(140)); }); let mut close = false; @@ -189,7 +192,7 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context, blocked: bool) { } if let Some(w) = &shown { // (the import review keeps its hand-picked photos however the click lands) - if !raw_blocked && app.ui.settings.close_dialogs_outside && !import && clicked_backdrop(ctx, w.response.rect, popup_was_open) { + if !raw_blocked && app.ui.settings.close_dialogs_outside && !import && clicked_backdrop(ctx, w.response.rect, title_bar, popup_was_open) { close = true; } else if raw_blocked { // A press made under the prompt or library window must not be remembered as a @@ -209,6 +212,9 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context, blocked: bool) { } ctx.move_to_top(w.response.layer_id); crate::widgets::register_response("dialog:window", w.response.rect, &w.response); + if let Some(band) = title_bar { + title_bar_handle(ctx, band, w.response.rect); + } } // (while the shortcuts editor records a key, it takes Esc itself to cancel; an open popup in // the dialog, such as a date picker's calendar, takes it to close itself) @@ -229,6 +235,43 @@ pub fn show(app: &mut LightcraftApp, ctx: &egui::Context, blocked: bool) { app.ui.dialog = if close { None } else { Some(dlg) }; } +/// The band of the top bar that doubles as the window's title bar, as drawn last frame: `None` +/// without the integrated title bar (Windows, Linux, the web), in macOS full screen, and while the +/// bar isn't on screen (the full-screen photo view). +fn title_bar_band(app: &LightcraftApp) -> Option { + // (the full-screen photo view draws no top bar, so the frame it is entered on still holds last frame's region) + if !app.integrated_titlebar || app.window_is_fullscreen || app.ui.fullscreen { + return None; + } + app.widgets.iter().find(|(id, _)| id == "region:titlebar").map(|(_, r)| *r) +} + +/// `screen` less the title bar band across its top. +fn below_title_bar(screen: egui::Rect, band: Option) -> egui::Rect { + band.map_or(screen, |b| egui::Rect::from_min_max(egui::pos2(screen.min.x, b.max.y.clamp(screen.min.y, screen.max.y)), screen.max)) +} + +/// The bar's controls are disabled under a dialog, and the modal layer takes the hits of the layers +/// below it: this keeps the bar a window handle (drag to move, double-click to zoom), above the +/// modal like a sheet's title bar. A tall dialog reaching into the band keeps its own clicks: the +/// handle is the band on either side of it, and isn't run while the pointer is on the dialog (each +/// handle records whether the window may move, and one the pointer isn't in would say yes). +fn title_bar_handle(ctx: &egui::Context, band: egui::Rect, dialog: egui::Rect) { + // (egui lifts an area that wasn't there the frame before above the rest: a handle drawn again as the pointer leaves + // the dialog would sit over an open dropdown and take the press meant for its item) + if egui::Popup::is_any_open(ctx) || ctx.pointer_hover_pos().is_some_and(|p| dialog.contains(p)) { + return; + } + let sides = + if band.intersects(dialog) { [(band.min.x, dialog.min.x), (dialog.max.x, band.max.x)] } else { [(band.min.x, band.max.x), (0.0, 0.0)] }; + for (i, (from, to)) in sides.into_iter().enumerate().filter(|(_, (from, to))| to - from > 1.0) { + let part = egui::Rect::from_x_y_ranges(from..=to, band.y_range()); + egui::Area::new(egui::Id::unique("dialog-title-bar").with(i)).order(egui::Order::Foreground).fixed_pos(part.min).show(ctx, |ui| { + craft_window::title_bar::empty_area(ui, part); + }); + } +} + /// The title of a dialog. fn dialog_title(app: &LightcraftApp, dlg: &Dialog) -> String { match dlg { diff --git a/crates/ui-egui/src/dialogs/frame.rs b/crates/ui-egui/src/dialogs/frame.rs index 007c733cb..ea385f433 100644 --- a/crates/ui-egui/src/dialogs/frame.rs +++ b/crates/ui-egui/src/dialogs/frame.rs @@ -8,7 +8,7 @@ use super::{BACKDROP_SHOWN, ControlSpec, Dialog, LightcraftApp, Tokens}; /// while the dialog is shown counts: in the frame a dialog appears, its input is the click that /// opened it (a quick click on a slow frame presses and releases in that one frame), so it never /// closes it. (Read from the raw events: egui forgets the press origin on release.) -pub(super) fn clicked_backdrop(ctx: &egui::Context, window: egui::Rect, popup_was_open: bool) -> bool { +pub(super) fn clicked_backdrop(ctx: &egui::Context, window: egui::Rect, title_bar: Option, popup_was_open: bool) -> bool { let press_id = egui::Id::unique("dialog-backdrop-press"); let frame = ctx.cumulative_frame_nr(); // the same frame too: a second pass of it gets no input @@ -27,12 +27,12 @@ pub(super) fn clicked_backdrop(ctx: &egui::Context, window: egui::Rect, popup_wa (at(true), at(false), i.pointer.primary_clicked()) }); if let Some(p) = press { - let outside = !window.contains(p) && !popup_was_open; + let outside = !window.contains(p) && !title_bar.is_some_and(|b| b.contains(p)) && !popup_was_open; ctx.data_mut(|d| d.insert_temp(press_id, outside)); } let Some(release) = release else { return false }; let pressed_outside = ctx.data_mut(|d| d.remove_temp::(press_id)).unwrap_or(false); - clicked && pressed_outside && !window.contains(release) + clicked && pressed_outside && !window.contains(release) && !title_bar.is_some_and(|b| b.contains(release)) } /// Room kept free above and below a dialog (#781). diff --git a/crates/ui-egui/src/panels/topbar.rs b/crates/ui-egui/src/panels/topbar.rs index 4511b4b7c..35e24a781 100644 --- a/crates/ui-egui/src/panels/topbar.rs +++ b/crates/ui-egui/src/panels/topbar.rs @@ -28,9 +28,15 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { (true, true) => 16, (false, _) => 10, }; - egui::Panel::top("top_bar") + // A dialog fades the disabled bar's controls, and its fill with them. A sheet leaves the title bar's + // color alone (the window buttons the OS draws over it stay lit), so the fill is painted unfaded + // beneath, in a slot reserved before the bar's own shapes. + let sheet = app.integrated_titlebar && !app.window_is_fullscreen && app.ui.dialog.is_some(); + let slot = sheet.then(|| ui.ctx().layer_painter(ui.layer_id()).add(egui::Shape::Noop)); + let fill = if sheet { egui::Color32::TRANSPARENT } else { t.chrome }; + let panel = egui::Panel::top("top_bar") .exact_size(t.top_bar_h) - .frame(egui::Frame::NONE.fill(t.chrome).inner_margin(egui::Margin { left, right: 12, top: 0, bottom: 0 })) + .frame(egui::Frame::NONE.fill(fill).inner_margin(egui::Margin { left, right: 12, top: 0, bottom: 0 })) .show(ui, |ui| { crate::widgets::announce_region(ui, egui::accesskit::Role::Toolbar, crate::i18n::tr("Top bar")); let full = ui.max_rect(); @@ -58,6 +64,9 @@ pub fn show(app: &mut LightcraftApp, ui: &mut egui::Ui) { let x = right_icons(app, ui, &t, full); appearance_button(app, ui, &t, full, x); }); + if let Some(slot) = slot { + ui.ctx().layer_painter(ui.layer_id()).set(slot, egui::Shape::rect_filled(panel.response.rect, 0.0, t.chrome)); + } } /// Sidebar toggle, back/forward and, without a native menu bar, the in-window menus. diff --git a/crates/ui-egui/src/tests/titlebar.rs b/crates/ui-egui/src/tests/titlebar.rs index 8d40f212e..d207fa81d 100644 --- a/crates/ui-egui/src/tests/titlebar.rs +++ b/crates/ui-egui/src/tests/titlebar.rs @@ -191,3 +191,169 @@ fn a_panel_floating_over_the_bar_keeps_the_window_from_moving() { play(&mut h); assert!(!hover(&mut h, empty), "the floating panel covers the empty bar there"); } + +// A dialog on the integrated title bar behaves like a macOS sheet: the backdrop leaves the bar +// undimmed (the window buttons AppKit draws over it stay lit beside a bar that looks live), while +// the bar's own controls stay inactive. + +fn pixel(img: &egui::ColorImage, p: egui::Pos2) -> egui::Color32 { + img.pixels[p.y as usize * img.size[0] + p.x as usize] +} + +fn with_export_dialog(mut h: Headless) -> Headless { + h.app.run("dialog.export", json!({})).unwrap(); + h.step(); + h.step(); + h.settle(T); + h +} + +// Scenario: the dialog's backdrop does not dim the integrated title bar +#[test] +fn a_dialog_leaves_the_integrated_title_bar_undimmed() { + let mut h = integrated(); + let spot = empty_spot(&h); + let below = egui::pos2(spot.x, widget(&h, "region:titlebar").bottom() + 60.0); + let closed = h.snapshot(T); + let mut h = with_export_dialog(h); + let open = h.snapshot(T); + assert_eq!(pixel(&open, spot), pixel(&closed, spot), "the bar keeps its color under a dialog"); + assert_ne!(pixel(&open, below), pixel(&closed, below), "the window below the bar is dimmed"); +} + +// Scenario: a window with its native title bar dims everything, the top bar included +#[test] +fn a_dialog_dims_the_top_bar_without_the_integrated_title_bar() { + 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)); + let spot = egui::pos2(20.0, 20.0); + let closed = h.snapshot(T); + let mut h = with_export_dialog(h); + let open = h.snapshot(T); + assert_ne!(pixel(&open, spot), pixel(&closed, spot), "the top bar is dimmed"); +} + +// Scenario: the full-screen photo view draws no top bar, so a dialog opened there has no title bar band to spare: the +// frame the view is entered on still holds the bar's region from the frame before, and must not lay a window handle +// over the backdrop (the band that handle covers is the strip the backdrop would leave undimmed) +#[test] +fn a_dialog_in_the_full_screen_photo_view_has_no_title_bar_handle() { + let mut h = integrated(); + assert!(h.app.widgets.iter().any(|(w, _)| w == "region:titlebar"), "the bar was drawn the frame before"); + h.app.run("view.fullScreenPreview", json!({})).unwrap(); + h.app.run("dialog.export", json!({})).unwrap(); + h.step(); + assert!(h.app.ui.fullscreen && h.app.ui.dialog.is_some()); + let handles = [0usize, 1].map(|i| egui::LayerId::new(egui::Order::Foreground, egui::Id::unique("dialog-title-bar").with(i))); + let handle = h.view.ctx.memory(|m| handles.iter().any(|layer| m.areas().visible_last_frame(layer))); + assert!(!handle, "no title bar handle over the photo"); +} + +// Scenario: a press on the backdrop just below the bar, released on the bar, is not a click outside the dialog +#[test] +fn a_press_below_the_title_bar_released_on_it_keeps_the_dialog_open() { + let mut h = integrated(); + h.app.ui.settings.close_dialogs_outside = true; + let mut h = with_export_dialog(h); + let spot = empty_spot(&h); + let bar = widget(&h, "region:titlebar"); + let below = egui::pos2(spot.x, bar.bottom() + 1.0); + let on_bar = egui::pos2(spot.x, bar.bottom() - 1.0); + h.request("ui.drag", json!({"x": below.x, "y": below.y, "toX": on_bar.x, "toY": on_bar.y}), T); + play(&mut h); + assert!(h.app.ui.dialog.is_some(), "the release on the bar is not a click on the backdrop"); +} + +// Scenario: a click on the undimmed bar is not a click outside the dialog +#[test] +fn a_click_on_the_title_bar_keeps_the_dialog_open() { + let mut h = integrated(); + h.app.ui.settings.close_dialogs_outside = true; + let mut h = with_export_dialog(h); + let at = empty_spot(&h); + h.request("ui.click", json!({"x": at.x, "y": at.y}), T); + play(&mut h); + assert!(h.app.ui.dialog.is_some(), "the bar is not the backdrop"); + let below = egui::pos2(at.x, widget(&h, "region:titlebar").bottom() + 60.0); + h.request("ui.click", json!({"x": below.x, "y": below.y}), T); + play(&mut h); + assert!(h.app.ui.dialog.is_none(), "the backdrop below it still closes the dialog"); +} + +// Scenario: the bar's controls take no clicks under a dialog +#[test] +fn the_title_bar_buttons_are_inactive_under_a_dialog() { + let mut h = with_export_dialog(integrated()); + let left = h.app.ui.left_panel; + let at = widget(&h, "icon:sidebar").center(); + h.request("ui.click", json!({"x": at.x, "y": at.y}), T); + play(&mut h); + assert_eq!(h.app.ui.left_panel, left, "the sidebar button does nothing"); + assert!(h.app.ui.dialog.is_some()); +} + +// Scenario: dragging the empty bar still moves the window under a dialog, whether the dialog is +// tall enough to reach into the bar (Export) or not (Copy Settings) +#[test] +fn dragging_the_empty_bar_moves_the_window_under_a_dialog() { + for command in ["dialog.export", "dialog.copySettings"] { + let mut h = integrated(); + h.app.run(command, json!({})).unwrap(); + h.step(); + h.step(); + h.settle(T); + let p = empty_spot(&h); + h.window_commands.clear(); + 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], "{command}"); + } +} + +// Scenario: a tall dialog reaching into the bar keeps a press on itself from moving the window +// (AppKit's `movable` is off while the pointer is over it), the bar beside it still moves it +#[test] +fn a_dialog_inside_the_bar_keeps_the_window_from_moving() { + let mut h = with_export_dialog(integrated()); + let bar = widget(&h, "region:titlebar"); + let window = widget(&h, "dialog:window"); + assert!(window.top() < bar.bottom(), "the dialog reaches into the bar"); + let hover = |h: &mut Headless, p: egui::Pos2| { + h.events.push(egui::Event::PointerMoved(p)); + play(h); + craft_window::title_bar::window_movable(&h.view.ctx) + }; + let spot = empty_spot(&h); + assert!(hover(&mut h, spot), "the bar beside the dialog moves the window"); + assert!(!hover(&mut h, egui::pos2(window.center().x, bar.center().y)), "the dialog over the bar does not"); +} + +// Scenario: with a dropdown open in the dialog, the bar's handle stays out of the way: egui lifts an area that was not +// there the frame before above the others, so a handle drawn again as the pointer leaves the dialog would take the +// press meant for the dropdown's item +#[test] +fn the_title_bar_handle_stays_below_a_dropdown_open_in_the_dialog() { + let mut h = with_export_dialog(integrated()); + let spot = empty_spot(&h); + let handle_at = |h: &mut Headless| { + h.events.push(egui::Event::PointerMoved(spot)); + play(h); + let handles = [0usize, 1].map(|i| egui::LayerId::new(egui::Order::Foreground, egui::Id::unique("dialog-title-bar").with(i))); + h.view.ctx.memory(|m| handles.iter().any(|layer| m.areas().visible_last_frame(layer))) + }; + assert!(handle_at(&mut h), "without a dropdown the bar beside the dialog is the handle's"); + let r = h.request("ui.clickWidget", json!({"id": "combo:exportPreset"}), T); + assert_eq!(r["ok"], true, "{r}"); + play(&mut h); + assert!(egui::Popup::is_any_open(&h.view.ctx), "the preset dropdown is open"); + assert!(!handle_at(&mut h), "with one open, no handle sits above it"); + assert!(egui::Popup::is_any_open(&h.view.ctx), "(and the pointer leaving the dialog did not close it)"); + // once the dropdown is closed the handle is back, so the bar beside the dialog moves the window again + h.request("ui.key", json!({"key": "Escape"}), T); + play(&mut h); + assert!(!egui::Popup::is_any_open(&h.view.ctx), "Escape closes the dropdown"); + assert!(h.app.ui.dialog.is_some(), "and leaves the dialog"); + assert!(handle_at(&mut h), "the handle is back"); +} diff --git a/docs/parity-checklist.md b/docs/parity-checklist.md index fdabcae2c..7886b97ae 100644 --- a/docs/parity-checklist.md +++ b/docs/parity-checklist.md @@ -1,6 +1,6 @@ # Lightroom parity checklist -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-27 · **Change:** minor (LR-BEHAV-ACCESS: the bars are toolbars and the panels landmarks for screen readers; LR-BEHAV-ACCESS: every dialog and prompt is exposed as a dialog (a modal one, an alert dialog for the prompts); LR-BEHAV-ACCESS: icon-only buttons are named by their label, with the shortcut as the description; MENU-HELP-HELP: on macOS the Help menu is registered with the application only once the menu bar is installed (muda ignores the call before), so AppKit knows which menu is Help; LR-BEHAV-ACCESS: a slider's value field is named ` value`, with how to use it as its description; MENU-BAR: every shortcut the app shows on macOS lists its modifiers ⌃⌥⇧⌘, as the menu bar does; MENU-BAR: on macOS two menu rows that run the same command each get their own native row and label (File ▸ Export… and File ▸ Export with Preset ▸ Custom…; File ▸ New Album… and Photo ▸ Add to Album ▸ New Album…); LR-BEHAV-DIALOGS: a one-line prompt that is refused stays open and says why, and a New Folder on Disk says it was made; LR-FILT-SEARCH-META: `>=` / `<=` in fielded search tokens; MENU-FILE-CLOSE, KEY-CLOSEWINDOW: the notes say Close Window quits the app, after the same check as Quit; MENU-EDIT-SELECTNONE, MENU-EDIT-PASTESELECTED: Edit lists Select Only Active Photo after Deselect All and Paste Settings from Previous after Paste Selected Settings…; on macOS no doubled separators are left where Settings… moved to the app menu; LR-BEHAV-TITLEBAR: a double-click follows the system setting, and a panel over the bar keeps the window still; KEYC-METADATA, LRC-LIB-KEYWORDS: ⌥0 / ⌥⇧0 go to the next / previous keyword set; LRC-DEV-HISTOGRAM-DRAG: the histogram's five tonal zones — hover for the value, drag to adjust the Basic control the press locked, double-click to reset; LR-VIEW-HISTOGRAM note updated; MENU-BAR: on macOS a menu item that yields its key to a focused text field or to shortcut recording really lets it go; LR-MERGE-HDR / LR-MERGE-PANO: Lanczos-3 warps; LR-IMP-LOCAL: browsing from the sidebar records the folder; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LR-IMP-ADD-DIALOG: hierarchical import album destination reconciled with main, #2362; KEY-SLIDER-NUDGE: vertical wheel nudges need a click or drag in the row; leaving it disarms them; LR-EDIT-OPTICS-PROFILE: Fujifilm nine-knot lens tables read at the plain radius; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LRC-LIB-LIBFOLDERS, LR-BEHAV-BG-TASKS: Synchronize Folder's scan shows its folder listing and stops it on ✕; LRC-LIB-LIBFOLDERS: Synchronize Folder says a folder is up to date; LR-LIB-STACK: filmstrip stack badge click; LRC-LIB-REFVIEW / CATALOG-SWITCH: Escape preserves edits; catalog-local pins; deleted and malformed references refused; LRC-LIB-COMPARE: the Info panel labels the active photo while culling, #2398; KEYC-RATING: `\` shows / hides the Filter Bar in the grids; LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched, not the whole list; earlier: LRC-PRINT-PAGE: Print Page Options; LR-META-XMP: fewer false unimported-XMP warnings; KEYC-METADATA: ⌘K / Ctrl+K Add Keywords; LRC-DEV-HISTOGRAM-DRAG: the histogram's five tonal zones — hover for the value, drag to adjust the Basic control the press locked, double-click to reset; LR-VIEW-HISTOGRAM note updated; MENU-BAR: on macOS a menu item that yields its key to a focused text field or to shortcut recording really lets it go; LR-MERGE-HDR / LR-MERGE-PANO: Lanczos-3 warps; LR-IMP-LOCAL: browsing from the sidebar records the folder; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LR-IMP-ADD-DIALOG: hierarchical import album destination reconciled with main, #2362; KEY-SLIDER-NUDGE: vertical wheel nudges need a click or drag in the row; leaving it disarms them; LR-EDIT-OPTICS-PROFILE: Fujifilm nine-knot lens tables read at the plain radius; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LRC-LIB-LIBFOLDERS, LR-BEHAV-BG-TASKS: Synchronize Folder's scan shows its folder listing and stops it on ✕; LRC-LIB-LIBFOLDERS: Synchronize Folder says a folder is up to date; LR-LIB-STACK: filmstrip stack badge click; LRC-LIB-REFVIEW / CATALOG-SWITCH: Escape preserves edits; catalog-local pins; deleted and malformed references refused; LRC-LIB-COMPARE: the Info panel labels the active photo while culling, #2398; KEYC-RATING: `\` shows / hides the Filter Bar in the grids; LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched, not the whole list; earlier: LRC-PRINT-PAGE: Print Page Options; LR-META-XMP: fewer false unimported-XMP warnings) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6 +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-10-27 · **Change:** minor (LR-BEHAV-TITLEBAR: a dialog leaves the title bar undimmed, as a macOS sheet does; LR-BEHAV-ACCESS: the bars are toolbars and the panels landmarks for screen readers; LR-BEHAV-ACCESS: every dialog and prompt is exposed as a dialog (a modal one, an alert dialog for the prompts); LR-BEHAV-ACCESS: icon-only buttons are named by their label, with the shortcut as the description; MENU-HELP-HELP: on macOS the Help menu is registered with the application only once the menu bar is installed (muda ignores the call before), so AppKit knows which menu is Help; LR-BEHAV-ACCESS: a slider's value field is named ` value`, with how to use it as its description; MENU-BAR: every shortcut the app shows on macOS lists its modifiers ⌃⌥⇧⌘, as the menu bar does; MENU-BAR: on macOS two menu rows that run the same command each get their own native row and label (File ▸ Export… and File ▸ Export with Preset ▸ Custom…; File ▸ New Album… and Photo ▸ Add to Album ▸ New Album…); LR-BEHAV-DIALOGS: a one-line prompt that is refused stays open and says why, and a New Folder on Disk says it was made; LR-FILT-SEARCH-META: `>=` / `<=` in fielded search tokens; MENU-FILE-CLOSE, KEY-CLOSEWINDOW: the notes say Close Window quits the app, after the same check as Quit; MENU-EDIT-SELECTNONE, MENU-EDIT-PASTESELECTED: Edit lists Select Only Active Photo after Deselect All and Paste Settings from Previous after Paste Selected Settings…; on macOS no doubled separators are left where Settings… moved to the app menu; LR-BEHAV-TITLEBAR: a double-click follows the system setting, and a panel over the bar keeps the window still; KEYC-METADATA, LRC-LIB-KEYWORDS: ⌥0 / ⌥⇧0 go to the next / previous keyword set; LRC-DEV-HISTOGRAM-DRAG: the histogram's five tonal zones — hover for the value, drag to adjust the Basic control the press locked, double-click to reset; LR-VIEW-HISTOGRAM note updated; MENU-BAR: on macOS a menu item that yields its key to a focused text field or to shortcut recording really lets it go; LR-MERGE-HDR / LR-MERGE-PANO: Lanczos-3 warps; LR-IMP-LOCAL: browsing from the sidebar records the folder; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LR-IMP-ADD-DIALOG: hierarchical import album destination reconciled with main, #2362; KEY-SLIDER-NUDGE: vertical wheel nudges need a click or drag in the row; leaving it disarms them; LR-EDIT-OPTICS-PROFILE: Fujifilm nine-knot lens tables read at the plain radius; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LRC-LIB-LIBFOLDERS, LR-BEHAV-BG-TASKS: Synchronize Folder's scan shows its folder listing and stops it on ✕; LRC-LIB-LIBFOLDERS: Synchronize Folder says a folder is up to date; LR-LIB-STACK: filmstrip stack badge click; LRC-LIB-REFVIEW / CATALOG-SWITCH: Escape preserves edits; catalog-local pins; deleted and malformed references refused; LRC-LIB-COMPARE: the Info panel labels the active photo while culling, #2398; KEYC-RATING: `\` shows / hides the Filter Bar in the grids; LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched, not the whole list; earlier: LRC-PRINT-PAGE: Print Page Options; LR-META-XMP: fewer false unimported-XMP warnings; KEYC-METADATA: ⌘K / Ctrl+K Add Keywords; LRC-DEV-HISTOGRAM-DRAG: the histogram's five tonal zones — hover for the value, drag to adjust the Basic control the press locked, double-click to reset; LR-VIEW-HISTOGRAM note updated; MENU-BAR: on macOS a menu item that yields its key to a focused text field or to shortcut recording really lets it go; LR-MERGE-HDR / LR-MERGE-PANO: Lanczos-3 warps; LR-IMP-LOCAL: browsing from the sidebar records the folder; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LR-IMP-ADD-DIALOG: hierarchical import album destination reconciled with main, #2362; KEY-SLIDER-NUDGE: vertical wheel nudges need a click or drag in the row; leaving it disarms them; LR-EDIT-OPTICS-PROFILE: Fujifilm nine-knot lens tables read at the plain radius; LR-VIEW-ZOOM: only a full-size drag within 20 ms clears a slow-drag verdict; LRC-LIB-LIBFOLDERS, LR-BEHAV-BG-TASKS: Synchronize Folder's scan shows its folder listing and stops it on ✕; LRC-LIB-LIBFOLDERS: Synchronize Folder says a folder is up to date; LR-LIB-STACK: filmstrip stack badge click; LRC-LIB-REFVIEW / CATALOG-SWITCH: Escape preserves edits; catalog-local pins; deleted and malformed references refused; LRC-LIB-COMPARE: the Info panel labels the active photo while culling, #2398; KEYC-RATING: `\` shows / hides the Filter Bar in the grids; LR-VIEW-PHOTOGRID: the grid reads only the photos an import batch or an edit touched, not the whole list; earlier: LRC-PRINT-PAGE: Print Page Options; LR-META-XMP: fewer false unimported-XMP warnings) · **Target:** Adobe Lightroom Classic 15.5 / Lightroom 9.6 The measured, row-by-row checklist: one row per Lightroom feature, menu item and shortcut, what we have and what is missing. It answers *does the feature exist?* (feature breadth). Whether it is good @@ -487,7 +487,7 @@ differences and missing bindings are in [ui-parity.md](ui-parity.md#shortcuts). | 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/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 does what System Settings › “Double-click a window's title bar to” says (zoom or restore, minimize, nothing; craft-window's `title_bar`); buttons and the search field keep their clicks; ignored in full screen, where the traffic lights are gone and the bar's left buttons move into their space (#1763). A press that starts on a button, the search field or a panel floating over the bar never moves the window (AppKit's `movable` is off while the pointer is there). 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) | +| LR-BEHAV-TITLEBAR | Title bar drag and double-click zoom (macOS) | P2 | 🟡 | `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 does what System Settings › “Double-click a window's title bar to” says (zoom or restore, minimize, nothing; craft-window's `title_bar`); buttons and the search field keep their clicks; ignored in full screen, where the traffic lights are gone and the bar's left buttons move into their space (#1763). A press that starts on a button, the search field or a panel floating over the bar never moves the window (AppKit's `movable` is off while the pointer is there). While a dialog is open the bar is not dimmed, so it stays level with the window buttons AppKit draws over it (as under a macOS sheet): its controls are inactive, a click on it does not close the dialog, and its empty space still moves the window; not on a window with its native title bar, nor in full screen. 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 @@ -972,3 +972,4 @@ Latin stays upright. Advanced Japanese composition remains open. Regression cove | 2026-10-28 | patch | LR-BEHAV-ACCESS: an icon-only button's accessible name is its label without the key hint, and the hint ("E", "Shift+P", the platform's own glyphs for Import) is its accessible description; the tooltip is unchanged; status unchanged (🟡) | | 2026-10-28 | patch | LR-BEHAV-ACCESS: every dialog, the library chooser and the quit, library-problem and settings-file prompts are exposed as a modal dialog (an alert dialog for the prompts), and the update check's answer as a non-modal one, instead of a group with the title, and an alert dialog carries what it says (the library problem's path, error and explanation; a prompt's text) as its description (`tests/it/dialog_role.rs`); not tried with a real screen reader; status stays 🟡 | | 2026-10-28 | patch | LR-BEHAV-ACCESS: the top bar, tool strip and bottom bar are announced as toolbars and the sidebar, photos, Tools / Presets panel and filmstrip as landmarks, each with a name; status stays 🟡 | +| 2026-10-29 | patch | LR-BEHAV-TITLEBAR: a dialog's backdrop leaves the integrated title bar undimmed, as a macOS sheet does (the bar's controls stay inactive, a click on it is not a click outside the dialog, dragging its empty space still moves the window); status unchanged (🟡) | diff --git a/docs/whats-new.md b/docs/whats-new.md index 57aa625f5..73bd30214 100644 --- a/docs/whats-new.md +++ b/docs/whats-new.md @@ -26,6 +26,14 @@ drag to change it" is its description. It was named "Exposure: Click to type a value, drag to change it", so VoiceOver read the whole sentence each time it reached the field, once for every slider in the Edit panel. +## 2026-10-29 + +### A dialog no longer dims the macOS title bar +- On macOS the top bar is the window's title bar, and the window buttons drawn over it stayed bright while a dialog dimmed + the bar under them. As under a macOS sheet, the dialog's backdrop now leaves the bar alone: its buttons are inactive, a + click on it does not close the dialog, and dragging its empty space still moves the window. Windows, Linux and full + screen are unchanged. + ## 2026-10-28 ### The window's parts are named for screen readers