mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-10-27 19:48:14 -06:00
edit: commit the open paragraph when a click switches text boxes (#2302)
Clicking straight into another text box while one was open for typing replaced the editor and dropped the typed text: only a click on empty canvas (the editor losing focus) or Cmd+Enter applied it (#2022). The click now queues the open paragraph's edit and asks for the clicked box to open on the next frame, from the document that edit has been applied to, so the box's index and geometry are the new inventory's, not the one the click came with. A click on the open box's own frame keeps the editor and its draft instead of resetting it. The regression test types into the top of two boxes, clicks straight into the bottom one, and requires the first box's text applied ("Top edited"), the edit on the undo stack, and the second box open. On the old code it failed with ["Top", "Bottom"] — the draft had been dropped.
This commit is contained in:
@@ -410,6 +410,9 @@ pub struct DocView {
|
||||
/// line being edited.
|
||||
pub(crate) edit_lines: HashMap<usize, (u64, Vec<pdfcraft_engine::TextBlock>)>,
|
||||
pub line_editor: Option<crate::edit_text_ui::LineEditor>,
|
||||
/// The box a click switched to while another paragraph was being edited: it opens once the
|
||||
/// queued edit has applied, so its index and geometry come from the document it now is (#2022).
|
||||
pub(crate) pending_text_open: Option<(usize, usize)>,
|
||||
pub(crate) text_background: crate::edit_text_ui::TextBackground,
|
||||
pub(crate) move_background: crate::content_ui::MoveBackground,
|
||||
/// Edit text & images: the images per page (by document generation), and the selected one.
|
||||
@@ -664,6 +667,7 @@ impl DocView {
|
||||
pending_edit: None,
|
||||
edit_lines: HashMap::new(),
|
||||
line_editor: None,
|
||||
pending_text_open: None,
|
||||
text_background: Default::default(),
|
||||
move_background: Default::default(),
|
||||
edit_images: HashMap::new(),
|
||||
|
||||
@@ -834,6 +834,53 @@ pub(crate) fn image_input(
|
||||
true
|
||||
}
|
||||
|
||||
/// Open `block` on `page` in the inline editor. The box's geometry and source are read from the
|
||||
/// `lines` handed in, so callers pass the document's current inventory.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn open_editor(
|
||||
ui: &egui::Ui,
|
||||
xf: &PageXform,
|
||||
doc: &pdfcraft_engine::Document,
|
||||
lines: &[pdfcraft_engine::TextBlock],
|
||||
boxes: &[Rect],
|
||||
page: usize,
|
||||
block: usize,
|
||||
view: &mut DocView,
|
||||
) {
|
||||
view.pending_text_open = None;
|
||||
let (Some(l), Some(b)) = (lines.get(block), boxes.get(block)) else { return };
|
||||
view.image_selection = None;
|
||||
let source_lines = doc.text_lines(page);
|
||||
let text = l.editor_text.clone();
|
||||
// Screen pixels per point, from the box's width.
|
||||
let scale = (b.width() - 4.0) / ((l.rect[2] - l.rect[0]).max(1.0) as f32);
|
||||
// How far the editor box may grow: a single-line paragraph's rewrite grows to the
|
||||
// page's right edge, a multi-line paragraph rewraps to its own width.
|
||||
let right = xf.rect.right().min(view.viewport_rect().right()) - 6.0;
|
||||
let max_width = if l.rows.len() == 1 { (right - b.left()).max(b.width()) } else { b.width() };
|
||||
view.line_editor = Some(LineEditor {
|
||||
selection: None,
|
||||
font_resource: None,
|
||||
page,
|
||||
block,
|
||||
text: text.clone(),
|
||||
original: text,
|
||||
rect: *b,
|
||||
source_rect: l.rect.map(|v| v as f32),
|
||||
multiline: l.rows.len() > 1,
|
||||
source: Some((l.clone(), source_lines)),
|
||||
fonts: install_source_fonts(ui.ctx(), doc, page),
|
||||
overflow: false,
|
||||
max_width,
|
||||
size: (l.size as f32 * scale).clamp(8.0, 72.0),
|
||||
focus: true,
|
||||
look: look_of(l),
|
||||
look0: look_of(l),
|
||||
extras: Extras::default(),
|
||||
extras0: Extras::default(),
|
||||
});
|
||||
}
|
||||
|
||||
/// Lines and their screen boxes for a page; hover outlines, click opens the editor. Returns
|
||||
/// `true` when the pointer was used.
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
@@ -860,6 +907,15 @@ pub(crate) fn page_input(
|
||||
painter.rect_stroke(*b, CornerRadius::same(2), Stroke::new(0.75, ACCENT.gamma_multiply(0.35)), egui::StrokeKind::Outside);
|
||||
}
|
||||
}
|
||||
// A box a switching click asked for opens now, once the edit queued with that click has
|
||||
// applied and `lines`/`boxes` are the document's new inventory (#2022).
|
||||
if allow_interaction
|
||||
&& active.is_none()
|
||||
&& let Some((open_page, block)) = view.pending_text_open
|
||||
&& open_page == page
|
||||
{
|
||||
open_editor(ui, xf, doc, lines, &boxes, page, block, view);
|
||||
}
|
||||
if !allow_interaction {
|
||||
return false;
|
||||
}
|
||||
@@ -946,36 +1002,24 @@ pub(crate) fn page_input(
|
||||
});
|
||||
if resp.clicked() {
|
||||
view.image_selection = None;
|
||||
let l = &lines[hit];
|
||||
let source_lines = doc.text_lines(page);
|
||||
let text = l.editor_text.clone();
|
||||
// Screen pixels per point, from the box's width.
|
||||
let scale = (boxes[hit].width() - 4.0) / ((l.rect[2] - l.rect[0]).max(1.0) as f32);
|
||||
// How far the editor box may grow: a single-line paragraph's rewrite grows to the
|
||||
// page's right edge, a multi-line paragraph rewraps to its own width.
|
||||
let right = xf.rect.right().min(view.viewport_rect().right()) - 6.0;
|
||||
let max_width = if l.rows.len() == 1 { (right - boxes[hit].left()).max(boxes[hit].width()) } else { boxes[hit].width() };
|
||||
view.line_editor = Some(LineEditor {
|
||||
selection: None,
|
||||
font_resource: None,
|
||||
page,
|
||||
block: hit,
|
||||
text: text.clone(),
|
||||
original: text,
|
||||
rect: boxes[hit],
|
||||
source_rect: l.rect.map(|v| v as f32),
|
||||
multiline: l.rows.len() > 1,
|
||||
source: Some((l.clone(), source_lines)),
|
||||
fonts: install_source_fonts(ui.ctx(), doc, page),
|
||||
overflow: false,
|
||||
max_width,
|
||||
size: (l.size as f32 * scale).clamp(8.0, 72.0),
|
||||
focus: true,
|
||||
look: look_of(l),
|
||||
look0: look_of(l),
|
||||
extras: Extras::default(),
|
||||
extras0: Extras::default(),
|
||||
});
|
||||
// A click that moves the editor to another paragraph applies the open one first, so its
|
||||
// typed text is not dropped with the switched-away editor (#2022). A click on the open
|
||||
// box's own frame, which the editor's overlay does not cover, keeps it open instead.
|
||||
if let Some(open) = view.line_editor.as_ref().filter(|e| e.has_changes()) {
|
||||
if open.page == page && open.block == hit {
|
||||
return true;
|
||||
}
|
||||
if let Some(old) = view.line_editor.take() {
|
||||
let style = old.style();
|
||||
if old.text != old.original || style != pdfcraft_engine::BlockStyle::default() {
|
||||
view.pending_edit = Some(Edit::EditTextBlock { page: old.page, block: old.block, text: old.text, style });
|
||||
}
|
||||
}
|
||||
// The clicked box opens next frame, from the document with that edit applied.
|
||||
view.pending_text_open = Some((page, hit));
|
||||
return true;
|
||||
}
|
||||
open_editor(ui, xf, doc, lines, &boxes, page, hit, view);
|
||||
}
|
||||
true
|
||||
}
|
||||
|
||||
@@ -2483,6 +2483,51 @@ fn switching_from_a_narrow_editor_keeps_one_frame_and_a_wide_heading_on_one_line
|
||||
assert!(!dirty(&h));
|
||||
}
|
||||
|
||||
/// #2022: clicking straight into another text box while one is open for typing must apply the
|
||||
/// open paragraph first; the typed text used to be dropped with the editor that switched away.
|
||||
#[test]
|
||||
fn switching_straight_to_another_box_applies_the_open_paragraph() {
|
||||
let bytes = source_font_fixture_with_text("BT /F1 24 Tf 20 220 Td (Top) Tj ET BT /F2 24 Tf 20 120 Td (Bottom) Tj ET");
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1400.0, 900.0)).build_eframe(move |_cc| {
|
||||
let mut app = PdfCraftApp::new();
|
||||
app.set_option("language", "en").unwrap();
|
||||
app.open_bytes("boxes.pdf", None, bytes).expect("opens");
|
||||
app
|
||||
});
|
||||
h.run_steps(4);
|
||||
assert!(h.state_mut().execute("edit.edit_text"));
|
||||
h.run_steps(2);
|
||||
let r = h.state().views[0].page_screen_rect(0).expect("on screen");
|
||||
let k = r.width() / 300.0;
|
||||
let mut blocks = {
|
||||
let s = h.state();
|
||||
s.session.get(s.views[0].id).unwrap().text_blocks(0)
|
||||
};
|
||||
blocks.sort_by(|a, b| b.rect[1].partial_cmp(&a.rect[1]).unwrap_or(std::cmp::Ordering::Equal));
|
||||
assert_eq!(blocks.iter().map(|b| b.text.as_str()).collect::<Vec<_>>(), ["Top", "Bottom"], "two editable boxes");
|
||||
let center = |b: &pdfcraft_engine::TextBlock| {
|
||||
egui::pos2(r.left() + ((b.rect[0] + b.rect[2]) / 2.0) as f32 * k, r.top() + (300.0 - (b.rect[1] + b.rect[3]) / 2.0) as f32 * k)
|
||||
};
|
||||
// Open the top box and type into it.
|
||||
click_at(&mut h, center(&blocks[0]));
|
||||
let ed = h.state().views[0].line_editor.clone().expect("the top box opens for typing");
|
||||
assert_eq!(ed.text, "Top");
|
||||
h.state_mut().views[0].line_editor.as_mut().unwrap().text = "Top edited".into();
|
||||
h.run_steps(1);
|
||||
// Click straight into the bottom box: the open paragraph is applied, and the bottom box opens.
|
||||
click_at(&mut h, center(&blocks[1]));
|
||||
let s = h.state();
|
||||
let doc = s.session.get(s.views[0].id).unwrap();
|
||||
assert_eq!(
|
||||
doc.text_blocks(0).iter().map(|b| b.text.clone()).collect::<Vec<_>>(),
|
||||
["Top edited", "Bottom"],
|
||||
"the typed text is applied, not dropped"
|
||||
);
|
||||
assert_eq!(doc.can_undo(), Some("Edit text"));
|
||||
let ed = s.views[0].line_editor.clone().expect("the bottom box is now open");
|
||||
assert_eq!(ed.text, "Bottom");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn editing_existing_text_in_place() {
|
||||
let mut h = harness(1, |_| {});
|
||||
|
||||
Reference in New Issue
Block a user