Merge PR #3332: LR-BEHAV-TITLEBAR: a dialog leaves the macOS title bar undimmed

Conflicts: docs unioned
This commit is contained in:
aiongg
2026-10-31 02:09:20 +00:00
6 changed files with 236 additions and 9 deletions
+45 -2
View File
@@ -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<egui::Rect> {
// (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>) -> 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 {
+3 -3
View File
@@ -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<egui::Rect>, 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::<bool>(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).
+11 -2
View File
@@ -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.
+166
View File
@@ -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");
}
+3 -2
View File
@@ -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 `<slider> 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 `<slider> 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 (🟡) |
+8
View File
@@ -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