mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-10-28 03:37:00 -06:00
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:
committed by
bflatastic
parent
b0d5ab3f68
commit
a8bdf5043f
+51
-10
@@ -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.
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in New Issue
Block a user