diff --git a/crates/ui-egui/src/dock.rs b/crates/ui-egui/src/dock.rs index 13e12f33b..0b845cfe4 100644 --- a/crates/ui-egui/src/dock.rs +++ b/crates/ui-egui/src/dock.rs @@ -723,6 +723,49 @@ mod tests { assert!(covered.is_empty(), "under the scroll bar at {bar:?}: {covered:#?}"); } + /// #3443: the Layers list's scroll bar sits beside the rows, not over the target and selection + /// marks at their right edge. + #[test] + fn the_layers_scroll_bar_covers_no_row() { + use egui::accesskit::Role; + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.run("file.new", json!({"width": 300, "height": 300})).unwrap(); + for k in 0..40 { + app.run("shape.rectangle", json!({"x": k, "y": k, "width": 20, "height": 20})).unwrap(); + } + app.run("window.panel", json!({"panel": "layers"})).unwrap(); + let ctx = egui::Context::default(); + ctx.enable_accesskit(); + theme::install_fonts(&ctx); + let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, vec2(1440.0, 600.0)); + let mut nodes = vec![]; + for k in 0..4 { + let input = egui::RawInput { time: Some(f64::from(k) * 0.1), screen_rect: Some(screen), ..Default::default() }; + let mut out = ctx.run_ui(input, |ui| show(&mut app, ui)); + out.textures_delta.clear(); + nodes = out.platform_output.accesskit_update.unwrap().nodes; + } + let rect = |n: &egui::accesskit::Node| { + n.bounds().map(|b| egui::Rect::from_min_max(egui::pos2(b.x0 as f32, b.y0 as f32), egui::pos2(b.x1 as f32, b.y1 as f32))) + }; + let bar = nodes + .iter() + .filter(|(_, n)| n.role() == Role::ScrollBar) + .filter_map(|(_, n)| rect(n)) + .max_by(|a, b| a.left().total_cmp(&b.left())) + .filter(|bar| bar.left() > screen.right() - DOCK_WIDTH) + .expect("40 layers scroll in a 600 pt window"); + let rows: Vec = nodes + .iter() + .filter(|(_, n)| n.role() != Role::ScrollBar) + .filter_map(|(_, n)| rect(n)) + .filter(|r| r.top() >= bar.top() && r.bottom() <= bar.bottom() && r.right() > bar.left() - 40.0) + .collect(); + assert!(!rows.is_empty(), "the rows beside the bar are in the tree"); + let covered: Vec<_> = rows.iter().filter(|r| r.intersect(bar).is_positive()).collect(); + assert!(covered.is_empty(), "under the scroll bar at {bar:?}: {covered:#?}"); + } + const SCREEN: egui::Vec2 = vec2(1600.0, 1000.0); /// A frame of the dock and its flyout on a 1600 × 1000 window. diff --git a/crates/ui-egui/src/panels/layers.rs b/crates/ui-egui/src/panels/layers.rs index 85e5a1035..fc3be3721 100644 --- a/crates/ui-egui/src/panels/layers.rs +++ b/crates/ui-egui/src/panels/layers.rs @@ -613,15 +613,27 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { let revealed = reveal.and_then(|id| list.iter().position(|r| r.node.id == id)); let scrolled = ui.scope(|ui| { ui.spacing_mut().item_spacing.y = 0.0; - egui::ScrollArea::vertical().max_height(list_h).auto_shrink([false, false]).show_rows(ui, view.h, list.len(), |ui, range| { - if let Some(i) = revealed.filter(|i| !range.contains(i)) { - let top = ui.max_rect().top() + (i as f32 - range.start as f32) * view.h; - ui.scroll_to_rect(egui::Rect::from_min_size(egui::pos2(ui.max_rect().left(), top), vec2(ui.max_rect().width(), view.h)), None); - } - for r in list.get(range).unwrap_or_default() { - row(ui, &view, r, &mut out); - } - }) + // The bar takes a column of its own, so it never covers the target and selection marks at + // the rows' right edge (#3443). Whether the rows overflow is known before they are laid + // out, so the bar is decided up front and the rows never shift when it appears. + ui.spacing_mut().scroll = crate::widgets::list_scroll_style(); + let overflows = view.h * list.len() as f32 > list_h; + let bar = + if overflows { egui::scroll_area::ScrollBarVisibility::AlwaysVisible } else { egui::scroll_area::ScrollBarVisibility::AlwaysHidden }; + egui::ScrollArea::vertical().max_height(list_h).auto_shrink([false, false]).scroll_bar_visibility(bar).show_rows( + ui, + view.h, + list.len(), + |ui, range| { + if let Some(i) = revealed.filter(|i| !range.contains(i)) { + let top = ui.max_rect().top() + (i as f32 - range.start as f32) * view.h; + ui.scroll_to_rect(egui::Rect::from_min_size(egui::pos2(ui.max_rect().left(), top), vec2(ui.max_rect().width(), view.h)), None); + } + for r in list.get(range).unwrap_or_default() { + row(ui, &view, r, &mut out); + } + }, + ) }); let list_rect = scrolled.inner.inner_rect; resolve_click(ui, &view, &list, &mut out);