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)" 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"),