Merge pull request #3550 from Taha-Mahmoodi/fix/3093-panel-scrollbars

Fix #3093: panel scroll bars take a column of their own instead of covering controls
This commit is contained in:
BFlat
2026-10-22 16:17:03 +07:00
committed by GitHub
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 {