mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-10 18:31:10 -06:00
fix(ui): keep pane resize edges right of the border, off the page scrollbar (#5730)
The centred 4 pt strip still covered the right 3 pt of the page's scrollbar beside a right-docked pane. Register the host's resize edge again from the border rightwards, on the host's own layer, and leave an inert edge inert. The test now hovers and drags both scrollbars beside a right-docked Tools pane (CodeRabbit) and checks the width is unchanged.
This commit is contained in:
@@ -390,6 +390,31 @@ fn open_panes(app: &PdfCraftApp) -> Vec<Pane> {
|
||||
open
|
||||
}
|
||||
|
||||
/// Width of a pane's resize edge that takes the pointer, right of its border.
|
||||
const RESIZE_GRIP: f32 = 4.0;
|
||||
|
||||
/// Moves each open pane's resize edge to the right of its border (#5730).
|
||||
///
|
||||
/// The host centres the edge on the border and widens it, so it reaches over whatever ends left
|
||||
/// of the border. egui puts a vertical scrollbar at the right of its area, so that is the
|
||||
/// scrollbar of the page beside a right-docked pane, or of the Inspector beside a right-docked
|
||||
/// Tools pane, and its right half became the edge. Right of a border starts the next area, never
|
||||
/// with a scrollbar: a right-docked pane's own scrollbar is at its far side, and a left-docked
|
||||
/// pane keeps a gutter left of its border.
|
||||
///
|
||||
/// The edge is the host's widget registered again with a smaller rect; egui keeps the last rect
|
||||
/// for the next frame's hit test, and the layer is the one the host used.
|
||||
fn keep_resize_edges_off_scrollbars(ui: &mut egui::Ui, host: egui::Id, open: &[Pane]) {
|
||||
for pane in open {
|
||||
let id = host.with(("pane", *pane)).with("resize");
|
||||
// An inert edge (bad geometry) senses no drag: leave it so.
|
||||
let Some(edge) = ui.ctx().read_response(id).filter(|r| r.sense.senses_drag() && r.rect.is_positive()) else { continue };
|
||||
let border = edge.rect.center().x;
|
||||
let grip = egui::Rect::from_x_y_ranges(border..=border + RESIZE_GRIP, edge.rect.y_range());
|
||||
ui.new_child(egui::UiBuilder::new().layer_id(edge.layer_id)).interact(grip, id, egui::Sense::drag());
|
||||
}
|
||||
}
|
||||
|
||||
/// Draws the rail and the open panes. Call after the top bars and before the central panel.
|
||||
pub fn show(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
|
||||
if let Some(panel) = app.right {
|
||||
@@ -411,7 +436,8 @@ pub fn show(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
|
||||
let labels = PaneLabels { resize: crate::i18n::t("Resize panels").to_owned(), ..PaneLabels::default() };
|
||||
let panels = app.side_panels;
|
||||
// Columns: both panes may share the right edge, Tools outermost, as `open` lists them.
|
||||
let output = PaneHost::new(egui::Id::unique("pdfcraft-side-panels"))
|
||||
let host = egui::Id::unique("pdfcraft-side-panels");
|
||||
let output = PaneHost::new(host)
|
||||
.policy(SidePolicy::Columns)
|
||||
.f6(false)
|
||||
.labels(labels)
|
||||
@@ -419,21 +445,7 @@ pub fn show(app: &mut PdfCraftApp, ui: &mut egui::Ui) {
|
||||
.body_margin(egui::Margin::ZERO)
|
||||
.show(ui, &open, |pane| Some(state_of(&panels, *pane)), &mut Content { app });
|
||||
|
||||
// Ensure panel resize handles do not bleed inward over panel interiors and scrollbars.
|
||||
// The handle's active target is kept to the divider strip centered on the border.
|
||||
for pane in &open {
|
||||
let host_id = egui::Id::unique("pdfcraft-side-panels");
|
||||
let s_id = host_id.with(("pane", *pane));
|
||||
let resize_id = s_id.with("resize");
|
||||
if let Some(resp) = ui.ctx().read_response(resize_id) {
|
||||
let edge_layer = egui::LayerId::new(ui.layer_id().order, s_id.with("resize-layer"));
|
||||
let edge_ui = ui.new_child(egui::UiBuilder::new().layer_id(edge_layer));
|
||||
let old_rect = resp.rect;
|
||||
let border = (old_rect.min.x + old_rect.max.x) / 2.0;
|
||||
let new_rect = egui::Rect::from_x_y_ranges((border - 2.0)..=(border + 2.0), old_rect.y_range());
|
||||
edge_ui.interact(new_rect, resize_id, egui::Sense::drag());
|
||||
}
|
||||
}
|
||||
keep_resize_edges_off_scrollbars(ui, host, &open);
|
||||
for request in output.requests {
|
||||
match request {
|
||||
PaneRequest::Resize(pane, width) => app.side_panels.set_width(pane, width),
|
||||
|
||||
@@ -87,11 +87,11 @@ fn a_dragged_width_is_remembered_across_restarts() {
|
||||
// The side panel's resize edge: the one right of the page, left of the panel's text.
|
||||
let edge = h
|
||||
.get_all_by_label("Resize panels")
|
||||
.map(|n| n.rect().center())
|
||||
.map(|n| n.rect().left_center())
|
||||
.filter(|c| c.x < side_left && c.x > WIDTH / 2.0)
|
||||
.min_by(|a, b| (side_left - a.x).total_cmp(&(side_left - b.x)))
|
||||
.expect("the side panel has a resize edge")
|
||||
// The pane's half of the edge: the page, drawn after the panes, takes the other half.
|
||||
// 2 pt inside the panel: the edge lies right of the border, clear of the page's scrollbar.
|
||||
+ egui::vec2(2.0, 0.0);
|
||||
h.hover_at(edge);
|
||||
h.run_steps(1);
|
||||
@@ -197,7 +197,8 @@ fn scrollbar_and_panel_resize_handle_do_not_collide_on_either_dock_side() {
|
||||
assert!(after_width > initial_width);
|
||||
assert!((after_width - (initial_width + 40.0)).abs() <= 5.0);
|
||||
|
||||
// 2. Right docking: tools panel on the right (resize handle on the left).
|
||||
// 2. Right docking: tools panel on the right (resize handle on the left), next to the page's
|
||||
// scrollbar (#5730).
|
||||
let mut h_right = harness_sized(egui::vec2(1200.0, 450.0), |app| {
|
||||
app.set_option("tools-side", "right").unwrap();
|
||||
app.all_tools_expanded = true;
|
||||
@@ -207,6 +208,51 @@ fn scrollbar_and_panel_resize_handle_do_not_collide_on_either_dock_side() {
|
||||
let splitters_right: Vec<_> = h_right.get_all_by_label("Resize panels").collect();
|
||||
assert!(!splitters_right.is_empty());
|
||||
let right_splitter_rect = splitters_right[0].rect();
|
||||
let bars: Vec<egui::Rect> = h_right.get_all_by_role(Role::ScrollBar).map(|s| s.rect()).filter(|r| r.height() > 0.0).collect();
|
||||
// The page's scrollbar ends where the pane begins; the pane's own one is at its far side.
|
||||
let page_bar = bars
|
||||
.iter()
|
||||
.copied()
|
||||
.filter(|r| r.min.x < right_splitter_rect.center().x)
|
||||
.max_by(|a, b| a.max.x.total_cmp(&b.max.x))
|
||||
.expect("the page has a scrollbar");
|
||||
assert!(
|
||||
(right_splitter_rect.center().x - page_bar.max.x).abs() < 6.0,
|
||||
"the page's scrollbar is beside the edge: {page_bar:?} {right_splitter_rect:?}"
|
||||
);
|
||||
let tools_bar = bars.iter().copied().find(|r| r.min.x > right_splitter_rect.center().x).expect("Tools panel scrollbar should exist");
|
||||
let initial_right_width = h_right.state().side_panels.tools_width;
|
||||
|
||||
// All of either scrollbar is the scrollbar, not the resize edge.
|
||||
for bar in [page_bar, tools_bar] {
|
||||
for x in [bar.min.x + 0.5, bar.center().x, bar.max.x - 0.5] {
|
||||
h_right.hover_at(egui::pos2(x, bar.center().y));
|
||||
h_right.run_steps(1);
|
||||
let cursor = h_right.output().platform_output.cursor_icon;
|
||||
assert_ne!(cursor, egui::CursorIcon::ResizeHorizontal, "Hovering scrollbar {bar:?} at x={x} should not trigger resize handle");
|
||||
}
|
||||
}
|
||||
|
||||
// Dragging either scrollbar by its right edge scrolls its content and leaves the width alone.
|
||||
let drag = |h: &mut Harness<'static, PdfCraftApp>, at: egui::Pos2| {
|
||||
h.hover_at(at);
|
||||
h.run_steps(1);
|
||||
h.drag_at(at);
|
||||
h.run_steps(1);
|
||||
h.hover_at(at + egui::vec2(0.0, 60.0));
|
||||
h.run_steps(1);
|
||||
h.drop_at(at + egui::vec2(0.0, 60.0));
|
||||
h.run_steps(2);
|
||||
};
|
||||
let page_top = |h: &Harness<'static, PdfCraftApp>| h.state().views[0].page_screen_rect(0).expect("page on screen").min.y;
|
||||
let before = page_top(&h_right);
|
||||
drag(&mut h_right, egui::pos2(page_bar.max.x - 1.0, page_bar.center().y));
|
||||
assert!(page_top(&h_right) < before, "dragging the page's scrollbar scrolls the page");
|
||||
assert_eq!(h_right.state().side_panels.tools_width, initial_right_width, "Dragging scrollbar must not resize the panel");
|
||||
let before = h_right.get_by_label("Edit a PDF").rect().top();
|
||||
drag(&mut h_right, egui::pos2(tools_bar.max.x - 1.0, tools_bar.center().y));
|
||||
assert!(h_right.get_by_label("Edit a PDF").rect().top() < before, "Content should scroll up when dragging scrollbar down");
|
||||
assert_eq!(h_right.state().side_panels.tools_width, initial_right_width, "Dragging scrollbar must not resize the panel");
|
||||
|
||||
// Splitter is at the left boundary of the right-docked pane.
|
||||
h_right.hover_at(right_splitter_rect.center());
|
||||
@@ -214,7 +260,6 @@ fn scrollbar_and_panel_resize_handle_do_not_collide_on_either_dock_side() {
|
||||
assert_eq!(h_right.output().platform_output.cursor_icon, egui::CursorIcon::ResizeHorizontal);
|
||||
|
||||
// Dragging the splitter on right-docked pane smoothly resizes.
|
||||
let initial_right_width = h_right.state().side_panels.tools_width;
|
||||
h_right.hover_at(right_splitter_rect.center());
|
||||
h_right.run_steps(1);
|
||||
h_right.drag_at(right_splitter_rect.center());
|
||||
|
||||
Reference in New Issue
Block a user