Fix #3093: panel scroll bars take a column of their own instead of covering controls

The panels' scroll areas used egui's default floating scroll bar, which
is drawn over the right edge of the content: in a window too short for
the Properties panel it covered the Image Trace button's dropdown arrow
and the W/H constrain link, and in a dock column too tall for the window
it covered the panels' menu buttons and right-hand controls. Menus
(#2086, list_scroll_style) and the toolbar and dock icon column (#808,
strip_scroll) had already been given bars that keep clear of their
items; the panel bodies hadn't.

- widgets: panel_scroll shows a panel's scroll area with the menus' slim
  solid bar (list_scroll_style), in a column of its own at the right
  edge; scroll areas inside the body keep their own style.
- dock.rs: the docked Properties panel and Properties in a flyout,
  floating group or shared dock group use it.
- floating.rs: a dock column too tall for the window uses it.
- panel_docking.rs: a panel's body in the shared dock uses it, laid out
  as wide as the scroll area leaves beside the bar; the shared dock's
  icon column scrolls with strip_scroll, as the dock's icon column does.
- test: the_properties_scroll_bar_covers_no_control (a 1440x600 frame of
  the dock with an image selected: no control's rect meets the
  Properties scroll bar). It fails on main with "Tracing Presets" (the
  Image Trace arrow) and "Constrain Width and Height Proportions" under
  the bar.

