diff --git a/crates/ui-egui/src/panes/side.rs b/crates/ui-egui/src/panes/side.rs index 7c4d3a26dd..fd2984f9d3 100644 --- a/crates/ui-egui/src/panes/side.rs +++ b/crates/ui-egui/src/panes/side.rs @@ -72,6 +72,9 @@ pub(super) fn styles(app: &mut WordApp, ui: &mut Ui) { if ui.selectable_label(app.session.view.style_inspector, tl!("Style Inspector")).clicked() { let _ = app.run("styles.inspector", json!({})); } + // A solid scroll bar takes its own room beside the list instead of floating over the a / ¶ markers. + // Its handle keeps the text colour: the solid default fills it like a button, white on white here. + ui.spacing_mut().scroll = egui::style::ScrollStyle { foreground_color: true, ..egui::style::ScrollStyle::solid() }; ui.separator(); let mut list: Vec<(String, String, bool, bool)> = app .session diff --git a/crates/ui-egui/src/panes/tests.rs b/crates/ui-egui/src/panes/tests.rs index 31662bb0d5..d6597c3729 100644 --- a/crates/ui-egui/src/panes/tests.rs +++ b/crates/ui-egui/src/panes/tests.rs @@ -468,3 +468,59 @@ fn a_long_comment_card_keeps_its_buttons_and_the_next_card_in_view() { assert_eq!(go_to.len(), 2, "one Go to button per card"); assert!(go_to.iter().all(|r| r.max.y < 900.0), "both cards' buttons are in view: {go_to:?}"); } + +/// #4753: the Styles pane's scroll bar has a column of its own, so with the pointer over it the +/// "a" and "¶" markers at the right of each row stay clear of it; its handle still shows, light +/// or dark. +#[test] +#[cfg(not(target_arch = "wasm32"))] +fn the_styles_pane_scroll_bar_leaves_the_style_markers_clear() { + use egui_kittest::kittest::Queryable; + for appearance in [crate::theme::Appearance::Light, crate::theme::Appearance::Dark] { + let app = crate::WordApp::new(wordcraft_engine::Session::new(wordcraft_doc::Document::from_text("Hello")), crate::Services::default()); + let size = egui::vec2(240.0, 400.0); + let right = std::rc::Rc::new(std::cell::Cell::new(0.0)); + let edge = right.clone(); + let mut fonts = false; + let mut h = egui_kittest::Harness::builder().with_size(size).build_ui_state( + move |ui, app: &mut crate::WordApp| { + if !fonts { + crate::theme::install_fonts(ui.ctx()); + crate::theme::apply(ui.ctx(), appearance); + fonts = true; + return; + } + edge.set(ui.max_rect().right()); + super::styles(app, ui); + }, + app, + ); + h.run_steps(4); + // On the bar, along the pane's right edge, where it is at its widest. + h.hover_at(egui::pos2(right.get() - 3.0, size.y - 60.0)); + h.run_steps(20); + let markers: Vec = h.get_all_by_label("¶").chain(h.get_all_by_label("a")).map(|n| n.rect()).collect(); + assert!(markers.len() > 5, "{appearance:?}: the list shows its markers: {markers:?}"); + // Only a scroll bar is a narrow, tall filled rectangle along the list's right edge. + let bars: Vec<(egui::Rect, egui::Color32)> = h + .output() + .shapes + .iter() + .filter_map(|c| match &c.shape { + egui::Shape::Rect(r) if r.fill.a() > 0 && r.rect.width() < 20.0 && r.rect.height() > 3.0 * r.rect.width() => { + Some((r.rect.intersect(c.clip_rect), r.fill)) + } + _ => None, + }) + .collect(); + assert!(bars.iter().any(|(b, _)| b.x_range().contains(right.get() - 3.0)), "{appearance:?}: the pointer is on the bar: {bars:?}"); + let covered: Vec<_> = markers.iter().filter(|m| bars.iter().any(|(b, _)| b.intersects(**m))).collect(); + assert!(covered.is_empty(), "{appearance:?}: the scroll bar {bars:?} covers markers {covered:?}"); + // The track is the pane's field colour; the handle stands out from it. + let track = h.ctx.global_style().visuals.extreme_bg_color; + assert!( + bars.iter().any(|(_, fill)| fill.r().abs_diff(track.r()) > 60), + "{appearance:?}: the handle shows against the track {track:?}: {bars:?}" + ); + } +}