mirror of
https://github.com/storytold/figcraft.git
synced 2026-10-29 12:56:47 -06:00
fix(ui): dropdown menus grow back after a search with no matches (#1429)
* feat(ui-egui): font family menu keeps its search field and grows back after a filter clears Repin craft-libs to the shared dropdown (craft-ui), move the font family menu to craft_ui::combo::Menu with the search in a fixed header, and use craft_ui::ComboBox for the credits sort. clippy bans egui::ComboBox. * chore(deps): craft-libs 60d60d48 (dropdown fixes) * chore(deps): craft-libs 4d7b82f7 (dropdown fixes) * chore(deps): craft-libs 92d392cf (#387 merged) * fix(ui-egui): craft-ui on every target (wasm uses the shared ComboBox too)
This commit is contained in:
Generated
+211
-20
@@ -942,12 +942,60 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "craft-docpath"
|
||||
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"
|
||||
|
||||
[[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-ui"
|
||||
version = "0.1.0"
|
||||
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",
|
||||
"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",
|
||||
@@ -972,7 +1020,7 @@ dependencies = [
|
||||
[[package]]
|
||||
name = "craft-window"
|
||||
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 = [
|
||||
"wayland-client",
|
||||
]
|
||||
@@ -1844,6 +1892,7 @@ version = "0.11.2"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"chrono",
|
||||
"craft-ui",
|
||||
"craft-update",
|
||||
"egui",
|
||||
"egui_extras",
|
||||
@@ -1913,6 +1962,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"
|
||||
@@ -2399,9 +2459,9 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "icu_collections"
|
||||
version = "2.4.0"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "fe090a42a0d3140fed8c63263ed1fecc02422cb2f4bfe55cd8de2de1b8e105dd"
|
||||
checksum = "fa68d21081c4a05d5a901a1c62add574c77048b6a1c67be3b50ce0b60d4ca513"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"potential_utf",
|
||||
@@ -2412,43 +2472,110 @@ dependencies = [
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_locale_core"
|
||||
version = "2.4.0"
|
||||
name = "icu_decimal"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "25d29b57c17c6528d0dc4c2141cfa2d08200465acb537a3909c8b9b8a4a984c0"
|
||||
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"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"litemap",
|
||||
"serde",
|
||||
"tinystr",
|
||||
"writeable",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer"
|
||||
version = "2.4.0"
|
||||
name = "icu_locale_fallback"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "404a3b8c486f205c87fe7f23ab6894d84a4ad76d6e4671ae1981ba7def87c599"
|
||||
checksum = "251af8e57c9400e3eb58242fe5b8b1152b2a64fdf4cf632f923c38ccee6f2fa9"
|
||||
dependencies = [
|
||||
"icu_locale_core",
|
||||
"icu_locale_fallback_data",
|
||||
"icu_provider",
|
||||
"potential_utf",
|
||||
"tinystr",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_locale_fallback_data"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "decf2a22ec8fa68f1a0c1129a3f8583f8f8bc24e8b9ccbe98ead99f62a4dc3a8"
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "12f9cf5f235641ed274641dd81c3f28d870e276763d0797aeeab72317b1c646f"
|
||||
dependencies = [
|
||||
"icu_collections",
|
||||
"icu_normalizer_data",
|
||||
"icu_properties",
|
||||
"icu_provider",
|
||||
"smallvec",
|
||||
"utf16_iter",
|
||||
"utf8_iter",
|
||||
"write16",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "icu_normalizer_data"
|
||||
version = "2.4.0"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "20f0088bfb7441f7a37fa0c9e20cb1e9cba1cac6b9ce8cd2a6dc44294f8e8cbc"
|
||||
checksum = "1563da1ed3e0b3bf3d74c9b85917ac9c56464d2f57242270c09c9e752f8021a0"
|
||||
|
||||
[[package]]
|
||||
name = "icu_pattern"
|
||||
version = "0.5.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "bc43ce0e76ee8cb0482dc6e2dbb9b76278e8038f45daabf2a2877577e2ceb18f"
|
||||
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.4.0"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "2ea83d991c56158f7cf898bf86436c1dfe7c07ee82fe88951f2695c9f37a379f"
|
||||
checksum = "7e7ca276ad3145661a65914e6daf131ca5120cd3dcee8f8f3214b8875184a148"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_collections",
|
||||
@@ -2461,18 +2588,21 @@ dependencies = [
|
||||
|
||||
[[package]]
|
||||
name = "icu_properties_data"
|
||||
version = "2.4.0"
|
||||
version = "2.3.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b98f29af82d02b163fe50161388350e743bb3107cc8e8c2f7314cb505faddfcf"
|
||||
checksum = "e590f038c1464a96894fd6d10127e90a8be4509f56ff7ecef851b15cee0b7caa"
|
||||
|
||||
[[package]]
|
||||
name = "icu_provider"
|
||||
version = "2.4.0"
|
||||
version = "2.3.1"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "413099315636c588ee8f011190b329b971a09a8fae9d157e01dcae8379468514"
|
||||
checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"icu_locale_core",
|
||||
"postcard",
|
||||
"serde",
|
||||
"stable_deref_trait",
|
||||
"writeable",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
@@ -2480,6 +2610,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 = "idna"
|
||||
version = "1.1.0"
|
||||
@@ -2731,7 +2885,7 @@ checksum = "e2db585e1d738fc771bf08a151420d3ed193d9d895a36df7f6f8a9456b911ddc"
|
||||
[[package]]
|
||||
name = "krilla"
|
||||
version = "0.8.2"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=495e5971bc024aa96e6ed318cb6e7cd3a0103184#495e5971bc024aa96e6ed318cb6e7cd3a0103184"
|
||||
source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0"
|
||||
dependencies = [
|
||||
"base64 0.22.1",
|
||||
"bumpalo",
|
||||
@@ -2844,6 +2998,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"
|
||||
@@ -3684,6 +3841,8 @@ version = "0.1.6"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661"
|
||||
dependencies = [
|
||||
"serde_core",
|
||||
"writeable",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
@@ -4681,6 +4840,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 = "tar"
|
||||
version = "0.4.46"
|
||||
@@ -4855,6 +5026,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "abc840c238154d58f9e86b2792261edfde565a17696e1cda1a449de27979801a"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"serde_core",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
@@ -5165,6 +5337,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"
|
||||
@@ -6152,11 +6330,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"
|
||||
@@ -6424,8 +6611,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f"
|
||||
dependencies = [
|
||||
"displaydoc",
|
||||
"litemap",
|
||||
"serde_core",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerovec",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
@@ -6434,6 +6624,7 @@ version = "0.11.9"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "f58a2799cfe9a1670c7ea8edbd52ef8edb93cb37a7f83bf31ba9e35d9dae6a40"
|
||||
dependencies = [
|
||||
"serde",
|
||||
"yoke",
|
||||
"zerofrom",
|
||||
"zerovec-derive",
|
||||
|
||||
+6
-4
@@ -35,13 +35,15 @@ figcraft-plugin = { path = "crates/plugin" }
|
||||
|
||||
# craft-libs crates all pin one rev (craftrules standards/shared-libraries.md): bump them together.
|
||||
# Which paths stored in a document may be followed (craft-libs).
|
||||
craft-docpath = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "495e5971bc024aa96e6ed318cb6e7cd3a0103184" }
|
||||
craft-docpath = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0" }
|
||||
# Who draws the window frame on Linux — the compositor or the app — so every Craft app shows the
|
||||
# same native titlebar per desktop (craft-libs). FigCraft only reads the probe result and maps it
|
||||
# to its own `TitleBar`, so it does not enable craft-window's optional `egui` helpers (which would
|
||||
# pull a second egui family version).
|
||||
craft-window = { git = "https://github.com/storytold/craft-libs", rev = "495e5971bc024aa96e6ed318cb6e7cd3a0103184" }
|
||||
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" }
|
||||
craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0", features = ["egui"] }
|
||||
# Shared dropdown menus: a combo box whose menu grows back after shrinking (craft-libs).
|
||||
craft-ui = { git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0" }
|
||||
|
||||
# foundation
|
||||
kurbo = { version = "0.13", features = ["serde"] }
|
||||
@@ -160,4 +162,4 @@ winit = { git = "https://github.com/storytold/winit.git", rev = "a517b3846ee8457
|
||||
|
||||
# craft-libs' vendored krilla 0.8.2: decoded PNGs are capped at 1 GiB and reserved fallibly, and a
|
||||
# panic while drawing unwinds without aborting.
|
||||
krilla = { git = "https://github.com/storytold/craft-libs", rev = "495e5971bc024aa96e6ed318cb6e7cd3a0103184" }
|
||||
krilla = { git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0" }
|
||||
|
||||
@@ -4,3 +4,6 @@ allow-unwrap-in-tests = true
|
||||
allow-expect-in-tests = true
|
||||
allow-panic-in-tests = true
|
||||
too-many-arguments-threshold = 10
|
||||
disallowed-types = [
|
||||
{ path = "egui::ComboBox", reason = "use craft_ui::ComboBox: egui's combo box menu can't grow back while open (egui#5225)" },
|
||||
]
|
||||
|
||||
@@ -9,6 +9,7 @@ description = "FigCraft egui frontend (the Figma-style UI: canvas, toolbar, laye
|
||||
workspace = true
|
||||
|
||||
[dependencies]
|
||||
craft-ui = { workspace = true }
|
||||
figcraft-geom = { workspace = true }
|
||||
figcraft-color = { workspace = true }
|
||||
figcraft-doc = { workspace = true }
|
||||
|
||||
@@ -267,7 +267,7 @@ fn contributors_in(ui: &mut egui::Ui, contributors: &[Contributor], total_commit
|
||||
}
|
||||
ui.separator();
|
||||
ui.label(RichText::new(tr("Sort")).color(t.text_secondary));
|
||||
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;
|
||||
|
||||
@@ -0,0 +1,122 @@
|
||||
//! The font family menu follows its list's height while it is open: filtering to nothing shrinks
|
||||
//! it, clearing the filter grows it back, and the search field stays where it is (egui#5225).
|
||||
use egui::accesskit::{Node, Role};
|
||||
use egui::{Event, Key, Modifiers, Pos2, RawInput, Rect, vec2};
|
||||
use figcraft_engine::Session;
|
||||
use serde_json::json;
|
||||
|
||||
use crate::{FigApp, NoServices};
|
||||
|
||||
struct Panel {
|
||||
app: FigApp,
|
||||
ctx: egui::Context,
|
||||
time: f64,
|
||||
}
|
||||
|
||||
impl Panel {
|
||||
fn new() -> Self {
|
||||
let mut s = Session::new();
|
||||
let text = s.execute("text.create", &json!({"text": "Typography"})).unwrap()["id"].as_u64().unwrap();
|
||||
s.execute("select.set", &json!({"ids": [text]})).unwrap();
|
||||
let mut p = Self { app: FigApp::new(s, Box::new(NoServices)), ctx: egui::Context::default(), time: 0.0 };
|
||||
crate::theme::install_fonts(&p.ctx, false, 0);
|
||||
crate::theme::apply(&p.ctx, crate::theme::Tokens::light());
|
||||
p.ctx.enable_accesskit();
|
||||
for _ in 0..3 {
|
||||
p.frame(vec![]);
|
||||
}
|
||||
p
|
||||
}
|
||||
|
||||
fn frame(&mut self, events: Vec<Event>) -> Vec<Node> {
|
||||
self.time += 1.0 / 60.0;
|
||||
let input =
|
||||
RawInput { screen_rect: Some(Rect::from_min_size(Pos2::ZERO, vec2(280.0, 800.0))), time: Some(self.time), events, ..Default::default() };
|
||||
let mut out = self.ctx.run_ui(input, |ui| {
|
||||
let ds = self.app.session.ds().unwrap();
|
||||
let doc = ds.doc.clone();
|
||||
let sel = ds.selection.clone();
|
||||
crate::typography::typography(&mut self.app, ui, &doc, &sel);
|
||||
});
|
||||
out.textures_delta.clear();
|
||||
out.platform_output.accesskit_update.map(|u| u.nodes.into_iter().map(|(_, n)| n).collect()).unwrap_or_default()
|
||||
}
|
||||
|
||||
/// Settled frames; returns the last frame's accessibility tree.
|
||||
fn settle(&mut self, events: Vec<Event>) -> Vec<Node> {
|
||||
let mut nodes = self.frame(events);
|
||||
for _ in 0..4 {
|
||||
nodes = self.frame(vec![]);
|
||||
}
|
||||
nodes
|
||||
}
|
||||
|
||||
fn click(&mut self, at: Pos2) {
|
||||
self.frame(vec![Event::PointerMoved(at)]);
|
||||
let button = |pressed| Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: Modifiers::NONE };
|
||||
self.frame(vec![button(true)]);
|
||||
self.settle(vec![button(false)]);
|
||||
}
|
||||
|
||||
/// The popup's rectangle: the one foreground area that wasn't there before the menu opened.
|
||||
fn menu_rect(&self, before: &[egui::LayerId]) -> Rect {
|
||||
let layers: Vec<egui::LayerId> = self.ctx.memory(|m| m.layer_ids().collect());
|
||||
layers
|
||||
.into_iter()
|
||||
.filter(|l| l.order == egui::Order::Foreground && !before.contains(l))
|
||||
.filter_map(|l| self.ctx.memory(|m| m.area_rect(l.id)))
|
||||
.max_by(|a, b| a.height().total_cmp(&b.height()))
|
||||
.expect("the font menu is open")
|
||||
}
|
||||
}
|
||||
|
||||
fn centre(n: &Node) -> Pos2 {
|
||||
let b = n.bounds().unwrap();
|
||||
Pos2::new(((b.x0 + b.x1) / 2.0) as f32, ((b.y0 + b.y1) / 2.0) as f32)
|
||||
}
|
||||
|
||||
/// The top of the search field: the text input as wide as `menu` (the panel's own fields are narrower).
|
||||
fn search_top(nodes: &[Node], menu: Rect) -> f32 {
|
||||
let field = nodes
|
||||
.iter()
|
||||
.filter(|n| n.role() == Role::TextInput)
|
||||
.filter_map(|n| n.bounds())
|
||||
.find(|b| menu.contains(Pos2::new(b.x0 as f32 + 1.0, b.y0 as f32 + 1.0)) && b.x1 - b.x0 > f64::from(menu.width()) - 40.0)
|
||||
.expect("the menu has a search field");
|
||||
field.y0 as f32
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_font_menu_regrows_after_a_search_matches_nothing() {
|
||||
figcraft_text::scan_system_fonts();
|
||||
if figcraft_text::family_names().len() < 3 {
|
||||
eprintln!("fewer than three font families; skipping");
|
||||
return;
|
||||
}
|
||||
let mut p = Panel::new();
|
||||
let nodes = p.settle(vec![]);
|
||||
let family =
|
||||
nodes.iter().find(|n| n.role() == Role::ComboBox && n.value() == Some("Inter")).expect("the Typography section has a font family field");
|
||||
let before: Vec<egui::LayerId> = p.ctx.memory(|m| m.layer_ids().collect());
|
||||
p.click(centre(family));
|
||||
let nodes = p.settle(vec![]);
|
||||
let open = p.menu_rect(&before);
|
||||
let top = search_top(&nodes, open);
|
||||
assert!(open.height() > 100.0, "the menu lists fonts: {open:?}");
|
||||
assert!(open.width() >= 240.0, "the menu keeps the 240 minimum width under a narrower field: {open:?}");
|
||||
|
||||
let type_text = |p: &mut Panel, s: &str| p.settle(vec![Event::Text(s.into())]);
|
||||
let nodes = type_text(&mut p, "zzzzqqqq");
|
||||
let none = p.menu_rect(&before);
|
||||
assert!(none.height() < open.height() - 20.0, "no match shrinks the menu: {open:?} → {none:?}");
|
||||
assert_eq!(search_top(&nodes, none), top, "the search field stays put while the list is empty");
|
||||
|
||||
let mut nodes = nodes;
|
||||
for _ in 0.."zzzzqqqq".len() {
|
||||
let key = Event::Key { key: Key::Backspace, physical_key: None, pressed: true, repeat: false, modifiers: Modifiers::NONE };
|
||||
nodes = p.settle(vec![key]);
|
||||
}
|
||||
let again = p.menu_rect(&before);
|
||||
assert!((again.height() - open.height()).abs() < 1.0, "clearing the search grows the menu back: {open:?} → {again:?}");
|
||||
assert_eq!(search_top(&nodes, again), top, "the search field stays put when the list grows back");
|
||||
}
|
||||
@@ -78,6 +78,8 @@ mod export_preview_tests;
|
||||
#[cfg(test)]
|
||||
mod field_tests;
|
||||
#[cfg(test)]
|
||||
mod font_menu_regrow_tests;
|
||||
#[cfg(test)]
|
||||
mod frame_label_tests;
|
||||
#[cfg(test)]
|
||||
mod gap_handle_tests;
|
||||
|
||||
@@ -22,21 +22,17 @@ pub(super) fn font_rows(
|
||||
row(ui, 32.0, |ui| {
|
||||
let w = ui.available_width();
|
||||
let resp = font_dropdown(app, ui, ids, "fontFamily", family.as_deref(), w, editing);
|
||||
menu_popup(&resp).width(w.max(240.0)).close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside).show(|ui| {
|
||||
if !editing && apply_variable_item(ui, ids, "fontFamily", &resp) {
|
||||
return;
|
||||
}
|
||||
let pick = font_picker(ui, family.as_deref(), resp.clicked());
|
||||
let before_search = |ui: &mut Ui| !editing && apply_variable_item(ui, ids, "fontFamily", &resp);
|
||||
if let Some(pick) = font_picker(ui, &resp, family.as_deref(), w.max(240.0), before_search) {
|
||||
if let Some(f) = pick.picked {
|
||||
remember_family(ui.ctx(), &f);
|
||||
apply_style(app, "fontFamily", json!(f));
|
||||
ui.close();
|
||||
} else if let Some(h) = pick.hovered.or(pick.highlighted) {
|
||||
// The canvas shows the hovered family (or the one the arrow keys are on);
|
||||
// closing without picking ends the preview.
|
||||
app.preview_text(ui.ctx(), "text.style", text_style_params(ids, range, json!({"fontFamily": h})));
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
// Style + size.
|
||||
|
||||
@@ -191,12 +191,33 @@ mod tests {
|
||||
assert_eq!(parse_letter_spacing("x", Some(LetterSpacing::Percent(0.0))), None);
|
||||
}
|
||||
|
||||
/// One frame of the font picker with `events`.
|
||||
/// One frame of the font picker under a field, with `events`. `opened` clicks the field first
|
||||
/// (a press frame, then the release frame this returns).
|
||||
fn pick(ctx: &egui::Context, current: &str, opened: bool, events: Vec<egui::Event>) -> FontPick {
|
||||
let mut out = FontPick::default();
|
||||
let mut shown = ctx.run_ui(egui::RawInput { events, ..Default::default() }, |ui| out = font_picker(ui, Some(current), opened));
|
||||
shown.textures_delta.clear();
|
||||
out
|
||||
let field = egui::Rect::from_min_size(egui::pos2(10.0, 10.0), egui::vec2(200.0, 24.0));
|
||||
let frame = |events: Vec<egui::Event>| {
|
||||
let mut out = FontPick::default();
|
||||
let mut shown = ctx.run_ui(egui::RawInput { events, ..Default::default() }, |ui| {
|
||||
let resp = ui.interact(field, egui::Id::unique("font_field"), egui::Sense::click());
|
||||
out = font_picker(ui, &resp, Some(current), 240.0, |_| false).unwrap_or_default();
|
||||
});
|
||||
shown.textures_delta.clear();
|
||||
out
|
||||
};
|
||||
if opened {
|
||||
let button = |pressed| egui::Event::PointerButton {
|
||||
pos: field.center(),
|
||||
button: egui::PointerButton::Primary,
|
||||
pressed,
|
||||
modifiers: egui::Modifiers::NONE,
|
||||
};
|
||||
// The field is laid out once before the pointer can hit it.
|
||||
frame(vec![]);
|
||||
frame(vec![egui::Event::PointerMoved(field.center())]);
|
||||
frame(vec![button(true)]);
|
||||
return frame(vec![button(false)]);
|
||||
}
|
||||
frame(events)
|
||||
}
|
||||
|
||||
fn key(key: egui::Key) -> egui::Event {
|
||||
|
||||
@@ -41,149 +41,199 @@ pub(crate) struct FontPick {
|
||||
pub hovered: Option<String>,
|
||||
}
|
||||
|
||||
/// The searchable family list: click a family, or step through it with Up and Down and press Enter.
|
||||
/// Hovering a family previews it on the canvas (closing without picking restores the original).
|
||||
pub(crate) fn font_picker(ui: &mut Ui, current: Option<&str>, just_opened: bool) -> FontPick {
|
||||
/// The most the font menu is tall (its search field and the "Apply variable" row above the list
|
||||
/// included); the list scrolls past that.
|
||||
const MENU_HEIGHT: f32 = LIST_HEIGHT + 40.0;
|
||||
|
||||
/// What the menu's header worked out for its list.
|
||||
#[derive(Default)]
|
||||
struct Header {
|
||||
rows: Vec<PickRow>,
|
||||
/// The row Up and Down are on.
|
||||
hi: Option<usize>,
|
||||
/// The rows Up and Down moved this frame (the list scrolls to the row they are on).
|
||||
step: i64,
|
||||
/// Enter picked this family.
|
||||
entered: Option<String>,
|
||||
}
|
||||
|
||||
/// The searchable family menu under `anchor`: click a family, or step through it with Up and Down
|
||||
/// and press Enter. Hovering a family previews it on the canvas (closing without picking restores
|
||||
/// the original). `before_search` draws the rows above the search field and returns true when one
|
||||
/// of them handled the menu (it closed it). None while the menu is closed.
|
||||
///
|
||||
/// The search field and those rows stay in place; only the list scrolls. Headers ("Recent",
|
||||
/// "All fonts") are as tall as family rows, so the list is uniform and virtualised.
|
||||
pub(crate) fn font_picker(
|
||||
ui: &Ui,
|
||||
anchor: &egui::Response,
|
||||
current: Option<&str>,
|
||||
width: f32,
|
||||
before_search: impl FnOnce(&mut Ui) -> bool,
|
||||
) -> Option<FontPick> {
|
||||
let t = Tokens::get(ui.ctx());
|
||||
let just_opened = anchor.clicked();
|
||||
let hi_id = Id::unique("font_highlight");
|
||||
let q_id = Id::unique("font_query");
|
||||
// The row Up and Down are on, and the search it is a row of.
|
||||
let hi_id = Id::unique("font_highlight");
|
||||
let mut q: String = ui.data(|d| d.get_temp(q_id)).unwrap_or_default();
|
||||
if just_opened {
|
||||
q.clear();
|
||||
ui.data_mut(|d| d.remove::<(String, usize)>(hi_id));
|
||||
}
|
||||
// Taken before the search field sees them (it would move its cursor).
|
||||
let step = ui.input_mut(|i| {
|
||||
i.count_and_consume_key(egui::Modifiers::NONE, egui::Key::ArrowDown) as i64
|
||||
- i.count_and_consume_key(egui::Modifiers::NONE, egui::Key::ArrowUp) as i64
|
||||
});
|
||||
let search = ui.add(egui::TextEdit::singleline(&mut q).hint_text(tr("Search fonts")).desired_width(ui.available_width()).font(regular(11.5)));
|
||||
crate::widgets::select_all_on_focus(ui, &search, &q);
|
||||
if just_opened {
|
||||
search.request_focus();
|
||||
}
|
||||
ui.data_mut(|d| d.insert_temp(q_id, q.clone()));
|
||||
let ql = q.trim().to_lowercase();
|
||||
let all = figcraft_text::family_names();
|
||||
let matches: Vec<String> = all.iter().filter(|f| ql.is_empty() || f.to_lowercase().contains(&ql)).cloned().collect();
|
||||
let mut rows: Vec<PickRow> = Vec::new();
|
||||
if ql.is_empty() {
|
||||
let recent: Vec<String> = recent_families(ui.ctx()).into_iter().filter(|f| all.iter().any(|a| a.eq_ignore_ascii_case(f))).collect();
|
||||
if !recent.is_empty() {
|
||||
rows.push(PickRow::Header(tr("Recent")));
|
||||
rows.extend(recent.into_iter().map(PickRow::Family));
|
||||
rows.push(PickRow::Header(tr("All fonts")));
|
||||
}
|
||||
}
|
||||
if matches.is_empty() {
|
||||
widgets::label(ui, tr("No fonts match"), t.text_tertiary);
|
||||
return FontPick::default();
|
||||
}
|
||||
rows.extend(matches.into_iter().map(PickRow::Family));
|
||||
let family = |i: usize| match rows.get(i) {
|
||||
Some(PickRow::Family(f)) => Some(f.clone()),
|
||||
_ => None,
|
||||
};
|
||||
// A new search starts again; the first step starts from the current family.
|
||||
let mut hi = ui.data(|d| d.get_temp::<(String, usize)>(hi_id)).filter(|(hq, _)| *hq == q).map(|(_, i)| i);
|
||||
let stride = ROW + ui.spacing().item_spacing.y;
|
||||
let mut offset = None;
|
||||
if step != 0 {
|
||||
let next = |i: usize| if step > 0 { i.checked_add(1) } else { i.checked_sub(1) };
|
||||
let mut from =
|
||||
hi.or_else(|| rows.iter().rposition(|r| matches!(r, PickRow::Family(f) if current.is_some_and(|c| c.eq_ignore_ascii_case(f)))));
|
||||
for _ in 0..step.unsigned_abs() {
|
||||
let mut at = match from {
|
||||
Some(i) => next(i),
|
||||
None if step > 0 => Some(0),
|
||||
None => rows.len().checked_sub(1),
|
||||
};
|
||||
// Past the headers; at either end it stays where it was.
|
||||
while let Some(i) = at.filter(|i| *i < rows.len() && family(*i).is_none()) {
|
||||
at = next(i);
|
||||
}
|
||||
if let Some(i) = at.filter(|i| family(*i).is_some()) {
|
||||
from = Some(i);
|
||||
hi = Some(i);
|
||||
}
|
||||
}
|
||||
if let Some(i) = hi {
|
||||
// Scrolled just enough to show it (the list is shorter than its tallest where the
|
||||
// window has less room).
|
||||
let (seen, height): (f32, f32) = ui.data(|d| d.get_temp(hi_id.with("view"))).unwrap_or((0.0, LIST_HEIGHT));
|
||||
let top = i as f32 * stride;
|
||||
offset = Some(seen.max(top + stride - height).min(top));
|
||||
}
|
||||
}
|
||||
match hi {
|
||||
Some(i) => ui.data_mut(|d| {
|
||||
d.insert_temp(hi_id, (q.clone(), i));
|
||||
}),
|
||||
None => ui.data_mut(|d| d.remove::<(String, usize)>(hi_id)),
|
||||
}
|
||||
if search.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
|
||||
let first = rows.iter().find_map(|r| match r {
|
||||
PickRow::Family(f) => Some(f.clone()),
|
||||
PickRow::Header(_) => None,
|
||||
});
|
||||
return FontPick { picked: hi.and_then(family).or(first), highlighted: None, hovered: None };
|
||||
}
|
||||
let mut budget = 4;
|
||||
let mut picked = None;
|
||||
let mut hovered_family = None;
|
||||
let mut area = egui::ScrollArea::vertical().max_height(LIST_HEIGHT).auto_shrink([false, true]);
|
||||
if let Some(y) = offset {
|
||||
area = area.vertical_scroll_offset(y);
|
||||
}
|
||||
let shown = area.show_rows(ui, ROW, rows.len(), |ui, range| {
|
||||
for (i, r) in rows.iter().enumerate().skip(range.start).take(range.len()) {
|
||||
let w = ui.available_width();
|
||||
let (rect, resp) = ui.allocate_exact_size(vec2(w, ROW), if matches!(r, PickRow::Family(_)) { Sense::click() } else { Sense::hover() });
|
||||
match r {
|
||||
PickRow::Header(h) => {
|
||||
ui.painter().text(pos2(rect.min.x + 8.0, rect.center().y), Align2::LEFT_CENTER, *h, semibold(10.5), t.text_secondary);
|
||||
craft_ui::combo::Menu::new(ui, anchor)
|
||||
.height(MENU_HEIGHT)
|
||||
.close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside)
|
||||
.style(crate::right::menu_style)
|
||||
.show_rows_with_header(
|
||||
|ui| {
|
||||
// The menu is at least `width` wide, even under a narrower field.
|
||||
ui.set_min_width(width);
|
||||
if before_search(ui) {
|
||||
return Header::default();
|
||||
}
|
||||
let mut q: String = ui.data(|d| d.get_temp(q_id)).unwrap_or_default();
|
||||
if just_opened {
|
||||
q.clear();
|
||||
ui.data_mut(|d| d.remove::<(String, usize)>(hi_id));
|
||||
}
|
||||
// Taken before the search field sees them (it would move its cursor).
|
||||
let step = ui.input_mut(|i| {
|
||||
i.count_and_consume_key(egui::Modifiers::NONE, egui::Key::ArrowDown) as i64
|
||||
- i.count_and_consume_key(egui::Modifiers::NONE, egui::Key::ArrowUp) as i64
|
||||
});
|
||||
let search =
|
||||
ui.add(egui::TextEdit::singleline(&mut q).hint_text(tr("Search fonts")).desired_width(ui.available_width()).font(regular(11.5)));
|
||||
crate::widgets::select_all_on_focus(ui, &search, &q);
|
||||
if just_opened {
|
||||
search.request_focus();
|
||||
}
|
||||
PickRow::Family(f) => {
|
||||
let hovered = resp.hovered() || hi == Some(i);
|
||||
if hovered {
|
||||
ui.painter().rect_filled(rect, CornerRadius::same(5), t.accent);
|
||||
ui.data_mut(|d| d.insert_temp(q_id, q.clone()));
|
||||
let ql = q.trim().to_lowercase();
|
||||
let all = figcraft_text::family_names();
|
||||
let matches: Vec<String> = all.iter().filter(|f| ql.is_empty() || f.to_lowercase().contains(&ql)).cloned().collect();
|
||||
let mut rows: Vec<PickRow> = Vec::new();
|
||||
if ql.is_empty() {
|
||||
let recent: Vec<String> =
|
||||
recent_families(ui.ctx()).into_iter().filter(|f| all.iter().any(|a| a.eq_ignore_ascii_case(f))).collect();
|
||||
if !recent.is_empty() {
|
||||
rows.push(PickRow::Header(tr("Recent")));
|
||||
rows.extend(recent.into_iter().map(PickRow::Family));
|
||||
rows.push(PickRow::Header(tr("All fonts")));
|
||||
}
|
||||
let col = if hovered { t.accent_on } else { t.text };
|
||||
if current.is_some_and(|c| c.eq_ignore_ascii_case(f)) {
|
||||
icons::paint(
|
||||
ui.painter(),
|
||||
Rect::from_center_size(pos2(rect.min.x + 10.0, rect.center().y), vec2(12.0, 12.0)),
|
||||
Icon::Check,
|
||||
col,
|
||||
);
|
||||
}
|
||||
if matches.is_empty() {
|
||||
widgets::label(ui, tr("No fonts match"), t.text_tertiary);
|
||||
return Header::default();
|
||||
}
|
||||
rows.extend(matches.into_iter().map(PickRow::Family));
|
||||
let family = |i: usize| match rows.get(i) {
|
||||
Some(PickRow::Family(f)) => Some(f.clone()),
|
||||
_ => None,
|
||||
};
|
||||
// A new search starts again; the first step starts from the current family.
|
||||
let mut hi = ui.data(|d| d.get_temp::<(String, usize)>(hi_id)).filter(|(hq, _)| *hq == q).map(|(_, i)| i);
|
||||
if step != 0 {
|
||||
let next = |i: usize| if step > 0 { i.checked_add(1) } else { i.checked_sub(1) };
|
||||
let mut from = hi
|
||||
.or_else(|| rows.iter().rposition(|r| matches!(r, PickRow::Family(f) if current.is_some_and(|c| c.eq_ignore_ascii_case(f)))));
|
||||
for _ in 0..step.unsigned_abs() {
|
||||
let mut at = match from {
|
||||
Some(i) => next(i),
|
||||
None if step > 0 => Some(0),
|
||||
None => rows.len().checked_sub(1),
|
||||
};
|
||||
// Past the headers; at either end it stays where it was.
|
||||
while let Some(i) = at.filter(|i| *i < rows.len() && family(*i).is_none()) {
|
||||
at = next(i);
|
||||
}
|
||||
if let Some(i) = at.filter(|i| family(*i).is_some()) {
|
||||
from = Some(i);
|
||||
hi = Some(i);
|
||||
}
|
||||
}
|
||||
let text_min = pos2(rect.min.x + 22.0, rect.min.y);
|
||||
match crate::font_preview::family(ui.ctx(), f, 13.0, &mut budget) {
|
||||
Some(tex) => {
|
||||
crate::font_preview::paint(
|
||||
ui.painter(),
|
||||
&tex,
|
||||
pos2(text_min.x, rect.center().y),
|
||||
rect.max.x - text_min.x - 6.0,
|
||||
ROW,
|
||||
col,
|
||||
);
|
||||
}
|
||||
match hi {
|
||||
Some(i) => ui.data_mut(|d| {
|
||||
d.insert_temp(hi_id, (q.clone(), i));
|
||||
}),
|
||||
None => ui.data_mut(|d| d.remove::<(String, usize)>(hi_id)),
|
||||
}
|
||||
let mut entered = None;
|
||||
if search.lost_focus() && ui.input(|i| i.key_pressed(egui::Key::Enter)) {
|
||||
let first = rows.iter().find_map(|r| match r {
|
||||
PickRow::Family(f) => Some(f.clone()),
|
||||
PickRow::Header(_) => None,
|
||||
});
|
||||
entered = hi.and_then(family).or(first);
|
||||
ui.close();
|
||||
}
|
||||
Header { rows, hi, step, entered }
|
||||
},
|
||||
ROW,
|
||||
|h| h.rows.len(),
|
||||
|ui, range, h| {
|
||||
if let Some(f) = h.entered {
|
||||
return FontPick { picked: Some(f), highlighted: None, hovered: None };
|
||||
}
|
||||
if let (Some(i), true) = (h.hi, h.step != 0) {
|
||||
// Scrolled just enough to show the row Up or Down moved to; it may be out of
|
||||
// the range laid out, so its rect comes from the list's top.
|
||||
let stride = ROW + ui.spacing().item_spacing.y;
|
||||
let top = ui.max_rect().top() - range.start as f32 * stride + i as f32 * stride;
|
||||
ui.scroll_to_rect(Rect::from_min_size(pos2(ui.max_rect().left(), top), vec2(ui.max_rect().width(), ROW)), None);
|
||||
}
|
||||
let mut budget = 4;
|
||||
let mut picked = None;
|
||||
let mut hovered_family = None;
|
||||
let hi = h.hi;
|
||||
for (i, r) in h.rows.iter().enumerate().skip(range.start).take(range.len()) {
|
||||
let w = ui.available_width();
|
||||
let (rect, resp) =
|
||||
ui.allocate_exact_size(vec2(w, ROW), if matches!(r, PickRow::Family(_)) { Sense::click() } else { Sense::hover() });
|
||||
match r {
|
||||
PickRow::Header(label) => {
|
||||
ui.painter().text(pos2(rect.min.x + 8.0, rect.center().y), Align2::LEFT_CENTER, *label, semibold(10.5), t.text_secondary);
|
||||
}
|
||||
None => {
|
||||
ui.painter().text(pos2(text_min.x, rect.center().y), Align2::LEFT_CENTER, f, regular(12.0), col);
|
||||
PickRow::Family(f) => {
|
||||
let hovered = resp.hovered() || hi == Some(i);
|
||||
if hovered {
|
||||
ui.painter().rect_filled(rect, CornerRadius::same(5), t.accent);
|
||||
}
|
||||
let col = if hovered { t.accent_on } else { t.text };
|
||||
if current.is_some_and(|c| c.eq_ignore_ascii_case(f)) {
|
||||
icons::paint(
|
||||
ui.painter(),
|
||||
Rect::from_center_size(pos2(rect.min.x + 10.0, rect.center().y), vec2(12.0, 12.0)),
|
||||
Icon::Check,
|
||||
col,
|
||||
);
|
||||
}
|
||||
let text_min = pos2(rect.min.x + 22.0, rect.min.y);
|
||||
match crate::font_preview::family(ui.ctx(), f, 13.0, &mut budget) {
|
||||
Some(tex) => {
|
||||
crate::font_preview::paint(
|
||||
ui.painter(),
|
||||
&tex,
|
||||
pos2(text_min.x, rect.center().y),
|
||||
rect.max.x - text_min.x - 6.0,
|
||||
ROW,
|
||||
col,
|
||||
);
|
||||
}
|
||||
None => {
|
||||
ui.painter().text(pos2(text_min.x, rect.center().y), Align2::LEFT_CENTER, f, regular(12.0), col);
|
||||
}
|
||||
}
|
||||
if resp.clicked() {
|
||||
picked = Some(f.clone());
|
||||
ui.close();
|
||||
}
|
||||
if resp.hovered() {
|
||||
hovered_family = Some(f.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
if resp.clicked() {
|
||||
picked = Some(f.clone());
|
||||
}
|
||||
if resp.hovered() {
|
||||
hovered_family = Some(f.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.data_mut(|d| d.insert_temp(hi_id.with("view"), (shown.state.clamped_offset().y, shown.inner_rect.height())));
|
||||
FontPick { highlighted: hi.and_then(family), picked, hovered: hovered_family }
|
||||
let highlighted = hi.and_then(|i| match h.rows.get(i) {
|
||||
Some(PickRow::Family(f)) => Some(f.clone()),
|
||||
_ => None,
|
||||
});
|
||||
FontPick { highlighted, picked, hovered: hovered_family }
|
||||
},
|
||||
)
|
||||
}
|
||||
|
||||
@@ -857,25 +857,26 @@ fn text_property_fields(ui: &mut Ui, doc: &Document, style: &Style, def: &TextSt
|
||||
|
||||
/// The font menu: Apply variable, then the installed fonts. Picking a font keeps its style when the
|
||||
/// family has one, else falls back to Regular or the first style.
|
||||
fn font_family_menu(id: &str, def: &TextStyle, family_resp: &Response, out: &mut Frame) {
|
||||
crate::right::menu_popup(family_resp).width(260.0).close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside).show(|ui| {
|
||||
if crate::right::menu_item(ui, tr("Apply variable"), "") {
|
||||
fn font_family_menu(ui: &Ui, id: &str, def: &TextStyle, family_resp: &Response, out: &mut Frame) {
|
||||
let apply_variable = |ui: &mut Ui| {
|
||||
let picked = crate::right::menu_item(ui, tr("Apply variable"), "");
|
||||
if picked {
|
||||
crate::variable_ui::open_style(ui, id, "fontFamily", VariableType::String, family_resp.rect.left_bottom());
|
||||
ui.close();
|
||||
return;
|
||||
} else {
|
||||
ui.separator();
|
||||
}
|
||||
ui.separator();
|
||||
if let Some(f) = crate::typography::font_picker(ui, Some(&def.font_family), family_resp.clicked()).picked {
|
||||
set_text(def, id, out, |d| {
|
||||
d.font_family = f.clone();
|
||||
let styles = figcraft_text::family_styles(&f);
|
||||
if !styles.is_empty() && !styles.contains(&d.font_style) {
|
||||
d.set_font_style(styles.iter().find(|s| s.eq_ignore_ascii_case("Regular")).or(styles.first()).cloned().unwrap_or_default());
|
||||
}
|
||||
});
|
||||
ui.close();
|
||||
}
|
||||
});
|
||||
picked
|
||||
};
|
||||
if let Some(f) = crate::typography::font_picker(ui, family_resp, Some(&def.font_family), 260.0, apply_variable).and_then(|p| p.picked) {
|
||||
set_text(def, id, out, |d| {
|
||||
d.font_family = f.clone();
|
||||
let styles = figcraft_text::family_styles(&f);
|
||||
if !styles.is_empty() && !styles.contains(&d.font_style) {
|
||||
d.set_font_style(styles.iter().find(|s| s.eq_ignore_ascii_case("Regular")).or(styles.first()).cloned().unwrap_or_default());
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/// The font style menu: Apply variable, then the styles of the current font, the current one checked.
|
||||
@@ -909,7 +910,7 @@ fn text_properties(ui: &mut Ui, doc: &Document, style: &Style, def: &TextStyle,
|
||||
// The dropdowns' menus open outside the dark scope (they're regular popups).
|
||||
let (family_resp, style_resp) = crate::theme::on_dark(ui, |ui| text_property_fields(ui, doc, style, def, out));
|
||||
if let Some(family_resp) = family_resp {
|
||||
font_family_menu(id, def, &family_resp, out);
|
||||
font_family_menu(ui, id, def, &family_resp, out);
|
||||
}
|
||||
if let Some(sr) = style_resp {
|
||||
font_style_menu(id, def, &sr, out);
|
||||
|
||||
Reference in New Issue
Block a user