mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-07 03:01:55 +00:00
fix(ui): mode tabs keep the hand over a page scrolled under them (#2755)
With the All tools panel closed, page text scrolled up under the mode bar gave the Read, Edit, Convert and E-Sign tabs the page's cursor: the Select tool's I-beam, or Edit text's four-way arrow (the I-beam once a paragraph was open). The page code tested egui's raw hover position against the page's whole rectangle, which runs on under the chrome when the page is scrolled, and it sets the cursor after the mode bar does. The text layer and Edit text now set their cursor only while the pages' widget contains the pointer, as Fill & Sign already does; a selection being dragged keeps the I-beam past the edge. The open paragraph's editor, a foreground Area placed on its paragraph, also floated over the tabs and took their clicks: it is now clipped and hit-tested to the document viewport and stays on its paragraph instead of being pushed back into the window. Fixes #2755
This commit is contained in:
1 parent
f0c9901c39
commit
3d567bd732
3 files changed
+124
-14
No files matched your search
@@ -3148,7 +3148,9 @@ pub fn document_area(app: &mut PdfCraftApp, index: usize, ui: &mut egui::Ui) {
|
||||
let column = tool == QuickTool::ColumnSelect || ui.input(|inp| inp.modifiers.alt);
|
||||
// A link or a form field under the pointer keeps its own cursor: a check box
|
||||
// drawn with a glyph, or set on printed text, shows the hand, not the I-beam.
|
||||
let cursor_free = !over_link && !over_field;
|
||||
// So do the mode bar and side panels over a page scrolled under them (#2755),
|
||||
// unless a selection is being dragged there.
|
||||
let cursor_free = !over_link && !over_field && (resp.contains_pointer() || resp.dragged());
|
||||
if column && cursor_free {
|
||||
ui.ctx().set_cursor_icon(egui::CursorIcon::Crosshair);
|
||||
} else if over_text && cursor_free {
|
||||
|
||||
@@ -1081,14 +1081,18 @@ pub(crate) fn page_input(
|
||||
draw_handles(boxes[hit]);
|
||||
}
|
||||
// A closed paragraph moves when dragged (the four-way arrow says so; a click still opens it
|
||||
// for typing); an edge rewraps it. The open one takes the text cursor.
|
||||
ui.ctx().set_cursor_icon(if on_edge {
|
||||
egui::CursorIcon::ResizeHorizontal
|
||||
} else if active.is_none() {
|
||||
egui::CursorIcon::Move
|
||||
} else {
|
||||
egui::CursorIcon::Text
|
||||
});
|
||||
// for typing); an edge rewraps it. The open one takes the text cursor. Only where the page
|
||||
// shows, so the mode bar and side panels over a paragraph scrolled under them keep their own
|
||||
// cursor (#2755).
|
||||
if resp.contains_pointer() {
|
||||
ui.ctx().set_cursor_icon(if on_edge {
|
||||
egui::CursorIcon::ResizeHorizontal
|
||||
} else if active.is_none() {
|
||||
egui::CursorIcon::Move
|
||||
} else {
|
||||
egui::CursorIcon::Text
|
||||
});
|
||||
}
|
||||
if resp.clicked() {
|
||||
view.image_selection = None;
|
||||
// A click that moves the editor to another paragraph applies the open one first, so its
|
||||
@@ -1313,6 +1317,7 @@ pub(crate) fn overlay(ctx: &egui::Context, view: &mut DocView, info: &DocInfo) -
|
||||
.as_ref()
|
||||
.is_some_and(|ed| view.text_background.ready(ed.page, ed.block) && !ed.source.as_ref().is_some_and(|(b, _)| b.invisible));
|
||||
let draft_rect = view.text_background.draft_rect.map(|r| xf.user_rect(info, page, r.map(|v| v as f32)));
|
||||
let viewport = view.viewport_rect();
|
||||
let ed = view.line_editor.as_mut()?;
|
||||
if hold {
|
||||
ed.focus = true;
|
||||
@@ -1332,9 +1337,15 @@ pub(crate) fn overlay(ctx: &egui::Context, view: &mut DocView, info: &DocInfo) -
|
||||
let font = editor_font(&ed.look, ed.size);
|
||||
let text_color = color32(ed.look.color);
|
||||
let mut done = None;
|
||||
egui::Area::new(egui::Id::new("edit-text-line")).order(egui::Order::Foreground).fixed_pos(Pos2::new(ed.rect.left(), ed.rect.top())).show(
|
||||
ctx,
|
||||
|ui| {
|
||||
// Over the page, so only where the document shows: a paragraph scrolled under the mode bar or
|
||||
// a side panel must leave them their pointer (#2755). It stays at its paragraph, cut off as
|
||||
// the page is, rather than pushed back into the window.
|
||||
egui::Area::new(egui::Id::new("edit-text-line"))
|
||||
.order(egui::Order::Foreground)
|
||||
.fixed_pos(Pos2::new(ed.rect.left(), ed.rect.top()))
|
||||
.constrain_to(viewport)
|
||||
.constrain(false)
|
||||
.show(ctx, |ui| {
|
||||
// The editor opens at the source size. Once text changes, show the real draft
|
||||
// extent: single rows grow toward the page edge, paragraphs rewrap at their
|
||||
// source width, and the visible frame grows/shrinks vertically with their rows.
|
||||
@@ -1358,6 +1369,7 @@ pub(crate) fn overlay(ctx: &egui::Context, view: &mut DocView, info: &DocInfo) -
|
||||
// On the frame, a press takes hold of the box (a click selects it, a drag moves it):
|
||||
// the pointer says so there, and reads as text inside.
|
||||
if let Some(p) = ui.input(|i| i.pointer.hover_pos())
|
||||
&& ui.clip_rect().contains(p)
|
||||
&& live.expand(2.0 + FRAME_REACH).contains(p)
|
||||
&& !live.expand(2.0).contains(p)
|
||||
{
|
||||
@@ -1412,8 +1424,7 @@ pub(crate) fn overlay(ctx: &egui::Context, view: &mut DocView, info: &DocInfo) -
|
||||
done = Some(true);
|
||||
}
|
||||
});
|
||||
},
|
||||
);
|
||||
});
|
||||
match done {
|
||||
Some(apply) => {
|
||||
let ed = view.line_editor.take()?;
|
||||
|
||||
@@ -4673,3 +4673,100 @@ fn leaving_organize_goes_back_to_the_workspace_panel() {
|
||||
h.run_steps(3);
|
||||
assert_eq!(h.state().left, LeftPanel::AllTools);
|
||||
}
|
||||
|
||||
/// Where a mode bar tab is, by its label.
|
||||
fn mode_tab(h: &Harness<'static, PdfCraftApp>, label: &str) -> egui::Rect {
|
||||
h.get_all_by_role_and_label(Role::Button, label).map(|n| n.rect()).find(|r| r.bottom() < 120.0).expect("a mode tab")
|
||||
}
|
||||
|
||||
/// The cursor over each mode bar tab that page 1's text lies under, once that text is scrolled
|
||||
/// up under the bar. With the All tools panel closed it lies under Read, Edit, Convert and E-Sign.
|
||||
fn mode_tab_cursors(h: &mut Harness<'static, PdfCraftApp>, text: egui::Rect) -> [(&'static str, egui::CursorIcon); 4] {
|
||||
h.state_mut().views[0].goto = Some((0, 0.5));
|
||||
h.run_steps(4);
|
||||
let offset = h.state().views[0].page_screen_rect(0).expect("on screen").top();
|
||||
["Read", "Edit", "Convert", "E-Sign"].map(|label| {
|
||||
let at = mode_tab(h, label).center();
|
||||
assert!(text.translate(egui::vec2(0.0, offset)).contains(at), "the text lies under {label}");
|
||||
h.hover_at(at);
|
||||
h.run_steps(2);
|
||||
(label, h.output().platform_output.cursor_icon)
|
||||
})
|
||||
}
|
||||
|
||||
const HAND_ON_EVERY_TAB: [(&str, egui::CursorIcon); 4] = [
|
||||
("Read", egui::CursorIcon::PointingHand),
|
||||
("Edit", egui::CursorIcon::PointingHand),
|
||||
("Convert", egui::CursorIcon::PointingHand),
|
||||
("E-Sign", egui::CursorIcon::PointingHand),
|
||||
];
|
||||
|
||||
/// Page 1's text block on screen, relative to the page's top edge, in the window as it is now.
|
||||
fn page_text(h: &Harness<'static, PdfCraftApp>) -> egui::Rect {
|
||||
let id = h.state().views[0].id;
|
||||
let block = h.state().session.get(id).unwrap().text_blocks(0)[0].clone();
|
||||
let r = h.state().views[0].page_screen_rect(0).expect("on screen");
|
||||
let k = r.width() / 200.0;
|
||||
let at = |x: f64, y: f64| egui::pos2(r.left() + x as f32 * k, (300.0 - y as f32) * k);
|
||||
egui::Rect::from_two_pos(at(block.rect[0], block.rect[1]), at(block.rect[2], block.rect[3]))
|
||||
}
|
||||
|
||||
/// Text scrolled up under the mode bar leaves its tabs the hand (#2755). The Select tool's
|
||||
/// text layer showed its I-beam there, over chrome that hides the page.
|
||||
#[test]
|
||||
fn mode_tabs_keep_the_hand_over_page_text_scrolled_under_them() {
|
||||
let mut h = harness(2, |app| app.set_option("left", "closed").unwrap());
|
||||
let text = page_text(&h);
|
||||
// Where the page shows, its text takes the I-beam, once the page's text has arrived.
|
||||
h.state_mut().views[0].goto = Some((0, 0.3));
|
||||
h.run_steps(4);
|
||||
let top = h.state().views[0].page_screen_rect(0).expect("on screen").top();
|
||||
let mut cursor = egui::CursorIcon::Default;
|
||||
for _ in 0..50 {
|
||||
h.hover_at(text.center() + egui::vec2(0.0, top));
|
||||
h.run_steps(2);
|
||||
cursor = h.output().platform_output.cursor_icon;
|
||||
if cursor == egui::CursorIcon::Text {
|
||||
break;
|
||||
}
|
||||
}
|
||||
assert_eq!(cursor, egui::CursorIcon::Text);
|
||||
assert_eq!(mode_tab_cursors(&mut h, text), HAND_ON_EVERY_TAB);
|
||||
}
|
||||
|
||||
/// Edit text, then the All tools panel closed, as reported (#2755): a paragraph scrolled up
|
||||
/// under the mode bar gave its tabs the four-way arrow, or the I-beam once a paragraph was open.
|
||||
/// The open paragraph's editor also covered the tabs and took their clicks.
|
||||
#[test]
|
||||
fn mode_tabs_keep_the_hand_over_a_paragraph_scrolled_under_them() {
|
||||
let mut h = harness(2, |_| {});
|
||||
assert!(h.state_mut().execute("edit.edit_text"));
|
||||
h.state_mut().set_option("left", "closed").unwrap();
|
||||
h.run_steps(4);
|
||||
let text = page_text(&h);
|
||||
assert_eq!(mode_tab_cursors(&mut h, text), HAND_ON_EVERY_TAB, "a closed paragraph under the tabs");
|
||||
// Open it, where it shows.
|
||||
h.state_mut().views[0].goto = Some((0, 0.3));
|
||||
h.run_steps(4);
|
||||
let top = h.state().views[0].page_screen_rect(0).expect("on screen").top();
|
||||
click_at(&mut h, text.center() + egui::vec2(0.0, top));
|
||||
assert!(h.state().views[0].line_editor.is_some());
|
||||
assert_eq!(mode_tab_cursors(&mut h, text), HAND_ON_EVERY_TAB, "the open paragraph under the tabs");
|
||||
// Nor does the editor's frame, with the paragraph's top edge just under a tab (the frame
|
||||
// reaches 2 to 6 points outside it).
|
||||
let convert = mode_tab(&h, "Convert").center();
|
||||
h.state_mut().views[0].goto = Some((0, 0.0));
|
||||
h.run_steps(4);
|
||||
let page = h.state().views[0].page_screen_rect(0).expect("on screen");
|
||||
h.state_mut().views[0].goto = Some((0, (page.top() - (convert.y + 4.0 - text.top())) / page.height()));
|
||||
h.run_steps(4);
|
||||
let page = h.state().views[0].page_screen_rect(0).expect("on screen");
|
||||
assert!((page.top() + text.top() - convert.y - 4.0).abs() < 1.0, "Convert lies on the paragraph's frame");
|
||||
h.hover_at(convert);
|
||||
h.run_steps(2);
|
||||
assert_eq!(h.output().platform_output.cursor_icon, egui::CursorIcon::PointingHand, "the open paragraph's frame under a tab");
|
||||
h.state_mut().views[0].goto = Some((0, 0.5));
|
||||
h.run_steps(4);
|
||||
click_at(&mut h, convert);
|
||||
assert_eq!(h.state().mode, pdfcraft_ui_egui::Mode::Convert, "the tab takes the click, not the paragraph's editor");
|
||||
}
|
||||
Reference in new issue
Block a user