mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-28 20:18:06 -06:00
fix(ui): on-page text editors leave the mode tabs their pointer
Follows up #2755, which fixed the Edit text paragraph editor. The form text field editor, the Fill & Sign text box and the Add text box are foreground Areas placed on the page too. Scrolled up under the mode bar with the All tools panel closed, they were drawn over the Read, Edit, Convert and E-Sign tabs: the tabs showed the editor's I-beam and a click on one went to the editor, so the mode didn't change. The three Areas are now constrained to the document viewport without being pushed back into it, as the paragraph editor is: they are clipped and hit-tested only where the document shows, and stay on their field or point. The form field cursor and Add text's I-beam, move and resize cursors read the raw pointer against the page's whole rectangle, which runs on under the chrome; they are now set only while the pages' widget contains the pointer, as Fill & Sign already does.
This commit is contained in:
@@ -357,8 +357,13 @@ pub(crate) fn page_input(
|
||||
.and_then(|(_, si)| items.iter().find(|(i, _)| *i == si))
|
||||
.map(|(_, a)| screen_rect(xf, info, page, a.content.rect()));
|
||||
let corner = selected_rect.and_then(|r| handles(r).into_iter().find(|(c, ..)| c.distance(p) <= 6.0));
|
||||
// Cursors only where the page shows: the mode bar and side panels over a page scrolled under
|
||||
// them keep their own (#2755).
|
||||
let shows = resp.contains_pointer();
|
||||
if adding_text && hit.is_none() {
|
||||
ui.ctx().set_cursor_icon(egui::CursorIcon::Text);
|
||||
if shows {
|
||||
ui.ctx().set_cursor_icon(egui::CursorIcon::Text);
|
||||
}
|
||||
if resp.clicked() {
|
||||
// Text that can't be drawn stays where it is being typed (#125).
|
||||
if cv.hold_blocked() {
|
||||
@@ -383,9 +388,9 @@ pub(crate) fn page_input(
|
||||
}
|
||||
return consumed;
|
||||
}
|
||||
if let Some((_, left, top)) = corner {
|
||||
if let Some((_, left, top)) = corner.filter(|_| shows) {
|
||||
ui.ctx().set_cursor_icon(if left == top { egui::CursorIcon::ResizeNwSe } else { egui::CursorIcon::ResizeNeSw });
|
||||
} else if hit.is_some() {
|
||||
} else if hit.is_some() && shows {
|
||||
ui.ctx().set_cursor_icon(egui::CursorIcon::Move);
|
||||
}
|
||||
if resp.drag_started() {
|
||||
@@ -523,56 +528,63 @@ pub(crate) fn editor(ctx: &egui::Context, view: &mut DocView, info: &DocInfo, ad
|
||||
let (mut commit, mut cancel, mut done) = (false, false, false);
|
||||
let mut undrawable = None;
|
||||
let memo = &view.content.undrawable;
|
||||
egui::Area::new(egui::Id::new(("added-text", view.id.0))).order(egui::Order::Foreground).fixed_pos(r.min).constrain(false).show(ctx, |ui| {
|
||||
let Some(t) = view.content.draft.as_mut() else { return };
|
||||
ui.set_width(r.width() + 64.0);
|
||||
if let Some(source_rect) = source_rect.filter(|_| !background_ready) {
|
||||
ui.painter().rect_filled(source_rect.union(r).expand(1.0), CornerRadius::ZERO, Color32::WHITE);
|
||||
}
|
||||
// The box is white in every theme, so the caret must not follow the theme (pale in dark mode) (#655).
|
||||
ui.visuals_mut().text_cursor.stroke.color = crate::edit_text_ui::caret_color_on(Color32::WHITE);
|
||||
let [cr, cg, cb] = t.style.color.map(|v| (v.clamp(0.0, 1.0) * 255.0) as u8);
|
||||
ui.horizontal_top(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 4.0;
|
||||
let resp = ui.add(
|
||||
egui::TextEdit::multiline(&mut t.text)
|
||||
.font(font)
|
||||
.desired_width(r.width().max(60.0))
|
||||
.desired_rows(1)
|
||||
.background_color(if background_ready { Color32::TRANSPARENT } else { Color32::WHITE })
|
||||
.text_color(Color32::from_rgb(cr, cg, cb))
|
||||
.hint_text(tl!("Type text"))
|
||||
.id_salt("added-text-edit"),
|
||||
);
|
||||
if t.focus {
|
||||
resp.request_focus();
|
||||
t.focus = false;
|
||||
// Over the page, so only where the document shows: text scrolled under the mode bar or a side
|
||||
// panel must leave them their pointer (#2755).
|
||||
egui::Area::new(egui::Id::new(("added-text", view.id.0)))
|
||||
.order(egui::Order::Foreground)
|
||||
.fixed_pos(r.min)
|
||||
.constrain_to(view.viewport_rect())
|
||||
.constrain(false)
|
||||
.show(ctx, |ui| {
|
||||
let Some(t) = view.content.draft.as_mut() else { return };
|
||||
ui.set_width(r.width() + 64.0);
|
||||
if let Some(source_rect) = source_rect.filter(|_| !background_ready) {
|
||||
ui.painter().rect_filled(source_rect.union(r).expand(1.0), CornerRadius::ZERO, Color32::WHITE);
|
||||
}
|
||||
// The box is white in every theme, so the caret must not follow the theme (pale in dark mode) (#655).
|
||||
ui.visuals_mut().text_cursor.stroke.color = crate::edit_text_ui::caret_color_on(Color32::WHITE);
|
||||
let [cr, cg, cb] = t.style.color.map(|v| (v.clamp(0.0, 1.0) * 255.0) as u8);
|
||||
ui.horizontal_top(|ui| {
|
||||
ui.spacing_mut().item_spacing.x = 4.0;
|
||||
let resp = ui.add(
|
||||
egui::TextEdit::multiline(&mut t.text)
|
||||
.font(font)
|
||||
.desired_width(r.width().max(60.0))
|
||||
.desired_rows(1)
|
||||
.background_color(if background_ready { Color32::TRANSPARENT } else { Color32::WHITE })
|
||||
.text_color(Color32::from_rgb(cr, cg, cb))
|
||||
.hint_text(tl!("Type text"))
|
||||
.id_salt("added-text-edit"),
|
||||
);
|
||||
if t.focus {
|
||||
resp.request_focus();
|
||||
t.focus = false;
|
||||
}
|
||||
let buttons = egui::Frame::NONE
|
||||
.fill(tokens.card)
|
||||
.stroke(Stroke::new(1.0, tokens.border))
|
||||
.corner_radius(CornerRadius::same(6))
|
||||
.inner_margin(2)
|
||||
.show(ui, |ui| {
|
||||
ui.spacing_mut().item_spacing = egui::vec2(2.0, 2.0);
|
||||
ui.horizontal(|ui| {
|
||||
done = crate::icons::button(ui, "check", 24.0, false, tl!("Done adding text")).clicked();
|
||||
cancel = crate::icons::button(ui, "x", 24.0, false, tl!("Discard this text (Esc)")).clicked();
|
||||
});
|
||||
})
|
||||
.response
|
||||
.rect;
|
||||
// Pressing a button takes the focus from the text: that is not a click away.
|
||||
let on_buttons = ui.input(|i| i.pointer.interact_pos()).is_some_and(|p| buttons.contains(p));
|
||||
cancel |= ui.input(|i| i.key_pressed(egui::Key::Escape));
|
||||
commit = done || resp.lost_focus() && !on_buttons && !view.objects.hold_editor;
|
||||
});
|
||||
// Page text is drawn with the standard fonts; anything else would be saved as `?` (#125).
|
||||
undrawable = self::undrawable(t, memo);
|
||||
if let Some(c) = undrawable {
|
||||
ui.label(egui::RichText::new(undrawable_message(c)).small().color(UNDRAWABLE));
|
||||
}
|
||||
let buttons = egui::Frame::NONE
|
||||
.fill(tokens.card)
|
||||
.stroke(Stroke::new(1.0, tokens.border))
|
||||
.corner_radius(CornerRadius::same(6))
|
||||
.inner_margin(2)
|
||||
.show(ui, |ui| {
|
||||
ui.spacing_mut().item_spacing = egui::vec2(2.0, 2.0);
|
||||
ui.horizontal(|ui| {
|
||||
done = crate::icons::button(ui, "check", 24.0, false, tl!("Done adding text")).clicked();
|
||||
cancel = crate::icons::button(ui, "x", 24.0, false, tl!("Discard this text (Esc)")).clicked();
|
||||
});
|
||||
})
|
||||
.response
|
||||
.rect;
|
||||
// Pressing a button takes the focus from the text: that is not a click away.
|
||||
let on_buttons = ui.input(|i| i.pointer.interact_pos()).is_some_and(|p| buttons.contains(p));
|
||||
cancel |= ui.input(|i| i.key_pressed(egui::Key::Escape));
|
||||
commit = done || resp.lost_focus() && !on_buttons && !view.objects.hold_editor;
|
||||
});
|
||||
// Page text is drawn with the standard fonts; anything else would be saved as `?` (#125).
|
||||
undrawable = self::undrawable(t, memo);
|
||||
if let Some(c) = undrawable {
|
||||
ui.label(egui::RichText::new(undrawable_message(c)).small().color(UNDRAWABLE));
|
||||
}
|
||||
});
|
||||
if cancel {
|
||||
view.content.draft = None;
|
||||
return (None, false);
|
||||
|
||||
@@ -706,33 +706,41 @@ pub(crate) fn type_box(ctx: &egui::Context, view: &mut DocView, info: &DocInfo,
|
||||
let zoom = xf.rect.width() / xf.pw.max(1.0);
|
||||
let mut commit = false;
|
||||
let mut cancel = false;
|
||||
egui::Area::new(egui::Id::new(("fill-text", view.id.0))).order(egui::Order::Foreground).fixed_pos(pos).show(ctx, |ui| {
|
||||
let Some(t) = view.fill_text.as_mut() else { return };
|
||||
// The box is white in every theme, so the caret must not follow the theme (pale in dark mode) (#655).
|
||||
ui.visuals_mut().text_cursor.stroke.color = crate::edit_text_ui::caret_color_on(Color32::WHITE);
|
||||
let width = ((t.text.chars().count().max(8) as f32) * t.size as f32 * 0.6 * zoom).clamp(60.0, 600.0);
|
||||
// The area's max rect is last frame's size, which caps the edit's width; widen it so the
|
||||
// box grows as text is typed.
|
||||
ui.set_min_width(width);
|
||||
let r = ui.add(
|
||||
egui::TextEdit::singleline(&mut t.text)
|
||||
.font(egui::FontId::proportional((t.size as f32 * zoom).max(8.0)))
|
||||
.desired_width(width)
|
||||
.background_color(Color32::from_rgba_unmultiplied(255, 255, 255, 230))
|
||||
.text_color(Color32::BLACK)
|
||||
.hint_text(tl!("Type text"))
|
||||
.id_salt("fill-text-edit"),
|
||||
);
|
||||
if t.focus {
|
||||
r.request_focus();
|
||||
t.focus = false;
|
||||
}
|
||||
if ui.input(|i| i.key_pressed(egui::Key::Escape)) {
|
||||
cancel = true;
|
||||
} else if r.lost_focus() {
|
||||
commit = true;
|
||||
}
|
||||
});
|
||||
// Over the page, so only where the document shows: a box scrolled under the mode bar or a
|
||||
// side panel must leave them their pointer (#2755). It stays where it was placed, cut off as
|
||||
// the page is, rather than pushed back into the window.
|
||||
egui::Area::new(egui::Id::new(("fill-text", view.id.0)))
|
||||
.order(egui::Order::Foreground)
|
||||
.fixed_pos(pos)
|
||||
.constrain_to(view.viewport_rect())
|
||||
.constrain(false)
|
||||
.show(ctx, |ui| {
|
||||
let Some(t) = view.fill_text.as_mut() else { return };
|
||||
// The box is white in every theme, so the caret must not follow the theme (pale in dark mode) (#655).
|
||||
ui.visuals_mut().text_cursor.stroke.color = crate::edit_text_ui::caret_color_on(Color32::WHITE);
|
||||
let width = ((t.text.chars().count().max(8) as f32) * t.size as f32 * 0.6 * zoom).clamp(60.0, 600.0);
|
||||
// The area's max rect is last frame's size, which caps the edit's width; widen it so the
|
||||
// box grows as text is typed.
|
||||
ui.set_min_width(width);
|
||||
let r = ui.add(
|
||||
egui::TextEdit::singleline(&mut t.text)
|
||||
.font(egui::FontId::proportional((t.size as f32 * zoom).max(8.0)))
|
||||
.desired_width(width)
|
||||
.background_color(Color32::from_rgba_unmultiplied(255, 255, 255, 230))
|
||||
.text_color(Color32::BLACK)
|
||||
.hint_text(tl!("Type text"))
|
||||
.id_salt("fill-text-edit"),
|
||||
);
|
||||
if t.focus {
|
||||
r.request_focus();
|
||||
t.focus = false;
|
||||
}
|
||||
if ui.input(|i| i.key_pressed(egui::Key::Escape)) {
|
||||
cancel = true;
|
||||
} else if r.lost_focus() {
|
||||
commit = true;
|
||||
}
|
||||
});
|
||||
if cancel {
|
||||
view.fill_text = None;
|
||||
return None;
|
||||
|
||||
@@ -132,11 +132,15 @@ pub(crate) fn page_input(
|
||||
// their action.
|
||||
let pressable = f.kind == FormFieldKind::PushButton && !f.read_only() && f.button.is_some();
|
||||
let usable = allowed && (fillable(f) || pressable || f.kind == FormFieldKind::Signature);
|
||||
ui.ctx().set_cursor_icon(match (usable, f.kind) {
|
||||
(false, _) => egui::CursorIcon::NotAllowed,
|
||||
(true, FormFieldKind::Text) => egui::CursorIcon::Text,
|
||||
_ => egui::CursorIcon::PointingHand,
|
||||
});
|
||||
// Only where the page shows: the mode bar and side panels over a field scrolled under them
|
||||
// keep their own cursor (#2755).
|
||||
if resp.contains_pointer() {
|
||||
ui.ctx().set_cursor_icon(match (usable, f.kind) {
|
||||
(false, _) => egui::CursorIcon::NotAllowed,
|
||||
(true, FormFieldKind::Text) => egui::CursorIcon::Text,
|
||||
_ => egui::CursorIcon::PointingHand,
|
||||
});
|
||||
}
|
||||
// A text field opens on the press rather than the click, so a drag that starts on it
|
||||
// selects its text (#2213).
|
||||
let pressed = f.kind == FormFieldKind::Text && usable && resp.is_pointer_button_down_on() && ui.input(|i| i.pointer.primary_pressed());
|
||||
@@ -485,78 +489,87 @@ pub(crate) fn overlay(ctx: &egui::Context, view: &mut DocView, info: &DocInfo, f
|
||||
match f.kind {
|
||||
FormFieldKind::Text => {
|
||||
let multiline = f.has(field_flags::MULTILINE);
|
||||
egui::Area::new(egui::Id::new(("form-editor", view.id.0))).order(egui::Order::Foreground).fixed_pos(rect.min).show(ctx, |ui| {
|
||||
ui.set_min_size(rect.size());
|
||||
let Some(fx) = view.forms.focus.as_mut() else { return };
|
||||
// Sized from the text being typed, so an auto-sized field shrinks to keep it in view.
|
||||
let font = (field_text_size(ui.painter(), f, w, &fx.text) * zoom).clamp(3.0, 64.0);
|
||||
// The field is pale yellow in every theme, so the caret must not follow the theme (pale in dark mode) (#655).
|
||||
ui.visuals_mut().text_cursor.stroke.color = crate::edit_text_ui::caret_color_on(FIELD_FILL);
|
||||
let mut te = if multiline { egui::TextEdit::multiline(&mut fx.text) } else { egui::TextEdit::singleline(&mut fx.text) };
|
||||
te = te
|
||||
// Wraps (inside the 3 px side margins) at the width the size was fitted to:
|
||||
// the box less 3 pt each side, scaled.
|
||||
.desired_width((rect.width() - 6.0 * zoom + 6.0).max(1.0))
|
||||
.font(egui::FontId::proportional(font))
|
||||
.background_color(FIELD_FILL)
|
||||
.text_color(Color32::BLACK)
|
||||
.margin(egui::Margin::symmetric(3, 1))
|
||||
.password(f.has(field_flags::PASSWORD))
|
||||
.id(egui::Id::new(("form-field", view.id.0, &f.name, focus.widget)));
|
||||
if let Some(max) = f.max_len {
|
||||
te = te.char_limit(max);
|
||||
}
|
||||
if multiline {
|
||||
te = te.desired_rows(((rect.height() / (font * 1.3)).floor() as usize).max(1));
|
||||
}
|
||||
let mut laid_out = None;
|
||||
let r = ui.add_sized(rect.size(), |ui: &mut egui::Ui| {
|
||||
let out = te.show(ui);
|
||||
laid_out = Some((out.galley, out.galley_pos));
|
||||
out.response.response
|
||||
});
|
||||
if let Some(press) = fx.press.take()
|
||||
&& let Some((galley, pos)) = laid_out
|
||||
&& let Some(mut st) = egui::TextEdit::load_state(ui.ctx(), r.id)
|
||||
{
|
||||
// The press went to the page, not to this editor, which did not exist yet.
|
||||
let at = galley.cursor_from_pos(press - pos + vec2(galley.rect.left(), 0.0));
|
||||
st.cursor.set_char_range(Some(egui::text::CCursorRange::one(at)));
|
||||
st.store(ui.ctx(), r.id);
|
||||
if ui.input(|i| i.pointer.primary_down()) {
|
||||
ui.ctx().set_dragged_id(r.id);
|
||||
// Over the page, so only where the document shows: a field scrolled under the mode bar
|
||||
// or a side panel must leave them their pointer (#2755). It stays on its field, cut off
|
||||
// as the page is, rather than pushed back into the window.
|
||||
egui::Area::new(egui::Id::new(("form-editor", view.id.0)))
|
||||
.order(egui::Order::Foreground)
|
||||
.fixed_pos(rect.min)
|
||||
.constrain_to(view.viewport_rect())
|
||||
.constrain(false)
|
||||
.show(ctx, |ui| {
|
||||
ui.set_min_size(rect.size());
|
||||
let Some(fx) = view.forms.focus.as_mut() else { return };
|
||||
// Sized from the text being typed, so an auto-sized field shrinks to keep it in view.
|
||||
let font = (field_text_size(ui.painter(), f, w, &fx.text) * zoom).clamp(3.0, 64.0);
|
||||
// The field is pale yellow in every theme, so the caret must not follow the theme (pale in dark mode) (#655).
|
||||
ui.visuals_mut().text_cursor.stroke.color = crate::edit_text_ui::caret_color_on(FIELD_FILL);
|
||||
let mut te = if multiline { egui::TextEdit::multiline(&mut fx.text) } else { egui::TextEdit::singleline(&mut fx.text) };
|
||||
te = te
|
||||
// Wraps (inside the 3 px side margins) at the width the size was fitted to:
|
||||
// the box less 3 pt each side, scaled.
|
||||
.desired_width((rect.width() - 6.0 * zoom + 6.0).max(1.0))
|
||||
.font(egui::FontId::proportional(font))
|
||||
.background_color(FIELD_FILL)
|
||||
.text_color(Color32::BLACK)
|
||||
.margin(egui::Margin::symmetric(3, 1))
|
||||
.password(f.has(field_flags::PASSWORD))
|
||||
.id(egui::Id::new(("form-field", view.id.0, &f.name, focus.widget)));
|
||||
if let Some(max) = f.max_len {
|
||||
te = te.char_limit(max);
|
||||
}
|
||||
}
|
||||
if fx.request_focus {
|
||||
r.request_focus();
|
||||
fx.request_focus = false;
|
||||
if std::mem::take(&mut fx.select_all)
|
||||
if multiline {
|
||||
te = te.desired_rows(((rect.height() / (font * 1.3)).floor() as usize).max(1));
|
||||
}
|
||||
let mut laid_out = None;
|
||||
let r = ui.add_sized(rect.size(), |ui: &mut egui::Ui| {
|
||||
let out = te.show(ui);
|
||||
laid_out = Some((out.galley, out.galley_pos));
|
||||
out.response.response
|
||||
});
|
||||
if let Some(press) = fx.press.take()
|
||||
&& let Some((galley, pos)) = laid_out
|
||||
&& let Some(mut st) = egui::TextEdit::load_state(ui.ctx(), r.id)
|
||||
{
|
||||
let n = fx.text.chars().count();
|
||||
st.cursor.set_char_range(Some(egui::text::CCursorRange::two(egui::text::CCursor::new(0), egui::text::CCursor::new(n))));
|
||||
// The press went to the page, not to this editor, which did not exist yet.
|
||||
let at = galley.cursor_from_pos(press - pos + vec2(galley.rect.left(), 0.0));
|
||||
st.cursor.set_char_range(Some(egui::text::CCursorRange::one(at)));
|
||||
st.store(ui.ctx(), r.id);
|
||||
if ui.input(|i| i.pointer.primary_down()) {
|
||||
ui.ctx().set_dragged_id(r.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
let (enter, esc, tab, shift) = ui
|
||||
.input(|i| (i.key_pressed(egui::Key::Enter), i.key_pressed(egui::Key::Escape), i.key_pressed(egui::Key::Tab), i.modifiers.shift));
|
||||
if esc {
|
||||
close = true;
|
||||
} else if tab {
|
||||
next = Some(!shift);
|
||||
} else if r.lost_focus()
|
||||
&& (!multiline || !enter)
|
||||
&& !fx.calendar_rect.is_some_and(|c| ui.input(|i| i.pointer.interact_pos().is_some_and(|p| c.contains(p))))
|
||||
{
|
||||
if pressed {
|
||||
// The field reopened with a refused value; keep it open and focused.
|
||||
fx.request_focus = true;
|
||||
} else {
|
||||
// Enter in a single-line field, or clicking elsewhere, commits.
|
||||
commit(view, form);
|
||||
if fx.request_focus {
|
||||
r.request_focus();
|
||||
fx.request_focus = false;
|
||||
if std::mem::take(&mut fx.select_all)
|
||||
&& let Some(mut st) = egui::TextEdit::load_state(ui.ctx(), r.id)
|
||||
{
|
||||
let n = fx.text.chars().count();
|
||||
st.cursor.set_char_range(Some(egui::text::CCursorRange::two(egui::text::CCursor::new(0), egui::text::CCursor::new(n))));
|
||||
st.store(ui.ctx(), r.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
let (enter, esc, tab, shift) = ui.input(|i| {
|
||||
(i.key_pressed(egui::Key::Enter), i.key_pressed(egui::Key::Escape), i.key_pressed(egui::Key::Tab), i.modifiers.shift)
|
||||
});
|
||||
if esc {
|
||||
close = true;
|
||||
} else if tab {
|
||||
next = Some(!shift);
|
||||
} else if r.lost_focus()
|
||||
&& (!multiline || !enter)
|
||||
&& !fx.calendar_rect.is_some_and(|c| ui.input(|i| i.pointer.interact_pos().is_some_and(|p| c.contains(p))))
|
||||
{
|
||||
if pressed {
|
||||
// The field reopened with a refused value; keep it open and focused.
|
||||
fx.request_focus = true;
|
||||
} else {
|
||||
// Enter in a single-line field, or clicking elsewhere, commits.
|
||||
commit(view, form);
|
||||
}
|
||||
}
|
||||
});
|
||||
// Date fields: a calendar under the field (Acrobat's date picker).
|
||||
if let pdfcraft_engine::form_scripts::Format::Date(fmt) = &f.actions.format
|
||||
&& let Some(picked) = calendar(ctx, view, rect, fmt, today)
|
||||
|
||||
@@ -585,3 +585,75 @@ fn released_text_does_not_jump_back_before_the_page_is_redrawn() {
|
||||
let (old, new) = ink_at(&mut h, from, to);
|
||||
assert!(new > 20 && old == 0, "the redrawn page has the text in its new place ({old} old, {new} new)");
|
||||
}
|
||||
|
||||
/// 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(egui::accesskit::Role::Button, label).map(|n| n.rect()).find(|r| r.bottom() < 120.0).expect("a mode tab")
|
||||
}
|
||||
|
||||
/// Scroll the page until `what` (on screen) lies across the mode bar, then the cursor over each
|
||||
/// tab it lies under: with the All tools panel closed, Read, Edit, Convert and E-Sign.
|
||||
fn mode_tab_cursors(
|
||||
h: &mut Harness<'static, PdfCraftApp>,
|
||||
what: impl Fn(&Harness<'static, PdfCraftApp>) -> egui::Rect,
|
||||
) -> [(&'static str, egui::CursorIcon); 4] {
|
||||
let page = |h: &Harness<'static, PdfCraftApp>| h.state().views[0].page_screen_rect(0).expect("on screen");
|
||||
let y = what(h).center().y - page(h).top();
|
||||
let tabs = mode_tab(h, "Convert").center().y;
|
||||
h.state_mut().views[0].goto = Some((0, 0.0));
|
||||
h.run_steps(4);
|
||||
let top = page(h).top();
|
||||
h.state_mut().views[0].goto = Some((0, (top - (tabs - y)) / page(h).height()));
|
||||
h.run_steps(4);
|
||||
let under = what(h);
|
||||
["Read", "Edit", "Convert", "E-Sign"].map(|label| {
|
||||
let at = mode_tab(h, label).center();
|
||||
assert!(under.contains(at), "{under:?} lies under {label} at {at:?}");
|
||||
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),
|
||||
];
|
||||
|
||||
/// Add text, with the page scrolled up under the mode bar, leaves its tabs the hand (#2755): the
|
||||
/// tool gave them its I-beam, added text its four-way arrow, and the open editor floated over
|
||||
/// them and took their clicks.
|
||||
#[test]
|
||||
fn mode_tabs_keep_the_hand_over_added_text_scrolled_under_them() {
|
||||
let mut h = harness();
|
||||
assert!(h.state_mut().execute("edit.text"));
|
||||
h.state_mut().set_option("left", "closed").unwrap();
|
||||
// The page across the window, so text placed at its left edge reaches under the tabs.
|
||||
h.state_mut().views[0].fit = pdfcraft_ui_egui::canvas::Fit::Width;
|
||||
h.run_steps(4);
|
||||
assert_eq!(h.state().quick_tool, QuickTool::AddText);
|
||||
// The empty page, ready for text.
|
||||
assert_eq!(mode_tab_cursors(&mut h, |h| item_on_screen(h, [10.0, 290.0, 210.0, 305.0])), HAND_ON_EVERY_TAB, "the page under the tabs");
|
||||
// Text kept by a click lower down, which starts a second box (#74).
|
||||
h.state_mut().views[0].goto = Some((0, 0.0));
|
||||
h.run_steps(4);
|
||||
let p = at(&h, 10.0, 40.0);
|
||||
click(&mut h, p);
|
||||
h.event(egui::Event::Text("Reviewed".into()));
|
||||
h.run_steps(2);
|
||||
let p = at(&h, 10.0, 120.0);
|
||||
click(&mut h, p);
|
||||
h.event(egui::Event::Text("Approved".into()));
|
||||
h.run_steps(2);
|
||||
assert_eq!(texts(&h), ["Reviewed"]);
|
||||
let kept = added(&h)[0].content.rect();
|
||||
assert_eq!(mode_tab_cursors(&mut h, |h| item_on_screen(h, kept)), HAND_ON_EVERY_TAB, "added text under the tabs");
|
||||
// The second box, still being typed.
|
||||
let draft = |h: &Harness<'static, PdfCraftApp>| item_on_screen(h, h.state().views[0].content.draft.as_ref().expect("the open box").rect);
|
||||
assert_eq!(mode_tab_cursors(&mut h, draft), HAND_ON_EVERY_TAB, "the open box under the tabs");
|
||||
let convert = mode_tab(&h, "Convert").center();
|
||||
click(&mut h, convert);
|
||||
assert_eq!(h.state().mode, pdfcraft_ui_egui::Mode::Convert, "the tab takes the click, not the box");
|
||||
}
|
||||
|
||||
@@ -1515,3 +1515,64 @@ fn photos_and_images_dropped_on_the_signature_pad_are_imported_not_opened() {
|
||||
assert_eq!(h.state().views.len(), tabs, "nothing opened behind the dialog");
|
||||
std::fs::remove_file(photo).unwrap();
|
||||
}
|
||||
|
||||
/// 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(egui::accesskit::Role::Button, label).map(|n| n.rect()).find(|r| r.bottom() < 120.0).expect("a mode tab")
|
||||
}
|
||||
|
||||
/// Scroll the page until `what` (on screen) lies across the mode bar, then the cursor over each
|
||||
/// tab it lies under: with the All tools panel closed, Read, Edit, Convert and E-Sign.
|
||||
fn mode_tab_cursors(
|
||||
h: &mut Harness<'static, PdfCraftApp>,
|
||||
what: impl Fn(&Harness<'static, PdfCraftApp>) -> egui::Rect,
|
||||
) -> [(&'static str, egui::CursorIcon); 4] {
|
||||
let page = |h: &Harness<'static, PdfCraftApp>| h.state().views[0].page_screen_rect(0).expect("on screen");
|
||||
let y = what(h).center().y - page(h).top();
|
||||
let tabs = mode_tab(h, "Convert").center().y;
|
||||
h.state_mut().views[0].goto = Some((0, 0.0));
|
||||
h.run_steps(4);
|
||||
let top = page(h).top();
|
||||
h.state_mut().views[0].goto = Some((0, (top - (tabs - y)) / page(h).height()));
|
||||
h.run_steps(4);
|
||||
let under = what(h);
|
||||
["Read", "Edit", "Convert", "E-Sign"].map(|label| {
|
||||
let at = mode_tab(h, label).center();
|
||||
assert!(under.contains(at), "{under:?} lies under {label} at {at:?}");
|
||||
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),
|
||||
];
|
||||
|
||||
/// The Fill & Sign text box scrolled up under the mode bar leaves its tabs the hand (#2755): it
|
||||
/// floated over them, gave them its I-beam and took their clicks.
|
||||
#[test]
|
||||
fn mode_tabs_keep_the_hand_over_a_type_box_scrolled_under_them() {
|
||||
let mut h = harness();
|
||||
assert!(h.state_mut().execute("sign.fill.text"));
|
||||
h.state_mut().set_option("left", "closed").unwrap();
|
||||
// The page across the window, so the box reaches under the tabs.
|
||||
h.state_mut().views[0].fit = pdfcraft_ui_egui::canvas::Fit::Width;
|
||||
h.run_steps(4);
|
||||
assert_eq!(h.state().quick_tool, QuickTool::Fill(FillTool::Text));
|
||||
click(&mut h, 10.0, 360.0);
|
||||
h.event(egui::Event::Text("Ada Lovelace, Countess of Lovelace".into()));
|
||||
h.run_steps(3);
|
||||
assert_eq!(mode_tab_cursors(&mut h, type_box_rect), HAND_ON_EVERY_TAB);
|
||||
let convert = mode_tab(&h, "Convert").center();
|
||||
h.hover_at(convert);
|
||||
h.run_steps(1);
|
||||
h.drag_at(convert);
|
||||
h.run_steps(1);
|
||||
h.drop_at(convert);
|
||||
h.run_steps(3);
|
||||
assert_eq!(h.state().mode, pdfcraft_ui_egui::Mode::Convert, "the tab takes the click, not the text box");
|
||||
}
|
||||
|
||||
@@ -36,6 +36,10 @@ fn click_field(h: &mut Harness<'static, PdfCraftApp>, name: &str, widget: usize)
|
||||
let f = doc.form.iter().find(|f| f.name == name).unwrap();
|
||||
pdfcraft_ui_egui::forms_ui::field_screen_rect(&s.views[0], &doc.info, f, widget).expect("on screen").center()
|
||||
};
|
||||
click_at(h, p);
|
||||
}
|
||||
|
||||
fn click_at(h: &mut Harness<'static, PdfCraftApp>, p: egui::Pos2) {
|
||||
h.hover_at(p);
|
||||
h.run_steps(1);
|
||||
h.drag_at(p);
|
||||
@@ -1008,3 +1012,66 @@ fn auto_size_fields_shrink_and_wrap_typed_text_to_fit_their_box() {
|
||||
// An explicit size is kept, as in Acrobat: the text may then run past the box.
|
||||
assert_eq!(on_screen_fit(&h, "fixed", long).0, 10.0);
|
||||
}
|
||||
|
||||
/// Where a mode bar tab is, by its label.
|
||||
fn mode_tab(h: &Harness<'static, PdfCraftApp>, label: &str) -> egui::Rect {
|
||||
use egui_kittest::kittest::Queryable;
|
||||
h.get_all_by_role_and_label(egui::accesskit::Role::Button, label).map(|n| n.rect()).find(|r| r.bottom() < 120.0).expect("a mode tab")
|
||||
}
|
||||
|
||||
/// Scroll the page until `what` (on screen) lies across the mode bar, then the cursor over each
|
||||
/// tab it lies under: with the All tools panel closed, Read, Edit, Convert and E-Sign.
|
||||
fn mode_tab_cursors(
|
||||
h: &mut Harness<'static, PdfCraftApp>,
|
||||
what: impl Fn(&Harness<'static, PdfCraftApp>) -> egui::Rect,
|
||||
) -> [(&'static str, egui::CursorIcon); 4] {
|
||||
let page = |h: &Harness<'static, PdfCraftApp>| h.state().views[0].page_screen_rect(0).expect("on screen");
|
||||
let y = what(h).center().y - page(h).top();
|
||||
let tabs = mode_tab(h, "Convert").center().y;
|
||||
h.state_mut().views[0].goto = Some((0, 0.0));
|
||||
h.run_steps(4);
|
||||
let top = page(h).top();
|
||||
h.state_mut().views[0].goto = Some((0, (top - (tabs - y)) / page(h).height()));
|
||||
h.run_steps(4);
|
||||
let under = what(h);
|
||||
["Read", "Edit", "Convert", "E-Sign"].map(|label| {
|
||||
let at = mode_tab(h, label).center();
|
||||
assert!(under.contains(at), "{under:?} lies under {label} at {at:?}");
|
||||
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),
|
||||
];
|
||||
|
||||
/// A text field scrolled up under the mode bar leaves its tabs the hand (#2755). The field gave
|
||||
/// them its I-beam, and its open editor floated over them and took their clicks.
|
||||
#[test]
|
||||
fn mode_tabs_keep_the_hand_over_a_text_field_scrolled_under_them() {
|
||||
let mut h = harness();
|
||||
// The page across the window, so the name field reaches under the tabs.
|
||||
h.state_mut().views[0].fit = pdfcraft_ui_egui::canvas::Fit::Width;
|
||||
h.run_steps(4);
|
||||
let name = |h: &Harness<'static, PdfCraftApp>| {
|
||||
let s = h.state();
|
||||
let doc = s.session.get(s.views[0].id).unwrap();
|
||||
let f = doc.form.iter().find(|f| f.name == "name").unwrap();
|
||||
pdfcraft_ui_egui::forms_ui::field_screen_rect(&s.views[0], &doc.info, f, 0).expect("on screen")
|
||||
};
|
||||
assert_eq!(mode_tab_cursors(&mut h, name), HAND_ON_EVERY_TAB, "the field under the tabs");
|
||||
// Open it, where it shows.
|
||||
h.state_mut().views[0].goto = Some((0, 0.0));
|
||||
h.run_steps(4);
|
||||
click_field(&mut h, "name", 0);
|
||||
assert!(h.state().views[0].forms.focus.is_some(), "the editor opened");
|
||||
assert_eq!(mode_tab_cursors(&mut h, name), HAND_ON_EVERY_TAB, "the open field under the tabs");
|
||||
let convert = mode_tab(&h, "Convert").center();
|
||||
click_at(&mut h, convert);
|
||||
assert_eq!(h.state().mode, pdfcraft_ui_egui::Mode::Convert, "the tab takes the click, not the field's editor");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user