diff --git a/ATTRIBUTION.md b/ATTRIBUTION.md
index 5cfee8d55..75ebd1d7c 100644
--- a/ATTRIBUTION.md
+++ b/ATTRIBUTION.md
@@ -4,7 +4,7 @@
Every asset PdfCraft includes, bundles or uses to build its published material, with its author, source and licence. The policy is in [AGENTS.md](AGENTS.md) §1. The machine-readable list, with SHA-256 hashes, is [ATTRIBUTION.toml](ATTRIBUTION.toml). Licence texts are kept beside the assets and summarised in [NOTICE](NOTICE).
-## In this repository (272)
+## In this repository (276)
| Asset | Title | Author | Licence | Source | Used for |
|---|---|---|---|---|---|
@@ -280,6 +280,10 @@ Every asset PdfCraft includes, bundles or uses to build its published material,
| `crates/ui-egui/src/i18n/vi.tsv` | Vietnamese interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels; PhotoCraft and VectorCraft Vietnamese catalogs used only as translation memory when English source and semantic context matched; original catalog reviewed by a native Vietnamese-speaking contributor; 2026-10-15 coverage additions are AI-assisted and not separately native-reviewed | Vietnamese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs |
| `crates/ui-egui/src/i18n/sl.tsv` | Slovene interface translation catalog | PdfCraft contributors | MIT OR Apache-2.0 | Contributor-original: clean-room translations of PdfCraft's English UI labels, no proprietary localisation resources | Slovene UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs |
| `vendor/vello_common/assets/probe.png` | vello_common probe reference image (rendered from probe.rgba) | Linebender vello contributors | MIT OR Apache-2.0 | vello_common 0.0.8 (crates.io), vendored in vendor/vello_common; regenerated by its vello_sparse_tests regenerate_probe_reference binary | Reference picture of the vendored vello_common probe module, for people only; the build reads probe.rgba, not this file |
+| `assets/icons/superscript.svg` | Lucide icon "superscript" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/superscript.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
+| `assets/icons/subscript.svg` | Lucide icon "subscript" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/subscript.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
+| `assets/icons/pilcrow.svg` | Lucide icon "pilcrow" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/pilcrow.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
+| `assets/icons/move-horizontal.svg` | Lucide icon "move-horizontal" | Lucide Contributors (portions Cole Bemis, Feather) | ISC | https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/move-horizontal.svg) | UI icon, embedded via crates/ui-egui/src/icon_data.rs |
## Compiled in through dependencies (6)
diff --git a/ATTRIBUTION.toml b/ATTRIBUTION.toml
index fbabdf6d4..514b5c2f0 100644
--- a/ATTRIBUTION.toml
+++ b/ATTRIBUTION.toml
@@ -7,7 +7,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Greek UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
-sha256 = "e2ea97b4c68e13c22ef7f6f39f9a29d21af0e562005e2fdc9e7dfdf29effe0bb"
+sha256 = "467f1e5b55c0f036b35bb72b8a23c7cd0a8bd96ae188d742393b7dc0635166df"
# ATTRIBUTION.toml: one entry per asset (AGENTS.md §1). The source of truth for provenance and licences.
# ATTRIBUTION.md is generated from this file: `cargo xtask assets --write`. Checked by `cargo xtask assets` (part of `xtask ci`).
@@ -30,7 +30,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Simplified Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
-sha256 = "ac0a78f36b91c005b9543e6028b78bb44949fea1bf7991bf0791d407696cd832"
+sha256 = "4a9d4428c3f67e256aa2f9e9d2d057c0e40f60a4446a0b384f3055894a9a9213"
[[asset]]
path = "crates/ui-egui/src/i18n/zh-hant.tsv"
@@ -41,7 +41,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Traditional Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
-sha256 = "c52b9a2aa403937e70859fe60b51719c97475c24927322d7534f5da0bdb5f420"
+sha256 = "f22298173d1e174feb83f6fb20e7079bb9091df86f396c69648b974f429be158"
[[asset]]
path = "crates/ui-egui/src/i18n/cs.tsv"
@@ -52,7 +52,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Czech UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
-sha256 = "0dcad418fa6399955adf2a8398aec307c2c73b13a37ca78ffe142e00491ef586"
+sha256 = "e320d5f50795dc1c46d589d72c7a27facb6164938cf9d75151e904b7450b142d"
[[asset]]
path = "crates/ui-egui/src/i18n/pt-br.tsv"
@@ -63,7 +63,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Brazilian Portuguese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
-sha256 = "130f0256d4ba09bc9cb6a87fc973bad96d6b155703b713341f0afead9ad6b4b3"
+sha256 = "0dd7e0e0b0843b519cebc5504ce7006e4c9cb1eef02b270ed52d008972ee7ff3"
[[asset]]
path = "crates/ui-egui/src/i18n/es.tsv"
@@ -74,7 +74,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Spanish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
-sha256 = "d1093647d2af3d51f77a7b53f05ef1f45442128d40e696022624fbbece5bd0f0"
+sha256 = "dda5b3aea60d718eb2fe28e02a76728a3c895487e03f9319c2bcdef7ea2012db"
[[asset]]
path = "crates/ui-egui/src/i18n/fr.tsv"
@@ -85,7 +85,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "French UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
-sha256 = "135d00bc60ef62f45ebee7c7fd83bc7433fd4054a04fb3901b2d2e8bdd2871c7"
+sha256 = "3351f6abd5e2796ad5e70b00604c9a7093387243ad1951713a717d06bffe419d"
[[asset]]
path = "crates/ui-egui/src/i18n/de.tsv"
@@ -96,7 +96,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "German UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
-sha256 = "0cb38f42fe45831495699f81837fb0b2d5bb16b1b69153359c4376e9bc1e6e04"
+sha256 = "2595a893a3cb84b6f0344b9e2144d9e956e80f778eb7d045fa2ad503e3cfb267"
[[asset]]
path = "crates/ui-egui/src/i18n/te.tsv"
@@ -107,7 +107,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Telugu UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
-sha256 = "ce97821a0fd6bf758e88e654f70985ed1e94c81e36f4e31ee3fc2f05ca39d99f"
+sha256 = "ec2d2afca09390c809e805a52108f5e8c94bf61feef7d0ba2901723db747323d"
[[asset]]
path = "crates/ui-egui/src/i18n/it.tsv"
@@ -118,7 +118,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Italian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
-sha256 = "45567329bdb7884b0b54e5c5161e6e73452d85200945f1ba3661eab1fa35da06"
+sha256 = "683f515a21dbc4b22b312c63e39b4d2a572a1f1cf733f02127f1ee62dbb7a05e"
[[asset]]
path = "crates/ui-egui/src/i18n/pl.tsv"
@@ -129,7 +129,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Polish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
-sha256 = "421ef329305624ebfc661a8d9cd82131e79ec279b8be47d4baec29a226e494d9"
+sha256 = "e31d3969393994c33b90cd5fa82e80584955e4e0676df37136e345db476623ed"
[[asset]]
path = "crates/ui-egui/src/i18n/ru.tsv"
@@ -140,7 +140,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Russian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
-sha256 = "97ed35e8959777a5f15b087136e53385430241e8522c7dc72a383c72dc0a27fd"
+sha256 = "fee17b5090aba63d3129772c271d9802444918fe3a905734b41e006e320e9234"
[[asset]]
path = "crates/ui-egui/src/i18n/hu.tsv"
@@ -151,7 +151,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Hungarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
-sha256 = "bc012e0dd9ad0d212a32b71bfc75a42ffcd058484aab28d173078384c9da480f"
+sha256 = "ef26690aeec1c4b6b648ac50bc2433fec681524ab98e6270cabd3d76be6d3e14"
[[asset]]
path = "crates/ui-egui/src/i18n/bg.tsv"
@@ -162,7 +162,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Bulgarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
-sha256 = "bd611d470f40a01b4442119f7189dabbe58b4904dbce398115399f02b2ecddaa"
+sha256 = "2452a0f56f455ad71cae439a2ecf6da0ae5e2aaa90790de4d16e222393030283"
[[asset]]
path = "crates/ui-egui/src/i18n/kk.tsv"
@@ -173,7 +173,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Kazakh UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
-sha256 = "1132e904bc1a2bcc8239987aa74b2fa75ca17d8eb20b5458eec7e2c075c0445a"
+sha256 = "bbf8c454edc80ddeaecce63e56f5bceba335ae6cce8e4456b14742cd3a820bfe"
[[asset]]
path = "crates/ui-egui/src/i18n/ja.tsv"
@@ -184,7 +184,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Japanese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
-sha256 = "11dd8250c667ca49c40b827632b2d83cad93d07a9d5a5f27b5b9d657293efb6b"
+sha256 = "64d3c1c532ecb94b44b2c894acdaf171a130cbaa8e173b099f9b9b249d2134f4"
[[asset]]
path = "crates/ui-egui/src/i18n/uk.tsv"
@@ -195,7 +195,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Ukrainian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
-sha256 = "6aa0788d784532fb67a0b589cecc27f727e6178221ee892c3de7fe8f75790607"
+sha256 = "9e87a8f108b49213c7bb0f077a2e2f1bae449e1459a908d8202beba280d8055c"
[[asset]]
path = "crates/ui-egui/src/i18n/ar.tsv"
@@ -206,7 +206,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Arabic UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
-sha256 = "ed84a08b83035bc6f4d328b916692d40140c2748d383741d04fba6e013a76bb3"
+sha256 = "921e8e906bdc42675fe8922fc00835a255e019ed61688a01095367f6e881f187"
[[asset]]
path = "crates/ui-egui/src/i18n/tr.tsv"
@@ -217,7 +217,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Turkish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
-sha256 = "1a5b3e9ad3f524e04bdb60e11f4a1424ca749a5ed4840e22abeaa97d24d235e8"
+sha256 = "59070be5688d7d183a7354f08f9604e8f962dcc5e223dd91d1cd940997c47399"
[[asset]]
path = "crates/ui-egui/src/i18n/lt.tsv"
@@ -228,7 +228,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Lithuanian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
-sha256 = "136f1e68fc4caa5e6e8fe395551bffdc9ace93bb00e2da5ba59318846494420d"
+sha256 = "f48accb38d4c824cb3d2f71fcffe0d8edb0a9a5f7412b9c479da6fb2479e45d2"
[[asset]]
@@ -3352,7 +3352,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Vietnamese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
-sha256 = "052ecb803601ebc547934136c936fb5527d6dc97e49cf8cfbde0aa2b1a6cb8c7"
+sha256 = "42212a46ddca212bb94c15062be3b444b33181ea68cdd9880fc5191f56d986a5"
[[asset]]
path = "crates/ui-egui/src/i18n/sl.tsv"
@@ -3363,7 +3363,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT"
kind = "translation"
usage = "Slovene UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
-sha256 = "299c274d071478c49430d8dbc2a1979085fa4f8735324697e8ac0db3d3adce7c"
+sha256 = "fcc38245d923c4ddefb5560fe7fdfe5bc6c1b9c49d4fcb25f2873e63dff4f806"
[[asset]]
@@ -3376,3 +3376,47 @@ licence_file = "vendor/vello_common/LICENSE-MIT"
kind = "image"
usage = "Reference picture of the vendored vello_common probe module, for people only; the build reads probe.rgba, not this file"
sha256 = "78c59a0cfd2c829909d77006f94e34dac1ae5d1f90e7f10f72e84de541fef697"
+
+[[asset]]
+path = "assets/icons/superscript.svg"
+title = "Lucide icon \"superscript\""
+author = "Lucide Contributors (portions Cole Bemis, Feather)"
+source = "https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/superscript.svg)"
+licence = "ISC"
+licence_file = "assets/icons/LICENSE-lucide.txt"
+kind = "icon"
+usage = "UI icon, embedded via crates/ui-egui/src/icon_data.rs"
+sha256 = "34af289365aaa19295f874e4fda3b56196e203785d6fe1d9c3c929ae290518a0"
+
+[[asset]]
+path = "assets/icons/subscript.svg"
+title = "Lucide icon \"subscript\""
+author = "Lucide Contributors (portions Cole Bemis, Feather)"
+source = "https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/subscript.svg)"
+licence = "ISC"
+licence_file = "assets/icons/LICENSE-lucide.txt"
+kind = "icon"
+usage = "UI icon, embedded via crates/ui-egui/src/icon_data.rs"
+sha256 = "06462f57e87a14e6496da9216eee9906b83ded0ff99b70498efa37ae88a5cba4"
+
+[[asset]]
+path = "assets/icons/pilcrow.svg"
+title = "Lucide icon \"pilcrow\""
+author = "Lucide Contributors (portions Cole Bemis, Feather)"
+source = "https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/pilcrow.svg)"
+licence = "ISC"
+licence_file = "assets/icons/LICENSE-lucide.txt"
+kind = "icon"
+usage = "UI icon, embedded via crates/ui-egui/src/icon_data.rs"
+sha256 = "d344d9f9222f6c1b698c0c82a753a2212d9bd7785f06c00cd6e4910fe2fee8f8"
+
+[[asset]]
+path = "assets/icons/move-horizontal.svg"
+title = "Lucide icon \"move-horizontal\""
+author = "Lucide Contributors (portions Cole Bemis, Feather)"
+source = "https://github.com/lucide-icons/lucide (npm lucide-static 1.49.0, icons/move-horizontal.svg)"
+licence = "ISC"
+licence_file = "assets/icons/LICENSE-lucide.txt"
+kind = "icon"
+usage = "UI icon, embedded via crates/ui-egui/src/icon_data.rs"
+sha256 = "864ab2f19254e6ac7240bc6a98323197332e8f714d4d3f94831f41309e5ef2f7"
diff --git a/assets/icons/move-horizontal.svg b/assets/icons/move-horizontal.svg
new file mode 100644
index 000000000..7be9e3af6
--- /dev/null
+++ b/assets/icons/move-horizontal.svg
@@ -0,0 +1,17 @@
+
+
diff --git a/assets/icons/pilcrow.svg b/assets/icons/pilcrow.svg
new file mode 100644
index 000000000..1a1b38b15
--- /dev/null
+++ b/assets/icons/pilcrow.svg
@@ -0,0 +1,17 @@
+
+
diff --git a/assets/icons/subscript.svg b/assets/icons/subscript.svg
new file mode 100644
index 000000000..a37e9c42d
--- /dev/null
+++ b/assets/icons/subscript.svg
@@ -0,0 +1,17 @@
+
+
diff --git a/assets/icons/superscript.svg b/assets/icons/superscript.svg
new file mode 100644
index 000000000..c7978fb09
--- /dev/null
+++ b/assets/icons/superscript.svg
@@ -0,0 +1,17 @@
+
+
diff --git a/crates/ui-egui/src/content_ui.rs b/crates/ui-egui/src/content_ui.rs
index 5f5ef32f7..87f4c7756 100644
--- a/crates/ui-egui/src/content_ui.rs
+++ b/crates/ui-egui/src/content_ui.rs
@@ -4,7 +4,7 @@
//! Delete to remove it. Each change is one undoable engine edit.
use egui::{Color32, CornerRadius, Pos2, Rect, Stroke};
-use pdfcraft_engine::{Added, AddedContent, AddedText, Edit, FontFamily, TextAlign};
+use pdfcraft_engine::{Added, AddedContent, AddedText, Edit};
use pdfcraft_render::DocInfo;
use crate::canvas::{DocView, PageXform};
@@ -605,51 +605,6 @@ fn finish(cv: &mut ContentView, d: TextDraft, added: &[Added]) -> Option {
}
}
-/// Format text (shown in the Edit panel while a text item is selected, or for new text).
-/// Returns the changed style; for a selected item the caller turns it into an update.
-pub(crate) fn format_panel(ui: &mut egui::Ui, t: &Tokens, style: &AddedText) -> Option {
- let mut s = style.clone();
- widgets::section_title(ui, tl!("Format text"));
- ui.horizontal(|ui| {
- egui::ComboBox::from_id_salt("font-family").selected_text(s.family.label()).width(110.0).show_ui(ui, |ui| {
- for f in [FontFamily::Helvetica, FontFamily::Times, FontFamily::Courier] {
- ui.selectable_value(&mut s.family, f, f.label());
- }
- });
- // A list rather than a drag value: every change is an undo step.
- egui::ComboBox::from_id_salt("font-size").selected_text(format!("{} pt", s.size)).width(70.0).show_ui(ui, |ui| {
- for size in [8.0, 9.0, 10.0, 11.0, 12.0, 14.0, 16.0, 18.0, 20.0, 24.0, 28.0, 36.0, 48.0, 72.0] {
- ui.selectable_value(&mut s.size, size, format!("{size} pt"));
- }
- });
- });
- ui.horizontal(|ui| {
- if ui.selectable_label(s.bold, egui::RichText::new("B").strong()).on_hover_text(tl!("Bold")).clicked() {
- s.bold = !s.bold;
- }
- if ui.selectable_label(s.italic, egui::RichText::new("I").italics()).on_hover_text(tl!("Italic")).clicked() {
- s.italic = !s.italic;
- }
- ui.separator();
- for (a, icon, tip) in [
- (TextAlign::Left, "align-left", tl!("Align left")),
- (TextAlign::Center, "align-center", tl!("Centre")),
- (TextAlign::Right, "align-right", tl!("Align right")),
- (TextAlign::Justify, "align-justify", tl!("Justify")),
- ] {
- if crate::icons::button(ui, icon, 26.0, s.align == a, tip).clicked() {
- s.align = a;
- }
- }
- });
- let c = s.color;
- if let Some(picked) = crate::comments::swatch_grid(ui, Some(c)) {
- s.color = picked;
- }
- ui.label(egui::RichText::new(tl!("Thai and Arabic use regular fallback fonts when available.")).small().color(t.text_faint));
- (s != *style).then_some(s)
-}
-
/// How to use the tools, under the Add content list.
pub(crate) fn hint(ui: &mut egui::Ui, t: &Tokens) {
ui.label(
diff --git a/crates/ui-egui/src/edit_text_ui.rs b/crates/ui-egui/src/edit_text_ui.rs
index 66ad98ffe..d272480d2 100644
--- a/crates/ui-egui/src/edit_text_ui.rs
+++ b/crates/ui-egui/src/edit_text_ui.rs
@@ -162,6 +162,10 @@ pub struct LineEditor {
/// The Format text panel's values, and what the paragraph had (to send only changes).
pub look: pdfcraft_engine::AddedText,
look0: pdfcraft_engine::AddedText,
+ /// A standard family was picked from the font list. The family guessed from a PDF font name
+ /// (Calibri reads as Helvetica) can be the one picked, so comparing `look` with `look0` alone
+ /// would miss the choice.
+ family_chosen: bool,
/// Underline, line spacing (× size; 0 = the paragraph's own), character spacing (pt) and
/// horizontal scale (%), and what they were.
pub extras: Extras,
@@ -182,36 +186,42 @@ impl Default for Extras {
}
}
-/// Underline, line spacing, character spacing and horizontal scale for the paragraph being
-/// edited (under Format text). Returns `true` when something changed.
-pub(crate) fn extras_panel(ui: &mut egui::Ui, e: &mut Extras) -> bool {
- let before = *e;
- ui.horizontal(|ui| {
- if crate::icons::button(ui, "underline", 26.0, e.underline, tl!("Underline")).clicked() {
- e.underline = !e.underline;
- }
- let zero = tl!("Line spacing").to_string();
- let label = |v: f64| if v == 0.0 { zero.clone() } else { format!("{v:.2}×") };
- egui::ComboBox::from_id_salt("line-spacing").selected_text(label(e.line_spacing)).width(110.0).show_ui(ui, |ui| {
- for v in [1.0, 1.15, 1.5, 2.0] {
- ui.selectable_value(&mut e.line_spacing, v, label(v));
- }
- });
- });
- ui.horizontal(|ui| {
- let l = ui.label(tl!("Character spacing"));
- ui.add(egui::DragValue::new(&mut e.char_spacing).range(-5.0..=50.0).speed(0.1).suffix(" pt")).labelled_by(l.id);
- });
- ui.horizontal(|ui| {
- let l = ui.label(tl!("Horizontal scale"));
- ui.add(egui::DragValue::new(&mut e.scale).range(10.0..=400.0).speed(1.0).suffix(" %")).labelled_by(l.id);
- });
- *e != before
-}
-
impl LineEditor {
+ /// What Format text's font list shows: the chosen page font, a standard family picked
+ /// instead, or the paragraph's own font (its `/BaseFont` without the subset tag).
+ pub(crate) fn font_shown(&self, sources: &[(String, String)]) -> String {
+ if let Some(face) = self.font_resource.as_ref().and_then(|r| sources.iter().find(|(res, _)| res == r)).map(|(_, face)| face) {
+ return face.clone();
+ }
+ if self.look.family != self.look0.family || self.family_chosen {
+ return self.look.family.label().to_string();
+ }
+ let base = self.source.as_ref().map(|(block, _)| block.base_font.as_str()).unwrap_or_default();
+ // "ABCDEF+Calibri" is a subset of Calibri.
+ let name = match base.split_once('+') {
+ Some((tag, rest)) if tag.len() == 6 && tag.bytes().all(|b| b.is_ascii_uppercase()) => rest,
+ _ => base,
+ };
+ if name.is_empty() { tl!("Keep source fonts").to_string() } else { name.to_string() }
+ }
+
+ /// Back to the paragraph's own font: no page font chosen and the family it started with.
+ pub(crate) fn keep_source_font(&mut self) {
+ self.font_resource = None;
+ self.look.family = self.look0.family;
+ self.family_chosen = false;
+ }
+
+ /// A standard family picked from the font list, replacing the paragraph's font even when it
+ /// is the family the paragraph's font name reads as.
+ pub(crate) fn choose_family(&mut self, family: pdfcraft_engine::FontFamily) {
+ self.font_resource = None;
+ self.look.family = family;
+ self.family_chosen = true;
+ }
+
pub(crate) fn has_changes(&self) -> bool {
- self.text != self.original || self.look != self.look0 || self.extras != self.extras0
+ self.text != self.original || self.look != self.look0 || self.extras != self.extras0 || self.family_chosen
}
/// Extra width permitted by the editor. Source containers now remain fixed.
@@ -229,7 +239,7 @@ impl LineEditor {
let (l, o) = (&self.look, &self.look0);
let mut style = pdfcraft_engine::BlockStyle {
resource: self.font_resource.clone(),
- family: (l.family != o.family || l.bold != o.bold || l.italic != o.italic).then_some((l.family, l.bold, l.italic)),
+ family: (l.family != o.family || self.family_chosen || l.bold != o.bold || l.italic != o.italic).then_some((l.family, l.bold, l.italic)),
size: (l.size != o.size).then_some(l.size),
color: (l.color != o.color).then_some(l.color),
align: (l.align != o.align).then_some(l.align),
@@ -267,6 +277,7 @@ impl LineEditor {
pub fn applied(&mut self) {
self.selection = None;
self.font_resource = None;
+ self.family_chosen = false;
self.look0 = self.look.clone();
self.extras0 = self.extras;
self.original = self.text.clone();
@@ -965,6 +976,7 @@ fn open_editor(
focus: true,
look: look_of(l),
look0: look_of(l),
+ family_chosen: false,
extras: Extras::default(),
extras0: Extras::default(),
});
@@ -1177,7 +1189,7 @@ fn editor_job(ui: &egui::Ui, ed: &LineEditor, text: &str, scale: f32, width: f32
let Some(line) = lines.get(index) else { continue };
let mut look = source_look(&line.base_font, line.size, line.color, line.bold, line.italic);
look.size = line.size;
- if ed.look.family != ed.look0.family {
+ if ed.look.family != ed.look0.family || ed.family_chosen {
look.family = ed.look.family;
}
if ed.look.bold != ed.look0.bold {
@@ -1193,7 +1205,8 @@ fn editor_job(ui: &egui::Ui, ed: &LineEditor, text: &str, scale: f32, width: f32
look.size = ed.look.size;
}
let mut font = editor_font(&look, (look.size as f32 * scale).clamp(1.0, 300.0));
- let original_face = ed.look.family == ed.look0.family && ed.look.bold == ed.look0.bold && ed.look.italic == ed.look0.italic;
+ let original_face =
+ ed.look.family == ed.look0.family && !ed.family_chosen && ed.look.bold == ed.look0.bold && ed.look.italic == ed.look0.italic;
let embedded = original_face && ed.fonts.contains_key(&line.font);
if embedded && let Some(name) = ed.fonts.get(&line.font) {
font.family = FontFamily::Name(name.clone().into());
@@ -1474,12 +1487,31 @@ mod tests {
focus: true,
look: look_of(&block),
look0: look_of(&block),
+ family_chosen: false,
extras: Extras::default(),
extras0: Extras::default(),
};
(app, ed)
}
+ #[test]
+ fn picking_the_family_the_font_name_reads_as_still_changes_the_font() {
+ // A Calibri paragraph reads as Helvetica; picking Helvetica from the list used to leave
+ // `look` equal to what the paragraph had, so no family change was sent and Calibri stayed.
+ let (_app, mut ed) = preview_app();
+ let family = ed.look0.family;
+ assert_eq!(ed.style().family, None, "nothing chosen yet");
+ ed.choose_family(family);
+ assert_eq!(ed.style().family, Some((family, ed.look.bold, ed.look.italic)));
+ assert!(ed.has_changes());
+ assert_eq!(ed.font_shown(&[]), family.label());
+ ed.keep_source_font();
+ assert_eq!(ed.style().family, None, "Keep source fonts withdraws the choice");
+ ed.choose_family(family);
+ ed.applied();
+ assert_eq!(ed.style().family, None, "an applied choice is the paragraph's font now");
+ }
+
#[test]
fn a_loaded_font_is_applied_to_the_paragraph_being_edited() {
// Loading a font used to only register it on the page: the paragraph kept its source
diff --git a/crates/ui-egui/src/format_text_ui.rs b/crates/ui-egui/src/format_text_ui.rs
new file mode 100644
index 000000000..798b844a9
--- /dev/null
+++ b/crates/ui-egui/src/format_text_ui.rs
@@ -0,0 +1,333 @@
+//! Format text, laid out as Acrobat's panel: the font across the panel; size, colour, lists and
+//! alignment; bold, italic, underline, superscript and subscript; line and paragraph spacing;
+//! horizontal scale and character spacing. One panel serves a paragraph being edited (with the
+//! fonts already on its page and the spacing controls) and added text (the standard families,
+//! size, colour, style and alignment). What PdfCraft can't apply yet (lists, superscript and
+//! subscript, paragraph spacing, or spacing for added text) is shown disabled and says so.
+//!
+//! The icons are Lucide's (AGENTS.md §1); only the arrangement follows Acrobat.
+
+use egui::{Align, Color32, CornerRadius, Layout, Rect, Sense, Stroke, vec2};
+use pdfcraft_engine::{AddedText, FontFamily, TextAlign};
+
+use crate::edit_text_ui::Extras;
+use crate::theme::Tokens;
+use crate::{icons, widgets};
+
+/// A font picked from the font list. The caller applies it, since keeping the paragraph's own
+/// font also means restoring the family it started with.
+#[derive(Clone, Debug, PartialEq)]
+pub(crate) enum FontChoice {
+ /// The fonts the paragraph already uses.
+ KeepSource,
+ /// One of the page's fonts, by resource name.
+ Resource(String),
+ /// A standard family.
+ Family(FontFamily),
+ /// Load font…: pick a font file.
+ Load,
+ /// A font file installed on this computer.
+ Installed(std::path::PathBuf),
+}
+
+/// What the font list offers and shows.
+pub(crate) struct Fonts<'a> {
+ /// The fonts on the paragraph's page (resource, face); `None` for added text, which has the
+ /// standard families only.
+ pub sources: Option<&'a [(String, String)]>,
+ /// The name the list shows for the current choice.
+ pub shown: String,
+}
+
+/// What the panel changed: the style (in place) and a font picked from the list.
+#[derive(Default)]
+pub(crate) struct Outcome {
+ pub changed: bool,
+ pub font: Option,
+}
+
+const SIZES: [f64; 14] = [8.0, 9.0, 10.0, 11.0, 12.0, 14.0, 16.0, 18.0, 20.0, 24.0, 28.0, 36.0, 48.0, 72.0];
+const LINE_SPACINGS: [f64; 7] = [1.0, 1.15, 1.2, 1.5, 2.0, 2.5, 3.0];
+const SCALES: [f64; 8] = [50.0, 75.0, 90.0, 100.0, 110.0, 125.0, 150.0, 200.0];
+const CHAR_SPACINGS: [f64; 8] = [-1.0, -0.5, 0.0, 0.5, 1.0, 2.0, 3.0, 5.0];
+/// A row's height: the tallest control in it (the 30 pt icon buttons). Every control is centred
+/// on the row, whatever its own height, so lists, swatches and buttons line up (#3037).
+const ROW: f32 = 30.0;
+/// The gap between a row's controls.
+const GAP: f32 = 6.0;
+/// The icon naming a list or field ([`labelled_icon`]).
+const LABEL_ICON: f32 = 22.0;
+/// The button beside a number field that opens its list of common values: just an arrow, so the
+/// field keeps the room its value needs.
+const PRESET_LIST: f32 = 22.0;
+/// What can be typed: horizontal scale in percent, character spacing in points.
+const SCALE_RANGE: std::ops::RangeInclusive = 10.0..=400.0;
+const CHAR_SPACING_RANGE: std::ops::RangeInclusive = -5.0..=50.0;
+
+/// A number as the lists show it: no trailing zeros ("1.2", "100", "-0.5").
+fn number(v: f64) -> String {
+ let s = format!("{v:.2}");
+ s.trim_end_matches('0').trim_end_matches('.').to_string()
+}
+
+pub(crate) fn panel(ui: &mut egui::Ui, t: &Tokens, look: &mut AddedText, extras: Option<&mut Extras>, fonts: Fonts) -> Outcome {
+ let before = (look.clone(), extras.as_deref().copied());
+ let mut out = Outcome::default();
+ widgets::section_title(ui, tl!("Format text"));
+
+ // The font, across the panel.
+ let width = ui.available_width();
+ egui::ComboBox::from_id_salt("format-font")
+ .selected_text(fonts.shown.as_str())
+ .width(width)
+ .show_ui(ui, |ui| {
+ if let Some(sources) = fonts.sources {
+ if ui.selectable_label(false, tl!("Keep source fonts")).clicked() {
+ out.font = Some(FontChoice::KeepSource);
+ }
+ for (resource, face) in sources.iter().filter(|(r, _)| !r.starts_with("PCApproved")) {
+ if ui.selectable_label(false, face).clicked() {
+ out.font = Some(FontChoice::Resource(resource.clone()));
+ }
+ }
+ ui.separator();
+ }
+ for f in [FontFamily::Helvetica, FontFamily::Times, FontFamily::Courier] {
+ if ui.selectable_label(fonts.sources.is_none() && look.family == f, f.label()).clicked() {
+ out.font = Some(FontChoice::Family(f));
+ }
+ }
+ if fonts.sources.is_some() {
+ ui.separator();
+ if ui.selectable_label(false, tl!("Load font…")).clicked() {
+ out.font = Some(FontChoice::Load);
+ }
+ #[cfg(not(target_arch = "wasm32"))]
+ ui.menu_button(tl!("Installed fonts…"), |ui| match crate::files::installed_font_files(ui.ctx()) {
+ Some(list) if list.is_empty() => {
+ ui.label(tl!("No openly licensed document fonts found. Load a font file."));
+ }
+ Some(list) => {
+ for font in list {
+ if ui.button(&font.face).clicked() {
+ out.font = Some(FontChoice::Installed(font.path));
+ ui.close();
+ }
+ }
+ }
+ None => {
+ ui.label(tl!("Finding document fonts…"));
+ }
+ });
+ }
+ })
+ .response
+ .on_hover_text(format!("{}\n{}", tl!("Font"), tl!("Thai and Arabic use regular fallback fonts when available.")));
+ ui.add_space(4.0);
+
+ // Size and colour | bullets, numbering and alignment.
+ row(ui, |ui| {
+ let _ = egui::ComboBox::from_id_salt("format-size")
+ .selected_text(number(look.size))
+ .width(64.0)
+ .show_ui(ui, |ui| {
+ for size in SIZES {
+ ui.selectable_value(&mut look.size, size, number(size));
+ }
+ })
+ .response
+ .on_hover_text(tl!("Font size"));
+ let swatch = colour_button(ui, t, look.color);
+ egui::Popup::menu(&swatch).show(|ui| {
+ if let Some(picked) = crate::comments::swatch_grid(ui, Some(look.color)) {
+ look.color = picked;
+ ui.close();
+ }
+ });
+ let (sep, _) = ui.allocate_exact_size(vec2(9.0, 26.0), Sense::hover());
+ ui.painter().vline(sep.center().x, sep.y_range().shrink(3.0), Stroke::new(1.0, t.divider));
+ planned(ui, "list", tl!("Bulleted list"));
+ planned(ui, "list-ordered", tl!("Numbered list"));
+ let current = match look.align {
+ TextAlign::Left => "align-left",
+ TextAlign::Center => "align-center",
+ TextAlign::Right => "align-right",
+ TextAlign::Justify => "align-justify",
+ };
+ let align = icons::button(ui, current, 30.0, false, tl!("Alignment"));
+ let align = flyout(ui, t, align);
+ egui::Popup::menu(&align).show(|ui| {
+ ui.horizontal(|ui| {
+ for (a, icon, tip) in [
+ (TextAlign::Left, "align-left", tl!("Align left")),
+ (TextAlign::Center, "align-center", tl!("Centre")),
+ (TextAlign::Right, "align-right", tl!("Align right")),
+ (TextAlign::Justify, "align-justify", tl!("Justify")),
+ ] {
+ if icons::button(ui, icon, 30.0, look.align == a, tip).clicked() {
+ look.align = a;
+ ui.close();
+ }
+ }
+ });
+ });
+ });
+
+ // Bold, italic, underline, superscript, subscript.
+ let mut extras = extras;
+ row(ui, |ui| {
+ if icons::button(ui, "bold", 30.0, look.bold, tl!("Bold")).clicked() {
+ look.bold = !look.bold;
+ }
+ if icons::button(ui, "italic", 30.0, look.italic, tl!("Italic")).clicked() {
+ look.italic = !look.italic;
+ }
+ match extras.as_deref_mut() {
+ Some(e) => {
+ if icons::button(ui, "underline", 30.0, e.underline, tl!("Underline")).clicked() {
+ e.underline = !e.underline;
+ }
+ }
+ None => planned(ui, "underline", tl!("Underline")),
+ }
+ planned(ui, "superscript", tl!("Superscript"));
+ planned(ui, "subscript", tl!("Subscript"));
+ });
+
+ // Line and paragraph spacing; horizontal scale and character spacing: two columns, each an
+ // icon and its control, `half` wide.
+ let half = ((ui.available_width() - 2.0 * LABEL_ICON - 8.0 - 3.0 * GAP) / 2.0).max(40.0);
+ ui.add_enabled_ui(extras.is_some(), |ui| {
+ let mut fallback = Extras::default();
+ let e = extras.as_deref_mut().unwrap_or(&mut fallback);
+ row(ui, |ui| {
+ labelled_icon(ui, t, "arrow-up-down", tl!("Line spacing"));
+ let own = tl!("Auto").to_string();
+ let shown = if e.line_spacing == 0.0 { own.clone() } else { number(e.line_spacing) };
+ egui::ComboBox::from_id_salt("format-line-spacing").selected_text(shown).width(half).show_ui(ui, |ui| {
+ ui.selectable_value(&mut e.line_spacing, 0.0, own.as_str());
+ for v in LINE_SPACINGS {
+ ui.selectable_value(&mut e.line_spacing, v, number(v));
+ }
+ });
+ ui.add_space(8.0);
+ ui.add_enabled_ui(false, |ui| {
+ labelled_icon(ui, t, "pilcrow", tl!("Paragraph spacing"));
+ let tip = format!("{} — {}", tl!("Paragraph spacing"), tl!("Planned"));
+ let _ = egui::ComboBox::from_id_salt("format-paragraph-spacing")
+ .selected_text("0")
+ .width(half)
+ .show_ui(ui, |_| {})
+ .response
+ .on_disabled_hover_text(tip);
+ });
+ });
+ // Any value the engine takes can be typed; the lists hold the common ones.
+ row(ui, |ui| {
+ let label = labelled_icon(ui, t, "move-horizontal", tl!("Horizontal scale"));
+ let scale = Number { value: &mut e.scale, range: SCALE_RANGE, speed: 1.0, suffix: " %" };
+ number_with_presets(ui, scale, &SCALES, half, (label.id, tl!("Horizontal scale")));
+ ui.add_space(8.0);
+ let label = labelled_icon(ui, t, "arrow-left-right", tl!("Character spacing"));
+ let spacing = Number { value: &mut e.char_spacing, range: CHAR_SPACING_RANGE, speed: 0.1, suffix: " pt" };
+ number_with_presets(ui, spacing, &CHAR_SPACINGS, half, (label.id, tl!("Character spacing")));
+ });
+ });
+
+ out.changed = (look.clone(), extras.as_deref().copied()) != before;
+ ui.add_space(6.0);
+ ui.separator();
+ out
+}
+
+/// One row of the panel, [`ROW`] high, its controls [`GAP`] apart and centred on it.
+fn row(ui: &mut egui::Ui, add: impl FnOnce(&mut egui::Ui) -> R) -> R {
+ let size = vec2(ui.available_width(), ROW);
+ ui.allocate_ui_with_layout(size, Layout::left_to_right(Align::Center), |ui| {
+ ui.spacing_mut().item_spacing.x = GAP;
+ // A list lays itself out from the default control height and grows downward, which leaves
+ // it below the row's middle; at a list's own height it is centred like the rest.
+ let list = ui.text_style_height(&egui::TextStyle::Button) + 2.0 * ui.spacing().button_padding.y;
+ ui.spacing_mut().interact_size.y = ui.spacing().interact_size.y.max(list);
+ add(ui)
+ })
+ .inner
+}
+
+/// A control PdfCraft doesn't have yet: its icon, disabled, saying so when hovered.
+fn planned(ui: &mut egui::Ui, icon: &str, label: &str) {
+ let _ = ui
+ .add_enabled_ui(false, |ui| icons::button(ui, icon, 30.0, false, label))
+ .inner
+ .on_disabled_hover_text(format!("{label} — {}", tl!("Planned")));
+}
+
+/// The small corner mark Acrobat puts on a button that opens a menu.
+fn flyout(ui: &egui::Ui, t: &Tokens, resp: egui::Response) -> egui::Response {
+ let r = resp.rect;
+ let tri = vec![r.right_bottom() + vec2(-3.0, -3.0), r.right_bottom() + vec2(-8.0, -3.0), r.right_bottom() + vec2(-3.0, -8.0)];
+ ui.painter().add(egui::Shape::convex_polygon(tri, t.text_faint, Stroke::NONE));
+ resp
+}
+
+/// An icon naming the list beside it, for screen readers too. Returns it, for the control it
+/// labels.
+fn labelled_icon(ui: &mut egui::Ui, t: &Tokens, icon: &str, label: &str) -> egui::Response {
+ let (rect, resp) = ui.allocate_exact_size(vec2(LABEL_ICON, 26.0), Sense::hover());
+ resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Label, ui.is_enabled(), label));
+ let tint = if ui.is_enabled() { t.icon } else { t.text_faint };
+ icons::paint(ui, Rect::from_center_size(rect.center(), vec2(18.0, 18.0)), icon, 17.0, tint);
+ resp.on_hover_text(label)
+}
+
+/// A value typed in a number field: what it edits, what may be typed, and how it reads.
+struct Number<'a> {
+ value: &'a mut f64,
+ range: std::ops::RangeInclusive,
+ speed: f64,
+ suffix: &'static str,
+}
+
+/// A number field with an arrow beside it that lists common values, in `width` all told: type any
+/// value in its range, or pick one. Both are named by `label`: the icon before them and its text.
+fn number_with_presets(ui: &mut egui::Ui, n: Number<'_>, presets: &[f64], width: f32, label: (egui::Id, &str)) {
+ ui.scope(|ui| {
+ ui.spacing_mut().item_spacing.x = 2.0;
+ let field = egui::DragValue::new(&mut *n.value).range(n.range).speed(n.speed).max_decimals(2).suffix(n.suffix);
+ let height = ui.spacing().interact_size.y;
+ ui.add_sized([(width - PRESET_LIST - 2.0).max(30.0), height], field).labelled_by(label.0);
+ let list = icons::button(ui, "chevron-down", PRESET_LIST, false, label.1);
+ egui::Popup::menu(&list).show(|ui| {
+ for &v in presets {
+ if ui.selectable_label(*n.value == v, number(v)).clicked() {
+ *n.value = v;
+ ui.close();
+ }
+ }
+ });
+ });
+}
+
+/// The text colour as a square swatch that opens the palette.
+fn colour_button(ui: &mut egui::Ui, t: &Tokens, color: [f64; 3]) -> egui::Response {
+ let (rect, resp) = ui.allocate_exact_size(vec2(28.0, 28.0), Sense::click());
+ resp.widget_info(|| egui::WidgetInfo::labeled(egui::WidgetType::Button, true, tl!("Colour")));
+ let [r, g, b] = color.map(|v| (v.clamp(0.0, 1.0) * 255.0).round() as u8);
+ let fill = rect.shrink(4.0);
+ ui.painter().rect_filled(fill, CornerRadius::same(2), Color32::from_rgb(r, g, b));
+ let stroke = if resp.hovered() { t.text } else { t.border };
+ ui.painter().rect_stroke(fill, CornerRadius::same(2), Stroke::new(1.0, stroke), egui::StrokeKind::Outside);
+ crate::theme::hand(resp).on_hover_text(tl!("Colour"))
+}
+
+#[cfg(test)]
+mod tests {
+ #[test]
+ fn numbers_read_like_the_lists() {
+ assert_eq!(super::number(1.2), "1.2");
+ assert_eq!(super::number(100.0), "100");
+ assert_eq!(super::number(-0.5), "-0.5");
+ assert_eq!(super::number(0.0), "0");
+ assert_eq!(super::number(1.15), "1.15");
+ }
+}
diff --git a/crates/ui-egui/src/i18n/ar.tsv b/crates/ui-egui/src/i18n/ar.tsv
index 4bb58ba8e..68413b493 100644
--- a/crates/ui-egui/src/i18n/ar.tsv
+++ b/crates/ui-egui/src/i18n/ar.tsv
@@ -2711,5 +2711,11 @@ certificate Version الإصدار
Make properties default جعل الخصائص افتراضية
Pages selected الصفحات المحددة
Page properties خصائص الصفحة
+ Bulleted list قائمة نقطية
+ Numbered list قائمة مرقمة
+ Alignment المحاذاة
+ Superscript نص مرتفع
+ Subscript نص منخفض
+ Paragraph spacing تباعد الفقرات
Less أقل
Other options خيارات أخرى
diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv
index 584fafda3..dabbc53d7 100644
--- a/crates/ui-egui/src/i18n/bg.tsv
+++ b/crates/ui-egui/src/i18n/bg.tsv
@@ -2397,5 +2397,12 @@ animation effects Off Изключени
Make properties default Задаване като свойства по подразбиране
Pages selected Избрани страници
Page properties Свойства на страницата
+ Font Шрифт
+ Bulleted list Списък с водещи символи
+ Numbered list Номериран списък
+ Alignment Подравняване
+ Superscript Горен индекс
+ Subscript Долен индекс
+ Paragraph spacing Разстояние между абзаците
Less По-малко
Other options Други опции
diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv
index d347ca95d..3d6d65baf 100644
--- a/crates/ui-egui/src/i18n/cs.tsv
+++ b/crates/ui-egui/src/i18n/cs.tsv
@@ -2508,5 +2508,12 @@ animation effects Off Vypnuto
Make properties default Nastavit vlastnosti jako výchozí
Pages selected Vybrané stránky
Page properties Vlastnosti stránky
+ Font Písmo
+ Bulleted list Odrážkový seznam
+ Numbered list Číslovaný seznam
+ Alignment Zarovnání
+ Superscript Horní index
+ Subscript Dolní index
+ Paragraph spacing Mezera mezi odstavci
Less Méně
Other options Další možnosti
diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv
index cb1ffc2fd..916a03b94 100644
--- a/crates/ui-egui/src/i18n/de.tsv
+++ b/crates/ui-egui/src/i18n/de.tsv
@@ -2435,5 +2435,12 @@ animation effects Off Aus
Make properties default Eigenschaften als Standard festlegen
Pages selected Ausgewählte Seiten
Page properties Seiteneigenschaften
+ Font Schrift
+ Bulleted list Aufzählung
+ Numbered list Nummerierte Liste
+ Alignment Ausrichtung
+ Superscript Hochgestellt
+ Subscript Tiefgestellt
+ Paragraph spacing Absatzabstand
Less Weniger
Other options Weitere Optionen
diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv
index 2bde0b50b..6ace4f902 100644
--- a/crates/ui-egui/src/i18n/el.tsv
+++ b/crates/ui-egui/src/i18n/el.tsv
@@ -2440,5 +2440,12 @@ animation effects Off Ανενεργά
Make properties default Ορισμός ιδιοτήτων ως προεπιλογή
Pages selected Επιλεγμένες σελίδες
Page properties Ιδιότητες σελίδας
+ Font Γραμματοσειρά
+ Bulleted list Λίστα με κουκκίδες
+ Numbered list Αριθμημένη λίστα
+ Alignment Στοίχιση
+ Superscript Εκθέτης
+ Subscript Δείκτης
+ Paragraph spacing Απόσταση παραγράφων
Less Λιγότερα
Other options Άλλες επιλογές
diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv
index fbc9f54c9..07c1bfd6c 100644
--- a/crates/ui-egui/src/i18n/es.tsv
+++ b/crates/ui-egui/src/i18n/es.tsv
@@ -2570,6 +2570,13 @@ animation effects Off Desactivados
Make properties default Usar como propiedades predeterminadas
Pages selected Páginas seleccionadas
Page properties Propiedades de página
+ Font Fuente
+ Bulleted list Lista con viñetas
+ Numbered list Lista numerada
+ Alignment Alineación
+ Superscript Superíndice
+ Subscript Subíndice
+ Paragraph spacing Espaciado entre párrafos
Less Menos
Other options Otras opciones
diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv
index bde247a8d..59a2c74f1 100644
--- a/crates/ui-egui/src/i18n/fr.tsv
+++ b/crates/ui-egui/src/i18n/fr.tsv
@@ -2434,5 +2434,12 @@ animation effects Off Désactivés
Make properties default Définir comme propriétés par défaut
Pages selected Pages sélectionnées
Page properties Propriétés de la page
+ Font Police
+ Bulleted list Liste à puces
+ Numbered list Liste numérotée
+ Alignment Alignement
+ Superscript Exposant
+ Subscript Indice
+ Paragraph spacing Espacement des paragraphes
Less Moins
Other options Autres options
diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv
index 16efc30a0..6fb896ea3 100644
--- a/crates/ui-egui/src/i18n/hu.tsv
+++ b/crates/ui-egui/src/i18n/hu.tsv
@@ -2433,5 +2433,12 @@ animation effects Off Ki
Make properties default Tulajdonságok beállítása alapértelmezettként
Pages selected Kijelölt oldalak
Page properties Oldaltulajdonságok
+ Font Betűtípus
+ Bulleted list Felsorolás
+ Numbered list Számozott lista
+ Alignment Igazítás
+ Superscript Felső index
+ Subscript Alsó index
+ Paragraph spacing Bekezdéstávolság
Less Kevesebb
Other options Egyéb beállítások
diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv
index c46f9cf81..e3390dfb1 100644
--- a/crates/ui-egui/src/i18n/it.tsv
+++ b/crates/ui-egui/src/i18n/it.tsv
@@ -2646,5 +2646,12 @@ certificate Key usage Utilizzo chiave
certificate Self-signed Autofirmato
certificate SHA-1 digest Impronta SHA-1
certificate SHA-256 digest Impronta SHA-256
+ Font Carattere
+ Bulleted list Elenco puntato
+ Numbered list Elenco numerato
+ Alignment Allineamento
+ Superscript Apice
+ Subscript Pedice
+ Paragraph spacing Spaziatura paragrafi
Less Meno
Other options Altre opzioni
diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv
index 54bc54633..84b45a9cd 100644
--- a/crates/ui-egui/src/i18n/ja.tsv
+++ b/crates/ui-egui/src/i18n/ja.tsv
@@ -2434,5 +2434,12 @@ animation effects Off オフ
Make properties default プロパティをデフォルトに設定
Pages selected 選択したページ
Page properties ページのプロパティ
+ Font フォント
+ Bulleted list 箇条書き
+ Numbered list 番号付きリスト
+ Alignment 配置
+ Superscript 上付き
+ Subscript 下付き
+ Paragraph spacing 段落間隔
Less 折りたたむ
Other options その他のオプション
diff --git a/crates/ui-egui/src/i18n/kk.tsv b/crates/ui-egui/src/i18n/kk.tsv
index 5ca11b941..4306b5055 100644
--- a/crates/ui-egui/src/i18n/kk.tsv
+++ b/crates/ui-egui/src/i18n/kk.tsv
@@ -2432,5 +2432,12 @@ animation effects Off Өшірулі
Make properties default Қасиеттерді әдепкі ету
Pages selected Таңдалған беттер
Page properties Бет қасиеттері
+ Font Қаріп
+ Bulleted list Таңбалауыш тізім
+ Numbered list Нөмірленген тізім
+ Alignment Туралау
+ Superscript Жоғарғы индекс
+ Subscript Төменгі индекс
+ Paragraph spacing Абзац аралығы
Less Азырақ
Other options Басқа параметрлер
diff --git a/crates/ui-egui/src/i18n/lt.tsv b/crates/ui-egui/src/i18n/lt.tsv
index 2674dfc86..5a5306ba0 100644
--- a/crates/ui-egui/src/i18n/lt.tsv
+++ b/crates/ui-egui/src/i18n/lt.tsv
@@ -2685,5 +2685,11 @@ key Space Tarpas
The password must have at least 6 characters. Slaptažodį turi sudaryti bent 6 simboliai.
This file changed on disk. Reload it to see the new version; your unsaved changes will be lost. Šis failas diske pasikeitė. Įkelkite jį iš naujo, kad pamatytumėte naują versiją; neįrašyti pakeitimai bus prarasti.
theme Appearance Išvaizda
+ Bulleted list Sąrašas su ženkleliais
+ Numbered list Numeruotas sąrašas
+ Alignment Lygiuotė
+ Superscript Viršutinis indeksas
+ Subscript Apatinis indeksas
+ Paragraph spacing Tarpas tarp pastraipų
Less Mažiau
Other options Kitos parinktys
diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv
index d6d5e5e93..2d423c2a2 100644
--- a/crates/ui-egui/src/i18n/pl.tsv
+++ b/crates/ui-egui/src/i18n/pl.tsv
@@ -2410,5 +2410,12 @@ animation effects Off Wyłączone
Make properties default Ustaw właściwości jako domyślne
Pages selected Zaznaczone strony
Page properties Właściwości strony
+ Font Czcionka
+ Bulleted list Lista punktowana
+ Numbered list Lista numerowana
+ Alignment Wyrównanie
+ Superscript Indeks górny
+ Subscript Indeks dolny
+ Paragraph spacing Odstęp między akapitami
Less Mniej
Other options Inne opcje
diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv
index ee041f4af..2729e2b97 100644
--- a/crates/ui-egui/src/i18n/pt-br.tsv
+++ b/crates/ui-egui/src/i18n/pt-br.tsv
@@ -2448,5 +2448,12 @@ animation effects Off Desativados
Make properties default Tornar propriedades padrão
Pages selected Páginas selecionadas
Page properties Propriedades da página
+ Font Fonte
+ Bulleted list Lista com marcadores
+ Numbered list Lista numerada
+ Alignment Alinhamento
+ Superscript Sobrescrito
+ Subscript Subscrito
+ Paragraph spacing Espaçamento entre parágrafos
Less Menos
Other options Outras opções
diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv
index 72fcb1ddc..af7dc384b 100644
--- a/crates/ui-egui/src/i18n/ru.tsv
+++ b/crates/ui-egui/src/i18n/ru.tsv
@@ -2347,5 +2347,12 @@ animation effects Off Выкл
Make properties default Сделать свойства стандартными
Pages selected Выделенные страницы
Page properties Свойства страницы
+ Font Шрифт
+ Bulleted list Маркированный список
+ Numbered list Нумерованный список
+ Alignment Выравнивание
+ Superscript Надстрочный
+ Subscript Подстрочный
+ Paragraph spacing Интервал между абзацами
Less Меньше
Other options Другие параметры
diff --git a/crates/ui-egui/src/i18n/sl.tsv b/crates/ui-egui/src/i18n/sl.tsv
index 009131307..fd00ce09e 100644
--- a/crates/ui-egui/src/i18n/sl.tsv
+++ b/crates/ui-egui/src/i18n/sl.tsv
@@ -2439,5 +2439,12 @@ animation effects Off Izklopljeni
Make properties default Nastavi lastnosti kot privzete
Pages selected Izbrane strani
Page properties Lastnosti strani
+ Font Pisava
+ Bulleted list Označen seznam
+ Numbered list Oštevilčen seznam
+ Alignment Poravnava
+ Superscript Nadpisano
+ Subscript Podpisano
+ Paragraph spacing Razmik med odstavki
Less Manj
Other options Druge možnosti
diff --git a/crates/ui-egui/src/i18n/te.tsv b/crates/ui-egui/src/i18n/te.tsv
index 80008d191..f7e844496 100644
--- a/crates/ui-egui/src/i18n/te.tsv
+++ b/crates/ui-egui/src/i18n/te.tsv
@@ -2343,5 +2343,12 @@ animation effects Off ఆఫ్
Make properties default లక్షణాలను డిఫాల్ట్గా చేయి
Pages selected ఎంచుకున్న పేజీలు
Page properties పేజీ లక్షణాలు
+ Font ఫాంట్
+ Bulleted list బుల్లెట్ జాబితా
+ Numbered list సంఖ్యల జాబితా
+ Alignment అమరిక
+ Superscript సూపర్స్క్రిప్ట్
+ Subscript సబ్స్క్రిప్ట్
+ Paragraph spacing పేరా అంతరం
Less తక్కువ
Other options ఇతర ఎంపికలు
diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv
index b185f0262..925ea2c4b 100644
--- a/crates/ui-egui/src/i18n/tr.tsv
+++ b/crates/ui-egui/src/i18n/tr.tsv
@@ -2430,5 +2430,12 @@ animation effects Off Kapalı
Make properties default Özellikleri varsayılan yap
Pages selected Seçilen sayfalar
Page properties Sayfa özellikleri
+ Font Yazı tipi
+ Bulleted list Madde işaretli liste
+ Numbered list Numaralı liste
+ Alignment Hizalama
+ Superscript Üst simge
+ Subscript Alt simge
+ Paragraph spacing Paragraf aralığı
Less Daha az
Other options Diğer seçenekler
diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv
index c25eba25b..a64c31fe0 100644
--- a/crates/ui-egui/src/i18n/uk.tsv
+++ b/crates/ui-egui/src/i18n/uk.tsv
@@ -2456,5 +2456,12 @@ animation effects Off Вимкнено
Make properties default Зробити властивості типовими
Pages selected Вибрані сторінки
Page properties Властивості сторінки
+ Font Шрифт
+ Bulleted list Маркований список
+ Numbered list Нумерований список
+ Alignment Вирівнювання
+ Superscript Надрядковий
+ Subscript Підрядковий
+ Paragraph spacing Інтервал між абзацами
Less Менше
Other options Інші параметри
diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv
index dbf9eb224..b2a99ef4d 100644
--- a/crates/ui-egui/src/i18n/vi.tsv
+++ b/crates/ui-egui/src/i18n/vi.tsv
@@ -2683,6 +2683,12 @@ key Space Space
Words Từ
“{name}” changed on disk after it was opened or last saved. Saving now replaces that newer version with this one. “{name}” đã thay đổi trên đĩa sau khi được mở hoặc lưu lần cuối. Lưu lúc này sẽ thay thế phiên bản mới hơn đó bằng phiên bản này.
@id file.word_count Đếm từ…
+ Bulleted list Danh sách dấu đầu dòng
+ Numbered list Danh sách đánh số
+ Alignment Căn lề
+ Paragraph spacing Khoảng cách đoạn
+ Superscript Chỉ số trên
+ Subscript Chỉ số dưới
Less Ít hơn
Other options Tùy chọn khác
diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv
index a71b3f30e..80623decf 100644
--- a/crates/ui-egui/src/i18n/zh-hans.tsv
+++ b/crates/ui-egui/src/i18n/zh-hans.tsv
@@ -2398,5 +2398,12 @@ animation effects Off 关
Make properties default 将属性设为默认
Pages selected 选定的页面
Page properties 页面属性
+ Font 字体
+ Bulleted list 项目符号列表
+ Numbered list 编号列表
+ Alignment 对齐
+ Superscript 上标
+ Subscript 下标
+ Paragraph spacing 段落间距
Less 收起
Other options 其他选项
diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv
index 630bfbf34..cd8a1051e 100644
--- a/crates/ui-egui/src/i18n/zh-hant.tsv
+++ b/crates/ui-egui/src/i18n/zh-hant.tsv
@@ -2384,5 +2384,12 @@ animation effects Off 關
Make properties default 將內容設為預設
Pages selected 選取的頁面
Page properties 頁面屬性
+ Font 字型
+ Bulleted list 項目符號清單
+ Numbered list 編號清單
+ Alignment 對齊
+ Superscript 上標
+ Subscript 下標
+ Paragraph spacing 段落間距
Less 收起
Other options 其他選項
diff --git a/crates/ui-egui/src/icon_data.rs b/crates/ui-egui/src/icon_data.rs
index 6e3f26f57..f986cd6bf 100644
--- a/crates/ui-egui/src/icon_data.rs
+++ b/crates/ui-egui/src/icon_data.rs
@@ -185,6 +185,10 @@ pub static ICONS: &[(&str, &[u8])] = &[
("user-round", include_bytes!("../../../assets/icons/user-round.svg")),
("wand-sparkles", include_bytes!("../../../assets/icons/wand-sparkles.svg")),
("wrench", include_bytes!("../../../assets/icons/wrench.svg")),
+ ("move-horizontal", include_bytes!("../../../assets/icons/move-horizontal.svg")),
+ ("pilcrow", include_bytes!("../../../assets/icons/pilcrow.svg")),
+ ("subscript", include_bytes!("../../../assets/icons/subscript.svg")),
+ ("superscript", include_bytes!("../../../assets/icons/superscript.svg")),
("x", include_bytes!("../../../assets/icons/x.svg")),
("zoom-in", include_bytes!("../../../assets/icons/zoom-in.svg")),
("zoom-out", include_bytes!("../../../assets/icons/zoom-out.svg")),
diff --git a/crates/ui-egui/src/icons.rs b/crates/ui-egui/src/icons.rs
index bd084333b..79254afa2 100644
--- a/crates/ui-egui/src/icons.rs
+++ b/crates/ui-egui/src/icons.rs
@@ -99,7 +99,7 @@ mod tests {
#[test]
fn shared_catalog_preserves_existing_icons_and_fallback_uri() {
- assert_eq!(ICONS.len(), 186);
+ assert_eq!(ICONS.len(), 190);
assert!(exists("minimize-2"));
for (name, original) in ICONS {
let expected =
diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs
index 34e7f4b61..ba4fc21fc 100644
--- a/crates/ui-egui/src/lib.rs
+++ b/crates/ui-egui/src/lib.rs
@@ -87,6 +87,7 @@ mod files;
pub mod fill_sign;
mod fling;
pub mod folders_ui;
+mod format_text_ui;
pub mod forms_ui;
mod home;
mod icon_data;
diff --git a/crates/ui-egui/src/panels.rs b/crates/ui-egui/src/panels.rs
index 8d6660b64..829ebad7e 100644
--- a/crates/ui-egui/src/panels.rs
+++ b/crates/ui-egui/src/panels.rs
@@ -516,50 +516,42 @@ fn format_section(app: &mut PdfCraftApp, ui: &mut egui::Ui, t: &Tokens) {
if let Some((i, _)) = app.active_ids()
&& let Some(ed) = app.views[i].line_editor.clone()
{
+ use crate::format_text_ui::{FontChoice, Fonts};
let mut ed = ed;
- let mut changed = false;
- let font_choices = app.session.get(app.views[i].id).map(|d| d.text_editor_fonts(ed.page)).unwrap_or_default();
- egui::ComboBox::from_id_salt("document-font-resource")
- .selected_text(
- ed.font_resource
- .as_ref()
- .and_then(|r| font_choices.iter().find(|f| &f.resource == r))
- .map_or(tl!("Keep source fonts"), |f| f.face.as_str()),
- )
- .show_ui(ui, |ui| {
- changed |= ui.selectable_value(&mut ed.font_resource, None, tl!("Keep source fonts")).changed();
- for font in font_choices.iter().filter(|f| !f.resource.starts_with("PCApproved")) {
- changed |= ui.selectable_value(&mut ed.font_resource, Some(font.resource.clone()), &font.face).changed();
- }
- });
- if ui.button(tl!("Load font…")).clicked() {
- app.pick_files(crate::files::FilePurpose::TextFont { page: ed.page }, false);
- }
- #[cfg(not(target_arch = "wasm32"))]
- egui::ComboBox::from_id_salt("installed-document-font").selected_text(tl!("Installed fonts…")).show_ui(ui, |ui| {
- if let Some(fonts) = crate::files::installed_font_files(ui.ctx()) {
- if fonts.is_empty() {
- ui.label(tl!("No openly licensed document fonts found. Load a font file."));
- }
- for font in fonts {
- if ui.selectable_label(false, &font.face).clicked() {
- app.use_paths(crate::files::FilePurpose::TextFont { page: ed.page }, &[font.path]);
- }
- }
- } else {
- ui.label(tl!("Finding document fonts…"));
+ let sources: Vec<(String, String)> = app
+ .session
+ .get(app.views[i].id)
+ .map(|d| d.text_editor_fonts(ed.page))
+ .unwrap_or_default()
+ .into_iter()
+ .map(|f| (f.resource, f.face))
+ .collect();
+ let shown = ed.font_shown(&sources);
+ let out = crate::format_text_ui::panel(ui, t, &mut ed.look, Some(&mut ed.extras), Fonts { sources: Some(&sources), shown });
+ let mut changed = out.changed;
+ match out.font {
+ Some(FontChoice::KeepSource) => {
+ ed.keep_source_font();
+ changed = true;
}
- });
- if let Some(look) = crate::content_ui::format_panel(ui, t, &ed.look) {
- ed.look = look;
- changed = true;
+ Some(FontChoice::Resource(resource)) => {
+ ed.font_resource = Some(resource);
+ changed = true;
+ }
+ Some(FontChoice::Family(family)) => {
+ ed.choose_family(family);
+ changed = true;
+ }
+ Some(FontChoice::Load) => app.pick_files(crate::files::FilePurpose::TextFont { page: ed.page }, false),
+ #[cfg(not(target_arch = "wasm32"))]
+ Some(FontChoice::Installed(path)) => app.use_paths(crate::files::FilePurpose::TextFont { page: ed.page }, &[path]),
+ #[cfg(target_arch = "wasm32")]
+ Some(FontChoice::Installed(_)) => {}
+ None => {}
}
- changed |= crate::edit_text_ui::extras_panel(ui, &mut ed.extras);
if changed {
app.apply_line_format(i, ed, Some(ui.ctx()));
}
- ui.add_space(6.0);
- ui.separator();
return;
}
let image = app.active_ids().and_then(|(i, id)| {
@@ -593,21 +585,32 @@ fn format_section(app: &mut PdfCraftApp, ui: &mut egui::Ui, t: &Tokens) {
});
match selected {
Some((page, index, text)) => {
- if let Some(style) = crate::content_ui::format_panel(ui, t, &text) {
- app.text_style = pdfcraft_engine::AddedText { text: String::new(), rect: [0.0; 4], ..style.clone() };
- app.apply_edit(pdfcraft_engine::Edit::UpdateContent { page, index, content: pdfcraft_engine::AddedContent::Text(style) });
+ let mut style = text.clone();
+ added_text_format(ui, t, &mut style);
+ if style != text {
+ // The reusable style follows the selection only when the edit took, so a refused one adds nothing odd later.
+ if app.apply_edit(pdfcraft_engine::Edit::UpdateContent { page, index, content: pdfcraft_engine::AddedContent::Text(style.clone()) }) {
+ app.text_style = pdfcraft_engine::AddedText { text: String::new(), rect: [0.0; 4], ..style };
+ }
}
}
None if app.quick_tool == crate::QuickTool::AddText => {
crate::content_ui::hint(ui, t);
- if let Some(style) = crate::content_ui::format_panel(ui, t, &app.text_style) {
- app.text_style = style;
- }
+ let mut style = app.text_style.clone();
+ added_text_format(ui, t, &mut style);
+ app.text_style = style;
}
- None => return,
+ None => {}
+ }
+}
+
+/// Format text for added text: the standard families, size, colour, style and alignment.
+fn added_text_format(ui: &mut egui::Ui, t: &Tokens, style: &mut pdfcraft_engine::AddedText) {
+ let shown = style.family.label().to_string();
+ let out = crate::format_text_ui::panel(ui, t, style, None, crate::format_text_ui::Fonts { sources: None, shown });
+ if let Some(crate::format_text_ui::FontChoice::Family(family)) = out.font {
+ style.family = family;
}
- ui.add_space(6.0);
- ui.separator();
}
impl PdfCraftApp {
diff --git a/crates/ui-egui/tests/content.rs b/crates/ui-egui/tests/content.rs
index dd2c032ef..70e5a0dcb 100644
--- a/crates/ui-egui/tests/content.rs
+++ b/crates/ui-egui/tests/content.rs
@@ -236,7 +236,7 @@ fn typing_moving_styling_and_deleting_added_content() {
h.run_steps(2);
assert_eq!(h.state().views[0].content.selected, Some((0, 0)), "the new text is selected");
// Bold from the Format panel: one undoable change.
- h.get_by_label("B").click();
+ h.get_by_label("Bold").click();
h.run_steps(3);
let pdfcraft_engine::AddedContent::Text(t) = &added(&h)[0].content else { panic!() };
@@ -585,3 +585,60 @@ fn released_text_does_not_jump_back_before_the_page_is_redrawn() {
let (old, new) = ink_at(&mut h, from, to);
assert!(new > 20 && old == 0, "the redrawn page has the text in its new place ({old} old, {new} new)");
}
+
+/// Format text is laid out as Acrobat's: alignment from its flyout, style buttons by name, and
+/// the controls PdfCraft can't apply yet present but disabled.
+/// Format text lines its rows up (#3037): the size list is centred beside the colour swatch and
+/// the alignment button and is as tall as the font list, each spacing row's controls share one
+/// line, and nothing reaches past the font list, which spans the panel.
+#[test]
+fn format_text_rows_line_up_and_fit_the_panel() {
+ use egui::accesskit::Role;
+ let mut h = harness();
+ assert!(h.state_mut().execute("edit.text"));
+ h.run_steps(3);
+ h.get_by_label("FORMAT TEXT");
+ let combo = |h: &Harness<'static, PdfCraftApp>, value: &str| {
+ h.query_all_by_role(Role::ComboBox).find(|n| n.value().as_deref() == Some(value)).map(|n| n.rect()).expect(value)
+ };
+ let (font, size) = (combo(&h, "Helvetica"), combo(&h, "12"));
+ let middle = |r: egui::Rect| r.center().y;
+ for label in ["Colour", "Alignment"] {
+ let other = h.get_by_label(label).rect();
+ assert!((middle(size) - middle(other)).abs() < 0.5, "the size list ({size:?}) is centred with {label} ({other:?})");
+ }
+ assert_eq!(size.height(), font.height(), "the font and size lists are the same height");
+ // Line and paragraph spacing; scale and character spacing with their lists.
+ let line = [combo(&h, "Auto"), combo(&h, "0")];
+ assert!((middle(line[0]) - middle(line[1])).abs() < 0.5, "{line:?}");
+ // Each: its number field and the arrow that lists common values.
+ let scale_row: Vec =
+ ["Horizontal scale", "Character spacing"].iter().flat_map(|l| h.query_all_by_label(l).map(|n| n.rect())).collect();
+ assert_eq!(scale_row.len(), 4, "two fields and their two arrows: {scale_row:?}");
+ for r in &scale_row {
+ assert!((middle(*r) - middle(scale_row[0])).abs() < 0.5, "{scale_row:?}");
+ }
+ for r in h.query_all_by_role(Role::ComboBox).map(|n| n.rect()).chain(scale_row) {
+ assert!(r.right() <= font.right() + 0.5, "{r:?} reaches past the panel ({font:?})");
+ }
+}
+
+#[test]
+fn format_text_panel_sets_alignment_and_lists_planned_controls_disabled() {
+ use egui_kittest::kittest::NodeT;
+ let mut h = harness();
+ assert!(h.state_mut().execute("edit.text"));
+ h.run_steps(3);
+ h.get_by_label("FORMAT TEXT");
+ for planned in ["Bulleted list", "Numbered list", "Superscript", "Subscript"] {
+ assert!(h.get_by_label(planned).accesskit_node().is_disabled(), "{planned} is shown but disabled");
+ }
+ h.get_by_label("Alignment").click();
+ h.run_steps(2);
+ h.get_by_label("Centre").click();
+ h.run_steps(2);
+ assert_eq!(h.state().text_style.align, pdfcraft_engine::TextAlign::Center);
+ h.get_by_label("Bold").click();
+ h.run_steps(2);
+ assert!(h.state().text_style.bold);
+}
diff --git a/crates/ui-egui/tests/editing.rs b/crates/ui-egui/tests/editing.rs
index cced4fe03..6885a2bb6 100644
--- a/crates/ui-egui/tests/editing.rs
+++ b/crates/ui-egui/tests/editing.rs
@@ -3028,7 +3028,7 @@ fn the_format_panel_restyles_the_paragraph_being_edited() {
assert!(h.state().views[0].line_editor.is_some());
// The Format text panel shows the paragraph's look; making it bold applies at once.
h.get_by_label("FORMAT TEXT");
- h.get_by_label("B").click();
+ h.get_by_label("Bold").click();
h.run_steps(4);
let s = h.state();
let doc = s.session.get(s.views[0].id).unwrap();
@@ -3044,6 +3044,62 @@ fn the_format_panel_restyles_the_paragraph_being_edited() {
assert!(s.views[0].line_editor.as_ref().is_some_and(|e| e.extras.underline));
}
+/// Horizontal scale and Character spacing take any value in their ranges, not only the listed
+/// ones: values between the listed ones and beyond them, typed in Format text, reach the saved
+/// file as the engine writes them.
+#[test]
+fn the_format_panel_takes_any_scale_and_character_spacing() {
+ let mut h = harness(1, |_| {});
+ let id = h.state().views[0].id;
+ let width = |h: &Harness<'static, PdfCraftApp>, view: usize| {
+ let s = h.state();
+ let r = s.session.get(s.views[view].id).unwrap().text_blocks(0)[0].rect;
+ r[2] - r[0]
+ };
+ let plain = width(&h, 0);
+ 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 at = egui::pos2(r.left() + 40.0 / 200.0 * r.width(), r.top() + (300.0 - 158.0) / 300.0 * r.height());
+ h.hover_at(at);
+ h.run_steps(1);
+ h.drag_at(at);
+ h.run_steps(1);
+ h.drop_at(at);
+ h.run_steps(3);
+ assert!(h.state().views[0].line_editor.is_some());
+ // Typed the way a person does: click the field, type, Enter.
+ let type_in = |h: &mut Harness<'static, PdfCraftApp>, label: &str, value: &str| {
+ h.get_by_role_and_label(Role::SpinButton, label).click();
+ h.run_steps(2);
+ h.event(egui::Event::Text(value.into()));
+ h.run_steps(1);
+ h.key_press(Key::Enter);
+ h.run_steps(4);
+ };
+ let extras = |h: &Harness<'static, PdfCraftApp>| h.state().views[0].line_editor.as_ref().map(|e| (e.extras.scale, e.extras.char_spacing));
+ // Between the listed values (50…200 %, −1…5 pt) and beyond them, within what was always allowed.
+ type_in(&mut h, "Horizontal scale", "80");
+ assert_eq!(extras(&h), Some((80.0, 0.0)));
+ type_in(&mut h, "Horizontal scale", "250");
+ assert_eq!(extras(&h), Some((250.0, 0.0)));
+ type_in(&mut h, "Character spacing", "0.25");
+ assert_eq!(extras(&h), Some((250.0, 0.25)));
+ type_in(&mut h, "Character spacing", "-2");
+ assert_eq!(extras(&h), Some((250.0, -2.0)));
+ assert_eq!(h.state().session.get(id).unwrap().can_undo(), Some("Edit text"));
+ // Saved and reopened, the paragraph is as wide as the engine draws it with those values.
+ let bytes = h.state().session.save_bytes(id).unwrap();
+ h.state_mut().open_bytes("reference.pdf", None, fixture(1)).expect("opens");
+ let style = pdfcraft_engine::BlockStyle { scale: Some(250.0), char_spacing: Some(-2.0), ..Default::default() };
+ assert!(h.state_mut().apply_edit(pdfcraft_engine::Edit::EditTextBlock { page: 0, block: 0, text: "Page 1".into(), style }));
+ h.state_mut().open_bytes("saved.pdf", None, bytes.as_ref().clone()).expect("the saved file opens");
+ h.run_steps(3);
+ let (reference, saved) = (width(&h, 1), width(&h, 2));
+ assert!(saved > plain * 1.5, "250 % widens it: {plain} → {saved}");
+ assert!((saved - reference).abs() < 0.5, "saved {saved}, the engine's own {reference}");
+}
+
#[test]
fn selected_text_formatting_keeps_the_rest_of_the_paragraph_and_reopens() {
let mut h = harness(1, |_| {});
@@ -3062,7 +3118,7 @@ fn selected_text_formatting_keeps_the_rest_of_the_paragraph_and_reopens() {
h.key_press_modifiers(Modifiers::SHIFT, Key::ArrowLeft);
h.run_steps(2);
assert_eq!(h.state().views[0].line_editor.as_ref().unwrap().selection, Some(5..6));
- h.get_by_label("B").click();
+ h.get_by_label("Bold").click();
h.run_steps(5);
let id = h.state().views[0].id;
let doc = h.state().session.get(id).unwrap();
diff --git a/parity/acrobat-features.toml b/parity/acrobat-features.toml
index 95443d57d..42bbe8352 100644
--- a/parity/acrobat-features.toml
+++ b/parity/acrobat-features.toml
@@ -1986,8 +1986,8 @@ milestone = "M7"
status = "shipped"
commands = ["edit.edit_text"]
tools = ["text_edit"]
-evidence = ["crates/edit/src/tests.rs::justify_underline_and_spacing", "crates/edit/src/tests.rs::paragraphs_take_new_formatting", "crates/ui-egui/tests/editing.rs::the_format_panel_restyles_the_paragraph_being_edited"]
-notes = "character spacing in points and horizontal scaling in percent for a paragraph (Tc, Tz), rewrapped"
+evidence = ["crates/edit/src/tests.rs::justify_underline_and_spacing", "crates/edit/src/tests.rs::paragraphs_take_new_formatting", "crates/ui-egui/tests/editing.rs::the_format_panel_restyles_the_paragraph_being_edited", "crates/ui-egui/tests/editing.rs::the_format_panel_takes_any_scale_and_character_spacing"]
+notes = "character spacing in points (-5 to 50) and horizontal scaling in percent (10 to 400) for a paragraph (Tc, Tz), rewrapped; Format text takes any value in those ranges typed, or a common one from its list"
[[feature]]
id = "edit.lists"
@@ -2592,8 +2592,8 @@ milestone = "M7"
status = "shipped"
commands = ["edit.edit_text"]
tools = ["text_edit"]
-evidence = ["crates/edit/src/tests.rs::paragraphs_take_new_formatting", "crates/ui-egui/tests/editing.rs::the_format_panel_restyles_the_paragraph_being_edited", "crates/automation/tests/automation.rs::editing_existing_text_through_tools"]
-notes = "Edit a PDF ▸ Format text shows the paragraph being edited (family, bold/italic guessed from its font, size) and applies changes at once as one undo step each, keeping the paragraph open"
+evidence = ["crates/edit/src/tests.rs::paragraphs_take_new_formatting", "crates/ui-egui/tests/editing.rs::the_format_panel_restyles_the_paragraph_being_edited", "crates/automation/tests/automation.rs::editing_existing_text_through_tools", "crates/ui-egui/tests/content.rs::format_text_panel_sets_alignment_and_lists_planned_controls_disabled", "crates/ui-egui/tests/content.rs::format_text_rows_line_up_and_fit_the_panel"]
+notes = "Edit a PDF ▸ Format text, laid out as Acrobat's panel, for the paragraph being edited and for added text: font (the source font kept, the document's fonts, families, installed or loaded font files), size, colour, alignment, bold and italic, line spacing, horizontal scale and character spacing; each change applies at once as one undo step, keeping the paragraph open. Shown disabled with a Planned tooltip until they exist: bulleted and numbered lists (edit.lists), superscript and subscript (edit.text-super-subscript), paragraph spacing, and underline on added text."
[[feature]]
id = "edit.adobe-express-edits"