Verified live at 1440x600 with an image selected: before, the hovered
bar sat over the Image Trace arrow and the right ends of the Quick
Action buttons, and a dock column's bar over the Swatches panel menu;
after, both bars sit in their own 8 pt column and nothing is covered.
This commit is contained in:
Taha-Mahmoodi
2026-10-22 16:06:54 +07:00
committed by bflatastic
parent b0d5ab3f68
commit a8bdf5043f
4 changed files with 75 additions and 17 deletions
+51 -10
View File
@@ -166,10 +166,9 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) {
}
egui::Frame::NONE.inner_margin(egui::Margin { left: 12, right: 10, top: 10, bottom: 8 }).show(ui, |ui| match active {
DockTab::Properties => {
egui::ScrollArea::vertical()
.id_salt("props")
.auto_shrink([false, false])
.show(ui, |ui| panels::properties::show(app, ui));
widgets::panel_scroll(ui, egui::ScrollArea::vertical().id_salt("props").auto_shrink([false, false]), |ui| {
panels::properties::show(app, ui)
});
}
DockTab::Layers => panels::layers::show(app, ui),
DockTab::Libraries => panels::libraries(app, ui),
@@ -512,12 +511,11 @@ pub(crate) fn panel_body(app: &mut VectorcraftApp, ui: &mut Ui, id: &str, width:
// At least `tall` when the sections are taller: the flyout's area keeps the size its
// content needs, and egui sizes a new area at most 400 points high, so a scroll area
// that only took the room it was given never grew past it.
egui::ScrollArea::vertical()
.id_salt("props-flyout")
.max_height(tall)
.min_scrolled_height(tall)
.auto_shrink([false, true])
.show(ui, |ui| panels::properties::show(app, ui));
widgets::panel_scroll(
ui,
egui::ScrollArea::vertical().id_salt("props-flyout").max_height(tall).min_scrolled_height(tall).auto_shrink([false, true]),
|ui| panels::properties::show(app, ui),
);
}
Some(DockTab::Layers) => {
ui.set_max_height(tall);
@@ -682,6 +680,49 @@ mod tests {
assert!(icons.last().unwrap().bottom() <= 400.0, "scrolled into view: {:?}", icons.last());
}
/// #3093: in a window too short for the Properties panel, its scroll bar takes a column of its
/// own instead of floating over the panel's right edge, so it covers none of the controls
/// there, such as the arrow of an image's Image Trace button.
#[test]
fn the_properties_scroll_bar_covers_no_control() {
use egui::accesskit::{Action, Role};
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 300, "height": 300})).unwrap();
app.run("shape.rectangle", json!({"x": 10, "y": 10, "width": 50, "height": 40})).unwrap();
app.run("object.rasterize", json!({})).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)))
};
// The Properties panel's is the right-most (the icon column's slim strip bar is left of it).
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);
let bar = bar.expect("the Properties panel scrolls in a 600 pt window");
let mut covered = vec![];
for (_, n) in &nodes {
let Some(r) = rect(n) else { continue };
let control = n.role() != Role::ScrollBar && (n.supports_action(Action::Click) || n.supports_action(Action::Focus));
if control && r.intersect(bar).is_positive() {
covered.push(format!("{:?} {:?} at {r:?}", n.role(), n.label()));
}
}
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.
+2 -2
View File
@@ -29,7 +29,7 @@ use serde_json::{Value, json};
use crate::state::{DockTab, FloatingPanels, UiState, all_panels};
use crate::theme::{self, Tokens};
use crate::{VectorcraftApp, dock, icons, panels};
use crate::{VectorcraftApp, dock, icons, panels, widgets};
/// Height of a floating group's title bar (its grip and ×).
pub const TITLE: f32 = 14.0;
@@ -911,7 +911,7 @@ pub(crate) fn docked_columns(app: &mut VectorcraftApp, ui: &mut egui::Ui) -> Vec
// The open panels share the column's height, each at least `MIN_TALL`: more than fit
// scroll, with the column.
let tall = ((ui.available_height() - n * STRIP - (n - 1.0) * GAP - 24.0 * open) / open).max(MIN_TALL);
egui::ScrollArea::vertical().id_salt(("dock-column-scroll", first)).auto_shrink([false, false]).show(ui, |ui| {
widgets::panel_scroll(ui, egui::ScrollArea::vertical().id_salt(("dock-column-scroll", first)).auto_shrink([false, false]), |ui| {
ui.spacing_mut().item_spacing = Vec2::ZERO;
let width = ui.available_width();
let top = ui.cursor().top();
+6 -5
View File
@@ -771,10 +771,11 @@ impl craft_ui::docking::DockContent<String> for PanelContent<'_> {
if crate::state::DockTab::from_id(panel).is_some() {
crate::dock::panel_body(self.app, ui, panel, width, height);
} else {
egui::ScrollArea::vertical()
.id_salt(("shared-panel-body", panel))
.auto_shrink([false, false])
.show(ui, |ui| crate::dock::panel_body(self.app, ui, panel, width, height));
// The body is as wide as the scroll area leaves it beside its scroll bar.
crate::widgets::panel_scroll(ui, egui::ScrollArea::vertical().id_salt(("shared-panel-body", panel)).auto_shrink([false, false]), |ui| {
let width = ui.available_width();
crate::dock::panel_body(self.app, ui, panel, width, height)
});
}
}
@@ -1205,7 +1206,7 @@ fn icon_rail(app: &mut VectorcraftApp, ui: &mut egui::Ui) {
.frame(egui::Frame::NONE)
.show(ui, |ui| {
crate::theme::card(ui, &t, egui::Margin { left: 3, right: 3, top: 6, bottom: 6 }, |ui| {
egui::ScrollArea::vertical().id_salt("shared-panel-icons-scroll").show(ui, |ui| {
crate::widgets::strip_scroll(ui, "shared-panel-icons-scroll", |ui| {
ui.spacing_mut().item_spacing.y = 2.0;
// The tabbed group's panels dropped on the column head it, then the icon panels,
// then the other panels dropped on it (those the column has no place for).
+16
View File
@@ -1177,6 +1177,22 @@ pub fn strip_scroll<R>(ui: &mut Ui, id: &str, add_contents: impl FnOnce(&mut Ui)
.inner
}
/// A panel's scrolling body (Properties, a dock column, a panel in the shared dock): `area` with
/// the scroll bar of a menu's list ([`list_scroll_style`]), in a column of its own at the right
/// edge, so it never covers the controls there, such as a dropdown's arrow (#3093). The scroll
/// areas inside the body keep their own style.
pub fn panel_scroll<R>(ui: &mut Ui, area: egui::ScrollArea, add_contents: impl FnOnce(&mut Ui) -> R) -> R {
ui.scope(|ui| {
let inner = std::mem::replace(&mut ui.spacing_mut().scroll, list_scroll_style());
area.show(ui, |ui| {
ui.spacing_mut().scroll = inner;
add_contents(ui)
})
.inner
})
.inner
}
/// Whether the blend-mode list draws a separator above `BlendMode::ALL[i]`: where a
/// [`BlendMode::group`] starts.
pub fn blend_separator_before(i: usize) -> bool {