From 13d837363d8e0409da65d8a723345be536a91112 Mon Sep 17 00:00:00 2001 From: aiongg <67543387+aiongg@users.noreply.github.com> Date: Tue, 27 Oct 2026 10:36:58 -0400 Subject: [PATCH 1/2] Dropdowns: menus grow back after a search with no matches (#4337) * chore(deps): pin craft-libs to 62bb0e1d (shared combo box) * fix(ui-egui): dropdown menus grow back after their contents shrink Every combo box is craft_ui::ComboBox, whose menu follows its contents' height while it is open (egui#5225); clippy.toml bans egui::ComboBox. The font family menu is a craft_ui::combo::Menu under its field, with the search field and filters in its header and the family rows scrolling under them, still laid out only in view. The menu keeps to the room on its side of the field, so the list's minimum height and the popup's room are gone. * fix(ui-egui): the shortcut import's target picker grows back after a search Its search field, "No equivalent" and the row's choices are the header of a craft_ui::combo::Menu, and Keyboard Shortcuts' entries scroll under them. That was the last menu on the popup path that capped itself at the room left, so that path and combo's `searchable` flag are gone. * chore(deps): craft-libs 60d60d48 (dropdown fixes) * chore(deps): craft-libs 4d7b82f7 (dropdown fixes) * chore(deps): craft-libs 92d392cf (#387 merged) * chore(xtask): pdf-deps expects craft-libs 92d392cf --- ASSETS.md | 34 +-- Cargo.lock | 199 +++++++++++++- Cargo.toml | 26 +- NOTICE | 4 +- clippy.toml | 3 + crates/ui-egui/src/chrome/status_bar.rs | 4 +- crates/ui-egui/src/chrome/tabs.rs | 2 +- crates/ui-egui/src/credits.rs | 2 +- crates/ui-egui/src/dialogs/dimension.rs | 4 +- crates/ui-egui/src/dialogs/layer_options.rs | 2 +- crates/ui-egui/src/dialogs/shortcut_import.rs | 79 +++--- .../src/dialogs/tests_shortcut_import.rs | 57 +++- crates/ui-egui/src/dialogs/tests_warp.rs | 6 +- crates/ui-egui/src/dialogs/warp_effect.rs | 2 +- crates/ui-egui/src/font_menu.rs | 256 ++++++++++-------- crates/ui-egui/src/panels/image_trace.rs | 6 +- crates/ui-egui/src/panels/properties.rs | 2 +- crates/ui-egui/src/tests/font_menu.rs | 55 ++++ crates/ui-egui/src/tests/rotateview.rs | 2 +- crates/ui-egui/src/widgets.rs | 4 +- crates/ui-egui/src/widgets/blend.rs | 2 +- crates/ui-egui/src/widgets/dropdowns.rs | 89 +++--- crates/ui-egui/src/widgets/look_tests.rs | 2 +- docs/pdf-backend.md | 2 +- xtask/src/pdf_deps.rs | 2 +- 25 files changed, 583 insertions(+), 263 deletions(-) diff --git a/ASSETS.md b/ASSETS.md index 186b87bb0..26d463091 100644 --- a/ASSETS.md +++ b/ASSETS.md @@ -352,20 +352,20 @@ The shared PDF backend keeps the existing standard-font and CMap features. These | Crate / file | Author | Source | Licence | SHA-256 | Use | | --- | --- | --- | --- | --- | --- | -| `hayro-interpret 0.7.0 / assets/FoxitDingbats.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/a0916808f7d78bed8c7b90cdf937176015454a7f/vendor/hayro-interpret/assets/FoxitDingbats.pfb | BSD-3-Clause | `845c752392b6c914fb989c75a08b7792b88f542d2499042ef2889f8c814a16ed` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | -| `hayro-interpret 0.7.0 / assets/FoxitFixed.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/a0916808f7d78bed8c7b90cdf937176015454a7f/vendor/hayro-interpret/assets/FoxitFixed.pfb | BSD-3-Clause | `b6c8fe53f134b8b6d4578cd2d544df4cee9624c4efa8d51a560fb40ea296101b` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | -| `hayro-interpret 0.7.0 / assets/FoxitFixedBold.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/a0916808f7d78bed8c7b90cdf937176015454a7f/vendor/hayro-interpret/assets/FoxitFixedBold.pfb | BSD-3-Clause | `f1b7159702973f54fc86254ea38bcf3712b2a736eb3a0995751e9f4bb45ad603` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | -| `hayro-interpret 0.7.0 / assets/FoxitFixedBoldItalic.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/a0916808f7d78bed8c7b90cdf937176015454a7f/vendor/hayro-interpret/assets/FoxitFixedBoldItalic.pfb | BSD-3-Clause | `8a000945843bd31add06aee63bf9fd41b7578b4f3242a4b7cd46349ad9d24d4d` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | -| `hayro-interpret 0.7.0 / assets/FoxitFixedItalic.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/a0916808f7d78bed8c7b90cdf937176015454a7f/vendor/hayro-interpret/assets/FoxitFixedItalic.pfb | BSD-3-Clause | `5007faf8320fa1fcc08b8894b356ad976f60b992ba29ee5272578bbdebaf3876` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | -| `hayro-interpret 0.7.0 / assets/FoxitSans.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/a0916808f7d78bed8c7b90cdf937176015454a7f/vendor/hayro-interpret/assets/FoxitSans.pfb | BSD-3-Clause | `a02339e95d20db14cbdfc1c6baf359cd36a091d5434925c3a4040f60e1a1af9e` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | -| `hayro-interpret 0.7.0 / assets/FoxitSansBold.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/a0916808f7d78bed8c7b90cdf937176015454a7f/vendor/hayro-interpret/assets/FoxitSansBold.pfb | BSD-3-Clause | `7bdfa36de1d5757dcfaf8b8a4be95c6e41c40fa70dbed9d78bf979a7b9959054` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | -| `hayro-interpret 0.7.0 / assets/FoxitSansBoldItalic.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/a0916808f7d78bed8c7b90cdf937176015454a7f/vendor/hayro-interpret/assets/FoxitSansBoldItalic.pfb | BSD-3-Clause | `555ce45924338e2e4b32821910221ce85da691fd59a9723c4c971a7c5627fc8b` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | -| `hayro-interpret 0.7.0 / assets/FoxitSansItalic.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/a0916808f7d78bed8c7b90cdf937176015454a7f/vendor/hayro-interpret/assets/FoxitSansItalic.pfb | BSD-3-Clause | `7751cbefeda351708dae8c4a88db043411bc39ab2ae2097685da845d4d16f307` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | -| `hayro-interpret 0.7.0 / assets/FoxitSerif.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/a0916808f7d78bed8c7b90cdf937176015454a7f/vendor/hayro-interpret/assets/FoxitSerif.pfb | BSD-3-Clause | `4f57d2b9d884af8f907bf22df6019b52d86cbf6214fdbd02b1ae05472a543f35` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | -| `hayro-interpret 0.7.0 / assets/FoxitSerifBold.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/a0916808f7d78bed8c7b90cdf937176015454a7f/vendor/hayro-interpret/assets/FoxitSerifBold.pfb | BSD-3-Clause | `0bdf4b04e964139818d51eda03d566ba999fc3ba2421b1c6c51f9dc969022e80` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | -| `hayro-interpret 0.7.0 / assets/FoxitSerifBoldItalic.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/a0916808f7d78bed8c7b90cdf937176015454a7f/vendor/hayro-interpret/assets/FoxitSerifBoldItalic.pfb | BSD-3-Clause | `a406cac82583bf98175cb62c87ed5e95c45fbb34eece63a10b0a13e793cb2e10` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | -| `hayro-interpret 0.7.0 / assets/FoxitSerifItalic.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/a0916808f7d78bed8c7b90cdf937176015454a7f/vendor/hayro-interpret/assets/FoxitSerifItalic.pfb | BSD-3-Clause | `610ae0687198045c4db5d1a6650fd5e92536631706382eb8b72e38df578d0ae9` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | -| `hayro-interpret 0.7.0 / assets/FoxitSymbol.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/a0916808f7d78bed8c7b90cdf937176015454a7f/vendor/hayro-interpret/assets/FoxitSymbol.pfb | BSD-3-Clause | `47967d055530e7357088a08403115425643ec2cdfd6201ba8af0fbd7116c1539` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | -| `hayro-interpret 0.7.0 / assets/CGATS001Compat-v2-micro.icc` | Clinton Ingram (saucecontrol/Compact-ICC-Profiles) | https://github.com/storytold/craft-libs/blob/a0916808f7d78bed8c7b90cdf937176015454a7f/vendor/hayro-interpret/assets/CGATS001Compat-v2-micro.icc | CC0-1.0 | `73e1ba37d2bad5bab2a964f40a9eed96209666efc067c3322626214bbef234a0` | Default CMYK colour conversion (pinned hayro-interpret) | -| `hayro-cmap 0.1.0 / assets/cmaps.brotli` | Adobe | https://github.com/storytold/craft-libs/blob/a0916808f7d78bed8c7b90cdf937176015454a7f/vendor/hayro-cmap/assets/cmaps.brotli | BSD-3-Clause | `ddf37af521d904d5646c2fc37b2b7e912dc1b56f0d210190b61f598154758e46` | Character-code mapping tables required to read CJK PDFs. Non-visual dependency data; existing embedding unchanged | -| `hayro-interpret 0.7.0 / src/font/generated/metrics.rs` | Adobe (Core 14 AFM metrics, PDF specification encodings); tables by The Hayro Authors | https://github.com/storytold/craft-libs/blob/a0916808f7d78bed8c7b90cdf937176015454a7f/vendor/hayro-interpret/src/font/generated/metrics.rs | MIT OR Apache-2.0 | `2ec9e608573eda459de1e1d9bda3bb494315e36bee885acc88912a11fe0caa41` | Glyph widths and encodings for non-embedded standard fonts. Non-visual dependency data; existing embedding unchanged | +| `hayro-interpret 0.7.0 / assets/FoxitDingbats.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/92d392cff132eaf53e13afff77f38b5c11b73aa0/vendor/hayro-interpret/assets/FoxitDingbats.pfb | BSD-3-Clause | `845c752392b6c914fb989c75a08b7792b88f542d2499042ef2889f8c814a16ed` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | +| `hayro-interpret 0.7.0 / assets/FoxitFixed.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/92d392cff132eaf53e13afff77f38b5c11b73aa0/vendor/hayro-interpret/assets/FoxitFixed.pfb | BSD-3-Clause | `b6c8fe53f134b8b6d4578cd2d544df4cee9624c4efa8d51a560fb40ea296101b` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | +| `hayro-interpret 0.7.0 / assets/FoxitFixedBold.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/92d392cff132eaf53e13afff77f38b5c11b73aa0/vendor/hayro-interpret/assets/FoxitFixedBold.pfb | BSD-3-Clause | `f1b7159702973f54fc86254ea38bcf3712b2a736eb3a0995751e9f4bb45ad603` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | +| `hayro-interpret 0.7.0 / assets/FoxitFixedBoldItalic.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/92d392cff132eaf53e13afff77f38b5c11b73aa0/vendor/hayro-interpret/assets/FoxitFixedBoldItalic.pfb | BSD-3-Clause | `8a000945843bd31add06aee63bf9fd41b7578b4f3242a4b7cd46349ad9d24d4d` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | +| `hayro-interpret 0.7.0 / assets/FoxitFixedItalic.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/92d392cff132eaf53e13afff77f38b5c11b73aa0/vendor/hayro-interpret/assets/FoxitFixedItalic.pfb | BSD-3-Clause | `5007faf8320fa1fcc08b8894b356ad976f60b992ba29ee5272578bbdebaf3876` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | +| `hayro-interpret 0.7.0 / assets/FoxitSans.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/92d392cff132eaf53e13afff77f38b5c11b73aa0/vendor/hayro-interpret/assets/FoxitSans.pfb | BSD-3-Clause | `a02339e95d20db14cbdfc1c6baf359cd36a091d5434925c3a4040f60e1a1af9e` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | +| `hayro-interpret 0.7.0 / assets/FoxitSansBold.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/92d392cff132eaf53e13afff77f38b5c11b73aa0/vendor/hayro-interpret/assets/FoxitSansBold.pfb | BSD-3-Clause | `7bdfa36de1d5757dcfaf8b8a4be95c6e41c40fa70dbed9d78bf979a7b9959054` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | +| `hayro-interpret 0.7.0 / assets/FoxitSansBoldItalic.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/92d392cff132eaf53e13afff77f38b5c11b73aa0/vendor/hayro-interpret/assets/FoxitSansBoldItalic.pfb | BSD-3-Clause | `555ce45924338e2e4b32821910221ce85da691fd59a9723c4c971a7c5627fc8b` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | +| `hayro-interpret 0.7.0 / assets/FoxitSansItalic.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/92d392cff132eaf53e13afff77f38b5c11b73aa0/vendor/hayro-interpret/assets/FoxitSansItalic.pfb | BSD-3-Clause | `7751cbefeda351708dae8c4a88db043411bc39ab2ae2097685da845d4d16f307` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | +| `hayro-interpret 0.7.0 / assets/FoxitSerif.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/92d392cff132eaf53e13afff77f38b5c11b73aa0/vendor/hayro-interpret/assets/FoxitSerif.pfb | BSD-3-Clause | `4f57d2b9d884af8f907bf22df6019b52d86cbf6214fdbd02b1ae05472a543f35` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | +| `hayro-interpret 0.7.0 / assets/FoxitSerifBold.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/92d392cff132eaf53e13afff77f38b5c11b73aa0/vendor/hayro-interpret/assets/FoxitSerifBold.pfb | BSD-3-Clause | `0bdf4b04e964139818d51eda03d566ba999fc3ba2421b1c6c51f9dc969022e80` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | +| `hayro-interpret 0.7.0 / assets/FoxitSerifBoldItalic.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/92d392cff132eaf53e13afff77f38b5c11b73aa0/vendor/hayro-interpret/assets/FoxitSerifBoldItalic.pfb | BSD-3-Clause | `a406cac82583bf98175cb62c87ed5e95c45fbb34eece63a10b0a13e793cb2e10` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | +| `hayro-interpret 0.7.0 / assets/FoxitSerifItalic.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/92d392cff132eaf53e13afff77f38b5c11b73aa0/vendor/hayro-interpret/assets/FoxitSerifItalic.pfb | BSD-3-Clause | `610ae0687198045c4db5d1a6650fd5e92536631706382eb8b72e38df578d0ae9` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | +| `hayro-interpret 0.7.0 / assets/FoxitSymbol.pfb` | Foxit Software Inc. / PDFium Authors | https://github.com/storytold/craft-libs/blob/92d392cff132eaf53e13afff77f38b5c11b73aa0/vendor/hayro-interpret/assets/FoxitSymbol.pfb | BSD-3-Clause | `47967d055530e7357088a08403115425643ec2cdfd6201ba8af0fbd7116c1539` | Renders non-embedded standard-14 fonts (pinned hayro-interpret) | +| `hayro-interpret 0.7.0 / assets/CGATS001Compat-v2-micro.icc` | Clinton Ingram (saucecontrol/Compact-ICC-Profiles) | https://github.com/storytold/craft-libs/blob/92d392cff132eaf53e13afff77f38b5c11b73aa0/vendor/hayro-interpret/assets/CGATS001Compat-v2-micro.icc | CC0-1.0 | `73e1ba37d2bad5bab2a964f40a9eed96209666efc067c3322626214bbef234a0` | Default CMYK colour conversion (pinned hayro-interpret) | +| `hayro-cmap 0.1.0 / assets/cmaps.brotli` | Adobe | https://github.com/storytold/craft-libs/blob/92d392cff132eaf53e13afff77f38b5c11b73aa0/vendor/hayro-cmap/assets/cmaps.brotli | BSD-3-Clause | `ddf37af521d904d5646c2fc37b2b7e912dc1b56f0d210190b61f598154758e46` | Character-code mapping tables required to read CJK PDFs. Non-visual dependency data; existing embedding unchanged | +| `hayro-interpret 0.7.0 / src/font/generated/metrics.rs` | Adobe (Core 14 AFM metrics, PDF specification encodings); tables by The Hayro Authors | https://github.com/storytold/craft-libs/blob/92d392cff132eaf53e13afff77f38b5c11b73aa0/vendor/hayro-interpret/src/font/generated/metrics.rs | MIT OR Apache-2.0 | `2ec9e608573eda459de1e1d9bda3bb494315e36bee885acc88912a11fe0caa41` | Glyph widths and encodings for non-embedded standard fonts. Non-visual dependency data; existing embedding unchanged | diff --git a/Cargo.lock b/Cargo.lock index 926fea946..d329c26a7 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -803,6 +803,15 @@ dependencies = [ "x509-cert", ] +[[package]] +name = "cobs" +version = "0.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "0fa961b519f0b462e3a3b4a34b64d119eeaca1d59af726fe450bbba07a9fc0a1" +dependencies = [ + "thiserror 2.0.21", +] + [[package]] name = "codespan-reporting" version = "0.13.1" @@ -950,12 +959,40 @@ dependencies = [ [[package]] name = "craft-docpath" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=a0916808f7d78bed8c7b90cdf937176015454a7f#a0916808f7d78bed8c7b90cdf937176015454a7f" +source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0" + +[[package]] +name = "craft-fonts-build" +version = "0.1.0" +source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0" + +[[package]] +name = "craft-i18n" +version = "0.1.0" +source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0" +dependencies = [ + "fixed_decimal", + "icu_decimal", + "icu_locale_core", + "icu_locale_fallback", + "icu_normalizer", + "icu_plurals", + "icu_properties", + "icu_provider", + "icu_provider_adapters", + "icu_provider_blob", + "log", + "objc2-foundation 0.3.2", + "sys-locale", + "unicode-bidi", + "windows-registry", + "writeable", +] [[package]] name = "craft-keymap" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=a0916808f7d78bed8c7b90cdf937176015454a7f#a0916808f7d78bed8c7b90cdf937176015454a7f" +source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0" dependencies = [ "egui", "serde", @@ -965,7 +1002,7 @@ dependencies = [ [[package]] name = "craft-pdf" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=a0916808f7d78bed8c7b90cdf937176015454a7f#a0916808f7d78bed8c7b90cdf937176015454a7f" +source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0" dependencies = [ "hayro-interpret", "hayro-syntax", @@ -976,11 +1013,11 @@ dependencies = [ [[package]] name = "craft-print" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=a0916808f7d78bed8c7b90cdf937176015454a7f#a0916808f7d78bed8c7b90cdf937176015454a7f" +source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0" dependencies = [ "craft-winprint-gdi", - "miniz_oxide 0.8.9", - "roxmltree 0.20.0", + "miniz_oxide 0.9.1", + "roxmltree 0.21.1", "tempfile", "winprint", "winsafe", @@ -989,20 +1026,28 @@ dependencies = [ [[package]] name = "craft-ui" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=a0916808f7d78bed8c7b90cdf937176015454a7f#a0916808f7d78bed8c7b90cdf937176015454a7f" +source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0" dependencies = [ + "craft-fonts-build", + "craft-i18n", "egui", + "epaint", + "js-sys", "log", "objc2 0.6.5", "objc2-app-kit 0.3.2", "objc2-foundation 0.3.2", "serde", + "skrifa 0.44.0", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", ] [[package]] name = "craft-update" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=495e5971bc024aa96e6ed318cb6e7cd3a0103184#495e5971bc024aa96e6ed318cb6e7cd3a0103184" +source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0" dependencies = [ "chrono", "dunce", @@ -1027,7 +1072,7 @@ dependencies = [ [[package]] name = "craft-window" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=a0916808f7d78bed8c7b90cdf937176015454a7f#a0916808f7d78bed8c7b90cdf937176015454a7f" +source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0" dependencies = [ "egui", "objc2 0.6.5", @@ -1599,6 +1644,17 @@ version = "0.1.15" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1c66915cc5391097de7a2b0b972e8505aa33c0df401fd3a6811dbdb32881ba48" +[[package]] +name = "fixed_decimal" +version = "0.7.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2214fb00f669ee96bee5bb29dfb1ddabe058b2bc426817efc79afa03c9bc55ae" +dependencies = [ + "displaydoc", + "smallvec", + "writeable", +] + [[package]] name = "fixedbitset" version = "0.5.7" @@ -2117,7 +2173,7 @@ checksum = "9f4d0e94ddd48749f06bbe4e5389fb9799a0c45bcaf00495042076ef05e3241a" [[package]] name = "hayro-cmap" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=a0916808f7d78bed8c7b90cdf937176015454a7f#a0916808f7d78bed8c7b90cdf937176015454a7f" +source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0" dependencies = [ "brotli", "hayro-postscript", @@ -2126,7 +2182,7 @@ dependencies = [ [[package]] name = "hayro-interpret" version = "0.7.0" -source = "git+https://github.com/storytold/craft-libs?rev=a0916808f7d78bed8c7b90cdf937176015454a7f#a0916808f7d78bed8c7b90cdf937176015454a7f" +source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0" dependencies = [ "bitflags 2.13.2", "hayro-cmap", @@ -2144,7 +2200,7 @@ dependencies = [ [[package]] name = "hayro-jbig2" version = "0.3.0" -source = "git+https://github.com/storytold/craft-libs?rev=a0916808f7d78bed8c7b90cdf937176015454a7f#a0916808f7d78bed8c7b90cdf937176015454a7f" +source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0" dependencies = [ "fearless_simd", "hayro-ccitt", @@ -2162,12 +2218,12 @@ dependencies = [ [[package]] name = "hayro-postscript" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=a0916808f7d78bed8c7b90cdf937176015454a7f#a0916808f7d78bed8c7b90cdf937176015454a7f" +source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0" [[package]] name = "hayro-syntax" version = "0.7.2" -source = "git+https://github.com/storytold/craft-libs?rev=a0916808f7d78bed8c7b90cdf937176015454a7f#a0916808f7d78bed8c7b90cdf937176015454a7f" +source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0" dependencies = [ "flate2", "hayro-ccitt", @@ -2337,6 +2393,23 @@ dependencies = [ "zerovec", ] +[[package]] +name = "icu_decimal" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9eb8f655bba2d0c0459e43a5b0e6cd3cd388109898fb3d85d048165e8b0abd08" +dependencies = [ + "displaydoc", + "fixed_decimal", + "icu_locale_core", + "icu_pattern", + "icu_plurals", + "icu_provider", + "serde", + "writeable", + "zerovec", +] + [[package]] name = "icu_locale_core" version = "2.3.0" @@ -2382,6 +2455,9 @@ dependencies = [ "icu_properties", "icu_provider", "smallvec", + "utf16_iter", + "utf8_iter", + "write16", "zerovec", ] @@ -2391,6 +2467,32 @@ version = "2.3.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0" +[[package]] +name = "icu_pattern" +version = "0.5.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2b3db611446bd92c246b03e4aa0bafa71ddb194af431ddbb42b9d70e4bfce1a0" +dependencies = [ + "displaydoc", + "either", + "serde", + "writeable", + "zerovec", +] + +[[package]] +name = "icu_plurals" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "e475e6766ef87b1d3c1f97be6363995b0bfaeddbc789671615df598a97ff0593" +dependencies = [ + "displaydoc", + "fixed_decimal", + "icu_provider", + "serde", + "zerovec", +] + [[package]] name = "icu_properties" version = "2.3.0" @@ -2420,6 +2522,7 @@ checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" dependencies = [ "displaydoc", "icu_locale_core", + "postcard", "serde", "stable_deref_trait", "writeable", @@ -2429,6 +2532,30 @@ dependencies = [ "zerovec", ] +[[package]] +name = "icu_provider_adapters" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df887bc4f55dbc773c7c45a0c7dfa75be886f7b7812830a71ab7271cc1801820" +dependencies = [ + "icu_locale_fallback", + "icu_provider", +] + +[[package]] +name = "icu_provider_blob" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "5916b44fac319d3f58e4d8d805a627df19e27937b582cd99fcde3bb6b7ce1b0e" +dependencies = [ + "icu_provider", + "postcard", + "serde", + "writeable", + "zerotrie", + "zerovec", +] + [[package]] name = "icu_segmenter" version = "2.3.0" @@ -2704,7 +2831,7 @@ dependencies = [ [[package]] name = "krilla" version = "0.8.2" -source = "git+https://github.com/storytold/craft-libs?rev=a0916808f7d78bed8c7b90cdf937176015454a7f#a0916808f7d78bed8c7b90cdf937176015454a7f" +source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0" dependencies = [ "base64 0.22.1", "bumpalo", @@ -2834,6 +2961,9 @@ name = "litemap" version = "0.8.3" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "47d9d19d1d6efa0109d2f65ff4c85cddd50bd572e5a00127ab10987290bcefae" +dependencies = [ + "serde_core", +] [[package]] name = "litrs" @@ -3809,6 +3939,16 @@ dependencies = [ "portable-atomic", ] +[[package]] +name = "postcard" +version = "1.1.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6764c3b5dd454e283a30e6dfe78e9b31096d9e32036b5d1eaac7a6119ccb9a24" +dependencies = [ + "cobs", + "serde", +] + [[package]] name = "potential_utf" version = "0.1.6" @@ -5223,6 +5363,18 @@ dependencies = [ "syn 3.0.9", ] +[[package]] +name = "sys-locale" +version = "0.3.2" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "8eab9a99a024a169fe8a903cf9d4a3b3601109bcc13bd9e3c6fff259138626c4" +dependencies = [ + "js-sys", + "libc", + "wasm-bindgen", + "web-sys", +] + [[package]] name = "system-configuration" version = "0.7.0" @@ -5853,6 +6005,12 @@ dependencies = [ "xmlwriter", ] +[[package]] +name = "utf16_iter" +version = "1.0.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "c8232dd3cdaed5356e0f716d285e4b40b932ac434100fe9b7e0e8e935b9e6246" + [[package]] name = "utf8-zero" version = "0.8.1" @@ -7379,11 +7537,20 @@ dependencies = [ "read-fonts 0.39.2", ] +[[package]] +name = "write16" +version = "1.0.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "d1890f4022759daae28ed4fe62859b1236caebfc61ede2f63ed4e695f3f6d936" + [[package]] name = "writeable" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" +dependencies = [ + "either", +] [[package]] name = "x11-dl" @@ -7705,6 +7872,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" dependencies = [ "displaydoc", + "litemap", + "serde_core", "yoke", "zerofrom", "zerovec", diff --git a/Cargo.toml b/Cargo.toml index 5c411d256..644bffe9d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,15 +11,15 @@ rust-version = "1.95" repository = "https://github.com/storytold/vectorcraft" [workspace.dependencies] -craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "a0916808f7d78bed8c7b90cdf937176015454a7f", default-features = false, features = ["interpret", "write"] } -craft-print = { git = "https://github.com/storytold/craft-libs", rev = "a0916808f7d78bed8c7b90cdf937176015454a7f" } -craft-docpath = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "a0916808f7d78bed8c7b90cdf937176015454a7f" } -craft-ui = { git = "https://github.com/storytold/craft-libs", rev = "a0916808f7d78bed8c7b90cdf937176015454a7f", features = ["serde"] } -craft-keymap = { git = "https://github.com/storytold/craft-libs", rev = "a0916808f7d78bed8c7b90cdf937176015454a7f" } +craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0", default-features = false, features = ["interpret", "write"] } +craft-print = { git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0" } +craft-docpath = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0" } +craft-ui = { git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0", features = ["serde"] } +craft-keymap = { git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0" } # Decides who draws the window frame (desktop vs. app) so the suite shares one native titlebar per # desktop. The `egui` feature adds `craft_window::decorate`. (craft-libs#217.) -craft-window = { git = "https://github.com/storytold/craft-libs", rev = "a0916808f7d78bed8c7b90cdf937176015454a7f", features = ["egui"] } -craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "495e5971bc024aa96e6ed318cb6e7cd3a0103184", features = ["egui"] } +craft-window = { git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0", features = ["egui"] } +craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0", features = ["egui"] } # internal vectorcraft-geom = { path = "crates/geom" } vectorcraft-color = { path = "crates/color" } @@ -141,12 +141,12 @@ lto = "thin" codegen-units = 1 [patch.crates-io] -hayro-syntax = { git = "https://github.com/storytold/craft-libs", rev = "a0916808f7d78bed8c7b90cdf937176015454a7f" } -hayro-interpret = { git = "https://github.com/storytold/craft-libs", rev = "a0916808f7d78bed8c7b90cdf937176015454a7f" } -hayro-jbig2 = { git = "https://github.com/storytold/craft-libs", rev = "a0916808f7d78bed8c7b90cdf937176015454a7f" } -hayro-cmap = { git = "https://github.com/storytold/craft-libs", rev = "a0916808f7d78bed8c7b90cdf937176015454a7f" } -hayro-postscript = { git = "https://github.com/storytold/craft-libs", rev = "a0916808f7d78bed8c7b90cdf937176015454a7f" } -krilla = { git = "https://github.com/storytold/craft-libs", rev = "a0916808f7d78bed8c7b90cdf937176015454a7f" } +hayro-syntax = { git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0" } +hayro-interpret = { git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0" } +hayro-jbig2 = { git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0" } +hayro-cmap = { git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0" } +hayro-postscript = { git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0" } +krilla = { git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0" } ecolor = { git = "https://github.com/storytold/egui.git", rev = "6d518fc799681571963842f95b55a6b68431b15a" } emath = { git = "https://github.com/storytold/egui.git", rev = "6d518fc799681571963842f95b55a6b68431b15a" } epaint = { git = "https://github.com/storytold/egui.git", rev = "6d518fc799681571963842f95b55a6b68431b15a" } diff --git a/NOTICE b/NOTICE index c66f3efad..8698b2dfe 100644 --- a/NOTICE +++ b/NOTICE @@ -77,10 +77,10 @@ Shared PDF backend The patched hayro-interpret 0.7.0, hayro-syntax 0.7.2, hayro-jbig2 0.3.0, hayro-cmap 0.1.0 and hayro-postscript 0.1.0 are fetched from https://github.com/storytold/craft-libs at revision -a0916808f7d78bed8c7b90cdf937176015454a7f. Their upstream project is +92d392cff132eaf53e13afff77f38b5c11b73aa0. Their upstream project is https://github.com/LaurenzV/hayro, licensed MIT OR Apache-2.0. The snapshot retains each crate's upstream licence files. The patch ledger is at -https://github.com/storytold/craft-libs/blob/a0916808f7d78bed8c7b90cdf937176015454a7f/vendor/README.craft-reader.md. +https://github.com/storytold/craft-libs/blob/92d392cff132eaf53e13afff77f38b5c11b73aa0/vendor/README.craft-reader.md. Assets already bundled through Hayro's default features retain their licences: - Foxit standard-14 substitutes: Copyright 2014 PDFium Authors. All rights reserved. diff --git a/clippy.toml b/clippy.toml index 0369326bc..d02d2c40c 100644 --- a/clippy.toml +++ b/clippy.toml @@ -3,3 +3,6 @@ allow-unwrap-in-tests = true allow-expect-in-tests = true allow-panic-in-tests = true +disallowed-types = [ + { path = "egui::ComboBox", reason = "use craft_ui::ComboBox: egui's combo box menu can't grow back while open (egui#5225)" }, +] diff --git a/crates/ui-egui/src/chrome/status_bar.rs b/crates/ui-egui/src/chrome/status_bar.rs index 28b57a654..92d718fc9 100644 --- a/crates/ui-egui/src/chrome/status_bar.rs +++ b/crates/ui-egui/src/chrome/status_bar.rs @@ -13,7 +13,7 @@ pub fn status_bar(app: &mut VectorcraftApp, ui: &mut Ui) { let print = app.session.prefs.display_print_size; let shown = crate::state::zoom_display_percent(zoom, print); ui.name_inputs(tl!("Zoom"), |ui| { - egui::ComboBox::from_id_salt("zoom-combo") + craft_ui::ComboBox::from_id_salt("zoom-combo") .selected_text(egui::RichText::new(zoom_label(zoom, print)).size(11.5)) .width(76.0) .show_ui(ui, |ui| { @@ -36,7 +36,7 @@ pub fn status_bar(app: &mut VectorcraftApp, ui: &mut Ui) { }); let rot = app.view().map(|v| v.rotation).unwrap_or(0.0); ui.name_inputs(tl!("Rotation"), |ui| { - egui::ComboBox::from_id_salt("rot-combo") + craft_ui::ComboBox::from_id_salt("rot-combo") .selected_text(egui::RichText::new(format!("{rot:.0}°")).size(11.5)) .width(52.0) .show_ui(ui, |ui| { diff --git a/crates/ui-egui/src/chrome/tabs.rs b/crates/ui-egui/src/chrome/tabs.rs index 95847e27e..649030172 100644 --- a/crates/ui-egui/src/chrome/tabs.rs +++ b/crates/ui-egui/src/chrome/tabs.rs @@ -207,7 +207,7 @@ pub(super) fn artboard_list(app: &VectorcraftApp, ui: &mut Ui, cur: usize) -> Op ui.add_enabled_ui(!boards.is_empty(), |ui| { ui.set_max_width(FIELD_W); ui.name_inputs(tl!("Artboard"), |ui| { - egui::ComboBox::from_id_salt("artboard-list") + craft_ui::ComboBox::from_id_salt("artboard-list") .selected_text(egui::RichText::new(shown).size(11.5)) .width(FIELD_W) .wrap_mode(egui::TextWrapMode::Truncate) diff --git a/crates/ui-egui/src/credits.rs b/crates/ui-egui/src/credits.rs index 3936a805a..1f0a1dbbc 100644 --- a/crates/ui-egui/src/credits.rs +++ b/crates/ui-egui/src/credits.rs @@ -238,7 +238,7 @@ pub fn contributors_ui(app: &mut VectorcraftApp, ui: &mut egui::Ui) { ui.separator(); ui.label(tl!("Sort")); ui.name_inputs(tl!("Sort"), |ui| { - egui::ComboBox::from_id_salt("credits_sort").selected_text(v.key.label().0).show_ui(ui, |ui| { + craft_ui::ComboBox::from_id_salt("credits_sort").selected_text(v.key.label().0).show_ui(ui, |ui| { for k in SortKey::ALL { if ui.selectable_label(v.key == k, k.label().0).clicked() { v.key = k; diff --git a/crates/ui-egui/src/dialogs/dimension.rs b/crates/ui-egui/src/dialogs/dimension.rs index d79aea04e..06f89ce28 100644 --- a/crates/ui-egui/src/dialogs/dimension.rs +++ b/crates/ui-egui/src/dialogs/dimension.rs @@ -101,7 +101,7 @@ fn precision(ui: &mut Ui, fields: &mut serde_json::Map, id: &str) let mut value = before; ui.label(tl!("Precision:")); ui.name_inputs(tl!("Precision"), |ui| { - egui::ComboBox::from_id_salt(id).selected_text(value.to_string()).width(55.0).show_ui(ui, |ui| { + craft_ui::ComboBox::from_id_salt(id).selected_text(value.to_string()).width(55.0).show_ui(ui, |ui| { for n in 0..=6 { ui.selectable_value(&mut value, n, n.to_string()); } @@ -136,7 +136,7 @@ fn body(app: &mut VectorcraftApp, ui: &mut Ui, d: &mut Dialog) -> bool { f.insert("scale".into(), json!(scale)); } ui.name_inputs(tl!("Scale Presets"), |ui| { - egui::ComboBox::from_id_salt("dimension-scales").selected_text(tl!("Presets")).width(76.0).show_ui(ui, |ui| { + craft_ui::ComboBox::from_id_salt("dimension-scales").selected_text(tl!("Presets")).width(76.0).show_ui(ui, |ui| { for scale in ["1:1", "1:2", "1:5", "1:10", "1:20", "1:50", "1:100", "1:200", "2:1", "5:1", "10:1"] { if ui.selectable_label(false, scale).clicked() { f.insert("scale".into(), json!(scale)); diff --git a/crates/ui-egui/src/dialogs/layer_options.rs b/crates/ui-egui/src/dialogs/layer_options.rs index a8f8f9526..d33941dc6 100644 --- a/crates/ui-egui/src/dialogs/layer_options.rs +++ b/crates/ui-egui/src/dialogs/layer_options.rs @@ -122,7 +122,7 @@ const CHIP: egui::Vec2 = egui::vec2(16.0, 10.0); fn color_dropdown(ui: &mut egui::Ui, shown: &str) -> Option { let border = crate::theme::Tokens::get(ui.ctx()).input_border; let rows = LAYER_COLORS.iter().map(|(n, c)| (*n, Some(*c))).chain([("Custom", None)]); - widgets::combo(ui, "layer-color", tl!(shown), 140.0, false, |ui| { + widgets::combo(ui, "layer-color", tl!(shown), 140.0, |ui| { let mut chosen = None; for (i, (name, rgb)) in rows.enumerate() { let chip = egui::IdSalt::new(("layer-color-chip", i)); diff --git a/crates/ui-egui/src/dialogs/shortcut_import.rs b/crates/ui-egui/src/dialogs/shortcut_import.rs index 60c002e56..ab32ada06 100644 --- a/crates/ui-egui/src/dialogs/shortcut_import.rs +++ b/crates/ui-egui/src/dialogs/shortcut_import.rs @@ -923,40 +923,51 @@ fn rows_section(ui: &mut egui::Ui, d: &mut Dialog, review: &mut Review, statuses list.inner.inner_rect.height() } -/// Row `r`'s target picker: "No equivalent" and the row's choices, then Keyboard Shortcuts' entries -/// that match the search. → the target picked ("" for no equivalent). -fn target_picker(ui: &mut egui::Ui, i: usize, r: &Row) -> Option { +/// Row `r`'s target picker: the search field, "No equivalent" and the row's choices stay at the +/// top, over Keyboard Shortcuts' entries that match the search, which scroll. → the target picked +/// ("" for no equivalent). +pub(super) fn target_picker(ui: &mut egui::Ui, i: usize, r: &Row) -> Option { let current = target_label(r.target.as_deref()); - widgets::combo(ui, ("shortcut-import-target", i), ¤t, TARGET_W, true, |ui| { - let field = ui.scope_id().with("query"); - if ui.memory(|m| m.focused().is_none()) { - ui.memory_mut(|m| m.request_focus(field.with("edit"))); - } - let q = widgets::search_field(ui, field, tl!("Search")).to_lowercase(); - let mut picked = None; - let mut option = |ui: &mut egui::Ui, key: &str, label: &str| { - if ui.add(egui::Button::selectable(r.target.as_deref() == Some(key), label)).clicked() { - picked = Some(key.to_string()); - } - }; - option(ui, "", tl!("No equivalent")); - let list: Vec<&Entry> = if q.is_empty() { - for e in r.choices.iter().filter_map(|c| entry(c)) { - option(ui, &e.key, &shown(e)); - } - ui.separator(); - shortcut_editor::entries().iter().collect() - } else { - shortcut_editor::entries().iter().filter(|e| shown(e).to_lowercase().contains(&q) || e.key.to_lowercase().contains(&q)).collect() - }; - egui::ScrollArea::vertical().max_height(300.0).show_rows(ui, ROW_H, list.len(), |ui, range| { - for e in range.filter_map(|k| list.get(k)) { - option(ui, &e.key, &shown(e)); - } - }); - if picked.is_some() { - ui.close(); - } - picked + // The entries' list is up to 300 points tall under the header, which was this tall last frame. + let header_id = egui::Id::unique(("shortcut-import-target-header", i)); + let height = ui.data(|d| d.get_temp::(header_id)).unwrap_or(0.0) + 300.0; + let option = |ui: &mut egui::Ui, key: &str, label: &str| { + ui.add(egui::Button::selectable(r.target.as_deref() == Some(key), label)).clicked().then(|| key.to_string()) + }; + widgets::combo_menu(ui, ("shortcut-import-target", i), ¤t, TARGET_W, |menu| { + menu.height(height).show_rows_with_header( + |ui| { + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); + let field = ui.scope_id().with("query"); + if ui.memory(|m| m.focused().is_none()) { + ui.memory_mut(|m| m.request_focus(field.with("edit"))); + } + let q = widgets::search_field(ui, field, tl!("Search")).to_lowercase(); + let mut picked = option(ui, "", tl!("No equivalent")); + let list: Vec<&Entry> = if q.is_empty() { + for e in r.choices.iter().filter_map(|c| entry(c)) { + picked = option(ui, &e.key, &shown(e)).or(picked); + } + ui.separator(); + shortcut_editor::entries().iter().collect() + } else { + shortcut_editor::entries().iter().filter(|e| shown(e).to_lowercase().contains(&q) || e.key.to_lowercase().contains(&q)).collect() + }; + ui.data_mut(|d| d.insert_temp(header_id, ui.min_rect().height())); + (list, picked) + }, + ROW_H, + |(list, _)| list.len(), + |ui, range, (list, mut picked)| { + for e in range.filter_map(|k| list.get(k)) { + picked = option(ui, &e.key, &shown(e)).or(picked); + } + if picked.is_some() { + ui.close(); + } + picked + }, + ) }) + .flatten() } diff --git a/crates/ui-egui/src/dialogs/tests_shortcut_import.rs b/crates/ui-egui/src/dialogs/tests_shortcut_import.rs index 6673285b5..61bbc18e8 100644 --- a/crates/ui-egui/src/dialogs/tests_shortcut_import.rs +++ b/crates/ui-egui/src/dialogs/tests_shortcut_import.rs @@ -7,7 +7,7 @@ use craft_keymap::import::namematch::Candidate; use serde_json::{Value, json}; use vectorcraft_engine::Session; -use super::shortcut_import::{Code, KIND, Review, Row, Status, report}; +use super::shortcut_import::{Code, KIND, Review, Row, Status, report, target_picker}; use super::*; use crate::{io, shortcut_editor, theme}; @@ -576,3 +576,58 @@ fn long_names_match_and_apply() { let (out, ov) = apply_rows(&mut app, "defaults", &[(&name, "Menus", "edit.copy", "Cmd+Alt+Shift+F11")]); assert_eq!((out["applied"].clone(), ov), (json!(1), json!({"edit.copy": "Cmd+Alt+Shift+F11"}))); } + +/// A search in a row's target picker that matches nothing shrinks its menu to the search field +/// and "No equivalent"; clearing the search lists every entry again, and the menu grows back to +/// its height before the search (egui#5225). The search field stays where it was throughout. +#[test] +fn the_target_picker_grows_back_when_a_search_that_matched_nothing_is_cleared() { + let ctx = egui::Context::default(); + theme::install_fonts(&ctx); + theme::apply(&ctx, Default::default()); + let r = Row { name: "Acme Undo".into(), ..Default::default() }; + let frame = |events: Vec| { + let input = + egui::RawInput { events, screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(600.0, 800.0))), ..Default::default() }; + let mut at = egui::Pos2::ZERO; + let mut out = ctx.run_ui(input, |ui| { + at = ui.next_widget_position(); + target_picker(ui, 0, &r); + }); + out.textures_delta.clear(); + let text = |label: &str| { + out.shapes.iter().find_map(|s| match &s.shape { + egui::Shape::Text(t) if t.galley.text() == label => Some(t.pos), + _ => None, + }) + }; + // The row under the search field: it shows whatever the field holds. + let none_row = text("No equivalent"); + let menu = none_row.and_then(|p| ctx.layer_id_at(p)).and_then(|l| ctx.memory(|m| m.area_rect(l.id))); + (at, none_row, text("Search"), menu.map(|r| r.height())) + }; + let settle = |events: Vec| { + let mut last = frame(events); + for _ in 0..5 { + last = frame(vec![]); + } + last + }; + let (at, ..) = frame(vec![]); + let click = at + egui::vec2(40.0, 10.0); + let button = |pressed| egui::Event::PointerButton { pos: click, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() }; + frame(vec![egui::Event::PointerMoved(click), button(true)]); + let (_, none_row, search, full) = settle(vec![button(false)]); + let (none_row, search, full) = (none_row.expect("the menu is open"), search.expect("the search field shows"), full.expect("the menu's area")); + let nothing = "qqzzxxqqzz"; + let (_, n, _, small) = settle(vec![egui::Event::Text(nothing.into())]); + assert_eq!(n, Some(none_row), "the search field stays put while nothing matches"); + let small = small.expect("the menu stays open"); + assert!(small + 40.0 < full, "the menu shrinks to its search field: {small} of {full}"); + for _ in nothing.chars() { + frame(vec![press(egui::Key::Backspace)]); + } + let (_, n, s, grown) = settle(vec![]); + assert_eq!((n, s), (Some(none_row), Some(search)), "the search field is where it was"); + assert_eq!(grown, Some(full), "the menu grows back to its height before the search"); +} diff --git a/crates/ui-egui/src/dialogs/tests_warp.rs b/crates/ui-egui/src/dialogs/tests_warp.rs index 55f2bde99..8c6ffa66d 100644 --- a/crates/ui-egui/src/dialogs/tests_warp.rs +++ b/crates/ui-egui/src/dialogs/tests_warp.rs @@ -555,10 +555,10 @@ fn warp_options_style_keyboard_enter_does_not_accept_dialog() { assert_ne!(ctx.memory(|m| m.focused()), Some(style_id)); warp_key(&mut app, &ctx, egui::Key::Tab, egui::Modifiers::SHIFT); assert_eq!(ctx.memory(|m| m.focused()), Some(style_id)); - assert!(!egui::ComboBox::is_open(&ctx, style_id)); + assert!(!craft_ui::ComboBox::is_open(&ctx, style_id)); warp_key(&mut app, &ctx, egui::Key::Enter, egui::Modifiers::NONE); assert!(app.ui.dialog.is_some(), "WARP_STYLE_ENTER_OPEN: opening Style must not accept Warp Options"); - assert!(egui::ComboBox::is_open(&ctx, style_id)); + assert!(craft_ui::ComboBox::is_open(&ctx, style_id)); assert_eq!(app.ui.dialog.as_ref().unwrap().str("__effect"), "warp.arc"); assert_eq!(entries(&app.session.doc().unwrap().history.undo), undo); assert_eq!(entries(&app.session.doc().unwrap().history.redo), redo); @@ -567,7 +567,7 @@ fn warp_options_style_keyboard_enter_does_not_accept_dialog() { tab_to_warp_control(&mut app, &ctx, "Arc Lower", egui::Modifiers::NONE); warp_key(&mut app, &ctx, egui::Key::Enter, egui::Modifiers::NONE); assert!(app.ui.dialog.is_some(), "WARP_STYLE_ENTER_CHOICE: choosing Style must not accept Warp Options"); - assert!(!egui::ComboBox::is_open(&ctx, style_id)); + assert!(!craft_ui::ComboBox::is_open(&ctx, style_id)); assert_eq!(app.ui.dialog.as_ref().unwrap().str("__effect"), "warp.arcLower"); assert!(app.ui.dialog.as_ref().unwrap().bool("__warp_style_changed")); let state = app.session.doc().unwrap(); diff --git a/crates/ui-egui/src/dialogs/warp_effect.rs b/crates/ui-egui/src/dialogs/warp_effect.rs index e0a2490aa..ff530fbdf 100644 --- a/crates/ui-egui/src/dialogs/warp_effect.rs +++ b/crates/ui-egui/src/dialogs/warp_effect.rs @@ -71,7 +71,7 @@ pub(super) fn body(ui: &mut egui::Ui, d: &mut Dialog) { let current = WARP_STYLES.iter().find(|(style, _)| *style == suffix).map_or_else(|| suffix.to_string(), |(_, label)| shown(label)); widgets::label_row(ui, tl!("Style:"), LABEL_W, |ui| { style_preview(ui, suffix); - if let Some((style, label)) = widgets::combo(ui, "warp-style", ¤t, 170.0, false, |ui| { + if let Some((style, label)) = widgets::combo(ui, "warp-style", ¤t, 170.0, |ui| { let mut chosen = None; for (style, label) in WARP_STYLES { ui.horizontal(|ui| { diff --git a/crates/ui-egui/src/font_menu.rs b/crates/ui-egui/src/font_menu.rs index 0243f477d..c23035794 100644 --- a/crates/ui-egui/src/font_menu.rs +++ b/crates/ui-egui/src/font_menu.rs @@ -82,6 +82,8 @@ const STAR: f32 = 18.0; const NAME_SHARE: f32 = 0.5; /// Width of the disclosure triangle of a family with several styles. const DISCLOSURE: f32 = 14.0; +/// The list's greatest height, under the search field and filters. +const LIST_HEIGHT: f32 = 600.0; /// Samples rendered (or asked for) per frame, across every menu, so a long list doesn't stall the UI. const SAMPLES_PER_FRAME: usize = 4; /// Samples rendering at once (native). @@ -231,8 +233,8 @@ struct MenuState { /// The rows listed, and what for: built again only when that changes, not every frame. rows: Arc<[Row]>, rows_key: Option, - /// The list's scroll offset last frame. - scrolled: f32, + /// The height of the search field and filters last frame. + header: f32, } /// The font family menu showing `current`, `width` points wide, rows sampling `sample` (the @@ -247,12 +249,21 @@ pub fn font_menu( ) -> Option { let state_id = ui.scope_id().with(&id).with("font-menu"); let mut st: MenuState = ui.data(|d| d.get_temp(state_id)).unwrap_or_default(); - let mut drawn = false; - let picked = widgets::combo(ui, &id, current, width, true, |ui| { - drawn = true; - list(ui, state_id, &mut st, current, sample, look) + let height = st.header + LIST_HEIGHT; + let state = &mut st; + // The search field and filters stay in place over the rows, which scroll. The rows are all + // one height (a family's styles too), so only those in view are laid out: there are thousands + // of families. + let shown = widgets::combo_menu(ui, &id, current, width, |menu| { + menu.height(height).show_rows_with_header( + |ui| header(ui, state_id, state, current, look), + look.row, + |h| h.rows.len(), + |ui, range, h| rows(ui, range, h, sample, look), + ) }); - let mut out = picked; + let drawn = shown.is_some(); + let mut out = shown.flatten(); if !drawn && out.is_none() && st.previewed.take().is_some() { // Closed (Escape, a click outside) with a preview showing. out = Some(FontPick::EndPreview); @@ -264,10 +275,33 @@ pub fn font_menu( out } -/// The list inside the open menu. +/// What the menu's header hands its rows. +struct Listed<'s> { + st: &'s mut MenuState, + /// The rows listed. + rows: Arc<[Row]>, + /// The current family by its own name. + current: String, + /// Enter was pressed; ↑/↓/←/→ moved the highlight; the menu is opening. + enter: bool, + keyed: bool, + opening: bool, +} + +/// The menu's contents outside a menu, for the unit tests: the header over its rows, as +/// [`craft_ui::combo::Menu::show_rows_with_header`] lays them out. +#[cfg(test)] fn list(ui: &mut Ui, state_id: egui::Id, st: &mut MenuState, current: &str, sample: Option<&str>, look: MenuLook) -> Option { - let row_h = look.row; + let h = header(ui, state_id, st, current, look); + let total = h.rows.len(); + egui::ScrollArea::vertical().max_height(ui.available_height()).show_rows(ui, look.row, total, |ui, range| rows(ui, range, h, sample, look)).inner +} + +/// The top of the open menu, which stays in place: the search field, the filters, and the rows +/// they list. +fn header<'s>(ui: &mut Ui, state_id: egui::Id, st: &'s mut MenuState, current: &str, look: MenuLook) -> Listed<'s> { let t = Tokens::get(ui.ctx()); + ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); // Each time the menu opens: no query, the current family highlighted and in view (the // filters stay as they were). let pass = ui.ctx().cumulative_pass_nr(); @@ -407,122 +441,128 @@ fn list(ui: &mut Ui, state_id: egui::Id, st: &mut MenuState, current: &str, samp } keyed = true; } + st.header = ui.min_rect().height(); if rows.is_empty() { widgets::dim_label(ui, tl!("No matching fonts")); + } + ui.spacing_mut().item_spacing.y = 0.0; + // The scroll bar beside the rows, not over them: a floating one covered the stars at their + // right ends, so a favourite was hard to click (#2277). + ui.spacing_mut().scroll = widgets::list_scroll_style(); + Listed { st, rows, current: current.to_string(), enter, keyed, opening } +} + +/// The rows of the open menu in `range`, the ones in view: the menu's one scrolling part, under +/// the search field and filters (#555). +fn rows(ui: &mut Ui, range: std::ops::Range, h: Listed<'_>, sample: Option<&str>, look: MenuLook) -> Option { + let Listed { st, rows, current, enter, keyed, opening } = h; + let current = current.as_str(); + let row_h = look.row; + let t = Tokens::get(ui.ctx()); + let db = vectorcraft_text::FontDb::global(); + let favorite = |f: &str| look.favorites.iter().any(|x| x.eq_ignore_ascii_case(f)); + if rows.is_empty() { // A filter or a font rescan can remove the row whose font was previewed. // Do not leave that font applied while the list has no candidate to highlight. return st.previewed.take().map(|_| FontPick::EndPreview); } - // The rows: only those in view are laid out (there are thousands of families). The list is - // the menu's one scrolling part, under the search field and filters (#555): it fills the - // menu's room (its max rect) below them. - let room = (ui.max_rect().bottom() - ui.next_widget_position().y).clamp(120.0, 600.0); - let mut area = egui::ScrollArea::vertical().max_height(room).min_scrolled_height(room.min(row_h * rows.len() as f32)); + // Scrolls the list to row `i`'s place, laid out or not: the rows in view start at + // `range.start`. + let (left, width) = (ui.max_rect().left(), ui.max_rect().width()); + let top = ui.max_rect().top() - range.start as f32 * row_h; + let scroll_to = |ui: &Ui, i: usize, align| { + let place = egui::Rect::from_min_size(egui::pos2(left, top + i as f32 * row_h), vec2(width, row_h)); + ui.scroll_to_rect_animation(place, align, egui::style::ScrollAnimation::none()); + }; // Open at the recent section even when the current family is farther down the list. // Otherwise centre the current font; subsequent key presses keep the highlight in view. if opening && matches!(rows.first(), Some(Row::Heading("Recent Fonts"))) { - area = area.vertical_scroll_offset(0.0); - } else if let Some(top) = st.highlight.map(|i| i as f32 * row_h) { - let target = if opening { - Some(top - (room - row_h) * 0.5) - } else if keyed && top < st.scrolled { - Some(top) - } else if keyed && top + row_h > st.scrolled + room { - Some(top + row_h - room) - } else { - None - }; - if let Some(y) = target { - area = area.vertical_scroll_offset(y.max(0.0)); - } + scroll_to(ui, 0, Some(egui::Align::TOP)); + } else if let Some(i) = st.highlight + && (opening || keyed) + { + scroll_to(ui, i, opening.then_some(egui::Align::Center)); } let mut chosen: Option<(String, Option)> = None; let mut hovered: Option = None; let mut toggled: Option = None; let mut starred: Option = None; - ui.spacing_mut().item_spacing.y = 0.0; - // The scroll bar beside the rows, not over them: a floating one covered the stars at their - // right ends, so a favourite was hard to click (#2277). - ui.spacing_mut().scroll = widgets::list_scroll_style(); - let shown = area.show_rows(ui, row_h, rows.len(), |ui, range| { - for i in range { - let Some(row) = rows.get(i) else { continue }; - if let Row::Heading(label) = row { - let (rect, _) = ui.allocate_exact_size(vec2(ui.available_width(), row_h), Sense::hover()); - // Existing translated menu labels; the heading cannot preview or apply a font. - let label = match *label { - "Recent Fonts" => tl!("Recent Fonts"), - _ => tl!("Fonts"), - }; - ui.painter_at(rect).text( - rect.left_center() + vec2(4.0, 0.0), - egui::Align2::LEFT_CENTER, - label, - egui::FontId::proportional(11.0), - t.text_dim, - ); - continue; - } - let Some(family) = row.family() else { continue }; - let (rect, resp) = ui.allocate_exact_size(vec2(ui.available_width(), row_h), Sense::click()); - if resp.hovered() && ui.input(|inp| inp.pointer.delta() != egui::Vec2::ZERO || inp.smooth_scroll_delta != egui::Vec2::ZERO) { - hovered = Some(i); - } - let p = ui.painter_at(rect); - if st.highlight == Some(i) { - p.rect_filled(rect, 2.0, t.selection); - } else if matches!(row, Row::Family(f) if f.eq_ignore_ascii_case(current)) { - p.rect_filled(rect, 2.0, t.hover); - } - // The disclosure triangle of a family with several styles. - let tri = egui::Rect::from_min_size(rect.min, vec2(DISCLOSURE, row_h)); - let mut on_triangle = false; - if let Row::Family(f) = row - && db.styles(f).len() > 1 - { - let open = st.expanded.iter().any(|e| e.eq_ignore_ascii_case(f)); - p.text(tri.center(), egui::Align2::CENTER_CENTER, if open { "▾" } else { "▸" }, egui::FontId::proportional(11.0), t.text_dim); - on_triangle = resp.interact_pointer_pos().is_some_and(|pos| tri.contains(pos)); - if resp.clicked() && on_triangle { - toggled = Some(f.to_lowercase()); - } - } - // The star at the end of a family's row. - let star = egui::Rect::from_min_max(egui::pos2(rect.max.x - STAR, rect.min.y), rect.max); - let mut on_star = false; - if let Row::Family(f) = row { - let on = favorite(f); - let hot = resp.hover_pos().is_some_and(|pos| star.contains(pos)); - if on || hot || st.highlight == Some(i) { - let color = if on { t.accent } else { t.text_dim }; - p.text(star.center(), egui::Align2::CENTER_CENTER, if on { "★" } else { "☆" }, egui::FontId::proportional(12.0), color); - } - on_star = resp.interact_pointer_pos().is_some_and(|pos| star.contains(pos)); - if resp.clicked() && on_star { - starred = Some(f.clone()); - } - } - let indent = if row.style().is_some() { DISCLOSURE + 12.0 } else { DISCLOSURE }; - let name_w = rect.width() * NAME_SHARE; - let name_rect = egui::Rect::from_min_max(rect.min + vec2(indent, 0.0), egui::pos2(rect.min.x + name_w - 4.0, rect.max.y)); - // Family rows follow Show Font Names in English; style rows keep the style name (#394). - let name = match row { - Row::Style(_, style) => style.clone(), - Row::Family(f) => db.family_display_name(f, look.english_names), - Row::Heading(_) => continue, + for i in range { + let Some(row) = rows.get(i) else { continue }; + if let Row::Heading(label) = row { + let (rect, _) = ui.allocate_exact_size(vec2(ui.available_width(), row_h), Sense::hover()); + // Existing translated menu labels; the heading cannot preview or apply a font. + let label = match *label { + "Recent Fonts" => tl!("Recent Fonts"), + _ => tl!("Fonts"), }; - ui.painter_at(name_rect).text(name_rect.left_center(), egui::Align2::LEFT_CENTER, name, egui::FontId::proportional(12.0), t.text); - let sample_rect = egui::Rect::from_min_max(rect.min + vec2(name_w, 2.0), rect.max - vec2(STAR + 2.0, 2.0)); - if !look.samples { - } else if let Some(tex) = sample_texture(ui.ctx(), family, row.style(), sample, sample_rect.height()) { - paint_sample(ui, sample_rect, &tex, t.text); - } - if resp.clicked() && !on_triangle && !on_star { - chosen = row.pick(); + ui.painter_at(rect).text( + rect.left_center() + vec2(4.0, 0.0), + egui::Align2::LEFT_CENTER, + label, + egui::FontId::proportional(11.0), + t.text_dim, + ); + continue; + } + let Some(family) = row.family() else { continue }; + let (rect, resp) = ui.allocate_exact_size(vec2(ui.available_width(), row_h), Sense::click()); + if resp.hovered() && ui.input(|inp| inp.pointer.delta() != egui::Vec2::ZERO || inp.smooth_scroll_delta != egui::Vec2::ZERO) { + hovered = Some(i); + } + let p = ui.painter_at(rect); + if st.highlight == Some(i) { + p.rect_filled(rect, 2.0, t.selection); + } else if matches!(row, Row::Family(f) if f.eq_ignore_ascii_case(current)) { + p.rect_filled(rect, 2.0, t.hover); + } + // The disclosure triangle of a family with several styles. + let tri = egui::Rect::from_min_size(rect.min, vec2(DISCLOSURE, row_h)); + let mut on_triangle = false; + if let Row::Family(f) = row + && db.styles(f).len() > 1 + { + let open = st.expanded.iter().any(|e| e.eq_ignore_ascii_case(f)); + p.text(tri.center(), egui::Align2::CENTER_CENTER, if open { "▾" } else { "▸" }, egui::FontId::proportional(11.0), t.text_dim); + on_triangle = resp.interact_pointer_pos().is_some_and(|pos| tri.contains(pos)); + if resp.clicked() && on_triangle { + toggled = Some(f.to_lowercase()); } } - }); - st.scrolled = shown.state.clamped_offset().y; + // The star at the end of a family's row. + let star = egui::Rect::from_min_max(egui::pos2(rect.max.x - STAR, rect.min.y), rect.max); + let mut on_star = false; + if let Row::Family(f) = row { + let on = favorite(f); + let hot = resp.hover_pos().is_some_and(|pos| star.contains(pos)); + if on || hot || st.highlight == Some(i) { + let color = if on { t.accent } else { t.text_dim }; + p.text(star.center(), egui::Align2::CENTER_CENTER, if on { "★" } else { "☆" }, egui::FontId::proportional(12.0), color); + } + on_star = resp.interact_pointer_pos().is_some_and(|pos| star.contains(pos)); + if resp.clicked() && on_star { + starred = Some(f.clone()); + } + } + let indent = if row.style().is_some() { DISCLOSURE + 12.0 } else { DISCLOSURE }; + let name_w = rect.width() * NAME_SHARE; + let name_rect = egui::Rect::from_min_max(rect.min + vec2(indent, 0.0), egui::pos2(rect.min.x + name_w - 4.0, rect.max.y)); + // Family rows follow Show Font Names in English; style rows keep the style name (#394). + let name = match row { + Row::Style(_, style) => style.clone(), + Row::Family(f) => db.family_display_name(f, look.english_names), + Row::Heading(_) => continue, + }; + ui.painter_at(name_rect).text(name_rect.left_center(), egui::Align2::LEFT_CENTER, name, egui::FontId::proportional(12.0), t.text); + let sample_rect = egui::Rect::from_min_max(rect.min + vec2(name_w, 2.0), rect.max - vec2(STAR + 2.0, 2.0)); + if !look.samples { + } else if let Some(tex) = sample_texture(ui.ctx(), family, row.style(), sample, sample_rect.height()) { + paint_sample(ui, sample_rect, &tex, t.text); + } + if resp.clicked() && !on_triangle && !on_star { + chosen = row.pick(); + } + } if let Some(f) = toggled { if st.expanded.contains(&f) { st.expanded.retain(|e| *e != f); diff --git a/crates/ui-egui/src/panels/image_trace.rs b/crates/ui-egui/src/panels/image_trace.rs index 60c1774a1..f46124c8b 100644 --- a/crates/ui-egui/src/panels/image_trace.rs +++ b/crates/ui-egui/src/panels/image_trace.rs @@ -260,7 +260,7 @@ pub(crate) fn trace_button(app: &mut VectorcraftApp, ui: &mut Ui, width: f32) { /// The selected Image Trace object's preset (Control bar, Properties): choosing another traces it /// again with that preset. pub(crate) fn preset_dropdown(app: &mut VectorcraftApp, ui: &mut Ui, current: &str, width: f32) { - if let Some(p) = widgets::combo(ui, "trace-preset", tl!(current), width, false, |ui| preset_list(app, ui, current)) { + if let Some(p) = widgets::combo(ui, "trace-preset", tl!(current), width, |ui| preset_list(app, ui, current)) { trace_with(app, &p); } } @@ -436,7 +436,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { ui.horizontal(|ui| { widgets::dim_label(ui, tl!("Library:")); let key = st.params["library"].as_str().unwrap_or(DOCUMENT_SWATCHES).to_string(); - if let Some(k) = widgets::combo(ui, "it-library", &library_label(app, &key), 170.0, false, |ui| library_list(app, ui, &key)) { + if let Some(k) = widgets::combo(ui, "it-library", &library_label(app, &key), 170.0, |ui| library_list(app, ui, &key)) { if k != DOCUMENT_SWATCHES { st.params["colorGroup"] = Value::Null; } @@ -451,7 +451,7 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { widgets::dim_label(ui, tl!("Colors:")); let group = color_group(app, &st.params).map(str::to_string); let label = group.clone().unwrap_or_else(|| tl!("All").to_string()); - if let Some(g) = widgets::combo(ui, "it-color-group", &label, 170.0, false, |ui| color_group_list(app, ui, group.as_deref())) { + if let Some(g) = widgets::combo(ui, "it-color-group", &label, 170.0, |ui| color_group_list(app, ui, group.as_deref())) { st.params["colorGroup"] = json!(g); st.preset = "Custom".into(); retrace = true; diff --git a/crates/ui-egui/src/panels/properties.rs b/crates/ui-egui/src/panels/properties.rs index f43e49936..e242086bb 100644 --- a/crates/ui-egui/src/panels/properties.rs +++ b/crates/ui-egui/src/panels/properties.rs @@ -525,7 +525,7 @@ fn artboard_size_controls_inner( let mut fit = None; ui.horizontal(|ui| { dim_label(ui, tl!("Preset:")); - preset = widgets::combo(ui, "ab-preset", ¤t, 140.0, false, |ui| { + preset = widgets::combo(ui, "ab-preset", ¤t, 140.0, |ui| { let mut chosen = None; if let Some(enabled) = fits { for (selection, label) in [(false, tl!("Fit to Artwork Bounds")), (true, tl!("Fit to Selected Art"))] { diff --git a/crates/ui-egui/src/tests/font_menu.rs b/crates/ui-egui/src/tests/font_menu.rs index 6b52792e5..680afa6d1 100644 --- a/crates/ui-egui/src/tests/font_menu.rs +++ b/crates/ui-egui/src/tests/font_menu.rs @@ -395,6 +395,61 @@ fn the_search_field_stays_put_while_the_list_scrolls() { } } +/// A search that matches nothing shrinks the menu to its search field and filters; clearing the +/// search lists every family again, and the menu grows back to its height before the search +/// (egui#5225). The search field stays where it was throughout. +#[test] +fn the_menu_grows_back_when_a_search_that_matched_nothing_is_cleared() { + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + let frame = |events: Vec| { + let input = + egui::RawInput { events, screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(600.0, 800.0))), ..Default::default() }; + let mut at = egui::Pos2::ZERO; + let mut out = ctx.run_ui(input, |ui| { + at = ui.next_widget_position(); + font_menu(ui, "test-regrow", "Inter", 260.0, None, MenuLook { samples: false, ..Default::default() }); + }); + out.textures_delta.clear(); + let text = |label: &str| { + out.shapes.iter().find_map(|s| match &s.shape { + egui::Shape::Text(t) if t.galley.text() == label => Some(t.pos), + _ => None, + }) + }; + // The filter row under the search field: it shows whatever the field holds. + let filters = text("Any Kind"); + let menu = filters.and_then(|p| ctx.layer_id_at(p)).and_then(|l| ctx.memory(|m| m.area_rect(l.id))); + (at, filters, text("Search"), text("No matching fonts").is_some(), menu.map(|r| r.height())) + }; + let settle = |events: Vec| { + let mut last = frame(events); + for _ in 0..5 { + last = frame(vec![]); + } + last + }; + let (at, ..) = frame(vec![]); + let click = at + egui::vec2(40.0, 10.0); + let button = |pressed| egui::Event::PointerButton { pos: click, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() }; + frame(vec![egui::Event::PointerMoved(click), button(true)]); + let (_, filters, search, _, full) = settle(vec![button(false)]); + let (filters, search, full) = (filters.expect("the menu is open"), search.expect("the search field shows"), full.expect("the menu's area")); + let nothing = "qqzzxxqqzz"; + let (_, f, _, none, small) = settle(vec![egui::Event::Text(nothing.into())]); + assert!(none, "the search matches no family"); + assert_eq!(f, Some(filters), "the search field stays put while nothing matches"); + let small = small.expect("the menu stays open"); + assert!(small + MenuLook::default().row < full, "the menu shrinks to its search field: {small} of {full}"); + for _ in nothing.chars() { + frame(key(egui::Key::Backspace)); + } + let (_, f, s, none, grown) = settle(vec![]); + assert!(!none, "every family is listed again"); + assert_eq!((f, s), (Some(filters), Some(search)), "the search field is where it was"); + assert_eq!(grown, Some(full), "the menu grows back to its height before the search"); +} + /// #563: a preview stays on the text while the menu stays on its family. The menu's caller passes /// the text's font, which the preview changed; the menu keeps the font from before the preview as /// the current one, so it doesn't take the previewed family for it and end the preview, then diff --git a/crates/ui-egui/src/tests/rotateview.rs b/crates/ui-egui/src/tests/rotateview.rs index 541605a31..fb434fd2a 100644 --- a/crates/ui-egui/src/tests/rotateview.rs +++ b/crates/ui-egui/src/tests/rotateview.rs @@ -344,7 +344,7 @@ fn escape_after_closing_a_popup_or_during_a_ruler_origin_drag_keeps_the_rotation let combo = combo.expect("the status bar's angle menu"); w.click(0.0, egui::PointerButton::Primary, combo.rect.center()); w.after(0.0, vec![]); - assert!(egui::ComboBox::is_open(&w.ctx, combo.id), "the angle menu is open"); + assert!(craft_ui::ComboBox::is_open(&w.ctx, combo.id), "the angle menu is open"); w.escape(0.2); assert!(!egui::Popup::is_any_open(&w.ctx), "Escape closes it"); w.escape(0.2); diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index 03467a8ef..decab86ba 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -38,9 +38,9 @@ pub use color::{ color_slider_named, color_slider_stepped, color_slider_valued, harmony_wheel, hex_field, hex_field_named, region_texture, }; pub use drag::{PanelDrag, SwatchRows, art_drop, doc_preview, drag_source}; -pub(crate) use dropdowns::combo; #[cfg(test)] use dropdowns::combo_button_id; +pub(crate) use dropdowns::{combo, combo_menu}; pub use dropdowns::{ dropdown, dropdown_named, dropdown_names, dropdown_names_named, dropdown_with, dropdown_with_named, list_scroll_style, menu_room, menu_scroll, panel_scroll, popover, set_menu_floor, strip_scroll, @@ -227,7 +227,7 @@ pub(crate) mod accessibility_tests { angle_dial(ui, "angle", 45.0, 28.0); channel_slider_named(ui, "channel", "H", 0.5, vec2(22.0, 80.0), &|_| Color32::GRAY); harmony_wheel(ui, "wheel", 120.0, &[red], Some(0)); - combo(ui, "font", "Regular", 120.0, true, |_| None::); + combo_menu(ui, "font", "Regular", 120.0, |_| None::); }); let slider = |label| { let n = named(&tree, label); diff --git a/crates/ui-egui/src/widgets/blend.rs b/crates/ui-egui/src/widgets/blend.rs index 267bec339..32e2712ae 100644 --- a/crates/ui-egui/src/widgets/blend.rs +++ b/crates/ui-egui/src/widgets/blend.rs @@ -13,7 +13,7 @@ pub fn blend_separator_before(i: usize) -> bool { /// The blend-mode dropdown, its groups separated (Normal | darken | lighten | contrast | inversion /// | component modes). `None` (objects that differ) shows blank. Returns the chosen mode. pub fn blend_dropdown(ui: &mut Ui, id: impl std::hash::Hash, current: Option, width: f32) -> Option { - combo(ui, id, current.map_or("", |m| tl!(m.label())), width, false, |ui| { + combo(ui, id, current.map_or("", |m| tl!(m.label())), width, |ui| { let mut chosen = None; for (i, m) in BlendMode::ALL.into_iter().enumerate() { if blend_separator_before(i) { diff --git a/crates/ui-egui/src/widgets/dropdowns.rs b/crates/ui-egui/src/widgets/dropdowns.rs index d1bca8067..db391edec 100644 --- a/crates/ui-egui/src/widgets/dropdowns.rs +++ b/crates/ui-egui/src/widgets/dropdowns.rs @@ -21,7 +21,7 @@ pub fn dropdown_names(ui: &mut Ui, id: impl std::hash::Hash, current: &str, opti /// [`dropdown_names`] for a row whose visible label is not an adjacent label widget. pub fn dropdown_names_named(ui: &mut Ui, id: impl std::hash::Hash, name: Option<&str>, current: &str, options: &[&str], width: f32) -> Option { - combo_named(ui, id, name, current, width, false, |ui| { + combo_named(ui, id, name, current, width, |ui| { let mut chosen = None; for (i, o) in options.iter().enumerate() { if ui.add(egui::Button::selectable(*o == current, *o)).clicked() { @@ -55,7 +55,7 @@ pub fn dropdown_with_named( width: f32, enabled: impl Fn(usize) -> bool, ) -> Option { - combo_named(ui, id, name, tl!(current), width, false, |ui| { + combo_named(ui, id, name, tl!(current), width, |ui| { let mut chosen = None; for (i, o) in options.iter().enumerate() { if ui.add_enabled(enabled(i), egui::Button::selectable(*o == current, tl!(o))).clicked() { @@ -66,7 +66,7 @@ pub fn dropdown_with_named( }) } -/// The id egui's [`egui::ComboBox::from_id_salt`] gives its button in this `Ui`: `from_id_salt` +/// The id [`craft_ui::ComboBox::from_id_salt`] gives its button in this `Ui`: `from_id_salt` /// stores `IdSalt::new(salt)` and hashes it once more with the scope, so the app must hash `id` /// the same way to read the button's focus and open state before it is drawn. pub(super) fn combo_button_id(ui: &Ui, id: &impl std::hash::Hash) -> egui::Id { @@ -74,17 +74,9 @@ pub(super) fn combo_button_id(ui: &Ui, id: &impl std::hash::Hash) -> egui::Id { } /// The recessed combo box of [`dropdown`] showing `current`; `list` draws the options and returns -/// the chosen one. A `searchable` list (a search field over a list it scrolls itself) stays open -/// while it is clicked: it closes itself when an option is chosen. -pub(crate) fn combo( - ui: &mut Ui, - id: impl std::hash::Hash, - current: &str, - width: f32, - searchable: bool, - list: impl FnOnce(&mut Ui) -> Option, -) -> Option { - combo_named(ui, id, None, current, width, searchable, list) +/// the chosen one. +pub(crate) fn combo(ui: &mut Ui, id: impl std::hash::Hash, current: &str, width: f32, list: impl FnOnce(&mut Ui) -> Option) -> Option { + combo_named(ui, id, None, current, width, list) } /// [`combo`] for a row whose visible label is not an adjacent label widget. @@ -94,7 +86,6 @@ pub(crate) fn combo_named( name: Option<&str>, current: &str, width: f32, - searchable: bool, list: impl FnOnce(&mut Ui) -> Option, ) -> Option { let t = Tokens::get(ui.ctx()); @@ -104,12 +95,9 @@ pub(crate) fn combo_named( .stroke(Stroke::new(1.0, t.input_border)) .corner_radius(CornerRadius::same(3)) .show(ui, |ui| { - if searchable { - return searchable_combo(ui, id_with(ui.scope_id(), id), label, name, current, width - 4.0, list); - } let button_id = combo_button_id(ui, &id); - let enter_for_combo = ui.memory(|m| m.has_focus(button_id)) || egui::ComboBox::is_open(ui.ctx(), button_id); - let response = egui::ComboBox::from_id_salt(HashOnly(&id)) + let enter_for_combo = ui.memory(|m| m.has_focus(button_id)) || craft_ui::ComboBox::is_open(ui.ctx(), button_id); + let response = craft_ui::ComboBox::from_id_salt(HashOnly(&id)) .selected_text(egui::RichText::new(current).size(12.0)) .width(width - 4.0) .height(ui.spacing().combo_height) @@ -132,20 +120,35 @@ pub(crate) fn combo_named( .inner } -/// A dropdown whose list keeps its own search field and scrolling rows (the font menu): drawn as -/// egui's combo box, but in a popup without the scroll area egui puts around a combo's list, so -/// the field stays at the top and only the rows scroll (#555). A click outside closes it. -fn searchable_combo( +/// The recessed combo box of [`dropdown`] showing `current`, whose menu keeps a search field and +/// filters in place over a list that scrolls (the font menu and the shortcut import's target +/// picker, #555). `menu` shows the menu with one of [`craft_ui::combo::Menu`]'s header forms and +/// returns what it returned (`None` while the menu is closed). A click inside the menu keeps it +/// open: it closes itself when an option is chosen. A click outside closes it. +pub(crate) fn combo_menu( ui: &mut Ui, - id: egui::Id, - label: Option, - name: Option<&str>, + id: impl std::hash::Hash, current: &str, width: f32, - list: impl FnOnce(&mut Ui) -> Option, + menu: impl FnOnce(craft_ui::combo::Menu<'_>) -> Option, ) -> Option { - let popup = id.with("popup"); - let open = egui::Popup::is_id_open(ui.ctx(), popup); + let t = Tokens::get(ui.ctx()); + let label = take_previous_label(ui); + egui::Frame::NONE + .fill(t.input) + .stroke(Stroke::new(1.0, t.input_border)) + .corner_radius(CornerRadius::same(3)) + .show(ui, |ui| { + let resp = combo_field(ui, id_with(ui.scope_id(), id), label, None, current, width - 4.0); + menu(craft_ui::combo::Menu::new(ui, &resp).close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside)) + }) + .inner +} + +/// The field of a searchable combo box, drawn as egui's combo box button, with the id `id`: its +/// menu is the popup `id.with("popup")`. +fn combo_field(ui: &mut Ui, id: egui::Id, label: Option, name: Option<&str>, current: &str, width: f32) -> Response { + let open = egui::Popup::is_id_open(ui.ctx(), id.with("popup")); let pad = ui.spacing().button_padding; let icon = Vec2::splat(ui.spacing().icon_width); let galley = egui::WidgetText::from(egui::RichText::new(current).size(12.0)).into_galley( @@ -155,7 +158,10 @@ fn searchable_combo( egui::TextStyle::Button, ); let height = (galley.size().y.max(icon.y) + 2.0 * pad.y).max(ui.spacing().interact_size.y); - let (rect, resp) = ui.allocate_exact_size(vec2(width, height), Sense::click()); + let size = vec2(width, height); + let (_, space) = ui.allocate_space(size); + let rect = egui::Align2::CENTER_CENTER.align_size_within_rect(size, space); + let resp = ui.interact(rect, id, Sense::click()); // Screen readers get what egui's combo box gives them: its role and the text it shows. resp.widget_info(|| egui::WidgetInfo { enabled: ui.is_enabled(), @@ -177,26 +183,7 @@ fn searchable_combo( Stroke::NONE, )); } - // It opens below the button, or above it when there is more room there, and the list gets - // the room left (the popup's frame aside). - const GAP: f32 = 12.0; - let screen = ui.ctx().content_rect(); - let frame = ui.spacing().menu_margin.sum().y + GAP; - let (below, above) = (screen.bottom() - rect.bottom() - frame, rect.top() - screen.top() - frame); - let (align, room) = if below >= above { (egui::RectAlign::BOTTOM_START, below) } else { (egui::RectAlign::TOP_START, above) }; - egui::Popup::menu(&resp) - .id(popup) - .width(rect.width()) - .align(align) - .align_alternatives(&[]) - .close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside) - .show(|ui| { - ui.set_min_width(ui.available_width()); - ui.set_max_height(room); - ui.style_mut().wrap_mode = Some(egui::TextWrapMode::Extend); - list(ui) - }) - .and_then(|r| r.inner) + resp } /// A popover anchored to `resp`, opened and closed by `toggle` (usually `resp.clicked()`), and diff --git a/crates/ui-egui/src/widgets/look_tests.rs b/crates/ui-egui/src/widgets/look_tests.rs index d0032aa3f..a1ac9c3b8 100644 --- a/crates/ui-egui/src/widgets/look_tests.rs +++ b/crates/ui-egui/src/widgets/look_tests.rs @@ -324,7 +324,7 @@ fn a_combos_keyboard_guard_reads_the_combos_own_button_id() { // `ComboBox::from_id_salt` gives the button: they must be equal, or the guard // reads and gives back the keyboard of nothing. let expected = combo_button_id(ui, &"combo-guard-test"); - let response = egui::ComboBox::from_id_salt("combo-guard-test").selected_text("One").show_ui(ui, |_| ()); + let response = craft_ui::ComboBox::from_id_salt("combo-guard-test").selected_text("One").show_ui(ui, |_| ()); seen.set(expected == response.response.id); }); h.run_steps(2); diff --git a/docs/pdf-backend.md b/docs/pdf-backend.md index 95c672aa6..4aca4353a 100644 --- a/docs/pdf-backend.md +++ b/docs/pdf-backend.md @@ -1,7 +1,7 @@ # PDF backend PDF writing uses `craft-pdf` from `storytold/craft-libs`, pinned to -`a0916808f7d78bed8c7b90cdf937176015454a7f`. Its `write` feature supplies Krilla configuration, +`92d392cff132eaf53e13afff77f38b5c11b73aa0`. Its `write` feature supplies Krilla configuration, metadata construction, fallible font preparation and serialization, and native unwind guards. The dependency disables default features and enables `write` and `interpret`. The production writer retains Krilla’s raster-image support without its optional text shaper. diff --git a/xtask/src/pdf_deps.rs b/xtask/src/pdf_deps.rs index dba28f15d..be9458616 100644 --- a/xtask/src/pdf_deps.rs +++ b/xtask/src/pdf_deps.rs @@ -5,7 +5,7 @@ use std::process::Command; use serde_json::Value; -const REV: &str = "a0916808f7d78bed8c7b90cdf937176015454a7f"; +const REV: &str = "92d392cff132eaf53e13afff77f38b5c11b73aa0"; const PACKAGES: &[(&str, &str)] = &[ ("craft-pdf", "0.1.0"), ("krilla", "0.8.2"), From e6d84c9d601dfad9fe16de4fb7ce8fdfd9b434c3 Mon Sep 17 00:00:00 2001 From: skylar taylor-barrick Date: Tue, 27 Oct 2026 09:59:01 -0600 Subject: [PATCH 2/2] ci(deps): split Dependabot cargo group into lockstep families (#4223) Group lockstep crate families into one PR each and batch only patch bumps of everything else, so a crate that needs code changes no longer blocks the rest. No ignore rules; security updates unchanged. open-pull-requests-limit 10. --- .github/dependabot.yml | 28 ++++++++++++++++++++++++---- 1 file changed, 24 insertions(+), 4 deletions(-) diff --git a/.github/dependabot.yml b/.github/dependabot.yml index c9de87acf..d482d6611 100644 --- a/.github/dependabot.yml +++ b/.github/dependabot.yml @@ -10,11 +10,31 @@ updates: interval: "daily" time: "06:00" timezone: "America/New_York" - # Consolidate updates into a single PR rather than one PR per crate groups: - cargo-dependencies: - patterns: - - "*" + # A dependency that matches several groups goes to the first one it matches, so the + # lockstep families are listed before the catch-all cargo-patch group. + # Families that must move together; all update types of a family land in one PR. + egui-stack: # egui/eframe/winit are [patch.crates-io]'d to storytold forks: expect this PR red on upstream releases until the forks move + patterns: ["egui*", "eframe", "epaint*", "ecolor", "emath", "winit", "wgpu*", "naga", "accesskit*"] + font-stack: + patterns: ["harfrust", "skrifa", "read-fonts", "font-types", "fontique", "parley", "write-fonts", "skera", "ttf-parser"] + pdf-render-stack: + patterns: ["hayro*", "krilla*", "vello*", "peniko", "kurbo", "resvg", "usvg", "tiny-skia*", "svgtypes"] + icu-stack: + patterns: ["icu_*", "fixed_decimal"] + ml-stack: + patterns: ["candle-*", "rten*", "ocrs"] + platform-stack: + patterns: ["zbus*", "zvariant*", "x11rb*"] + apple-stack: + patterns: ["objc2*", "block2", "dispatch2"] + rustcrypto: + patterns: ["aes", "sha1", "sha2", "sha3", "md-5", "hmac", "pbkdf2", "digest", "cipher"] + # Everything else: patch bumps only (minor/major of other crates get one PR each). + cargo-patch: + update-types: ["patch"] + patterns: ["*"] + open-pull-requests-limit: 10 commit-message: prefix: "chore(deps)"