M7: lay out Format text as Acrobat's panel (#2249)

* M7: lay out Format text as Acrobat's panel

Format text was a row of separate combos, a Load font button, an
Installed fonts combo, a swatch grid and labelled drag values. It now
follows Acrobat's arrangement: the font across the panel (the page's
fonts, the standard families, Load font… and Installed fonts in one
list, showing the paragraph's own font by name); size and a colour
swatch, then lists and an alignment flyout; bold, italic, underline,
superscript and subscript; line and paragraph spacing; horizontal scale
and character spacing. One panel serves an edited paragraph and added
text. What PdfCraft can't apply yet (lists, superscript and subscript,
paragraph spacing, spacing for added text) is shown disabled and says
it is planned.

Icons stay Lucide's: superscript, subscript, pilcrow and
move-horizontal are added from lucide-static 1.49.0 with attribution.
Eight new strings are translated in every catalogue.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M7: parity notes for the Format text panel

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* i18n(vi): Superscript and Subscript for Format text

The Vietnamese catalogue came from main without the Format text panel's
Superscript and Subscript labels; vietnamese_covers_ui_literals lists them.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M7: Format text takes any horizontal scale and character spacing again

Review of #2249: the panel's preset lists narrowed what could be set to
50-200 % and -1..5 pt, where the old fields took 10-400 % and -5..50 pt.
Each now has a number field over the old range again, with the list of
common values beside it.

Test: the_format_panel_takes_any_scale_and_character_spacing types values
between the listed ones and beyond them, saves and reopens, and compares
the paragraph with the engine's own rewrite. It fails without the field.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* M7: Format text's rows line their controls up and fit the panel

Raised in #3037: the size list sat lower than the colour swatch and the
alignment button beside it. egui lays a list out from the default control
height and grows it downward, so a row's shorter controls ended up off its
middle. Each row is now one control height and centres what it holds, at
a list's own height.

The spacing rows' columns are measured from what they hold, so the second
list no longer runs past the panel; the number fields' lists open from a
small arrow, which leaves the field the room its value needs.

Test: format_text_rows_line_up_and_fit_the_panel checks the size list is
centred with the swatch and alignment button and as tall as the font list,
that each spacing row shares one line, and that nothing passes the panel.
It fails without the change.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Format text: choosing the family a font name reads as still changes the font

CodeRabbit on #2249: a Calibri paragraph is shown as Helvetica, so picking Helvetica left
look.family equal to look0.family and no family change was sent. The pick is now tracked
(LineEditor::choose_family) and counts as a change in style(), font_shown(), has_changes()
and the editor preview; Keep source fonts and an applied edit clear it.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>

* Format text: the reusable text style follows a selection only when its edit was applied

A refused UpdateContent used to leave the new style in app.text_style, so the next
added text inherited formatting the selected text never received.

Co-Authored-By: Claude Sonnet 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01Jk4iYSh3mEd145niec77LF

---------

