mirror of
https://github.com/storytold/pdfcraft.git
synced 2026-11-02 22:41:47 -06:00
fix(ui): formatting keeps per-row alignments, the selection, and Redo
- A row aligned on its own lost its alignment when the paragraph was then given a colour, size or font: the rewrite left the rows' alignments out once they had been applied. They now go with every rewrite. - egui drops a box's selection while the box is unfocused, so after one alignment or format a second choice went to the whole paragraph or box. The selection is given back when formatting returns the focus. - Redo from the menu or toolbar applied the box's typing first, a new edit that dropped the document's Redo. It now redoes in the box, as the key does.
This commit is contained in:
@@ -636,6 +636,9 @@ pub(crate) fn editor(ctx: &egui::Context, view: &mut DocView, info: &DocInfo, ad
|
||||
}
|
||||
if t.focus {
|
||||
resp.request_focus();
|
||||
if let Some(range) = &t.selection {
|
||||
crate::edit_text_ui::reselect(ui.ctx(), input, &t.text, range);
|
||||
}
|
||||
t.focus = false;
|
||||
}
|
||||
let buttons = egui::Frame::NONE
|
||||
|
||||
@@ -281,6 +281,19 @@ pub(crate) fn forget_typing_before(ctx: &egui::Context, id: egui::Id, from: &mut
|
||||
*from = Some(text.to_owned());
|
||||
}
|
||||
|
||||
/// Select `range` (bytes of `text`) in the page text box `id` again. egui keeps a box's selection
|
||||
/// only while it has the focus, so a click on Format text drops it; given back as formatting
|
||||
/// returns the focus, it stays shown, and a second choice reaches the same text.
|
||||
pub(crate) fn reselect(ctx: &egui::Context, id: egui::Id, text: &str, range: &std::ops::Range<usize>) {
|
||||
let chars = |at: usize| text.get(..at).map(|s| s.chars().count());
|
||||
let (Some(start), Some(end)) = (chars(range.start), chars(range.end)) else { return };
|
||||
if start < end {
|
||||
let mut state = egui::TextEdit::load_state(ctx, id).unwrap_or_default();
|
||||
state.cursor.set_char_range(Some(egui::text::CCursorRange::two(egui::text::CCursor::new(start), egui::text::CCursor::new(end))));
|
||||
egui::TextEdit::store_state(ctx, id, state);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
pub struct Extras {
|
||||
pub underline: bool,
|
||||
@@ -359,9 +372,6 @@ impl LineEditor {
|
||||
// The whole paragraph's alignment when it was picked, or when the rows' own were
|
||||
// dropped for it (the rows are where those put them, so leaving it out keeps them there).
|
||||
align: (l.align != o.align || l.aligns.is_empty() && !o.aligns.is_empty()).then_some(l.align),
|
||||
// Rows aligned on their own, sent with every rewrite of the paragraph while it is open:
|
||||
// nothing in the page records them, so a rewrite without them would undo them.
|
||||
aligns: if l.aligns != o.aligns || self.text != self.original { l.aligns.clone() } else { Vec::new() },
|
||||
underline: (self.extras.underline != self.extras0.underline).then_some(self.extras.underline),
|
||||
line_spacing: (self.extras.line_spacing != self.extras0.line_spacing && self.extras.line_spacing > 0.0)
|
||||
.then_some(self.extras.line_spacing),
|
||||
@@ -369,6 +379,12 @@ impl LineEditor {
|
||||
scale: (self.extras.scale != self.extras0.scale).then_some(self.extras.scale),
|
||||
..Default::default()
|
||||
};
|
||||
// Rows aligned on their own, sent with every rewrite of the paragraph while it is open
|
||||
// (a colour or size change too): nothing in the page records them, so a rewrite without
|
||||
// them would undo them. Alone, they are sent only when they changed.
|
||||
if l.aligns != o.aligns || self.text != self.original || style != pdfcraft_engine::BlockStyle::default() {
|
||||
style.aligns = l.aligns.clone();
|
||||
}
|
||||
if let Some(range) = &self.selection
|
||||
&& (style.family.is_some()
|
||||
|| style.resource.is_some()
|
||||
@@ -392,9 +408,9 @@ impl LineEditor {
|
||||
style
|
||||
}
|
||||
|
||||
/// After applying formatting: the paragraph now has it.
|
||||
/// After applying formatting: the paragraph now has it. The selection stays, as the box still
|
||||
/// shows it, so a second alignment or format picked for it applies to the same rows.
|
||||
pub fn applied(&mut self) {
|
||||
self.selection = None;
|
||||
self.font_resource = None;
|
||||
self.family_chosen = false;
|
||||
self.look0 = self.look.clone();
|
||||
@@ -409,6 +425,10 @@ impl LineEditor {
|
||||
self.multiline = block.rows.len() > 1;
|
||||
self.source = Some((block.clone(), lines));
|
||||
let before = std::mem::replace(&mut self.text, block.editor_text.clone());
|
||||
if self.text != before {
|
||||
// A byte range of the old text: it may name other characters, or none, in the new.
|
||||
self.selection = None;
|
||||
}
|
||||
self.follow_rows(&before);
|
||||
self.original = self.text.clone();
|
||||
}
|
||||
@@ -1755,6 +1775,9 @@ pub(crate) fn overlay(ctx: &egui::Context, view: &mut DocView, info: &DocInfo) -
|
||||
}
|
||||
if ed.focus {
|
||||
r.request_focus();
|
||||
if let Some(range) = &ed.selection {
|
||||
reselect(ui.ctx(), input, &ed.text, range);
|
||||
}
|
||||
ed.focus = false;
|
||||
}
|
||||
let esc = ui.input(|i| i.key_pressed(egui::Key::Escape));
|
||||
|
||||
@@ -222,11 +222,33 @@ impl PdfCraftApp {
|
||||
pub fn redo(&mut self) {
|
||||
if self.combine_showing() {
|
||||
self.combine_history_step(false);
|
||||
} else {
|
||||
} else if !self.redo_in_page_box() {
|
||||
self.history_step(false);
|
||||
}
|
||||
}
|
||||
|
||||
/// Redo from the menu or toolbar while a box on the page has typing of its own: the step is
|
||||
/// the box's, as the key's is. What Undo took back in the box comes back; typing the page
|
||||
/// doesn't have yet stays as it is, since applying it first is a new edit, which drops the
|
||||
/// document's Redo. True when the box took the step.
|
||||
fn redo_in_page_box(&mut self) -> bool {
|
||||
let Some(ctx) = self.ctx.clone() else { return false };
|
||||
let Some(view) = self.active.and_then(|i| self.views.get_mut(i)) else { return false };
|
||||
if let Some(ed) = view.line_editor.as_mut() {
|
||||
let Some(text) = redo_typing(&ctx, crate::edit_text_ui::input_id(), &ed.text) else { return ed.has_unsaved_text() };
|
||||
let before = std::mem::replace(&mut ed.text, text);
|
||||
ed.follow_rows(&before);
|
||||
return true;
|
||||
}
|
||||
if let Some(d) = view.content.draft.as_mut() {
|
||||
let Some(text) = redo_typing(&ctx, crate::content_ui::input_id(), &d.text) else { return d.has_unapplied_typing() };
|
||||
d.style.aligns = pdfcraft_engine::paragraph_align::remap(&d.text, &text, &d.style.aligns);
|
||||
d.text = text;
|
||||
return true;
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
fn history_step(&mut self, undo: bool) {
|
||||
let Some((i, id)) = self.active_ids() else { return };
|
||||
if !self.close_text_editors(i, id) {
|
||||
@@ -1041,6 +1063,18 @@ impl PdfCraftApp {
|
||||
|
||||
/// The single page a content edit changes (`None` for other edits). Like comment edits,
|
||||
/// these leave every other page's bytes, geometry and text alone, so their rasters stay valid.
|
||||
/// What the page text box `id`, showing `text`, gets back from its own Redo history, if anything;
|
||||
/// its caret goes where the Redo puts it.
|
||||
fn redo_typing(ctx: &egui::Context, id: egui::Id, text: &str) -> Option<String> {
|
||||
let mut state = egui::TextEdit::load_state(ctx, id)?;
|
||||
let mut undoer = state.undoer();
|
||||
let (range, redone) = undoer.redo(&(state.cursor.char_range()?, text.to_owned()))?.clone();
|
||||
state.set_undoer(undoer);
|
||||
state.cursor.set_char_range(Some(range));
|
||||
egui::TextEdit::store_state(ctx, id, state);
|
||||
Some(redone)
|
||||
}
|
||||
|
||||
fn content_page(edit: &Edit) -> Option<usize> {
|
||||
match edit {
|
||||
Edit::MoveObjects { page, .. }
|
||||
|
||||
@@ -939,7 +939,7 @@ fn a_resized_added_picture_shows_only_at_its_new_size() {
|
||||
}
|
||||
|
||||
/// Bug A for added text: with one line of the box selected, an alignment applies to that line
|
||||
/// only, the box stays open, and Undo puts it back and closes the box.
|
||||
/// only, as does a second one, the box stays open, and Undo puts it back and closes the box.
|
||||
#[test]
|
||||
fn aligning_a_selected_line_of_added_text_aligns_only_that_line() {
|
||||
let mut h = harness();
|
||||
@@ -964,10 +964,18 @@ fn aligning_a_selected_line_of_added_text_aligns_only_that_line() {
|
||||
};
|
||||
assert_eq!(aligns(&h), (pdfcraft_engine::TextAlign::Left, vec![None, Some(pdfcraft_engine::TextAlign::Right)]));
|
||||
assert!(h.state().views[0].content.draft.is_some(), "the box stays open");
|
||||
// A second choice, the line still selected, is for that line again.
|
||||
let button = h.get_by_label("Alignment").rect().center();
|
||||
click(&mut h, button);
|
||||
let item = h.get_by_label("Centre").rect().center();
|
||||
click(&mut h, item);
|
||||
assert_eq!(aligns(&h), (pdfcraft_engine::TextAlign::Left, vec![None, Some(pdfcraft_engine::TextAlign::Center)]));
|
||||
h.key_press_modifiers(egui::Modifiers::COMMAND, egui::Key::Z);
|
||||
h.run_steps(4);
|
||||
assert_eq!(aligns(&h), (pdfcraft_engine::TextAlign::Left, vec![]), "Undo puts it back");
|
||||
assert_eq!(aligns(&h), (pdfcraft_engine::TextAlign::Left, vec![None, Some(pdfcraft_engine::TextAlign::Right)]), "Undo puts it back");
|
||||
assert!(h.state().views[0].content.draft.is_none(), "and closes the box");
|
||||
assert!(h.state_mut().execute("edit.undo"));
|
||||
assert_eq!(aligns(&h), (pdfcraft_engine::TextAlign::Left, vec![]));
|
||||
}
|
||||
|
||||
/// The same in an added text box: Redo after Undo puts the typing back in the box, and the
|
||||
|
||||
@@ -6159,3 +6159,77 @@ fn redo_after_undoing_typing_puts_the_typing_back() {
|
||||
let doc = s.session.get(s.views[0].id).unwrap();
|
||||
assert_eq!((doc.can_redo(), doc.text_blocks(0)[0].base_font.as_str()), (Some("Edit text"), "Helvetica"), "the document's Redo still waits");
|
||||
}
|
||||
|
||||
/// A row aligned on its own keeps its alignment when the whole paragraph is then made bold: the
|
||||
/// rewrite for the bold carries the rows' alignments too, as nothing in the page records them.
|
||||
#[test]
|
||||
fn a_row_aligned_on_its_own_keeps_it_through_more_formatting() {
|
||||
let mut h = three_row_paragraph();
|
||||
let wide = row_rect(&h, "three");
|
||||
h.key_press_modifiers(Modifiers::COMMAND, Key::End);
|
||||
h.key_press_modifiers(Modifiers::SHIFT, Key::Home);
|
||||
h.run_steps(2);
|
||||
align_by_pointer(&mut h, "Align right");
|
||||
// Nothing selected: the bold is for the whole paragraph.
|
||||
h.key_press(Key::End);
|
||||
h.run_steps(2);
|
||||
assert_eq!(h.state().views[0].line_editor.as_ref().unwrap().selection, None);
|
||||
h.get_by_label("Bold").click();
|
||||
h.run_steps(4);
|
||||
assert_eq!(h.state().session.get(h.state().views[0].id).unwrap().text_blocks(0)[0].base_font, "Helvetica-Bold");
|
||||
let last = row_rect(&h, "nine");
|
||||
assert!(
|
||||
(last[2] - row_rect(&h, "three")[2].max(wide[2])).abs() < 1.0 && last[0] > wide[0] + 20.0,
|
||||
"the last row is still right-aligned: {last:?}"
|
||||
);
|
||||
}
|
||||
|
||||
/// A second alignment picked while the same row is still selected applies to that row again, not
|
||||
/// the whole paragraph.
|
||||
#[test]
|
||||
fn a_second_alignment_for_a_selected_row_stays_on_that_row() {
|
||||
let mut h = three_row_paragraph();
|
||||
let first = row_rect(&h, "One");
|
||||
let wide = row_rect(&h, "three");
|
||||
h.key_press_modifiers(Modifiers::COMMAND, Key::End);
|
||||
h.key_press_modifiers(Modifiers::SHIFT, Key::Home);
|
||||
h.run_steps(2);
|
||||
align_by_pointer(&mut h, "Align right");
|
||||
align_by_pointer(&mut h, "Centre");
|
||||
assert!((row_rect(&h, "One")[0] - first[0]).abs() < 0.5, "the first row stays put: {:?}", row_rect(&h, "One"));
|
||||
let last = row_rect(&h, "nine");
|
||||
let (mid, centre) = ((last[0] + last[2]) / 2.0, (wide[0] + wide[2]) / 2.0);
|
||||
assert!((mid - centre).abs() < 1.0, "the last row is centred: {last:?} in {wide:?}");
|
||||
}
|
||||
|
||||
/// Redo from the menu (not the key) after Undo in the box took the typing back: the typing comes
|
||||
/// back in the box, and the document's own Redo (a formatting change) still waits. The menu went
|
||||
/// to the document, which applied the box first: a new edit, which dropped that Redo.
|
||||
#[test]
|
||||
fn redo_from_the_menu_puts_the_typing_back_in_the_box() {
|
||||
let mut h = harness(1, |_| {});
|
||||
assert!(h.state_mut().execute("edit.edit_text"));
|
||||
h.run_steps(2);
|
||||
open_paragraph(&mut h);
|
||||
h.get_by_label("Bold").click();
|
||||
h.run_steps(4);
|
||||
// Undo the bold (closing the editor), so the document has it to redo.
|
||||
h.key_press_modifiers(Modifiers::COMMAND, Key::Z);
|
||||
h.run_steps(4);
|
||||
open_paragraph(&mut h);
|
||||
let original = h.state().views[0].line_editor.as_ref().unwrap().text.clone();
|
||||
h.key_press_modifiers(Modifiers::COMMAND, Key::End);
|
||||
h.event(egui::Event::Text(" more".into()));
|
||||
h.run_steps(2);
|
||||
let typed = h.state().views[0].line_editor.as_ref().unwrap().text.clone();
|
||||
h.key_press_modifiers(Modifiers::COMMAND, Key::Z);
|
||||
h.run_steps(3);
|
||||
assert_eq!(h.state().views[0].line_editor.as_ref().unwrap().text, original, "the typing is taken back");
|
||||
assert!(h.state_mut().execute("edit.redo"));
|
||||
h.run_steps(3);
|
||||
let s = h.state();
|
||||
let ed = s.views[0].line_editor.as_ref().expect("still typing");
|
||||
assert_eq!(ed.text, typed, "Redo puts the typing back");
|
||||
let doc = s.session.get(s.views[0].id).unwrap();
|
||||
assert_eq!((doc.can_redo(), doc.text_blocks(0)[0].base_font.as_str()), (Some("Edit text"), "Helvetica"), "the document's Redo still waits");
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user