diff --git a/crates/ui-egui/src/app/prefs.rs b/crates/ui-egui/src/app/prefs.rs index 1a8c2d269e..aa40ea0d24 100644 --- a/crates/ui-egui/src/app/prefs.rs +++ b/crates/ui-egui/src/app/prefs.rs @@ -127,6 +127,12 @@ impl PdfCraftApp { "border_shape": s.border_shape.name(), })) .collect::>(), + // Default font sizes of new text boxes and callouts (#5767). + "comment_font_sizes": self + .comment_prefs + .font_sizes() + .map(|(t, size)| serde_json::json!({ "tool": t.command(), "font_size": size })) + .collect::>(), // Drawn signatures keep their original form (older settings read the same). "signature": match &self.signature { Some(fill_sign::SavedSig::Drawn(s)) => Some(s), _ => None }, "signature_text": match &self.signature { Some(fill_sign::SavedSig::Typed(t)) => Some(t), _ => None }, @@ -420,6 +426,16 @@ impl PdfCraftApp { } } } + // Default font sizes (#5767), clamped by the setter; a missing or malformed size keeps + // the tool's own. + if let Some(sizes) = v["comment_font_sizes"].as_array() { + for e in sizes { + let Some(tool) = e["tool"].as_str().and_then(crate::comments::CommentTool::from_command) else { continue }; + if let Some(size) = e["font_size"].as_f64() { + self.comment_prefs.set_font_size(tool, size); + } + } + } } /// Saved signatures, stamps, actions and trusted certificates. diff --git a/crates/ui-egui/src/dialogs/comment_props.rs b/crates/ui-egui/src/dialogs/comment_props.rs index 12752dde3c..7b404c2f76 100644 --- a/crates/ui-egui/src/dialogs/comment_props.rs +++ b/crates/ui-egui/src/dialogs/comment_props.rs @@ -97,7 +97,7 @@ impl PdfCraftApp { } /// New comments of the kind at `(page, index)` take the look in `p`: its colour, opacity, - /// line width and (for shapes) fill. `p` may be a Comment Properties draft not applied yet. + /// line width, (for shapes) fill and (for text boxes and callouts) font size. `p` may be a Comment Properties draft not applied yet. pub fn make_props_default(&mut self, page: usize, index: usize, p: &CommentProps) { let Some((_, id)) = self.active_ids() else { return }; let Some(doc) = self.session.get(id) else { return }; @@ -143,6 +143,9 @@ impl PdfCraftApp { if let Some(w) = p.width { self.comment_prefs.set_width(tool, w); } + if let Some(size) = p.font_size { + self.comment_prefs.set_font_size(tool, size); + } self.notify_fmt("New {tool} comments will look like this one", &[("tool", &crate::i18n::in_sentence(tl!(tool.label())))]); } diff --git a/crates/ui-egui/src/tools/comments.rs b/crates/ui-egui/src/tools/comments.rs index 56301969a2..bfd0592a08 100644 --- a/crates/ui-egui/src/tools/comments.rs +++ b/crates/ui-egui/src/tools/comments.rs @@ -56,6 +56,9 @@ const SELECT_BLUE: Color32 = Color32::from_rgb(0x14, 0x73, 0xE6); /// Maximum stroke thickness offered by commenting controls, in PDF points. pub const MAX_LINE_WIDTH: f64 = 100.0; +/// The font sizes a text box or callout default may take (Comment Properties' range). +const MIN_FONT_SIZE: f64 = 1.0; +const MAX_FONT_SIZE: f64 = 400.0; #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)] pub enum CommentTool { @@ -258,6 +261,14 @@ impl CommentTool { matches!(self, Self::Rectangle | Self::Oval | Self::Polygon | Self::Cloud | Self::TextBox | Self::Callout) } + /// The font size new comments of this kind start with (`None`: no text). + fn default_font_size(self) -> Option { + match self.sample() { + Shape::TextBox { font_size, .. } | Shape::Callout { font_size, .. } => Some(font_size), + _ => None, + } + } + /// A placeholder shape of this kind (for per-tool default styles). fn sample(self) -> Shape { match self { @@ -309,6 +320,8 @@ pub struct CommentPrefs { /// `/T` of new comments (Acrobat: Preferences ▸ Identity, default the login name). pub author: String, styles: Vec<(CommentTool, Style)>, + /// The font size of new text boxes and callouts (Make Properties Default). + font_sizes: Vec<(CommentTool, f64)>, /// Keep the tool selected after use (the pin). pub pinned: bool, /// The tool each flyout group shows (the last one used). @@ -322,6 +335,7 @@ impl Default for CommentPrefs { Self { author: login_name(), styles: ALL.iter().map(|t| (*t, Style::default_for(&t.sample()))).collect(), + font_sizes: ALL.iter().filter_map(|t| t.default_font_size().map(|s| (*t, s))).collect(), pinned: false, group_tool: [CommentTool::Note, CommentTool::Highlight, CommentTool::Ink], hide_empty_highlight_hover: false, @@ -390,6 +404,26 @@ impl CommentPrefs { } } + /// The font size of new comments of `tool` (`None`: the tool has no text). + pub fn font_size(&self, tool: CommentTool) -> Option { + self.font_sizes.iter().find(|(t, _)| *t == tool).map(|(_, s)| *s) + } + + /// Every text tool with its default font size (the persisted settings read this). + pub fn font_sizes(&self) -> impl Iterator + '_ { + self.font_sizes.iter().copied() + } + + /// Clamp and store the font size (1–400 pt, as Comment Properties allows) for a text tool; + /// a non-finite size keeps the current one. + pub fn set_font_size(&mut self, tool: CommentTool, size: f64) { + if let Some((_, s)) = self.font_sizes.iter_mut().find(|(t, _)| *t == tool) + && size.is_finite() + { + *s = size.clamp(MIN_FONT_SIZE, MAX_FONT_SIZE); + } + } + pub fn set_style(&mut self, tool: CommentTool, style: Style) { if let Some((_, s)) = self.styles.iter_mut().find(|(t, _)| *t == tool) { *s = style; diff --git a/crates/ui-egui/src/tools/comments/composer.rs b/crates/ui-egui/src/tools/comments/composer.rs index 5ae441a0cd..41c3a1e6c5 100644 --- a/crates/ui-egui/src/tools/comments/composer.rs +++ b/crates/ui-egui/src/tools/comments/composer.rs @@ -125,16 +125,18 @@ pub(crate) fn composer(ctx: &egui::Context, view: &mut DocView, info: &DocInfo, view.comments.tool_done = true; let rotation = page_rotation(info, page); let border = prefs.style(CommentTool::TextBox).width; + let font_size = prefs.font_size(CommentTool::TextBox).unwrap_or(12.0); let rect = match (size, info.pages.get(page)) { - (Some(size), Some(p)) => drawn_text_box_rect(p, c.at, size, &text, 12.0, border), - _ => text_box_rect(c.at, &text, 12.0, rotation), + (Some(size), Some(p)) => drawn_text_box_rect(p, c.at, size, &text, font_size, border), + _ => text_box_rect(c.at, &text, font_size, rotation), }; - Some(new_comment(&cx, CommentTool::TextBox, Shape::TextBox { rect, font_size: 12.0 }, text)) + Some(new_comment(&cx, CommentTool::TextBox, Shape::TextBox { rect, font_size }, text)) } ComposerKind::Callout { point } => { view.comments.tool_done = true; // A callout's leader and box stay in user-space axes (its appearance isn't turned). - let rect = text_box_rect(c.at, &text, 10.0, 0); + let font_size = prefs.font_size(CommentTool::Callout).unwrap_or(10.0); + let rect = text_box_rect(c.at, &text, font_size, 0); let side = if point[0] < rect[0] { rect[0] } else if point[0] > rect[2] { @@ -143,7 +145,7 @@ pub(crate) fn composer(ctx: &egui::Context, view: &mut DocView, info: &DocInfo, (rect[0] + rect[2]) / 2.0 }; let knee = [(point[0] + side) / 2.0, (rect[1] + rect[3]) / 2.0]; - Some(new_comment(&cx, CommentTool::Callout, Shape::Callout { rect, knee, point, font_size: 10.0, ending: LineEnding::OpenArrow }, text)) + Some(new_comment(&cx, CommentTool::Callout, Shape::Callout { rect, knee, point, font_size, ending: LineEnding::OpenArrow }, text)) } ComposerKind::Replace => { view.comments.tool_done = true; diff --git a/crates/ui-egui/tests/it/comments.rs b/crates/ui-egui/tests/it/comments.rs index 56984b6882..532c45de91 100644 --- a/crates/ui-egui/tests/it/comments.rs +++ b/crates/ui-egui/tests/it/comments.rs @@ -4355,3 +4355,67 @@ fn escape_with_a_fill_tool_deselects_every_comment_then_drops_the_tool() { h.run_steps(2); assert_eq!(h.state().quick_tool, QuickTool::Select); } + +/// #5767: Make Properties Default on a text box or callout keeps its font size too, so the next +/// one (also after a restart) takes it, along with the colour. +#[test] +fn properties_dialog_makes_the_text_box_font_size_default() { + let blue = [0.0, 0.47, 0.84]; + let mut h = harness(|_| {}); + let font_size_of_last = |h: &Harness<'static, PdfCraftApp>| { + let s = h.state(); + let doc = s.session.get(s.views[0].id).unwrap(); + let last = doc.info.annotations.len() - 1; + doc.comment_props(0, last).unwrap() + }; + for (quick, size, first, second, third) in + [("freetext", 24.0, (40.0, 60.0), (40.0, 120.0), (40.0, 100.0)), ("callout", 18.0, (150.0, 40.0), (150.0, 100.0), (150.0, 60.0))] + { + h.state_mut().set_option("quick", quick).unwrap(); + if quick == "callout" { + drag_pt(&mut h, first, (first.0 + 60.0, first.1 + 20.0)); + } else { + click_pt(&mut h, first); + } + post_text(&mut h, "First"); + let index = comments(&h).len() - 1; + h.state_mut().open_comment_props(0, index); + h.run_steps(2); + { + let d = h.state_mut().comment_props.as_mut().expect("open"); + d.edited.font_size = Some(size); + d.edited.color = Some(blue); + } + h.get_by_label("Make Properties Default").click(); + h.run_steps(1); + h.get_by_label("OK").click(); + h.run_steps(3); + assert_eq!(font_size_of_last(&h).font_size, Some(size)); + + // The next one takes the font size and the colour. + h.state_mut().set_option("quick", quick).unwrap(); + if quick == "callout" { + drag_pt(&mut h, second, (second.0 + 60.0, second.1 + 20.0)); + } else { + click_pt(&mut h, second); + } + post_text(&mut h, "Second"); + let p = font_size_of_last(&h); + assert_eq!(p.font_size, Some(size), "{quick}: the next one takes the default font size"); + assert_eq!(p.color, Some(blue), "{quick}: and the default colour"); + + // The default survives a restart. + let json = h.state().persist(); + let mut fresh = harness(move |app| { + app.restore(&json); + app.set_option("quick", quick).unwrap(); + }); + if quick == "callout" { + drag_pt(&mut fresh, third, (third.0 + 60.0, third.1 + 20.0)); + } else { + click_pt(&mut fresh, third); + } + post_text(&mut fresh, "Third"); + assert_eq!(font_size_of_last(&fresh).font_size, Some(size), "{quick}: the default font size persisted"); + } +}