Fix #3443: the Layers list's scroll bar no longer covers the rows' selection marks

The Layers list scrolls in its own scroll area, which kept egui's floating
scroll bar when #3093 gave the panel bodies a bar in a column of their own.
Hovering the list showed the bar over the right edge of the rows, over the
target circles and the selection squares the user clicks to select a layer.

The list now uses the menus' slim solid bar (list_scroll_style) in its own
column, and the rows end beside it. The list knows its total height before
it lays the rows out, so the bar is shown exactly when the rows overflow
(decided up front): the rows never shift sideways a frame after the bar
appears.

Test: dock::tests::the_layers_scroll_bar_covers_no_row (40 layers in a
1440x600 window: no row's accessibility rect intersects the bar). It fails
without the change and passes with it. The Layers tests that pin row
positions across frames still pass. Checked live: the bar sits beside the
rows.
This commit is contained in:
Taha-Mahmoodi
2026-10-22 16:43:39 +07:00
committed by bflatastic
parent bbc9bd3de2
commit 7141932dfb
2 changed files with 64 additions and 9 deletions
+43
View File
@@ -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<egui::Rect> = 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.
+21 -9
View File
@@ -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);