mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-29 12:38:58 -06:00
Refactor ui tools for strict Clippy checks
This commit is contained in:
@@ -1,7 +1,10 @@
|
||||
use super::geometry::{drawn_text_box_rect, page_rotation, text_box_rect};
|
||||
use super::paint::{edited_in_view, new_comment};
|
||||
use super::state::{ComposerKind, PageCx};
|
||||
use super::*;
|
||||
use super::{
|
||||
Color32, CommentPrefs, CommentTool, CornerRadius, DocInfo, DocView, Edit, LineEnding, NoteIcon, PagePointer, QuickTool, Shape, Tokens, pos2,
|
||||
theme,
|
||||
};
|
||||
|
||||
/// The floating composer card (new note, new text box, edit). Returns the edit to apply.
|
||||
pub(crate) fn composer(ctx: &egui::Context, view: &mut DocView, info: &DocInfo, prefs: &CommentPrefs) -> Option<Edit> {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::state::PageCx;
|
||||
use super::*;
|
||||
use super::{Annotation, DocInfo, PageInfo, PageText, Pos2, QuickTool, Rect, pos2, vec2};
|
||||
|
||||
pub(super) fn is_markup(subtype: &str) -> bool {
|
||||
matches!(subtype, "Highlight" | "Underline" | "StrikeOut" | "Squiggly")
|
||||
|
||||
@@ -4,7 +4,7 @@ use super::geometry::{
|
||||
};
|
||||
use super::paint::{drawn_shape, edit_anchor, erase, new_comment};
|
||||
use super::state::{Composer, ComposerKind, Gesture, PageCx};
|
||||
use super::*;
|
||||
use super::{Annotation, CommentTool, DocInfo, DocView, Edit, Pos2, QuickTool, Rect, Shape, vec2};
|
||||
|
||||
/// Comment input on one page, before text selection runs. Returns `true` when the pointer
|
||||
/// gesture belongs to commenting (text selection must ignore it).
|
||||
@@ -31,247 +31,15 @@ pub(crate) fn page_input(ui: &egui::Ui, resp: &egui::Response, cx: &PageCx<'_>,
|
||||
cv.context_at = Some((cx.page, cx.to_user(p)));
|
||||
cv.selected = cx.hit(p).map(|a| (cx.page, a.index));
|
||||
}
|
||||
let gestures = Gestures { ui, resp, cx, pointer, origin, page_rect, pressed_here, over_page };
|
||||
match cx.tool {
|
||||
// The shape just drawn is selected with its handles showing: they resize it without a
|
||||
// trip to the Select tool (#2338). Anywhere else the tool keeps drawing.
|
||||
QuickTool::Comment(_) if draw_grab => select_input(ui, resp, cx, view, pointer, origin, pressed_here),
|
||||
QuickTool::Comment(tool) if tool.draws() => {
|
||||
if !cx.allowed {
|
||||
return false;
|
||||
}
|
||||
if over_page {
|
||||
ui.ctx().set_cursor_icon(egui::CursorIcon::Crosshair);
|
||||
}
|
||||
if resp.drag_started()
|
||||
&& pressed_here
|
||||
&& let Some(o) = origin
|
||||
{
|
||||
cv.gesture = Some(Gesture::Draw { page: cx.page, tool, points: vec![cx.to_user(o)] });
|
||||
cv.selected = None;
|
||||
}
|
||||
if let Some(Gesture::Draw { page, tool, points }) = cv.gesture.as_mut()
|
||||
&& *page == cx.page
|
||||
&& let Some(p) = pointer
|
||||
{
|
||||
let p = clamp_to(page_rect, p);
|
||||
let u = cx.to_user(p);
|
||||
if matches!(*tool, CommentTool::Ink | CommentTool::Eraser) {
|
||||
let last = points.last().map(|l| cx.to_screen(*l)).unwrap_or(p);
|
||||
if last.distance(p) >= 1.5 {
|
||||
points.push(u);
|
||||
}
|
||||
} else {
|
||||
points.truncate(1);
|
||||
points.push(u);
|
||||
}
|
||||
}
|
||||
if resp.drag_stopped()
|
||||
&& let Some(Gesture::Draw { page, tool, points }) = cv.gesture.clone()
|
||||
&& page == cx.page
|
||||
{
|
||||
cv.gesture = None;
|
||||
if tool == CommentTool::Eraser {
|
||||
view.pending_edit = erase(cx, &points);
|
||||
} else if let Some(shape) = drawn_shape(tool, &points) {
|
||||
let now = ui.input(|i| i.time);
|
||||
cv.settle(now, page, tool, &points);
|
||||
view.pending_edit = Some(new_comment(cx, tool, shape, String::new()));
|
||||
}
|
||||
}
|
||||
// A quick press/release does not start an egui drag, but Acrobat's Draw tool treats
|
||||
// it as a one-point ink stroke (a dot). The appearance uses round line caps, so a
|
||||
// single PDF point is visible and remains editable like any other drawing.
|
||||
if tool == CommentTool::Ink
|
||||
&& resp.clicked()
|
||||
&& over_page
|
||||
&& let Some(p) = pointer
|
||||
&& let Some(shape) = drawn_shape(tool, &[cx.to_user(p)])
|
||||
{
|
||||
cv.selected = None;
|
||||
let now = ui.input(|i| i.time);
|
||||
cv.settle(now, cx.page, tool, &[cx.to_user(p)]);
|
||||
view.pending_edit = Some(new_comment(cx, tool, shape, String::new()));
|
||||
}
|
||||
true
|
||||
}
|
||||
QuickTool::Comment(tool) if tool.clicks_points() => {
|
||||
if !cx.allowed {
|
||||
return false;
|
||||
}
|
||||
if over_page {
|
||||
ui.ctx().set_cursor_icon(egui::CursorIcon::Crosshair);
|
||||
}
|
||||
let finish = resp.double_clicked() || ui.input(|i| i.key_pressed(egui::Key::Enter));
|
||||
// Backspace (or Delete) takes back the last point, e.g. after a wrong click; Undo does too.
|
||||
// Only while a drawing is in progress: otherwise Delete belongs to the selected comment.
|
||||
if matches!(cv.gesture, Some(Gesture::Draw { page, .. }) if page == cx.page)
|
||||
&& !ui.ctx().egui_wants_keyboard_input()
|
||||
&& ui.input_mut(|i| {
|
||||
i.consume_key(egui::Modifiers::NONE, egui::Key::Backspace) || i.consume_key(egui::Modifiers::NONE, egui::Key::Delete)
|
||||
})
|
||||
{
|
||||
cv.undo_point();
|
||||
return true;
|
||||
}
|
||||
if resp.clicked()
|
||||
&& over_page
|
||||
&& let Some(p) = pointer
|
||||
{
|
||||
let u = cx.to_user(p);
|
||||
match cv.gesture.as_mut() {
|
||||
Some(Gesture::Draw { page, tool: t, points }) if *page == cx.page && *t == tool => {
|
||||
// Clicking the first point again closes a polygon.
|
||||
let closes = points.len() >= 3 && tool != CommentTool::PolyLine && cx.to_screen(points[0]).distance(p) <= 6.0;
|
||||
let repeat = points.last().is_some_and(|l| cx.to_screen(*l).distance(p) < 2.0);
|
||||
if closes {
|
||||
let pts = std::mem::take(points);
|
||||
cv.gesture = None;
|
||||
if let Some(shape) = drawn_shape(tool, &pts) {
|
||||
view.pending_edit = Some(new_comment(cx, tool, shape, String::new()));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if !repeat {
|
||||
points.push(u);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
cv.gesture = Some(Gesture::Draw { page: cx.page, tool, points: vec![u] });
|
||||
cv.selected = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
if finish
|
||||
&& let Some(Gesture::Draw { page, tool: t, points }) = cv.gesture.clone()
|
||||
&& page == cx.page
|
||||
&& t == tool
|
||||
{
|
||||
cv.gesture = None;
|
||||
if let Some(shape) = drawn_shape(tool, &points) {
|
||||
view.pending_edit = Some(new_comment(cx, tool, shape, String::new()));
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
QuickTool::Comment(CommentTool::Callout) => {
|
||||
// Drag from what the callout points at to where its text box goes.
|
||||
if !cx.allowed {
|
||||
return false;
|
||||
}
|
||||
if over_page {
|
||||
ui.ctx().set_cursor_icon(egui::CursorIcon::Crosshair);
|
||||
}
|
||||
if resp.drag_started()
|
||||
&& pressed_here
|
||||
&& let Some(o) = origin
|
||||
{
|
||||
cv.gesture = Some(Gesture::Draw { page: cx.page, tool: CommentTool::Callout, points: vec![cx.to_user(o)] });
|
||||
cv.selected = None;
|
||||
}
|
||||
if let Some(Gesture::Draw { page, points, .. }) = cv.gesture.as_mut()
|
||||
&& *page == cx.page
|
||||
&& let Some(p) = pointer
|
||||
{
|
||||
points.truncate(1);
|
||||
points.push(cx.to_user(clamp_to(page_rect, p)));
|
||||
}
|
||||
if resp.drag_stopped()
|
||||
&& let Some(Gesture::Draw { page, points, .. }) = cv.gesture.clone()
|
||||
&& page == cx.page
|
||||
{
|
||||
cv.gesture = None;
|
||||
if let [point, at] = points[..]
|
||||
&& (point[0] - at[0]).hypot(point[1] - at[1]) >= 8.0
|
||||
{
|
||||
cv.composer = Some(Composer { page, at, kind: ComposerKind::Callout { point }, text: String::new(), focus: true });
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
QuickTool::Comment(CommentTool::Attach) => {
|
||||
if !cx.allowed {
|
||||
return false;
|
||||
}
|
||||
if over_page {
|
||||
ui.ctx().set_cursor_icon(egui::CursorIcon::Crosshair);
|
||||
}
|
||||
if resp.clicked()
|
||||
&& over_page
|
||||
&& let Some(p) = pointer
|
||||
{
|
||||
cv.attach_at = Some((cx.page, cx.to_user(p)));
|
||||
cv.selected = None;
|
||||
}
|
||||
true
|
||||
}
|
||||
QuickTool::Comment(CommentTool::Caret) => {
|
||||
if !cx.allowed {
|
||||
return false;
|
||||
}
|
||||
if over_page {
|
||||
ui.ctx().set_cursor_icon(egui::CursorIcon::Text);
|
||||
}
|
||||
if resp.clicked()
|
||||
&& over_page
|
||||
&& let Some(p) = pointer
|
||||
{
|
||||
let (vx, vy) = cx.xf.screen_to_view(p);
|
||||
let snapped = cx.info.pages.get(cx.page).zip(caret_text.as_deref()).and_then(|(page, t)| snapped_caret(page, t, vx, vy));
|
||||
let (at, size) = snapped.unwrap_or_else(|| (cx.to_user(p), CARET_SIZE));
|
||||
cv.composer = Some(Composer { page: cx.page, at, kind: ComposerKind::Caret { size }, text: String::new(), focus: true });
|
||||
cv.selected = None;
|
||||
}
|
||||
true
|
||||
}
|
||||
QuickTool::Comment(tool @ (CommentTool::Note | CommentTool::TextBox)) => {
|
||||
if !cx.allowed {
|
||||
return false;
|
||||
}
|
||||
if over_page {
|
||||
ui.ctx().set_cursor_icon(egui::CursorIcon::Crosshair);
|
||||
}
|
||||
// A text box also takes a drag, as in Acrobat: the box is drawn at that size and its
|
||||
// text wraps inside it (#2669).
|
||||
if tool == CommentTool::TextBox {
|
||||
if resp.drag_started()
|
||||
&& pressed_here
|
||||
&& let Some(o) = origin
|
||||
{
|
||||
cv.gesture = Some(Gesture::Draw { page: cx.page, tool, points: vec![cx.to_user(o)] });
|
||||
cv.selected = None;
|
||||
}
|
||||
if let Some(Gesture::Draw { page, points, .. }) = cv.gesture.as_mut()
|
||||
&& *page == cx.page
|
||||
&& let Some(p) = pointer
|
||||
{
|
||||
points.truncate(1);
|
||||
points.push(cx.to_user(clamp_to(page_rect, p)));
|
||||
}
|
||||
if resp.drag_stopped()
|
||||
&& let Some(Gesture::Draw { page, points, .. }) = cv.gesture.clone()
|
||||
&& page == cx.page
|
||||
{
|
||||
cv.gesture = None;
|
||||
if let (Some(first), Some(last)) = (points.first(), points.last()) {
|
||||
let r = Rect::from_two_pos(cx.to_screen(*first), cx.to_screen(*last));
|
||||
// A drag of a few pixels is a click: the box fits its text.
|
||||
let drawn =
|
||||
(r.width() >= 6.0 && r.height() >= 6.0).then(|| cx.info.pages.get(page)).flatten().map(|p| drawn_box(p, *first, *last));
|
||||
let (at, size) = drawn.map_or((*first, None), |(at, size)| (at, Some(size)));
|
||||
cv.composer = Some(Composer { page, at, kind: ComposerKind::TextBox { size }, text: String::new(), focus: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
if resp.clicked()
|
||||
&& over_page
|
||||
&& let Some(p) = pointer
|
||||
{
|
||||
let kind = if tool == CommentTool::Note { ComposerKind::Note } else { ComposerKind::TextBox { size: None } };
|
||||
cv.composer = Some(Composer { page: cx.page, at: cx.to_user(p), kind, text: String::new(), focus: true });
|
||||
cv.selected = None;
|
||||
}
|
||||
true
|
||||
}
|
||||
QuickTool::Comment(tool) if tool.draws() => gestures.draw_shape(view, tool),
|
||||
QuickTool::Comment(tool) if tool.clicks_points() => gestures.click_points(view, tool),
|
||||
QuickTool::Comment(CommentTool::Callout) => gestures.callout(view),
|
||||
QuickTool::Comment(CommentTool::Attach) => gestures.attach(view),
|
||||
QuickTool::Comment(CommentTool::Caret) => gestures.caret(view, caret_text.as_deref()),
|
||||
QuickTool::Comment(tool @ (CommentTool::Note | CommentTool::TextBox)) => gestures.note_or_box(view, tool),
|
||||
QuickTool::Select => select_input(ui, resp, cx, view, pointer, origin, pressed_here),
|
||||
QuickTool::Fill(_) if fill_grab => select_input(ui, resp, cx, view, pointer, origin, pressed_here),
|
||||
_ => false,
|
||||
@@ -512,6 +280,274 @@ pub(crate) fn page_after_text(resp: &egui::Response, cx: &PageCx<'_>, view: &mut
|
||||
}
|
||||
}
|
||||
|
||||
/// What the pointer is doing on one page, for the comment tools that draw or place things.
|
||||
#[derive(Clone, Copy)]
|
||||
struct Gestures<'a> {
|
||||
ui: &'a egui::Ui,
|
||||
resp: &'a egui::Response,
|
||||
cx: &'a PageCx<'a>,
|
||||
pointer: Option<Pos2>,
|
||||
origin: Option<Pos2>,
|
||||
page_rect: Rect,
|
||||
pressed_here: bool,
|
||||
over_page: bool,
|
||||
}
|
||||
|
||||
impl Gestures<'_> {
|
||||
fn draw_shape(&self, view: &mut DocView, tool: CommentTool) -> bool {
|
||||
let Self { ui, resp, cx, pointer, origin, page_rect, pressed_here, over_page } = *self;
|
||||
let cv = &mut view.comments;
|
||||
if !cx.allowed {
|
||||
return false;
|
||||
}
|
||||
if over_page {
|
||||
ui.ctx().set_cursor_icon(egui::CursorIcon::Crosshair);
|
||||
}
|
||||
if resp.drag_started()
|
||||
&& pressed_here
|
||||
&& let Some(o) = origin
|
||||
{
|
||||
cv.gesture = Some(Gesture::Draw { page: cx.page, tool, points: vec![cx.to_user(o)] });
|
||||
cv.selected = None;
|
||||
}
|
||||
if let Some(Gesture::Draw { page, tool, points }) = cv.gesture.as_mut()
|
||||
&& *page == cx.page
|
||||
&& let Some(p) = pointer
|
||||
{
|
||||
let p = clamp_to(page_rect, p);
|
||||
let u = cx.to_user(p);
|
||||
if matches!(*tool, CommentTool::Ink | CommentTool::Eraser) {
|
||||
let last = points.last().map(|l| cx.to_screen(*l)).unwrap_or(p);
|
||||
if last.distance(p) >= 1.5 {
|
||||
points.push(u);
|
||||
}
|
||||
} else {
|
||||
points.truncate(1);
|
||||
points.push(u);
|
||||
}
|
||||
}
|
||||
if resp.drag_stopped()
|
||||
&& let Some(Gesture::Draw { page, tool, points }) = cv.gesture.clone()
|
||||
&& page == cx.page
|
||||
{
|
||||
cv.gesture = None;
|
||||
if tool == CommentTool::Eraser {
|
||||
view.pending_edit = erase(cx, &points);
|
||||
} else if let Some(shape) = drawn_shape(tool, &points) {
|
||||
let now = ui.input(|i| i.time);
|
||||
cv.settle(now, page, tool, &points);
|
||||
view.pending_edit = Some(new_comment(cx, tool, shape, String::new()));
|
||||
}
|
||||
}
|
||||
// A quick press/release does not start an egui drag, but Acrobat's Draw tool treats
|
||||
// it as a one-point ink stroke (a dot). The appearance uses round line caps, so a
|
||||
// single PDF point is visible and remains editable like any other drawing.
|
||||
if tool == CommentTool::Ink
|
||||
&& resp.clicked()
|
||||
&& over_page
|
||||
&& let Some(p) = pointer
|
||||
&& let Some(shape) = drawn_shape(tool, &[cx.to_user(p)])
|
||||
{
|
||||
cv.selected = None;
|
||||
let now = ui.input(|i| i.time);
|
||||
cv.settle(now, cx.page, tool, &[cx.to_user(p)]);
|
||||
view.pending_edit = Some(new_comment(cx, tool, shape, String::new()));
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn click_points(&self, view: &mut DocView, tool: CommentTool) -> bool {
|
||||
let Self { ui, resp, cx, pointer, over_page, .. } = *self;
|
||||
let cv = &mut view.comments;
|
||||
if !cx.allowed {
|
||||
return false;
|
||||
}
|
||||
if over_page {
|
||||
ui.ctx().set_cursor_icon(egui::CursorIcon::Crosshair);
|
||||
}
|
||||
let finish = resp.double_clicked() || ui.input(|i| i.key_pressed(egui::Key::Enter));
|
||||
// Backspace (or Delete) takes back the last point, e.g. after a wrong click; Undo does too.
|
||||
// Only while a drawing is in progress: otherwise Delete belongs to the selected comment.
|
||||
if matches!(cv.gesture, Some(Gesture::Draw { page, .. }) if page == cx.page)
|
||||
&& !ui.ctx().egui_wants_keyboard_input()
|
||||
&& ui.input_mut(|i| i.consume_key(egui::Modifiers::NONE, egui::Key::Backspace) || i.consume_key(egui::Modifiers::NONE, egui::Key::Delete))
|
||||
{
|
||||
cv.undo_point();
|
||||
return true;
|
||||
}
|
||||
if resp.clicked()
|
||||
&& over_page
|
||||
&& let Some(p) = pointer
|
||||
{
|
||||
let u = cx.to_user(p);
|
||||
match cv.gesture.as_mut() {
|
||||
Some(Gesture::Draw { page, tool: t, points }) if *page == cx.page && *t == tool => {
|
||||
// Clicking the first point again closes a polygon.
|
||||
let closes = points.len() >= 3 && tool != CommentTool::PolyLine && cx.to_screen(points[0]).distance(p) <= 6.0;
|
||||
let repeat = points.last().is_some_and(|l| cx.to_screen(*l).distance(p) < 2.0);
|
||||
if closes {
|
||||
let pts = std::mem::take(points);
|
||||
cv.gesture = None;
|
||||
if let Some(shape) = drawn_shape(tool, &pts) {
|
||||
view.pending_edit = Some(new_comment(cx, tool, shape, String::new()));
|
||||
}
|
||||
return true;
|
||||
}
|
||||
if !repeat {
|
||||
points.push(u);
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
cv.gesture = Some(Gesture::Draw { page: cx.page, tool, points: vec![u] });
|
||||
cv.selected = None;
|
||||
}
|
||||
}
|
||||
}
|
||||
if finish
|
||||
&& let Some(Gesture::Draw { page, tool: t, points }) = cv.gesture.clone()
|
||||
&& page == cx.page
|
||||
&& t == tool
|
||||
{
|
||||
cv.gesture = None;
|
||||
if let Some(shape) = drawn_shape(tool, &points) {
|
||||
view.pending_edit = Some(new_comment(cx, tool, shape, String::new()));
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn callout(&self, view: &mut DocView) -> bool {
|
||||
let Self { ui, resp, cx, pointer, origin, page_rect, pressed_here, over_page } = *self;
|
||||
let cv = &mut view.comments;
|
||||
// Drag from what the callout points at to where its text box goes.
|
||||
if !cx.allowed {
|
||||
return false;
|
||||
}
|
||||
if over_page {
|
||||
ui.ctx().set_cursor_icon(egui::CursorIcon::Crosshair);
|
||||
}
|
||||
if resp.drag_started()
|
||||
&& pressed_here
|
||||
&& let Some(o) = origin
|
||||
{
|
||||
cv.gesture = Some(Gesture::Draw { page: cx.page, tool: CommentTool::Callout, points: vec![cx.to_user(o)] });
|
||||
cv.selected = None;
|
||||
}
|
||||
if let Some(Gesture::Draw { page, points, .. }) = cv.gesture.as_mut()
|
||||
&& *page == cx.page
|
||||
&& let Some(p) = pointer
|
||||
{
|
||||
points.truncate(1);
|
||||
points.push(cx.to_user(clamp_to(page_rect, p)));
|
||||
}
|
||||
if resp.drag_stopped()
|
||||
&& let Some(Gesture::Draw { page, points, .. }) = cv.gesture.clone()
|
||||
&& page == cx.page
|
||||
{
|
||||
cv.gesture = None;
|
||||
if let [point, at] = points[..]
|
||||
&& (point[0] - at[0]).hypot(point[1] - at[1]) >= 8.0
|
||||
{
|
||||
cv.composer = Some(Composer { page, at, kind: ComposerKind::Callout { point }, text: String::new(), focus: true });
|
||||
}
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn attach(&self, view: &mut DocView) -> bool {
|
||||
let Self { ui, resp, cx, pointer, over_page, .. } = *self;
|
||||
let cv = &mut view.comments;
|
||||
if !cx.allowed {
|
||||
return false;
|
||||
}
|
||||
if over_page {
|
||||
ui.ctx().set_cursor_icon(egui::CursorIcon::Crosshair);
|
||||
}
|
||||
if resp.clicked()
|
||||
&& over_page
|
||||
&& let Some(p) = pointer
|
||||
{
|
||||
cv.attach_at = Some((cx.page, cx.to_user(p)));
|
||||
cv.selected = None;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn caret(&self, view: &mut DocView, caret_text: Option<&pdfcraft_render::PageText>) -> bool {
|
||||
let Self { ui, resp, cx, pointer, over_page, .. } = *self;
|
||||
let cv = &mut view.comments;
|
||||
if !cx.allowed {
|
||||
return false;
|
||||
}
|
||||
if over_page {
|
||||
ui.ctx().set_cursor_icon(egui::CursorIcon::Text);
|
||||
}
|
||||
if resp.clicked()
|
||||
&& over_page
|
||||
&& let Some(p) = pointer
|
||||
{
|
||||
let (vx, vy) = cx.xf.screen_to_view(p);
|
||||
let snapped = cx.info.pages.get(cx.page).zip(caret_text).and_then(|(page, t)| snapped_caret(page, t, vx, vy));
|
||||
let (at, size) = snapped.unwrap_or_else(|| (cx.to_user(p), CARET_SIZE));
|
||||
cv.composer = Some(Composer { page: cx.page, at, kind: ComposerKind::Caret { size }, text: String::new(), focus: true });
|
||||
cv.selected = None;
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
fn note_or_box(&self, view: &mut DocView, tool: CommentTool) -> bool {
|
||||
let Self { ui, resp, cx, pointer, origin, page_rect, pressed_here, over_page } = *self;
|
||||
let cv = &mut view.comments;
|
||||
if !cx.allowed {
|
||||
return false;
|
||||
}
|
||||
if over_page {
|
||||
ui.ctx().set_cursor_icon(egui::CursorIcon::Crosshair);
|
||||
}
|
||||
// A text box also takes a drag, as in Acrobat: the box is drawn at that size and its
|
||||
// text wraps inside it (#2669).
|
||||
if tool == CommentTool::TextBox {
|
||||
if resp.drag_started()
|
||||
&& pressed_here
|
||||
&& let Some(o) = origin
|
||||
{
|
||||
cv.gesture = Some(Gesture::Draw { page: cx.page, tool, points: vec![cx.to_user(o)] });
|
||||
cv.selected = None;
|
||||
}
|
||||
if let Some(Gesture::Draw { page, points, .. }) = cv.gesture.as_mut()
|
||||
&& *page == cx.page
|
||||
&& let Some(p) = pointer
|
||||
{
|
||||
points.truncate(1);
|
||||
points.push(cx.to_user(clamp_to(page_rect, p)));
|
||||
}
|
||||
if resp.drag_stopped()
|
||||
&& let Some(Gesture::Draw { page, points, .. }) = cv.gesture.clone()
|
||||
&& page == cx.page
|
||||
{
|
||||
cv.gesture = None;
|
||||
if let (Some(first), Some(last)) = (points.first(), points.last()) {
|
||||
let r = Rect::from_two_pos(cx.to_screen(*first), cx.to_screen(*last));
|
||||
// A drag of a few pixels is a click: the box fits its text.
|
||||
let drawn =
|
||||
(r.width() >= 6.0 && r.height() >= 6.0).then(|| cx.info.pages.get(page)).flatten().map(|p| drawn_box(p, *first, *last));
|
||||
let (at, size) = drawn.map_or((*first, None), |(at, size)| (at, Some(size)));
|
||||
cv.composer = Some(Composer { page, at, kind: ComposerKind::TextBox { size }, text: String::new(), focus: true });
|
||||
}
|
||||
}
|
||||
}
|
||||
if resp.clicked()
|
||||
&& over_page
|
||||
&& let Some(p) = pointer
|
||||
{
|
||||
let kind = if tool == CommentTool::Note { ComposerKind::Note } else { ComposerKind::TextBox { size: None } };
|
||||
cv.composer = Some(Composer { page: cx.page, at: cx.to_user(p), kind, text: String::new(), focus: true });
|
||||
cv.selected = None;
|
||||
}
|
||||
true
|
||||
}
|
||||
}
|
||||
|
||||
/// Delete / Escape handling for comments (only while no text field has focus).
|
||||
pub(crate) fn keys(ctx: &egui::Context, view: &mut DocView, tool: &mut QuickTool, allowed: bool) {
|
||||
if !ctx.input(|i| i.pointer.any_down()) {
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use super::paint::edit_anchor;
|
||||
use super::state::{Composer, ComposerKind};
|
||||
use super::*;
|
||||
use super::{
|
||||
Annotation, Color32, CommentPrefs, CommentTool, DocInfo, DocView, Edit, Rect, ReviewState, Rgb, SELECT_BLUE, SWATCHES, Sense, Stroke, color32,
|
||||
icons, theme, tool_for, vec2,
|
||||
};
|
||||
|
||||
/// Save attachment… and, for a PDF, Open in a new tab, first in a file attachment comment's
|
||||
/// menus as in the Attachments panel: the comment is where the reader finds its file. Reading
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
use super::geometry::{HANDLES, grabs_points, handle_pos, is_markup, reshapable, reshaped, resizable};
|
||||
use super::state::{Gesture, PageCx, SETTLE_SECS};
|
||||
use super::*;
|
||||
use super::{
|
||||
Annotation, Color32, CommentTool, CornerRadius, DocInfo, DocView, Edit, LineEnding, NewAnnotation, Pos2, QuickTool, Rect, SELECT_BLUE, Shape,
|
||||
Stroke, color32, vec2,
|
||||
};
|
||||
|
||||
/// Paint comment selection, hover and gesture previews on a page.
|
||||
pub(crate) fn paint_page(ui: &egui::Ui, painter: &egui::Painter, cx: &PageCx<'_>, view: &DocView) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::geometry::{corner_ratio, is_markup, resized};
|
||||
use super::*;
|
||||
use super::{Annotation, CommentPrefs, CommentTool, DocInfo, Edit, PagePointer, PageXform, Pos2, QuickTool, Rect};
|
||||
|
||||
/// The user's login name, which Acrobat uses as the default comment author.
|
||||
pub(super) fn login_name() -> String {
|
||||
|
||||
@@ -20,7 +20,7 @@ mod layout;
|
||||
|
||||
pub(crate) use background::TextBackground;
|
||||
pub use image::{CropMode, ImageSelection};
|
||||
pub(crate) use image::{ImageAction, image_input, image_owns_pointer};
|
||||
pub(crate) use image::{ImageAction, ImageCx, image_input, image_owns_pointer};
|
||||
#[cfg(test)]
|
||||
use image::{rotate_knob, turn_degrees};
|
||||
#[cfg(test)]
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::*;
|
||||
use super::{Arc, Color32, Edit, LineEditor, PageXform, Pos2, TextTarget};
|
||||
|
||||
/// A cached, read-only page background. The source glyphs are removed before rasterization;
|
||||
/// colour, pictures and unselected text beneath the editor remain visible.
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::*;
|
||||
use super::{ACCENT, Color32, CornerRadius, DocInfo, DocView, Edit, MAX_OUTLINES, PagePointer, PageXform, Pos2, Rect, Stroke, user_box};
|
||||
|
||||
/// A selected page image, and what the pointer is doing to it.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -46,6 +46,18 @@ impl CropMode {
|
||||
}
|
||||
}
|
||||
|
||||
/// What the pointer and the page are, for the image tools on one page.
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) struct ImageCx<'a> {
|
||||
pub ui: &'a egui::Ui,
|
||||
pub resp: &'a egui::Response,
|
||||
pub xf: &'a PageXform,
|
||||
pub ptr: &'a PagePointer,
|
||||
pub page: usize,
|
||||
pub info: &'a DocInfo,
|
||||
pub allow_interaction: bool,
|
||||
}
|
||||
|
||||
/// The screen boxes of the crop's eight handles: corners, then the top, right, bottom and left
|
||||
/// edge midpoints.
|
||||
fn crop_handles(k: Rect) -> [Pos2; 8] {
|
||||
@@ -54,19 +66,8 @@ fn crop_handles(k: Rect) -> [Pos2; 8] {
|
||||
|
||||
/// Crop mode for the selected image `i` whose visible frame is `b` (screen). Returns `true`
|
||||
/// (the pointer and keys are the crop's while it lasts).
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn crop_input(
|
||||
ui: &egui::Ui,
|
||||
resp: &egui::Response,
|
||||
xf: &PageXform,
|
||||
ptr: &PagePointer,
|
||||
page: usize,
|
||||
info: &DocInfo,
|
||||
view: &mut DocView,
|
||||
i: usize,
|
||||
b: Rect,
|
||||
allow_interaction: bool,
|
||||
) -> bool {
|
||||
fn crop_input(cx: &ImageCx<'_>, view: &mut DocView, i: usize, b: Rect) -> bool {
|
||||
let ImageCx { ui, resp, xf, ptr, page, info, allow_interaction } = *cx;
|
||||
let painter = ui.painter();
|
||||
let Some(mode) = view.image_selection.as_mut().and_then(|s| s.crop.as_mut()) else { return false };
|
||||
let hover_pos = ptr.pos;
|
||||
@@ -276,44 +277,26 @@ pub(super) fn turn_degrees(centre: Pos2, pointer: Pos2, snap: bool) -> f32 {
|
||||
if snap { (d / 15.0).round() * 15.0 } else { d }
|
||||
}
|
||||
|
||||
/// Images on a page: select, move, resize, right-click. Returns `true` when the pointer was used.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
pub(crate) fn image_input(
|
||||
ui: &egui::Ui,
|
||||
resp: &egui::Response,
|
||||
xf: &PageXform,
|
||||
ptr: &PagePointer,
|
||||
page: usize,
|
||||
info: &DocInfo,
|
||||
images: &[pdfcraft_engine::PageImage],
|
||||
view: &mut DocView,
|
||||
action: &mut Option<ImageAction>,
|
||||
allow_interaction: bool,
|
||||
) -> bool {
|
||||
let boxes: Vec<Rect> = images.iter().map(|im| im.visible_rect.map_or(Rect::NOTHING, |r| xf.user_rect(info, page, r.map(|v| v as f32)))).collect();
|
||||
let painter = ui.painter();
|
||||
let hover_pos = ptr.pos;
|
||||
let image_contains = |index: usize, point: Pos2| {
|
||||
let (x, y) = xf.screen_to_view(point);
|
||||
let Some(p) = info.pages.get(page) else { return false };
|
||||
let user = p.view_to_user(x, y);
|
||||
images.get(index).is_some_and(|im| im.contains(user.map(f64::from)))
|
||||
};
|
||||
let selected = view.image_selection.as_ref().filter(|s| s.page == page).map(|s| s.index).filter(|i| *i < boxes.len());
|
||||
let dragging = view.image_selection.as_ref().is_some_and(|s| s.drag.is_some());
|
||||
// Few images: faint outlines for all of them, as before. Many: only the selected and
|
||||
// hovered boxes, so a ~1000-image CAD page stays interactive in Edit text & images.
|
||||
// Fully clipped placements (empty visible frames) get no outline.
|
||||
let faint_all = boxes.len() <= MAX_OUTLINES;
|
||||
// The picture being dragged has its frame at the pointer; its outline where it was would
|
||||
// trail behind until the release.
|
||||
let moving = selected.filter(|_| dragging);
|
||||
/// Which boxes are outlined on the page, and why.
|
||||
struct OutlineState {
|
||||
selected: Option<usize>,
|
||||
hovered_box: Option<usize>,
|
||||
/// The picture being dragged, whose outline would trail behind until the release.
|
||||
moving: Option<usize>,
|
||||
dragging: bool,
|
||||
/// Few images: faint outlines for all of them.
|
||||
faint_all: bool,
|
||||
}
|
||||
|
||||
/// Outlines the images: all of them faintly when there are few, else the selected and hovered
|
||||
/// ones; the hovered or selected one is emphasised.
|
||||
fn draw_outlines(painter: &egui::Painter, boxes: &[Rect], state: OutlineState) {
|
||||
let OutlineState { selected, hovered_box, moving, dragging, faint_all } = state;
|
||||
if faint_all {
|
||||
for (_, b) in boxes.iter().enumerate().filter(|(i, b)| b.is_positive() && moving != Some(*i)) {
|
||||
painter.rect_stroke(*b, CornerRadius::ZERO, Stroke::new(0.75, ACCENT.gamma_multiply(0.35)), egui::StrokeKind::Outside);
|
||||
}
|
||||
}
|
||||
let hovered_box = hover_pos.and_then(|p| boxes.iter().enumerate().rposition(|(i, b)| b.is_positive() && b.contains(p) && image_contains(i, p)));
|
||||
if !faint_all {
|
||||
for i in selected.filter(|_| moving.is_none()).into_iter().chain(hovered_box.filter(|h| Some(*h) != selected)) {
|
||||
if let Some(b) = boxes.get(i).filter(|b| b.is_positive()) {
|
||||
@@ -332,24 +315,169 @@ pub(crate) fn image_input(
|
||||
painter.rect_stroke(b, CornerRadius::ZERO, Stroke::new(1.5, ACCENT.gamma_multiply(0.7)), egui::StrokeKind::Outside);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn draw_handles(painter: &egui::Painter, rect: Rect) {
|
||||
for c in [rect.left_top(), rect.right_top(), rect.left_bottom(), rect.right_bottom()] {
|
||||
painter.rect(
|
||||
Rect::from_center_size(c, egui::vec2(8.0, 8.0)),
|
||||
CornerRadius::ZERO,
|
||||
Color32::WHITE,
|
||||
Stroke::new(1.0, ACCENT),
|
||||
egui::StrokeKind::Middle,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
/// Turning the picture by its rotate handle: the turned outline as a preview (the engine redraws
|
||||
/// the pixels on release).
|
||||
fn turn_preview(cx: &ImageCx<'_>, view: &mut DocView, i: usize, b: Rect, knob: Pos2) {
|
||||
let ImageCx { ui, resp, ptr, page, .. } = *cx;
|
||||
let painter = ui.painter();
|
||||
let hover_pos = ptr.pos;
|
||||
let shift = ui.input(|i| i.modifiers.shift);
|
||||
let degrees = hover_pos.map_or(0.0, |p| turn_degrees(b.center(), p, shift));
|
||||
// The turned outline as a preview; the engine redraws the pixels on release.
|
||||
let centre = b.center();
|
||||
let (sin, cos) = degrees.to_radians().sin_cos();
|
||||
let turn = |c: Pos2| {
|
||||
let v = c - centre;
|
||||
centre + egui::vec2(v.x * cos - v.y * sin, v.x * sin + v.y * cos)
|
||||
};
|
||||
let outline = [b.left_top(), b.right_top(), b.right_bottom(), b.left_bottom()].map(turn).to_vec();
|
||||
painter.add(egui::Shape::closed_line(outline, Stroke::new(1.5, ACCENT)));
|
||||
let galley = painter.layout_no_wrap(format!("{degrees:.0}\u{b0}"), egui::FontId::proportional(12.0), ACCENT);
|
||||
let label = Rect::from_center_size(turn(knob) - egui::vec2(0.0, 14.0), galley.size() + egui::vec2(8.0, 4.0));
|
||||
painter.rect(label, CornerRadius::same(3), Color32::WHITE, Stroke::new(1.0, ACCENT), egui::StrokeKind::Middle);
|
||||
painter.galley(label.center() - galley.size() / 2.0, galley, ACCENT);
|
||||
ui.ctx().set_cursor_icon(egui::CursorIcon::Grabbing);
|
||||
if resp.drag_stopped() || !ptr.primary_down {
|
||||
if let Some(s) = view.image_selection.as_mut() {
|
||||
s.drag = None;
|
||||
s.turning = false;
|
||||
}
|
||||
if degrees.abs() >= 0.05 {
|
||||
view.pending_edit = Some(Edit::EditPageImage { page, index: i, change: pdfcraft_engine::ImageEdit::RotateBy(f64::from(degrees)) });
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Where the screen may still show the picture from before an applied move, and whether the
|
||||
/// page's whole raster is the old one.
|
||||
#[derive(Clone, Copy)]
|
||||
struct Blanked {
|
||||
left_at: Option<Rect>,
|
||||
raster_old: bool,
|
||||
}
|
||||
|
||||
/// A frame of the picture being moved or resized: the stretched picture, its frame and handles,
|
||||
/// and on release the edit.
|
||||
fn drag_preview(
|
||||
cx: &ImageCx<'_>,
|
||||
images: &[pdfcraft_engine::PageImage],
|
||||
view: &mut DocView,
|
||||
(i, b): (usize, Rect),
|
||||
(start, opposite, p): (Pos2, Option<Pos2>, Pos2),
|
||||
blanked: Blanked,
|
||||
) {
|
||||
let ImageCx { ui, resp, xf, ptr, page, info, .. } = *cx;
|
||||
let painter = ui.painter();
|
||||
let Blanked { left_at, raster_old } = blanked;
|
||||
let screen = |r: Rect| Rect::from_two_pos(xf.norm_to_screen(r.min.x, r.min.y), xf.norm_to_screen(r.max.x, r.max.y));
|
||||
let preview = match opposite {
|
||||
// Resize from the opposite corner, keeping the aspect ratio.
|
||||
Some(fixed) => {
|
||||
let (w0, h0) = (b.width().max(1.0), b.height().max(1.0));
|
||||
// Use the axis with the larger relative pointer change. Taking the maximum
|
||||
// absolute size freezes shrinking when the other axis hasn't moved. Measure
|
||||
// from the press point so grabbing a handle off-centre cannot cause a jump.
|
||||
let horizontal = (p.x - start.x) * (start.x - fixed.x).signum() / w0;
|
||||
let vertical = (p.y - start.y) * (start.y - fixed.y).signum() / h0;
|
||||
let delta = if horizontal.abs() > vertical.abs() { horizontal } else { vertical };
|
||||
let k = (1.0 + delta).max(0.05);
|
||||
let (w, h) = (w0 * k, h0 * k);
|
||||
let x = if start.x < fixed.x { fixed.x - w } else { fixed.x };
|
||||
let y = if start.y < fixed.y { fixed.y - h } else { fixed.y };
|
||||
Rect::from_min_size(Pos2::new(x, y), egui::vec2(w, h))
|
||||
}
|
||||
None => b.translate(p - start),
|
||||
};
|
||||
// Move/resize the full placement by the visible frame's change. The engine
|
||||
// transforms an isolated crop together with its image, preserving its contents.
|
||||
let raw = images.get(i).map_or(b, |im| xf.user_rect(info, page, im.rect.map(|v| v as f32)));
|
||||
// A zero-width or zero-height visible frame (or a degenerate ratio) keeps the
|
||||
// placement's scale instead of producing inf/NaN.
|
||||
let ratio = |new: f32, old: f32| if old.abs() > 1e-3 && (new / old).is_finite() { new / old } else { 1.0 };
|
||||
let factor = egui::vec2(ratio(preview.width(), b.width()), ratio(preview.height(), b.height()));
|
||||
let target = Rect::from_min_max(preview.min + (raw.min - b.min) * factor, preview.min + (raw.max - b.min) * factor);
|
||||
let visible = preview.intersect(xf.rect);
|
||||
if visible.is_positive() {
|
||||
// The pixels follow the frame: the page raster's picture of the image, stretched
|
||||
// into the preview over a blank where it was. Re-rendering every frame of a drag
|
||||
// would lag; the edit renders it properly on release.
|
||||
if preview != b
|
||||
&& let Some(tex) = view.page_raster(page)
|
||||
{
|
||||
// Its pixels: where the whole-page raster has it.
|
||||
let source = left_at.filter(|_| raster_old).map_or(b, screen);
|
||||
let blank: Vec<Rect> = left_at.map(screen).into_iter().chain(std::iter::once(b)).collect();
|
||||
stretched_picture(painter, xf, tex, &blank, source, preview);
|
||||
}
|
||||
painter.rect_filled(visible, CornerRadius::ZERO, ACCENT.gamma_multiply(0.04));
|
||||
painter.rect_stroke(visible, CornerRadius::ZERO, Stroke::new(1.5, ACCENT), egui::StrokeKind::Outside);
|
||||
draw_handles(painter, preview);
|
||||
}
|
||||
if resp.drag_stopped() || !ptr.primary_down {
|
||||
if let Some(s) = view.image_selection.as_mut() {
|
||||
s.drag = None;
|
||||
let norm = |r: Rect| {
|
||||
let ((u0, v0), (u1, v1)) = (xf.screen_to_norm(r.min), xf.screen_to_norm(r.max));
|
||||
Rect::from_two_pos(Pos2::new(u0, v0), Pos2::new(u1, v1))
|
||||
};
|
||||
s.settled = (preview != b).then(|| (left_at.unwrap_or_else(|| norm(b)), norm(preview)));
|
||||
}
|
||||
if preview != b {
|
||||
view.pending_edit =
|
||||
Some(Edit::EditPageImage { page, index: i, change: pdfcraft_engine::ImageEdit::Move(user_box(xf, info, page, target)) });
|
||||
}
|
||||
}
|
||||
ui.ctx().set_cursor_icon(match opposite {
|
||||
Some(fixed) if (start.x - fixed.x) * (start.y - fixed.y) < 0.0 => egui::CursorIcon::ResizeNeSw,
|
||||
Some(_) => egui::CursorIcon::ResizeNwSe,
|
||||
None => egui::CursorIcon::Move,
|
||||
});
|
||||
}
|
||||
|
||||
/// Images on a page: select, move, resize, right-click. Returns `true` when the pointer was used.
|
||||
pub(crate) fn image_input(cx: &ImageCx<'_>, images: &[pdfcraft_engine::PageImage], view: &mut DocView, action: &mut Option<ImageAction>) -> bool {
|
||||
let ImageCx { ui, resp, xf, ptr, page, info, allow_interaction } = *cx;
|
||||
let boxes: Vec<Rect> = images.iter().map(|im| im.visible_rect.map_or(Rect::NOTHING, |r| xf.user_rect(info, page, r.map(|v| v as f32)))).collect();
|
||||
let painter = ui.painter();
|
||||
let hover_pos = ptr.pos;
|
||||
let image_contains = |index: usize, point: Pos2| {
|
||||
let (x, y) = xf.screen_to_view(point);
|
||||
let Some(p) = info.pages.get(page) else { return false };
|
||||
let user = p.view_to_user(x, y);
|
||||
images.get(index).is_some_and(|im| im.contains(user.map(f64::from)))
|
||||
};
|
||||
let selected = view.image_selection.as_ref().filter(|s| s.page == page).map(|s| s.index).filter(|i| *i < boxes.len());
|
||||
let dragging = view.image_selection.as_ref().is_some_and(|s| s.drag.is_some());
|
||||
// Few images: faint outlines for all of them, as before. Many: only the selected and
|
||||
// hovered boxes, so a ~1000-image CAD page stays interactive in Edit text & images.
|
||||
// Fully clipped placements (empty visible frames) get no outline.
|
||||
let faint_all = boxes.len() <= MAX_OUTLINES;
|
||||
// The picture being dragged has its frame at the pointer; its outline where it was would
|
||||
// trail behind until the release.
|
||||
let moving = selected.filter(|_| dragging);
|
||||
let hovered_box = hover_pos.and_then(|p| boxes.iter().enumerate().rposition(|(i, b)| b.is_positive() && b.contains(p) && image_contains(i, p)));
|
||||
draw_outlines(painter, &boxes, OutlineState { selected, hovered_box, moving, dragging, faint_all });
|
||||
// The selected image: frame, corner handles, dragging.
|
||||
if let Some(i) = selected.filter(|i| boxes.get(*i).is_some_and(|b| b.is_positive())) {
|
||||
let b = boxes[i];
|
||||
if view.image_selection.as_ref().is_some_and(|s| s.crop.is_some()) {
|
||||
return crop_input(ui, resp, xf, ptr, page, info, view, i, b, allow_interaction);
|
||||
return crop_input(cx, view, i, b);
|
||||
}
|
||||
let corners = [b.left_top(), b.right_top(), b.left_bottom(), b.right_bottom()];
|
||||
let handles = |rect: Rect| {
|
||||
for c in [rect.left_top(), rect.right_top(), rect.left_bottom(), rect.right_bottom()] {
|
||||
painter.rect(
|
||||
Rect::from_center_size(c, egui::vec2(8.0, 8.0)),
|
||||
CornerRadius::ZERO,
|
||||
Color32::WHITE,
|
||||
Stroke::new(1.0, ACCENT),
|
||||
egui::StrokeKind::Middle,
|
||||
);
|
||||
}
|
||||
};
|
||||
// Until the page is rendered again its raster has an applied move or resize's picture
|
||||
// where it was: it stays blanked there and drawn where it went.
|
||||
// The whole-page raster and its tiles are replaced separately: the old frame stays blanked
|
||||
@@ -372,7 +500,7 @@ pub(crate) fn image_input(
|
||||
}
|
||||
if view.image_selection.as_ref().is_some_and(|s| s.drag.is_none()) {
|
||||
painter.rect_stroke(b, CornerRadius::ZERO, Stroke::new(1.5, ACCENT), egui::StrokeKind::Outside);
|
||||
handles(b);
|
||||
draw_handles(painter, b);
|
||||
}
|
||||
if allow_interaction && let Some(corner) = hover_pos.and_then(|p| corners.iter().position(|c| c.distance(p) < 8.0)) {
|
||||
ui.ctx().set_cursor_icon(if matches!(corner, 0 | 3) { egui::CursorIcon::ResizeNwSe } else { egui::CursorIcon::ResizeNeSw });
|
||||
@@ -408,99 +536,13 @@ pub(crate) fn image_input(
|
||||
}
|
||||
}
|
||||
if view.image_selection.as_ref().is_some_and(|s| s.turning) {
|
||||
let shift = ui.input(|i| i.modifiers.shift);
|
||||
let degrees = hover_pos.map_or(0.0, |p| turn_degrees(b.center(), p, shift));
|
||||
// The turned outline as a preview; the engine redraws the pixels on release.
|
||||
let centre = b.center();
|
||||
let (sin, cos) = degrees.to_radians().sin_cos();
|
||||
let turn = |c: Pos2| {
|
||||
let v = c - centre;
|
||||
centre + egui::vec2(v.x * cos - v.y * sin, v.x * sin + v.y * cos)
|
||||
};
|
||||
let outline = [b.left_top(), b.right_top(), b.right_bottom(), b.left_bottom()].map(turn).to_vec();
|
||||
painter.add(egui::Shape::closed_line(outline, Stroke::new(1.5, ACCENT)));
|
||||
let galley = painter.layout_no_wrap(format!("{degrees:.0}\u{b0}"), egui::FontId::proportional(12.0), ACCENT);
|
||||
let label = Rect::from_center_size(turn(knob) - egui::vec2(0.0, 14.0), galley.size() + egui::vec2(8.0, 4.0));
|
||||
painter.rect(label, CornerRadius::same(3), Color32::WHITE, Stroke::new(1.0, ACCENT), egui::StrokeKind::Middle);
|
||||
painter.galley(label.center() - galley.size() / 2.0, galley, ACCENT);
|
||||
ui.ctx().set_cursor_icon(egui::CursorIcon::Grabbing);
|
||||
if resp.drag_stopped() || !ptr.primary_down {
|
||||
if let Some(s) = view.image_selection.as_mut() {
|
||||
s.drag = None;
|
||||
s.turning = false;
|
||||
}
|
||||
if degrees.abs() >= 0.05 {
|
||||
view.pending_edit =
|
||||
Some(Edit::EditPageImage { page, index: i, change: pdfcraft_engine::ImageEdit::RotateBy(f64::from(degrees)) });
|
||||
}
|
||||
}
|
||||
turn_preview(cx, view, i, b, knob);
|
||||
return true;
|
||||
}
|
||||
if let Some((start, opposite)) = view.image_selection.as_ref().and_then(|s| s.drag)
|
||||
&& let Some(p) = hover_pos
|
||||
{
|
||||
let preview = match opposite {
|
||||
// Resize from the opposite corner, keeping the aspect ratio.
|
||||
Some(fixed) => {
|
||||
let (w0, h0) = (b.width().max(1.0), b.height().max(1.0));
|
||||
// Use the axis with the larger relative pointer change. Taking the maximum
|
||||
// absolute size freezes shrinking when the other axis hasn't moved. Measure
|
||||
// from the press point so grabbing a handle off-centre cannot cause a jump.
|
||||
let horizontal = (p.x - start.x) * (start.x - fixed.x).signum() / w0;
|
||||
let vertical = (p.y - start.y) * (start.y - fixed.y).signum() / h0;
|
||||
let delta = if horizontal.abs() > vertical.abs() { horizontal } else { vertical };
|
||||
let k = (1.0 + delta).max(0.05);
|
||||
let (w, h) = (w0 * k, h0 * k);
|
||||
let x = if start.x < fixed.x { fixed.x - w } else { fixed.x };
|
||||
let y = if start.y < fixed.y { fixed.y - h } else { fixed.y };
|
||||
Rect::from_min_size(Pos2::new(x, y), egui::vec2(w, h))
|
||||
}
|
||||
None => b.translate(p - start),
|
||||
};
|
||||
// Move/resize the full placement by the visible frame's change. The engine
|
||||
// transforms an isolated crop together with its image, preserving its contents.
|
||||
let raw = images.get(i).map_or(b, |im| xf.user_rect(info, page, im.rect.map(|v| v as f32)));
|
||||
// A zero-width or zero-height visible frame (or a degenerate ratio) keeps the
|
||||
// placement's scale instead of producing inf/NaN.
|
||||
let ratio = |new: f32, old: f32| if old.abs() > 1e-3 && (new / old).is_finite() { new / old } else { 1.0 };
|
||||
let factor = egui::vec2(ratio(preview.width(), b.width()), ratio(preview.height(), b.height()));
|
||||
let target = Rect::from_min_max(preview.min + (raw.min - b.min) * factor, preview.min + (raw.max - b.min) * factor);
|
||||
let visible = preview.intersect(xf.rect);
|
||||
if visible.is_positive() {
|
||||
// The pixels follow the frame: the page raster's picture of the image, stretched
|
||||
// into the preview over a blank where it was. Re-rendering every frame of a drag
|
||||
// would lag; the edit renders it properly on release.
|
||||
if preview != b
|
||||
&& let Some(tex) = view.page_raster(page)
|
||||
{
|
||||
// Its pixels: where the whole-page raster has it.
|
||||
let source = left_at.filter(|_| raster_old).map_or(b, screen);
|
||||
let blank: Vec<Rect> = left_at.map(screen).into_iter().chain(std::iter::once(b)).collect();
|
||||
stretched_picture(painter, xf, tex, &blank, source, preview);
|
||||
}
|
||||
painter.rect_filled(visible, CornerRadius::ZERO, ACCENT.gamma_multiply(0.04));
|
||||
painter.rect_stroke(visible, CornerRadius::ZERO, Stroke::new(1.5, ACCENT), egui::StrokeKind::Outside);
|
||||
handles(preview);
|
||||
}
|
||||
if resp.drag_stopped() || !ptr.primary_down {
|
||||
if let Some(s) = view.image_selection.as_mut() {
|
||||
s.drag = None;
|
||||
let norm = |r: Rect| {
|
||||
let ((u0, v0), (u1, v1)) = (xf.screen_to_norm(r.min), xf.screen_to_norm(r.max));
|
||||
Rect::from_two_pos(Pos2::new(u0, v0), Pos2::new(u1, v1))
|
||||
};
|
||||
s.settled = (preview != b).then(|| (left_at.unwrap_or_else(|| norm(b)), norm(preview)));
|
||||
}
|
||||
if preview != b {
|
||||
view.pending_edit =
|
||||
Some(Edit::EditPageImage { page, index: i, change: pdfcraft_engine::ImageEdit::Move(user_box(xf, info, page, target)) });
|
||||
}
|
||||
}
|
||||
ui.ctx().set_cursor_icon(match opposite {
|
||||
Some(fixed) if (start.x - fixed.x) * (start.y - fixed.y) < 0.0 => egui::CursorIcon::ResizeNeSw,
|
||||
Some(_) => egui::CursorIcon::ResizeNwSe,
|
||||
None => egui::CursorIcon::Move,
|
||||
});
|
||||
drag_preview(cx, images, view, (i, b), (start, opposite, p), Blanked { left_at, raster_old });
|
||||
return true;
|
||||
}
|
||||
if allow_interaction
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::*;
|
||||
use super::{Arc, BTreeMap, Color32, FontFamily, FontId, LineEditor, Rect};
|
||||
|
||||
/// The look shown for a paragraph: the family and weight guessed from its PDF font name.
|
||||
fn source_look(base_font: &str, size: f64, color: [f64; 3], detected_bold: bool, detected_italic: bool) -> pdfcraft_engine::AddedText {
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use super::*;
|
||||
use super::{
|
||||
Color32, CornerRadius, DEFAULT_SENSITIVITY, PREVIEW_MAX_SIDE, PhotoRect, Pos2, SCAN_MAX_SIDE, SavedSig, ScanError, Sense, SigDraft,
|
||||
SignatureImage, SignaturePhoto, Stroke, Tokens, Turn, image_preview, pos2, vec2,
|
||||
};
|
||||
|
||||
/// The Scan tab's photo, the frame around the signature and how strongly faint ink is picked up.
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
|
||||
@@ -623,6 +623,100 @@ pub(crate) fn text_box(doc: &pdfcraft_engine::Document, view: &DocView, form: &[
|
||||
no_scroll.then(|| doc.field_text_box(&fx.name, fx.widget)).flatten()
|
||||
}
|
||||
|
||||
/// After the text editor is drawn: keeps a field that doesn't scroll within what fits, puts the
|
||||
/// caret where the press that opened the editor landed, and takes focus when asked.
|
||||
fn settle_text_editor(
|
||||
ui: &mut egui::Ui,
|
||||
fx: &mut Focus,
|
||||
r: &egui::Response,
|
||||
laid_out: Option<(std::sync::Arc<egui::Galley>, Pos2)>,
|
||||
fitted: Option<(&form_appearance::TextBox, String)>,
|
||||
) {
|
||||
if let Some((tb, before)) = fitted
|
||||
&& fx.text != before
|
||||
{
|
||||
let (kept, caret) = tb.fitted_edit(&before, &fx.text);
|
||||
if kept != fx.text {
|
||||
fx.text = kept;
|
||||
if let Some(mut st) = egui::TextEdit::load_state(ui.ctx(), r.id) {
|
||||
st.cursor.set_char_range(Some(egui::text::CCursorRange::one(egui::text::CCursor::new(caret))));
|
||||
st.store(ui.ctx(), r.id);
|
||||
}
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Tab / Shift+Tab: commits the field and moves to the next fillable widget in the document's tab
|
||||
/// order, returning the edit to apply (the commit's, or `edit`).
|
||||
fn tab_to_next_field(view: &mut DocView, form: &[FormField], f: &FormField, widget: usize, forward: bool, edit: Option<Edit>) -> Option<Edit> {
|
||||
commit(view, form);
|
||||
let pending = view.pending_edit.take();
|
||||
// The next field that can be filled in, check boxes and radio buttons included (#2223), in
|
||||
// the document's tab order (each page's /Tabs: rows, columns or structure), wrapping.
|
||||
let mut order: Vec<(usize, &FormField, usize)> = form
|
||||
.iter()
|
||||
.filter(|x| fillable(x))
|
||||
.flat_map(|x| x.widgets.iter().enumerate().filter(|(_, w)| w.page.is_some() && !w.hidden).map(move |(wi, w)| (w.tab, x, wi)))
|
||||
.collect();
|
||||
order.sort_by_key(|o| o.0);
|
||||
if let Some(pos) =
|
||||
order.iter().position(|(_, x, wi)| x.name == f.name && *wi == widget).or_else(|| order.iter().position(|(_, x, _)| x.name == f.name))
|
||||
{
|
||||
let k = if forward { (pos + 1) % order.len() } else { (pos + order.len() - 1) % order.len() };
|
||||
let (_, target, wi) = order[k];
|
||||
view.forms.focus = Some(Focus { select_all: true, ..open_focus(target, wi) });
|
||||
if let Some(p) = target.widgets[wi].page
|
||||
&& view.page_screen_rect(p).is_none()
|
||||
{
|
||||
view.go_to_page(p);
|
||||
}
|
||||
}
|
||||
edit.or(pending)
|
||||
}
|
||||
|
||||
/// The arrows move to the group's next or previous button and select it, wrapping.
|
||||
fn step_radio(view: &mut DocView, f: &FormField, widget: usize, forward: bool) -> Option<Edit> {
|
||||
let mut edit = None;
|
||||
let stops: Vec<usize> = f.widgets.iter().enumerate().filter(|(_, w)| w.page.is_some() && !w.hidden).map(|(i, _)| i).collect();
|
||||
if let Some(pos) = stops.iter().position(|&i| i == widget) {
|
||||
let k = if forward { (pos + 1) % stops.len() } else { (pos + stops.len() - 1) % stops.len() };
|
||||
if let Some((wi, target)) = stops.get(k).and_then(|&wi| Some((wi, f.widgets.get(wi)?))) {
|
||||
view.forms.focus = Some(open_focus(f, wi));
|
||||
edit = select_edit(f, target);
|
||||
if let Some(p) = target.page
|
||||
&& view.page_screen_rect(p).is_none()
|
||||
{
|
||||
view.go_to_page(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
edit
|
||||
}
|
||||
|
||||
/// The in-place editor or option list for the focused field. Returns an edit to apply.
|
||||
/// `text_box`, for a text field that doesn't scroll, is how its widget lays text out: the editor
|
||||
/// then takes no more than fits.
|
||||
@@ -714,42 +808,7 @@ pub(crate) fn overlay(
|
||||
})
|
||||
// The form field's own logical name; not a visual/preordered label.
|
||||
.accessible_name(f.name.clone());
|
||||
if let (Some(tb), Some(before)) = (text_box, before)
|
||||
&& fx.text != before
|
||||
{
|
||||
let (kept, caret) = tb.fitted_edit(&before, &fx.text);
|
||||
if kept != fx.text {
|
||||
fx.text = kept;
|
||||
if let Some(mut st) = egui::TextEdit::load_state(ui.ctx(), r.id) {
|
||||
st.cursor.set_char_range(Some(egui::text::CCursorRange::one(egui::text::CCursor::new(caret))));
|
||||
st.store(ui.ctx(), r.id);
|
||||
}
|
||||
ui.ctx().request_repaint();
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
||||
settle_text_editor(ui, fx, &r, laid_out, text_box.zip(before));
|
||||
let lost = crate::edit_text_ui::lost_focus_once(&r, &mut fx.had_focus);
|
||||
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)
|
||||
@@ -890,20 +949,7 @@ pub(crate) fn overlay(
|
||||
// turns the selected button off.
|
||||
edit = if f.kind == FormFieldKind::Radio { select_edit(f, w) } else { toggle_edit(f, w) };
|
||||
} else if let Some(forward) = step {
|
||||
// The arrows move to the group's next or previous button and select it, wrapping.
|
||||
let stops: Vec<usize> = f.widgets.iter().enumerate().filter(|(_, w)| w.page.is_some() && !w.hidden).map(|(i, _)| i).collect();
|
||||
if let Some(pos) = stops.iter().position(|&i| i == focus.widget) {
|
||||
let k = if forward { (pos + 1) % stops.len() } else { (pos + stops.len() - 1) % stops.len() };
|
||||
if let Some((wi, target)) = stops.get(k).and_then(|&wi| Some((wi, f.widgets.get(wi)?))) {
|
||||
view.forms.focus = Some(open_focus(f, wi));
|
||||
edit = select_edit(f, target);
|
||||
if let Some(p) = target.page
|
||||
&& view.page_screen_rect(p).is_none()
|
||||
{
|
||||
view.go_to_page(p);
|
||||
}
|
||||
}
|
||||
}
|
||||
edit = step_radio(view, f, focus.widget, forward);
|
||||
}
|
||||
}
|
||||
_ => close = true,
|
||||
@@ -912,31 +958,7 @@ pub(crate) fn overlay(
|
||||
view.forms.focus = None;
|
||||
}
|
||||
if let Some(forward) = next {
|
||||
commit(view, form);
|
||||
let pending = view.pending_edit.take();
|
||||
// The next field that can be filled in, check boxes and radio buttons included (#2223), in
|
||||
// the document's tab order (each page's /Tabs: rows, columns or structure), wrapping.
|
||||
let mut order: Vec<(usize, &FormField, usize)> = form
|
||||
.iter()
|
||||
.filter(|x| fillable(x))
|
||||
.flat_map(|x| x.widgets.iter().enumerate().filter(|(_, w)| w.page.is_some() && !w.hidden).map(move |(wi, w)| (w.tab, x, wi)))
|
||||
.collect();
|
||||
order.sort_by_key(|o| o.0);
|
||||
if let Some(pos) = order
|
||||
.iter()
|
||||
.position(|(_, x, wi)| x.name == f.name && *wi == focus.widget)
|
||||
.or_else(|| order.iter().position(|(_, x, _)| x.name == f.name))
|
||||
{
|
||||
let k = if forward { (pos + 1) % order.len() } else { (pos + order.len() - 1) % order.len() };
|
||||
let (_, target, wi) = order[k];
|
||||
view.forms.focus = Some(Focus { select_all: true, ..open_focus(target, wi) });
|
||||
if let Some(p) = target.widgets[wi].page
|
||||
&& view.page_screen_rect(p).is_none()
|
||||
{
|
||||
view.go_to_page(p);
|
||||
}
|
||||
}
|
||||
edit = edit.or(pending);
|
||||
edit = tab_to_next_field(view, form, f, focus.widget, forward, edit);
|
||||
}
|
||||
edit.or_else(|| view.pending_edit.take())
|
||||
}
|
||||
|
||||
@@ -166,6 +166,42 @@ impl MeasureView {
|
||||
self.scale = None;
|
||||
}
|
||||
}
|
||||
/// Snaps `point` to the page's drawn geometry (endpoints, midpoints, intersections, paths),
|
||||
/// recording in `state` why it couldn't when the content is too complex or unreadable.
|
||||
fn snap_to_content(
|
||||
state: &mut MeasureView,
|
||||
doc: &Document,
|
||||
page: usize,
|
||||
view_xy: [f32; 2],
|
||||
pixels_per_unit: f32,
|
||||
point: Point,
|
||||
) -> Option<(Point, measure::snap::SnapKind)> {
|
||||
if state.paths.as_ref().is_none_or(|(p, g, _)| *p != page || *g != doc.edit_generation()) {
|
||||
state.paths = Some((page, doc.edit_generation(), doc.measurement_paths(page)));
|
||||
}
|
||||
if let Some((_, _, Err(e))) = &state.paths {
|
||||
state.error = Some(e.clone());
|
||||
}
|
||||
if let Some((_, _, Ok(paths))) = &state.paths {
|
||||
if paths.truncated {
|
||||
state.error = Some(tl!("This drawing exceeds the snapping limit; only the first paths are used.").into());
|
||||
} else if paths.unreadable > 0 {
|
||||
state.error = Some(tl!("Some page content couldn't be read; snapping uses the rest.").into());
|
||||
}
|
||||
let tolerance = doc
|
||||
.measurement_to_user(page, [f64::from(view_xy[0]) + state.sensitivity / f64::from(pixels_per_unit), f64::from(view_xy[1])])
|
||||
.map(|p| measure::distance(point, p))
|
||||
.unwrap_or(state.sensitivity / f64::from(pixels_per_unit));
|
||||
if state.snaps.intersections && paths.intersection_limited(point, tolerance) {
|
||||
state.error = Some(tl!("This area has too many overlapping paths; intersection snapping is limited.").into());
|
||||
}
|
||||
if let Ok(Some(hit)) = paths.snap(point, tolerance, state.snaps) {
|
||||
return Some((hit.point, hit.kind));
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub(crate) fn page_input(
|
||||
ui: &egui::Ui,
|
||||
resp: &egui::Response,
|
||||
@@ -204,32 +240,13 @@ pub(crate) fn page_input(
|
||||
let (vx, vy) = xf.screen_to_view(pos);
|
||||
let [x, y] = info.view_to_user(vx, vy);
|
||||
let mut point = [f64::from(x), f64::from(y)];
|
||||
let mut snapped = None;
|
||||
if state.snap_enabled {
|
||||
if state.paths.as_ref().is_none_or(|(p, g, _)| *p != page || *g != doc.edit_generation()) {
|
||||
state.paths = Some((page, doc.edit_generation(), doc.measurement_paths(page)));
|
||||
}
|
||||
let pixels_per_unit = xf.zoom().max(1e-6);
|
||||
if let Some((_, _, Err(e))) = &state.paths {
|
||||
state.error = Some(e.clone());
|
||||
}
|
||||
if let Some((_, _, Ok(paths))) = &state.paths {
|
||||
if paths.truncated {
|
||||
state.error = Some(tl!("This drawing exceeds the snapping limit; only the first paths are used.").into());
|
||||
} else if paths.unreadable > 0 {
|
||||
state.error = Some(tl!("Some page content couldn't be read; snapping uses the rest.").into());
|
||||
}
|
||||
let tolerance = doc
|
||||
.measurement_to_user(page, [f64::from(vx) + state.sensitivity / f64::from(pixels_per_unit), f64::from(vy)])
|
||||
.map(|p| measure::distance(point, p))
|
||||
.unwrap_or(state.sensitivity / f64::from(pixels_per_unit));
|
||||
if state.snaps.intersections && paths.intersection_limited(point, tolerance) {
|
||||
state.error = Some(tl!("This area has too many overlapping paths; intersection snapping is limited.").into());
|
||||
}
|
||||
if let Ok(Some(hit)) = paths.snap(point, tolerance, state.snaps) {
|
||||
point = hit.point;
|
||||
snap = Some(hit.kind);
|
||||
}
|
||||
}
|
||||
snapped = snap_to_content(state, doc, page, [vx, vy], xf.zoom().max(1e-6), point);
|
||||
}
|
||||
if let Some((snapped_point, kind)) = snapped {
|
||||
point = snapped_point;
|
||||
snap = Some(kind);
|
||||
}
|
||||
if ui.input(|i| i.modifiers.shift)
|
||||
&& let Some(last) = state.points.last()
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use super::*;
|
||||
use super::{Calculate, FieldLook, FieldProps, FormField, FormFieldKind, Format, Validate};
|
||||
|
||||
// ───────────────────────────────────────────────────────────────────────── Field Properties
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
use super::draft::{FieldDraft, FieldTab};
|
||||
use super::tabs::{actions_tab, calculate_tab, format_tab, validate_tab};
|
||||
use super::*;
|
||||
use super::{BorderStyle, FieldFont, FormFieldKind, theme, widgets};
|
||||
|
||||
/// A check box for a `/Ff` bit (`inverted`: checked when the bit is clear).
|
||||
fn flag_box(ui: &mut egui::Ui, flags: &mut u32, bit: u32, inverted: bool, label: &str) {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::draft::{ACTION_KINDS, ActionDraft, FieldDraft};
|
||||
use super::*;
|
||||
use super::{CalcOp, Calculate, DATE_PRESETS, Format, TIME_PRESETS, Validate, format_value, theme, widgets};
|
||||
|
||||
/// The categories of the Format tab.
|
||||
#[derive(Clone, Copy, PartialEq)]
|
||||
|
||||
@@ -1,4 +1,7 @@
|
||||
use super::*;
|
||||
use super::{
|
||||
Align, Appearance, AppearanceLabels, Color32, CornerRadius, KEY_ALGORITHMS, Layout, NewIdDraft, PdfCraftApp, Rect, SavedSig, SignGraphic,
|
||||
SignStep, Stroke, Tokens, icons, sign, theme, vec2, widgets,
|
||||
};
|
||||
|
||||
/// Draw the signing dialogs; returns `true` when the dialog should close.
|
||||
pub(crate) fn dialog(ui: &mut egui::Ui, app: &mut PdfCraftApp, t: &Tokens) -> bool {
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use super::dialog::title;
|
||||
use super::*;
|
||||
use super::{Align, Certificate, Color32, Layout, SignatureInfo, SignatureStatus, Tokens, icons, sign, theme, widgets};
|
||||
|
||||
/// The message bar for signed documents: (icon, colour, template, argument).
|
||||
/// The template renders through [`crate::i18n::tr_fmt`] at the call site, so the bar follows
|
||||
|
||||
Reference in New Issue
Block a user