Co-authored-by: abc2cb <271337822+abc2cb@users.noreply.github.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
abc2cb
2026-10-22 01:26:28 +03:00
committed by GitHub
parent 193754502f
commit 6dec57bf80
38 changed files with 863 additions and 155 deletions
+5 -1
View File
@@ -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). 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 | | 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/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 | | `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 | | `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) ## Compiled in through dependencies (6)
+66 -22
View File
@@ -7,7 +7,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT" licence_file = "LICENSE-MIT"
kind = "translation" kind = "translation"
usage = "Greek UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" 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.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`). # 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" licence_file = "LICENSE-MIT"
kind = "translation" kind = "translation"
usage = "Simplified Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" usage = "Simplified Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "ac0a78f36b91c005b9543e6028b78bb44949fea1bf7991bf0791d407696cd832" sha256 = "4a9d4428c3f67e256aa2f9e9d2d057c0e40f60a4446a0b384f3055894a9a9213"
[[asset]] [[asset]]
path = "crates/ui-egui/src/i18n/zh-hant.tsv" path = "crates/ui-egui/src/i18n/zh-hant.tsv"
@@ -41,7 +41,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT" licence_file = "LICENSE-MIT"
kind = "translation" kind = "translation"
usage = "Traditional Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" usage = "Traditional Chinese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "c52b9a2aa403937e70859fe60b51719c97475c24927322d7534f5da0bdb5f420" sha256 = "f22298173d1e174feb83f6fb20e7079bb9091df86f396c69648b974f429be158"
[[asset]] [[asset]]
path = "crates/ui-egui/src/i18n/cs.tsv" path = "crates/ui-egui/src/i18n/cs.tsv"
@@ -52,7 +52,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT" licence_file = "LICENSE-MIT"
kind = "translation" kind = "translation"
usage = "Czech UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" usage = "Czech UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "0dcad418fa6399955adf2a8398aec307c2c73b13a37ca78ffe142e00491ef586" sha256 = "e320d5f50795dc1c46d589d72c7a27facb6164938cf9d75151e904b7450b142d"
[[asset]] [[asset]]
path = "crates/ui-egui/src/i18n/pt-br.tsv" path = "crates/ui-egui/src/i18n/pt-br.tsv"
@@ -63,7 +63,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT" licence_file = "LICENSE-MIT"
kind = "translation" kind = "translation"
usage = "Brazilian Portuguese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" usage = "Brazilian Portuguese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "130f0256d4ba09bc9cb6a87fc973bad96d6b155703b713341f0afead9ad6b4b3" sha256 = "0dd7e0e0b0843b519cebc5504ce7006e4c9cb1eef02b270ed52d008972ee7ff3"
[[asset]] [[asset]]
path = "crates/ui-egui/src/i18n/es.tsv" path = "crates/ui-egui/src/i18n/es.tsv"
@@ -74,7 +74,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT" licence_file = "LICENSE-MIT"
kind = "translation" kind = "translation"
usage = "Spanish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" usage = "Spanish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "d1093647d2af3d51f77a7b53f05ef1f45442128d40e696022624fbbece5bd0f0" sha256 = "dda5b3aea60d718eb2fe28e02a76728a3c895487e03f9319c2bcdef7ea2012db"
[[asset]] [[asset]]
path = "crates/ui-egui/src/i18n/fr.tsv" path = "crates/ui-egui/src/i18n/fr.tsv"
@@ -85,7 +85,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT" licence_file = "LICENSE-MIT"
kind = "translation" kind = "translation"
usage = "French UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" usage = "French UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "135d00bc60ef62f45ebee7c7fd83bc7433fd4054a04fb3901b2d2e8bdd2871c7" sha256 = "3351f6abd5e2796ad5e70b00604c9a7093387243ad1951713a717d06bffe419d"
[[asset]] [[asset]]
path = "crates/ui-egui/src/i18n/de.tsv" path = "crates/ui-egui/src/i18n/de.tsv"
@@ -96,7 +96,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT" licence_file = "LICENSE-MIT"
kind = "translation" kind = "translation"
usage = "German UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" usage = "German UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "0cb38f42fe45831495699f81837fb0b2d5bb16b1b69153359c4376e9bc1e6e04" sha256 = "2595a893a3cb84b6f0344b9e2144d9e956e80f778eb7d045fa2ad503e3cfb267"
[[asset]] [[asset]]
path = "crates/ui-egui/src/i18n/te.tsv" path = "crates/ui-egui/src/i18n/te.tsv"
@@ -107,7 +107,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT" licence_file = "LICENSE-MIT"
kind = "translation" kind = "translation"
usage = "Telugu UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" usage = "Telugu UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "ce97821a0fd6bf758e88e654f70985ed1e94c81e36f4e31ee3fc2f05ca39d99f" sha256 = "ec2d2afca09390c809e805a52108f5e8c94bf61feef7d0ba2901723db747323d"
[[asset]] [[asset]]
path = "crates/ui-egui/src/i18n/it.tsv" path = "crates/ui-egui/src/i18n/it.tsv"
@@ -118,7 +118,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT" licence_file = "LICENSE-MIT"
kind = "translation" kind = "translation"
usage = "Italian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" usage = "Italian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "45567329bdb7884b0b54e5c5161e6e73452d85200945f1ba3661eab1fa35da06" sha256 = "683f515a21dbc4b22b312c63e39b4d2a572a1f1cf733f02127f1ee62dbb7a05e"
[[asset]] [[asset]]
path = "crates/ui-egui/src/i18n/pl.tsv" path = "crates/ui-egui/src/i18n/pl.tsv"
@@ -129,7 +129,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT" licence_file = "LICENSE-MIT"
kind = "translation" kind = "translation"
usage = "Polish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" usage = "Polish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "421ef329305624ebfc661a8d9cd82131e79ec279b8be47d4baec29a226e494d9" sha256 = "e31d3969393994c33b90cd5fa82e80584955e4e0676df37136e345db476623ed"
[[asset]] [[asset]]
path = "crates/ui-egui/src/i18n/ru.tsv" path = "crates/ui-egui/src/i18n/ru.tsv"
@@ -140,7 +140,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT" licence_file = "LICENSE-MIT"
kind = "translation" kind = "translation"
usage = "Russian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" usage = "Russian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "97ed35e8959777a5f15b087136e53385430241e8522c7dc72a383c72dc0a27fd" sha256 = "fee17b5090aba63d3129772c271d9802444918fe3a905734b41e006e320e9234"
[[asset]] [[asset]]
path = "crates/ui-egui/src/i18n/hu.tsv" path = "crates/ui-egui/src/i18n/hu.tsv"
@@ -151,7 +151,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT" licence_file = "LICENSE-MIT"
kind = "translation" kind = "translation"
usage = "Hungarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" usage = "Hungarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "bc012e0dd9ad0d212a32b71bfc75a42ffcd058484aab28d173078384c9da480f" sha256 = "ef26690aeec1c4b6b648ac50bc2433fec681524ab98e6270cabd3d76be6d3e14"
[[asset]] [[asset]]
path = "crates/ui-egui/src/i18n/bg.tsv" path = "crates/ui-egui/src/i18n/bg.tsv"
@@ -162,7 +162,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT" licence_file = "LICENSE-MIT"
kind = "translation" kind = "translation"
usage = "Bulgarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" usage = "Bulgarian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "bd611d470f40a01b4442119f7189dabbe58b4904dbce398115399f02b2ecddaa" sha256 = "2452a0f56f455ad71cae439a2ecf6da0ae5e2aaa90790de4d16e222393030283"
[[asset]] [[asset]]
path = "crates/ui-egui/src/i18n/kk.tsv" path = "crates/ui-egui/src/i18n/kk.tsv"
@@ -173,7 +173,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT" licence_file = "LICENSE-MIT"
kind = "translation" kind = "translation"
usage = "Kazakh UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" usage = "Kazakh UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "1132e904bc1a2bcc8239987aa74b2fa75ca17d8eb20b5458eec7e2c075c0445a" sha256 = "bbf8c454edc80ddeaecce63e56f5bceba335ae6cce8e4456b14742cd3a820bfe"
[[asset]] [[asset]]
path = "crates/ui-egui/src/i18n/ja.tsv" path = "crates/ui-egui/src/i18n/ja.tsv"
@@ -184,7 +184,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT" licence_file = "LICENSE-MIT"
kind = "translation" kind = "translation"
usage = "Japanese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" usage = "Japanese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "11dd8250c667ca49c40b827632b2d83cad93d07a9d5a5f27b5b9d657293efb6b" sha256 = "64d3c1c532ecb94b44b2c894acdaf171a130cbaa8e173b099f9b9b249d2134f4"
[[asset]] [[asset]]
path = "crates/ui-egui/src/i18n/uk.tsv" path = "crates/ui-egui/src/i18n/uk.tsv"
@@ -195,7 +195,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT" licence_file = "LICENSE-MIT"
kind = "translation" kind = "translation"
usage = "Ukrainian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" usage = "Ukrainian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "6aa0788d784532fb67a0b589cecc27f727e6178221ee892c3de7fe8f75790607" sha256 = "9e87a8f108b49213c7bb0f077a2e2f1bae449e1459a908d8202beba280d8055c"
[[asset]] [[asset]]
path = "crates/ui-egui/src/i18n/ar.tsv" path = "crates/ui-egui/src/i18n/ar.tsv"
@@ -206,7 +206,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT" licence_file = "LICENSE-MIT"
kind = "translation" kind = "translation"
usage = "Arabic UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" usage = "Arabic UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "ed84a08b83035bc6f4d328b916692d40140c2748d383741d04fba6e013a76bb3" sha256 = "921e8e906bdc42675fe8922fc00835a255e019ed61688a01095367f6e881f187"
[[asset]] [[asset]]
path = "crates/ui-egui/src/i18n/tr.tsv" path = "crates/ui-egui/src/i18n/tr.tsv"
@@ -217,7 +217,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT" licence_file = "LICENSE-MIT"
kind = "translation" kind = "translation"
usage = "Turkish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" usage = "Turkish UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "1a5b3e9ad3f524e04bdb60e11f4a1424ca749a5ed4840e22abeaa97d24d235e8" sha256 = "59070be5688d7d183a7354f08f9604e8f962dcc5e223dd91d1cd940997c47399"
[[asset]] [[asset]]
path = "crates/ui-egui/src/i18n/lt.tsv" path = "crates/ui-egui/src/i18n/lt.tsv"
@@ -228,7 +228,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT" licence_file = "LICENSE-MIT"
kind = "translation" kind = "translation"
usage = "Lithuanian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" usage = "Lithuanian UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "136f1e68fc4caa5e6e8fe395551bffdc9ace93bb00e2da5ba59318846494420d" sha256 = "f48accb38d4c824cb3d2f71fcffe0d8edb0a9a5f7412b9c479da6fb2479e45d2"
[[asset]] [[asset]]
@@ -3352,7 +3352,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT" licence_file = "LICENSE-MIT"
kind = "translation" kind = "translation"
usage = "Vietnamese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" usage = "Vietnamese UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "052ecb803601ebc547934136c936fb5527d6dc97e49cf8cfbde0aa2b1a6cb8c7" sha256 = "42212a46ddca212bb94c15062be3b444b33181ea68cdd9880fc5191f56d986a5"
[[asset]] [[asset]]
path = "crates/ui-egui/src/i18n/sl.tsv" path = "crates/ui-egui/src/i18n/sl.tsv"
@@ -3363,7 +3363,7 @@ licence = "MIT OR Apache-2.0"
licence_file = "LICENSE-MIT" licence_file = "LICENSE-MIT"
kind = "translation" kind = "translation"
usage = "Slovene UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs" usage = "Slovene UI strings, compiled in by crates/ui-egui/src/i18n/mod.rs"
sha256 = "299c274d071478c49430d8dbc2a1979085fa4f8735324697e8ac0db3d3adce7c" sha256 = "fcc38245d923c4ddefb5560fe7fdfe5bc6c1b9c49d4fcb25f2873e63dff4f806"
[[asset]] [[asset]]
@@ -3376,3 +3376,47 @@ licence_file = "vendor/vello_common/LICENSE-MIT"
kind = "image" kind = "image"
usage = "Reference picture of the vendored vello_common probe module, for people only; the build reads probe.rgba, not this file" usage = "Reference picture of the vendored vello_common probe module, for people only; the build reads probe.rgba, not this file"
sha256 = "78c59a0cfd2c829909d77006f94e34dac1ae5d1f90e7f10f72e84de541fef697" 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"
+17
View File
@@ -0,0 +1,17 @@
<!-- @license lucide-static v1.49.0 - ISC -->
<svg
class="lucide lucide-move-horizontal"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m18 8 4 4-4 4" />
<path d="M2 12h20" />
<path d="m6 8-4 4 4 4" />
</svg>

After

Width:  |  Height:  |  Size: 375 B

+17
View File
@@ -0,0 +1,17 @@
<!-- @license lucide-static v1.49.0 - ISC -->
<svg
class="lucide lucide-pilcrow"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="M13 4v16" />
<path d="M17 4v16" />
<path d="M19 4H9.5a4.5 4.5 0 0 0 0 9H13" />
</svg>

After

Width:  |  Height:  |  Size: 380 B

+17
View File
@@ -0,0 +1,17 @@
<!-- @license lucide-static v1.49.0 - ISC -->
<svg
class="lucide lucide-subscript"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m4 5 8 8" />
<path d="m12 5-8 8" />
<path d="M20 19h-4c0-1.5.44-2 1.5-2.5S20 15.33 20 14c0-.47-.17-.93-.48-1.29a2.11 2.11 0 0 0-2.62-.44c-.42.24-.74.62-.9 1.07" />
</svg>

After

Width:  |  Height:  |  Size: 467 B

+17
View File
@@ -0,0 +1,17 @@
<!-- @license lucide-static v1.49.0 - ISC -->
<svg
class="lucide lucide-superscript"
xmlns="http://www.w3.org/2000/svg"
width="24"
height="24"
viewBox="0 0 24 24"
fill="none"
stroke="currentColor"
stroke-width="2"
stroke-linecap="round"
stroke-linejoin="round"
>
<path d="m4 19 8-8" />
<path d="m12 19-8-8" />
<path d="M20 12h-4c0-1.5.442-2 1.5-2.5S20 8.334 20 7.002c0-.472-.17-.93-.484-1.29a2.105 2.105 0 0 0-2.617-.436c-.42.239-.738.614-.899 1.06" />
</svg>

After

Width:  |  Height:  |  Size: 486 B

+1 -46
View File
@@ -4,7 +4,7 @@
//! Delete to remove it. Each change is one undoable engine edit. //! Delete to remove it. Each change is one undoable engine edit.
use egui::{Color32, CornerRadius, Pos2, Rect, Stroke}; 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 pdfcraft_render::DocInfo;
use crate::canvas::{DocView, PageXform}; use crate::canvas::{DocView, PageXform};
@@ -605,51 +605,6 @@ fn finish(cv: &mut ContentView, d: TextDraft, added: &[Added]) -> Option<Edit> {
} }
} }
/// 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<AddedText> {
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. /// How to use the tools, under the Add content list.
pub(crate) fn hint(ui: &mut egui::Ui, t: &Tokens) { pub(crate) fn hint(ui: &mut egui::Ui, t: &Tokens) {
ui.label( ui.label(
+63 -31
View File
@@ -162,6 +162,10 @@ pub struct LineEditor {
/// The Format text panel's values, and what the paragraph had (to send only changes). /// The Format text panel's values, and what the paragraph had (to send only changes).
pub look: pdfcraft_engine::AddedText, pub look: pdfcraft_engine::AddedText,
look0: 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 /// Underline, line spacing (× size; 0 = the paragraph's own), character spacing (pt) and
/// horizontal scale (%), and what they were. /// horizontal scale (%), and what they were.
pub extras: Extras, 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 { 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 { 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. /// 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 (l, o) = (&self.look, &self.look0);
let mut style = pdfcraft_engine::BlockStyle { let mut style = pdfcraft_engine::BlockStyle {
resource: self.font_resource.clone(), 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), size: (l.size != o.size).then_some(l.size),
color: (l.color != o.color).then_some(l.color), color: (l.color != o.color).then_some(l.color),
align: (l.align != o.align).then_some(l.align), align: (l.align != o.align).then_some(l.align),
@@ -267,6 +277,7 @@ impl LineEditor {
pub fn applied(&mut self) { pub fn applied(&mut self) {
self.selection = None; self.selection = None;
self.font_resource = None; self.font_resource = None;
self.family_chosen = false;
self.look0 = self.look.clone(); self.look0 = self.look.clone();
self.extras0 = self.extras; self.extras0 = self.extras;
self.original = self.text.clone(); self.original = self.text.clone();
@@ -965,6 +976,7 @@ fn open_editor(
focus: true, focus: true,
look: look_of(l), look: look_of(l),
look0: look_of(l), look0: look_of(l),
family_chosen: false,
extras: Extras::default(), extras: Extras::default(),
extras0: 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 Some(line) = lines.get(index) else { continue };
let mut look = source_look(&line.base_font, line.size, line.color, line.bold, line.italic); let mut look = source_look(&line.base_font, line.size, line.color, line.bold, line.italic);
look.size = line.size; 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; look.family = ed.look.family;
} }
if ed.look.bold != ed.look0.bold { 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; look.size = ed.look.size;
} }
let mut font = editor_font(&look, (look.size as f32 * scale).clamp(1.0, 300.0)); 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); let embedded = original_face && ed.fonts.contains_key(&line.font);
if embedded && let Some(name) = ed.fonts.get(&line.font) { if embedded && let Some(name) = ed.fonts.get(&line.font) {
font.family = FontFamily::Name(name.clone().into()); font.family = FontFamily::Name(name.clone().into());
@@ -1474,12 +1487,31 @@ mod tests {
focus: true, focus: true,
look: look_of(&block), look: look_of(&block),
look0: look_of(&block), look0: look_of(&block),
family_chosen: false,
extras: Extras::default(), extras: Extras::default(),
extras0: Extras::default(), extras0: Extras::default(),
}; };
(app, ed) (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] #[test]
fn a_loaded_font_is_applied_to_the_paragraph_being_edited() { 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 // Loading a font used to only register it on the page: the paragraph kept its source
+333
View File
@@ -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<FontChoice>,
}
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<f64> = 10.0..=400.0;
const CHAR_SPACING_RANGE: std::ops::RangeInclusive<f64> = -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<R>(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<f64>,
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");
}
}
+6
View File
@@ -2711,5 +2711,11 @@ certificate Version الإصدار
Make properties default جعل الخصائص افتراضية Make properties default جعل الخصائص افتراضية
Pages selected الصفحات المحددة Pages selected الصفحات المحددة
Page properties خصائص الصفحة Page properties خصائص الصفحة
Bulleted list قائمة نقطية
Numbered list قائمة مرقمة
Alignment المحاذاة
Superscript نص مرتفع
Subscript نص منخفض
Paragraph spacing تباعد الفقرات
Less أقل Less أقل
Other options خيارات أخرى Other options خيارات أخرى
Can't render this file because it contains an unexpected character in line 6 and column 47.
+7
View File
@@ -2397,5 +2397,12 @@ animation effects Off Изключени
Make properties default Задаване като свойства по подразбиране Make properties default Задаване като свойства по подразбиране
Pages selected Избрани страници Pages selected Избрани страници
Page properties Свойства на страницата Page properties Свойства на страницата
Font Шрифт
Bulleted list Списък с водещи символи
Numbered list Номериран списък
Alignment Подравняване
Superscript Горен индекс
Subscript Долен индекс
Paragraph spacing Разстояние между абзаците
Less По-малко Less По-малко
Other options Други опции Other options Други опции
Can't render this file because it contains an unexpected character in line 8 and column 16.
+7
View File
@@ -2508,5 +2508,12 @@ animation effects Off Vypnuto
Make properties default Nastavit vlastnosti jako výchozí Make properties default Nastavit vlastnosti jako výchozí
Pages selected Vybrané stránky Pages selected Vybrané stránky
Page properties Vlastnosti 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ě Less Méně
Other options Další možnosti Other options Další možnosti
Can't render this file because it contains an unexpected character in line 6 and column 47.
+7
View File
@@ -2435,5 +2435,12 @@ animation effects Off Aus
Make properties default Eigenschaften als Standard festlegen Make properties default Eigenschaften als Standard festlegen
Pages selected Ausgewählte Seiten Pages selected Ausgewählte Seiten
Page properties Seiteneigenschaften Page properties Seiteneigenschaften
Font Schrift
Bulleted list Aufzählung
Numbered list Nummerierte Liste
Alignment Ausrichtung
Superscript Hochgestellt
Subscript Tiefgestellt
Paragraph spacing Absatzabstand
Less Weniger Less Weniger
Other options Weitere Optionen Other options Weitere Optionen
Can't render this file because it contains an unexpected character in line 6 and column 51.
+7
View File
@@ -2440,5 +2440,12 @@ animation effects Off Ανενεργά
Make properties default Ορισμός ιδιοτήτων ως προεπιλογή Make properties default Ορισμός ιδιοτήτων ως προεπιλογή
Pages selected Επιλεγμένες σελίδες Pages selected Επιλεγμένες σελίδες
Page properties Ιδιότητες σελίδας Page properties Ιδιότητες σελίδας
Font Γραμματοσειρά
Bulleted list Λίστα με κουκκίδες
Numbered list Αριθμημένη λίστα
Alignment Στοίχιση
Superscript Εκθέτης
Subscript Δείκτης
Paragraph spacing Απόσταση παραγράφων
Less Λιγότερα Less Λιγότερα
Other options Άλλες επιλογές Other options Άλλες επιλογές
Can't render this file because it contains an unexpected character in line 477 and column 9.
+7
View File
@@ -2570,6 +2570,13 @@ animation effects Off Desactivados
Make properties default Usar como propiedades predeterminadas Make properties default Usar como propiedades predeterminadas
Pages selected Páginas seleccionadas Pages selected Páginas seleccionadas
Page properties Propiedades de página 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 Less Menos
Other options Otras opciones Other options Otras opciones
Can't render this file because it contains an unexpected character in line 6 and column 60.
+7
View File
@@ -2434,5 +2434,12 @@ animation effects Off Désactivés
Make properties default Définir comme propriétés par défaut Make properties default Définir comme propriétés par défaut
Pages selected Pages sélectionnées Pages selected Pages sélectionnées
Page properties Propriétés de la page 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 Less Moins
Other options Autres options Other options Autres options
Can't render this file because it contains an unexpected character in line 6 and column 39.
+7
View File
@@ -2433,5 +2433,12 @@ animation effects Off Ki
Make properties default Tulajdonságok beállítása alapértelmezettként Make properties default Tulajdonságok beállítása alapértelmezettként
Pages selected Kijelölt oldalak Pages selected Kijelölt oldalak
Page properties Oldaltulajdonságok 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 Less Kevesebb
Other options Egyéb beállítások Other options Egyéb beállítások
Can't render this file because it contains an unexpected character in line 7 and column 44.
+7
View File
@@ -2646,5 +2646,12 @@ certificate Key usage Utilizzo chiave
certificate Self-signed Autofirmato certificate Self-signed Autofirmato
certificate SHA-1 digest Impronta SHA-1 certificate SHA-1 digest Impronta SHA-1
certificate SHA-256 digest Impronta SHA-256 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 Less Meno
Other options Altre opzioni Other options Altre opzioni
Can't render this file because it contains an unexpected character in line 6 and column 47.
+7
View File
@@ -2434,5 +2434,12 @@ animation effects Off オフ
Make properties default プロパティをデフォルトに設定 Make properties default プロパティをデフォルトに設定
Pages selected 選択したページ Pages selected 選択したページ
Page properties ページのプロパティ Page properties ページのプロパティ
Font フォント
Bulleted list 箇条書き
Numbered list 番号付きリスト
Alignment 配置
Superscript 上付き
Subscript 下付き
Paragraph spacing 段落間隔
Less 折りたたむ Less 折りたたむ
Other options その他のオプション Other options その他のオプション
Can't render this file because it contains an unexpected character in line 6 and column 47.
+7
View File
@@ -2432,5 +2432,12 @@ animation effects Off Өшірулі
Make properties default Қасиеттерді әдепкі ету Make properties default Қасиеттерді әдепкі ету
Pages selected Таңдалған беттер Pages selected Таңдалған беттер
Page properties Бет қасиеттері Page properties Бет қасиеттері
Font Қаріп
Bulleted list Таңбалауыш тізім
Numbered list Нөмірленген тізім
Alignment Туралау
Superscript Жоғарғы индекс
Subscript Төменгі индекс
Paragraph spacing Абзац аралығы
Less Азырақ Less Азырақ
Other options Басқа параметрлер Other options Басқа параметрлер
Can't render this file because it contains an unexpected character in line 826 and column 9.
+6
View File
@@ -2685,5 +2685,11 @@ key Space Tarpas
The password must have at least 6 characters. Slaptažodį turi sudaryti bent 6 simboliai. 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. 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 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 Less Mažiau
Other options Kitos parinktys Other options Kitos parinktys
Can't render this file because it contains an unexpected character in line 404 and column 9.
+7
View File
@@ -2410,5 +2410,12 @@ animation effects Off Wyłączone
Make properties default Ustaw właściwości jako domyślne Make properties default Ustaw właściwości jako domyślne
Pages selected Zaznaczone strony Pages selected Zaznaczone strony
Page properties Właściwości 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 Less Mniej
Other options Inne opcje Other options Inne opcje
Can't render this file because it contains an unexpected character in line 6 and column 47.
+7
View File
@@ -2448,5 +2448,12 @@ animation effects Off Desativados
Make properties default Tornar propriedades padrão Make properties default Tornar propriedades padrão
Pages selected Páginas selecionadas Pages selected Páginas selecionadas
Page properties Propriedades da página 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 Less Menos
Other options Outras opções Other options Outras opções
Can't render this file because it contains an unexpected character in line 9 and column 16.
+7
View File
@@ -2347,5 +2347,12 @@ animation effects Off Выкл
Make properties default Сделать свойства стандартными Make properties default Сделать свойства стандартными
Pages selected Выделенные страницы Pages selected Выделенные страницы
Page properties Свойства страницы Page properties Свойства страницы
Font Шрифт
Bulleted list Маркированный список
Numbered list Нумерованный список
Alignment Выравнивание
Superscript Надстрочный
Subscript Подстрочный
Paragraph spacing Интервал между абзацами
Less Меньше Less Меньше
Other options Другие параметры Other options Другие параметры
Can't render this file because it contains an unexpected character in line 9 and column 16.
+7
View File
@@ -2439,5 +2439,12 @@ animation effects Off Izklopljeni
Make properties default Nastavi lastnosti kot privzete Make properties default Nastavi lastnosti kot privzete
Pages selected Izbrane strani Pages selected Izbrane strani
Page properties Lastnosti 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 Less Manj
Other options Druge možnosti Other options Druge možnosti
Can't render this file because it contains an unexpected character in line 9 and column 16.
+7
View File
@@ -2343,5 +2343,12 @@ animation effects Off ఆఫ్
Make properties default లక్షణాలను డిఫాల్ట్‌గా చేయి Make properties default లక్షణాలను డిఫాల్ట్‌గా చేయి
Pages selected ఎంచుకున్న పేజీలు Pages selected ఎంచుకున్న పేజీలు
Page properties పేజీ లక్షణాలు Page properties పేజీ లక్షణాలు
Font ఫాంట్
Bulleted list బుల్లెట్ జాబితా
Numbered list సంఖ్యల జాబితా
Alignment అమరిక
Superscript సూపర్‌స్క్రిప్ట్
Subscript సబ్‌స్క్రిప్ట్
Paragraph spacing పేరా అంతరం
Less తక్కువ Less తక్కువ
Other options ఇతర ఎంపికలు Other options ఇతర ఎంపికలు
Can't render this file because it contains an unexpected character in line 13 and column 16.
+7
View File
@@ -2430,5 +2430,12 @@ animation effects Off Kapalı
Make properties default Özellikleri varsayılan yap Make properties default Özellikleri varsayılan yap
Pages selected Seçilen sayfalar Pages selected Seçilen sayfalar
Page properties Sayfa özellikleri 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 Less Daha az
Other options Diğer seçenekler Other options Diğer seçenekler
Can't render this file because it contains an unexpected character in line 825 and column 9.
+7
View File
@@ -2456,5 +2456,12 @@ animation effects Off Вимкнено
Make properties default Зробити властивості типовими Make properties default Зробити властивості типовими
Pages selected Вибрані сторінки Pages selected Вибрані сторінки
Page properties Властивості сторінки Page properties Властивості сторінки
Font Шрифт
Bulleted list Маркований список
Numbered list Нумерований список
Alignment Вирівнювання
Superscript Надрядковий
Subscript Підрядковий
Paragraph spacing Інтервал між абзацами
Less Менше Less Менше
Other options Інші параметри Other options Інші параметри
Can't render this file because it contains an unexpected character in line 6 and column 47.
+6
View File
@@ -2683,6 +2683,12 @@ key Space Space
Words Từ 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. “{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ừ… @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 Less Ít hơn
Other options Tùy chọn khác Other options Tùy chọn khác
Can't render this file because it contains an unexpected character in line 491 and column 12.
+7
View File
@@ -2398,5 +2398,12 @@ animation effects Off 关
Make properties default 将属性设为默认 Make properties default 将属性设为默认
Pages selected 选定的页面 Pages selected 选定的页面
Page properties 页面属性 Page properties 页面属性
Font 字体
Bulleted list 项目符号列表
Numbered list 编号列表
Alignment 对齐
Superscript 上标
Subscript 下标
Paragraph spacing 段落间距
Less 收起 Less 收起
Other options 其他选项 Other options 其他选项
Can't render this file because it contains an unexpected character in line 9 and column 16.
+7
View File
@@ -2384,5 +2384,12 @@ animation effects Off 關
Make properties default 將內容設為預設 Make properties default 將內容設為預設
Pages selected 選取的頁面 Pages selected 選取的頁面
Page properties 頁面屬性 Page properties 頁面屬性
Font 字型
Bulleted list 項目符號清單
Numbered list 編號清單
Alignment 對齊
Superscript 上標
Subscript 下標
Paragraph spacing 段落間距
Less 收起 Less 收起
Other options 其他選項 Other options 其他選項
Can't render this file because it contains an unexpected character in line 9 and column 16.
+4
View File
@@ -185,6 +185,10 @@ pub static ICONS: &[(&str, &[u8])] = &[
("user-round", include_bytes!("../../../assets/icons/user-round.svg")), ("user-round", include_bytes!("../../../assets/icons/user-round.svg")),
("wand-sparkles", include_bytes!("../../../assets/icons/wand-sparkles.svg")), ("wand-sparkles", include_bytes!("../../../assets/icons/wand-sparkles.svg")),
("wrench", include_bytes!("../../../assets/icons/wrench.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")), ("x", include_bytes!("../../../assets/icons/x.svg")),
("zoom-in", include_bytes!("../../../assets/icons/zoom-in.svg")), ("zoom-in", include_bytes!("../../../assets/icons/zoom-in.svg")),
("zoom-out", include_bytes!("../../../assets/icons/zoom-out.svg")), ("zoom-out", include_bytes!("../../../assets/icons/zoom-out.svg")),
+1 -1
View File
@@ -99,7 +99,7 @@ mod tests {
#[test] #[test]
fn shared_catalog_preserves_existing_icons_and_fallback_uri() { fn shared_catalog_preserves_existing_icons_and_fallback_uri() {
assert_eq!(ICONS.len(), 186); assert_eq!(ICONS.len(), 190);
assert!(exists("minimize-2")); assert!(exists("minimize-2"));
for (name, original) in ICONS { for (name, original) in ICONS {
let expected = let expected =
+1
View File
@@ -87,6 +87,7 @@ mod files;
pub mod fill_sign; pub mod fill_sign;
mod fling; mod fling;
pub mod folders_ui; pub mod folders_ui;
mod format_text_ui;
pub mod forms_ui; pub mod forms_ui;
mod home; mod home;
mod icon_data; mod icon_data;
+50 -47
View File
@@ -516,50 +516,42 @@ fn format_section(app: &mut PdfCraftApp, ui: &mut egui::Ui, t: &Tokens) {
if let Some((i, _)) = app.active_ids() if let Some((i, _)) = app.active_ids()
&& let Some(ed) = app.views[i].line_editor.clone() && let Some(ed) = app.views[i].line_editor.clone()
{ {
use crate::format_text_ui::{FontChoice, Fonts};
let mut ed = ed; let mut ed = ed;
let mut changed = false; let sources: Vec<(String, String)> = app
let font_choices = app.session.get(app.views[i].id).map(|d| d.text_editor_fonts(ed.page)).unwrap_or_default(); .session
egui::ComboBox::from_id_salt("document-font-resource") .get(app.views[i].id)
.selected_text( .map(|d| d.text_editor_fonts(ed.page))
ed.font_resource .unwrap_or_default()
.as_ref() .into_iter()
.and_then(|r| font_choices.iter().find(|f| &f.resource == r)) .map(|f| (f.resource, f.face))
.map_or(tl!("Keep source fonts"), |f| f.face.as_str()), .collect();
) let shown = ed.font_shown(&sources);
.show_ui(ui, |ui| { let out = crate::format_text_ui::panel(ui, t, &mut ed.look, Some(&mut ed.extras), Fonts { sources: Some(&sources), shown });
changed |= ui.selectable_value(&mut ed.font_resource, None, tl!("Keep source fonts")).changed(); let mut changed = out.changed;
for font in font_choices.iter().filter(|f| !f.resource.starts_with("PCApproved")) { match out.font {
changed |= ui.selectable_value(&mut ed.font_resource, Some(font.resource.clone()), &font.face).changed(); Some(FontChoice::KeepSource) => {
} ed.keep_source_font();
}); changed = true;
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…"));
} }
}); Some(FontChoice::Resource(resource)) => {
if let Some(look) = crate::content_ui::format_panel(ui, t, &ed.look) { ed.font_resource = Some(resource);
ed.look = look; changed = true;
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 { if changed {
app.apply_line_format(i, ed, Some(ui.ctx())); app.apply_line_format(i, ed, Some(ui.ctx()));
} }
ui.add_space(6.0);
ui.separator();
return; return;
} }
let image = app.active_ids().and_then(|(i, id)| { 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 { match selected {
Some((page, index, text)) => { Some((page, index, text)) => {
if let Some(style) = crate::content_ui::format_panel(ui, t, &text) { let mut style = text.clone();
app.text_style = pdfcraft_engine::AddedText { text: String::new(), rect: [0.0; 4], ..style.clone() }; added_text_format(ui, t, &mut style);
app.apply_edit(pdfcraft_engine::Edit::UpdateContent { page, index, content: pdfcraft_engine::AddedContent::Text(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 => { None if app.quick_tool == crate::QuickTool::AddText => {
crate::content_ui::hint(ui, t); crate::content_ui::hint(ui, t);
if let Some(style) = crate::content_ui::format_panel(ui, t, &app.text_style) { let mut style = app.text_style.clone();
app.text_style = style; 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 { impl PdfCraftApp {
+58 -1
View File
@@ -236,7 +236,7 @@ fn typing_moving_styling_and_deleting_added_content() {
h.run_steps(2); h.run_steps(2);
assert_eq!(h.state().views[0].content.selected, Some((0, 0)), "the new text is selected"); assert_eq!(h.state().views[0].content.selected, Some((0, 0)), "the new text is selected");
// Bold from the Format panel: one undoable change. // Bold from the Format panel: one undoable change.
h.get_by_label("B").click(); h.get_by_label("Bold").click();
h.run_steps(3); h.run_steps(3);
let pdfcraft_engine::AddedContent::Text(t) = &added(&h)[0].content else { panic!() }; 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); 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)"); 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<egui::Rect> =
["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);
}
+58 -2
View File
@@ -3028,7 +3028,7 @@ fn the_format_panel_restyles_the_paragraph_being_edited() {
assert!(h.state().views[0].line_editor.is_some()); assert!(h.state().views[0].line_editor.is_some());
// The Format text panel shows the paragraph's look; making it bold applies at once. // 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("FORMAT TEXT");
h.get_by_label("B").click(); h.get_by_label("Bold").click();
h.run_steps(4); h.run_steps(4);
let s = h.state(); let s = h.state();
let doc = s.session.get(s.views[0].id).unwrap(); 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)); 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] #[test]
fn selected_text_formatting_keeps_the_rest_of_the_paragraph_and_reopens() { fn selected_text_formatting_keeps_the_rest_of_the_paragraph_and_reopens() {
let mut h = harness(1, |_| {}); 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.key_press_modifiers(Modifiers::SHIFT, Key::ArrowLeft);
h.run_steps(2); h.run_steps(2);
assert_eq!(h.state().views[0].line_editor.as_ref().unwrap().selection, Some(5..6)); 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); h.run_steps(5);
let id = h.state().views[0].id; let id = h.state().views[0].id;
let doc = h.state().session.get(id).unwrap(); let doc = h.state().session.get(id).unwrap();
+4 -4
View File
@@ -1986,8 +1986,8 @@ milestone = "M7"
status = "shipped" status = "shipped"
commands = ["edit.edit_text"] commands = ["edit.edit_text"]
tools = ["text_edit"] 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"] 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 and horizontal scaling in percent for a paragraph (Tc, Tz), rewrapped" 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]] [[feature]]
id = "edit.lists" id = "edit.lists"
@@ -2592,8 +2592,8 @@ milestone = "M7"
status = "shipped" status = "shipped"
commands = ["edit.edit_text"] commands = ["edit.edit_text"]
tools = ["text_edit"] 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"] 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 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" 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]] [[feature]]
id = "edit.adobe-express-edits" id = "edit.adobe-express-edits"