diff --git a/Cargo.lock b/Cargo.lock index 247142a45..4a4043140 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -860,6 +860,15 @@ dependencies = [ "error-code", ] +[[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" @@ -1038,12 +1047,40 @@ dependencies = [ [[package]] name = "craft-docpath" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=512b54355cd1418f4e8c56a3fbd7483c3c274bf3#512b54355cd1418f4e8c56a3fbd7483c3c274bf3" +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-pdf" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=512b54355cd1418f4e8c56a3fbd7483c3c274bf3#512b54355cd1418f4e8c56a3fbd7483c3c274bf3" +source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0" dependencies = [ "hayro-syntax", "thiserror 2.0.21", @@ -1052,20 +1089,28 @@ dependencies = [ [[package]] name = "craft-ui" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=512b54355cd1418f4e8c56a3fbd7483c3c274bf3#512b54355cd1418f4e8c56a3fbd7483c3c274bf3" +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", + "wasm-bindgen", + "wasm-bindgen-futures", + "web-sys", ] [[package]] name = "craft-update" version = "0.1.0" -source = "git+https://github.com/storytold/craft-libs?rev=512b54355cd1418f4e8c56a3fbd7483c3c274bf3#512b54355cd1418f4e8c56a3fbd7483c3c274bf3" +source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0" dependencies = [ "chrono", "dunce", @@ -2446,6 +2491,17 @@ version = "0.1.14" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "aedcfb3409746eddb02b9e19ebda1c3394f759a152e48ee875a0844d1b955484" +[[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" @@ -2870,7 +2926,7 @@ checksum = "9f4d0e94ddd48749f06bbe4e5389fb9799a0c45bcaf00495042076ef05e3241a" [[package]] name = "hayro-jbig2" version = "0.3.0" -source = "git+https://github.com/storytold/craft-libs?rev=512b54355cd1418f4e8c56a3fbd7483c3c274bf3#512b54355cd1418f4e8c56a3fbd7483c3c274bf3" +source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0" dependencies = [ "hayro-ccitt", ] @@ -2884,7 +2940,7 @@ checksum = "c75ab947623ef4ccaa7acf0579edf7cbb5a73838e3839a7be73335e522f433a1" [[package]] name = "hayro-syntax" version = "0.7.2" -source = "git+https://github.com/storytold/craft-libs?rev=512b54355cd1418f4e8c56a3fbd7483c3c274bf3#512b54355cd1418f4e8c56a3fbd7483c3c274bf3" +source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0" dependencies = [ "hayro-jbig2", "hayro-jpeg2000", @@ -2942,6 +2998,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" @@ -2950,11 +3023,32 @@ checksum = "d56e28588da92eee5c3201a6eff33fabdd49b62269c8938d4ff050ce4d900deb" dependencies = [ "displaydoc", "litemap", + "serde", "tinystr", "writeable", "zerovec", ] +[[package]] +name = "icu_locale_fallback" +version = "2.3.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +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" @@ -2967,6 +3061,7 @@ dependencies = [ "icu_provider", "smallvec", "utf16_iter", + "utf8_iter", "write16", "zerovec", ] @@ -2977,6 +3072,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" @@ -3006,6 +3127,9 @@ checksum = "d27bbb9d3abbefac45d55f647c9de1d44aafcd1186eb91879afef17c396c3e73" dependencies = [ "displaydoc", "icu_locale_core", + "postcard", + "serde", + "stable_deref_trait", "writeable", "yoke", "zerofrom", @@ -3013,6 +3137,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" @@ -3328,6 +3476,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" @@ -4320,12 +4471,24 @@ 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" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d83eb9bc6d8e5cf568e7a1101d60ee05e81ed50ea106026f3d18deeb046d7661" dependencies = [ + "serde_core", + "writeable", "zerovec", ] @@ -5406,7 +5569,10 @@ 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]] @@ -5578,6 +5744,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "b1e27c91459209c2986af3dcf603a5a74a4368754ce37414f59acc971167f643" dependencies = [ "displaydoc", + "serde_core", "zerovec", ] @@ -6623,6 +6790,17 @@ dependencies = [ "windows-link", ] +[[package]] +name = "windows-registry" +version = "0.6.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "02752bf7fbdcce7f2a27a742f798510f3e5ad88dbe84871e5168e2120c3d5720" +dependencies = [ + "windows-link", + "windows-result 0.4.1", + "windows-strings", +] + [[package]] name = "windows-result" version = "0.1.2" @@ -6936,6 +7114,9 @@ name = "writeable" version = "0.6.4" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "3ad82d2a33cdc9674dc7465672f271e096168fcdbe0f799d9e6db8c5892679dc" +dependencies = [ + "either", +] [[package]] name = "x11-dl" @@ -7192,8 +7373,11 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "4ea269c3bd32f0a32c321907a2ae912ba6f4649bb0fc764a15627e99a7095a3f" dependencies = [ "displaydoc", + "litemap", + "serde_core", "yoke", "zerofrom", + "zerovec", ] [[package]] @@ -7202,6 +7386,7 @@ version = "0.11.8" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "bb0464e17806c1d976d5cba29399c7f08e516e279e2ba493f63123b5fca67dd8" dependencies = [ + "serde", "yoke", "zerofrom", "zerovec-derive", diff --git a/Cargo.toml b/Cargo.toml index fe6d2ddcc..79ec24655 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -11,9 +11,9 @@ rust-version = "1.95" repository = "https://github.com/storytold/effectcraft" [workspace.dependencies] -craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "512b54355cd1418f4e8c56a3fbd7483c3c274bf3", version = "=0.1.0", default-features = false, features = ["read"] } -craft-docpath = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "512b54355cd1418f4e8c56a3fbd7483c3c274bf3" } -craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "512b54355cd1418f4e8c56a3fbd7483c3c274bf3", features = ["egui"] } +craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0", version = "=0.1.0", default-features = false, features = ["read"] } +craft-docpath = { version = "0.1.0", 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"] } # internal effectcraft-time = { path = "crates/time" } effectcraft-geom = { path = "crates/geom" } @@ -108,7 +108,7 @@ pollster = "1" # ui # Shared frontend components; keep this dependency above the engine boundary. # Shared UI and the complete egui family select reviewed immutable commits below. -craft-ui = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "512b54355cd1418f4e8c56a3fbd7483c3c274bf3", features = ["serde"] } +craft-ui = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0", features = ["serde"] } # `default_fonts` and `monochrome_emoji_fonts` are explicit: the app's `FontDefinitions::default()` # and its `✔`/`▣` chrome glyphs need both, and kittest's own dev-dependencies do not propagate. egui = { version = "0.37", default-features = false, features = ["default_fonts", "monochrome_emoji_fonts"] } @@ -220,8 +220,8 @@ lto = "thin" codegen-units = 1 [patch.crates-io] -hayro-syntax = { git = "https://github.com/storytold/craft-libs", rev = "512b54355cd1418f4e8c56a3fbd7483c3c274bf3", version = "=0.7.2" } -hayro-jbig2 = { git = "https://github.com/storytold/craft-libs", rev = "512b54355cd1418f4e8c56a3fbd7483c3c274bf3", version = "=0.3.0" } +hayro-syntax = { git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0", version = "=0.7.2" } +hayro-jbig2 = { git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0", version = "=0.3.0" } 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/clippy.toml b/clippy.toml index 4b6c79f55..287e14edc 100644 --- a/clippy.toml +++ b/clippy.toml @@ -2,3 +2,6 @@ allow-unwrap-in-tests = true allow-expect-in-tests = true allow-panic-in-tests = true allow-indexing-slicing-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/pdf/README.md b/crates/pdf/README.md index 7389b00c8..20f655611 100644 --- a/crates/pdf/README.md +++ b/crates/pdf/README.md @@ -138,7 +138,7 @@ ffmpeg elsewhere) to check the code tables against an independent implementation ## Shared parser setup and bounds The workspace pins `craft-pdf` to `craft-libs` Git revision -`512b54355cd1418f4e8c56a3fbd7483c3c274bf3`, with `default-features = false` and only the explicit `read` +`92d392cff132eaf53e13afff77f38b5c11b73aa0`, with `default-features = false` and only the explicit `read` feature. This excludes the backend's bundled fonts and Adobe CMaps and preserves EffectCraft's font and asset policies. The shared parser does not change the supported drawing operators, image codecs, continuously rasterized SVG tree, editable shapes, or EPS interpreter. diff --git a/crates/ui-egui/src/credits.rs b/crates/ui-egui/src/credits.rs index e69c99ce3..6704017b9 100644 --- a/crates/ui-egui/src/credits.rs +++ b/crates/ui-egui/src/credits.rs @@ -254,7 +254,7 @@ pub fn contributors_ui(ui: &mut egui::Ui, auto: &mut Registry) { } ui.separator(); ui.label(tr("Sort")); - let combo = egui::ComboBox::from_id_salt("credits_sort").selected_text(v.key.label().0).show_ui(ui, |ui| { + let combo = craft_ui::ComboBox::from_id_salt("credits_sort").selected_text(v.key.label().0).show_ui(ui, |ui| { for k in SortKey::ALL { let r = ui.selectable_label(v.key == k, k.label().0); auto.add(&format!("about.credits.sort.{}", k.id()), r.rect, k.label().0); diff --git a/crates/ui-egui/src/panels/comp_settings.rs b/crates/ui-egui/src/panels/comp_settings.rs index 1dc6fad37..3f7cda26d 100644 --- a/crates/ui-egui/src/panels/comp_settings.rs +++ b/crates/ui-egui/src/panels/comp_settings.rs @@ -286,7 +286,7 @@ pub fn show( .or_else(|| PRESETS.iter().find(|(_, w, h, par, f)| matches(d, *w, *h, *par, *f)).map(|p| p.0)) .unwrap_or("Custom"); ui.horizontal(|ui| { - let r = egui::ComboBox::from_id_salt("comp-preset").width(300.0).selected_text(current).show_ui(ui, |ui| { + let r = craft_ui::ComboBox::from_id_salt("comp-preset").width(300.0).selected_text(current).show_ui(ui, |ui| { // Your own presets first, then the built-in ones. for q in custom { if ui.selectable_label(q.name == current, &q.name).clicked() { @@ -350,7 +350,7 @@ pub fn show( .find(|(_, v)| (v - d.pixel_aspect).abs() < 1e-3) .map(|p| p.0.to_string()) .unwrap_or_else(|| format!("{:.2}", d.pixel_aspect)); - let r = egui::ComboBox::from_id_salt("comp-par").width(220.0).selected_text(cur).show_ui(ui, |ui| { + let r = craft_ui::ComboBox::from_id_salt("comp-par").width(220.0).selected_text(cur).show_ui(ui, |ui| { for (name, v) in PIXEL_ASPECTS { if ui.selectable_label((v - d.pixel_aspect).abs() < 1e-3, *name).clicked() { d.pixel_aspect = *v; @@ -369,7 +369,7 @@ pub fn show( // Any rate can be typed; the list holds the common ones. let r = ui.add(crate::widgets::drag_value(egui::DragValue::new(&mut d.fps).range(1.0..=999.0).speed(0.01).max_decimals(3))); auto.add("dialog.comp.frameRate", r.rect, "Frame Rate"); - let r = egui::ComboBox::from_id_salt("comp-fps").width(18.0).selected_text("").show_ui(ui, |ui| { + let r = craft_ui::ComboBox::from_id_salt("comp-fps").width(18.0).selected_text("").show_ui(ui, |ui| { for f in FRAME_RATES { if ui.selectable_label((d.fps - f).abs() < 1e-3, format!("{f}")).clicked() { d.fps = *f; @@ -387,7 +387,7 @@ pub fn show( // the frame it renders beside it. ui.label(tr("Resolution:")); ui.horizontal(|ui| { - let r = egui::ComboBox::from_id_salt("comp-resolution").width(90.0).selected_text(tr(d.resolution.label())).show_ui(ui, |ui| { + let r = craft_ui::ComboBox::from_id_salt("comp-resolution").width(90.0).selected_text(tr(d.resolution.label())).show_ui(ui, |ui| { for res in Resolution::ALL { ui.selectable_value(&mut d.resolution, res, tr(res.label())); } @@ -474,7 +474,7 @@ pub fn show( Tab::Renderer => { ui.horizontal(|ui| { ui.label(tr("Renderer:")); - let r = egui::ComboBox::from_id_salt("comp-renderer").selected_text(tr(if d.advanced_3d { "Advanced 3D" } else { "Classic 3D" })).show_ui( + let r = craft_ui::ComboBox::from_id_salt("comp-renderer").selected_text(tr(if d.advanced_3d { "Advanced 3D" } else { "Classic 3D" })).show_ui( ui, |ui| { ui.selectable_value(&mut d.advanced_3d, false, tr("Classic 3D")); diff --git a/crates/ui-egui/src/panels/dialogs.rs b/crates/ui-egui/src/panels/dialogs.rs index 47de23d59..7c89ad32d 100644 --- a/crates/ui-egui/src/panels/dialogs.rs +++ b/crates/ui-egui/src/panels/dialogs.rs @@ -536,7 +536,7 @@ fn solid(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) { .find(|(_, v)| (v - par).abs() < 1e-3) .map(|p| p.0.to_string()) .unwrap_or_else(|| format!("{par:.2}")); - let r = egui::ComboBox::from_id_salt("solid-par").width(240.0).selected_text(cur).show_ui(ui, |ui| { + let r = craft_ui::ComboBox::from_id_salt("solid-par").width(240.0).selected_text(cur).show_ui(ui, |ui| { for (label, v) in super::comp_settings::PIXEL_ASPECTS { if ui.selectable_label((v - par).abs() < 1e-3, *label).clicked() { par = *v; diff --git a/crates/ui-egui/src/panels/dialogs_3d.rs b/crates/ui-egui/src/panels/dialogs_3d.rs index 01fd5b9e8..0a4636b87 100644 --- a/crates/ui-egui/src/panels/dialogs_3d.rs +++ b/crates/ui-egui/src/panels/dialogs_3d.rs @@ -172,7 +172,7 @@ pub fn camera(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) { super::dialogs::modal(ctx, "Camera Settings", vec2(520.0, 420.0), t, |ui| { egui::Grid::new("cam-grid").num_columns(2).spacing([14.0, 9.0]).show(ui, |ui| { ui.label(tr("Type")); - egui::ComboBox::from_id_salt("cam-type").selected_text(if d.two_node { "Two-Node Camera" } else { "One-Node Camera" }).show_ui(ui, |ui| { + craft_ui::ComboBox::from_id_salt("cam-type").selected_text(if d.two_node { "Two-Node Camera" } else { "One-Node Camera" }).show_ui(ui, |ui| { ui.selectable_value(&mut d.two_node, true, tr("Two-Node Camera")); ui.selectable_value(&mut d.two_node, false, tr("One-Node Camera")); }); @@ -184,7 +184,7 @@ pub fn camera(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) { ui.label(tr("Preset")); let focal = focal_for_zoom(w, d.zoom); let cur = PRESETS.iter().find(|(_, f)| (f - focal).abs() < 1e-6).map(|(n, _)| *n).unwrap_or("Custom"); - egui::ComboBox::from_id_salt("cam-preset").selected_text(cur).show_ui(ui, |ui| { + craft_ui::ComboBox::from_id_salt("cam-preset").selected_text(cur).show_ui(ui, |ui| { for (n, f) in PRESETS { if ui.selectable_label(cur == n, n).clicked() { d.zoom = zoom_for_focal(w, f); @@ -268,7 +268,7 @@ pub fn light(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) { crate::widgets::focus_on_open(ui.ctx(), &r, &d.name); ui.end_row(); ui.label(tr("Light Type")); - let combo = egui::ComboBox::from_id_salt("light-type").selected_text(tr(d.kind.label())).show_ui(ui, |ui| { + let combo = craft_ui::ComboBox::from_id_salt("light-type").selected_text(tr(d.kind.label())).show_ui(ui, |ui| { for (i, k) in LightKind::ALL.into_iter().enumerate() { let item = ui.selectable_value(&mut d.kind, k, tr(k.label())); app.auto.add(&format!("dialog.light.typeItem.{i}"), item.rect, k.label()); @@ -292,7 +292,7 @@ pub fn light(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) { } if falls_off(d.kind) { ui.label(tr("Falloff")); - egui::ComboBox::from_id_salt("light-falloff").selected_text(FALLOFFS[d.falloff.min(2)]).show_ui(ui, |ui| { + craft_ui::ComboBox::from_id_salt("light-falloff").selected_text(FALLOFFS[d.falloff.min(2)]).show_ui(ui, |ui| { for (i, f) in FALLOFFS.iter().enumerate() { ui.selectable_value(&mut d.falloff, i, *f); } diff --git a/crates/ui-egui/src/panels/essential.rs b/crates/ui-egui/src/panels/essential.rs index a61e2a8a9..a08b96b62 100644 --- a/crates/ui-egui/src/panels/essential.rs +++ b/crates/ui-egui/src/panels/essential.rs @@ -76,7 +76,7 @@ fn body(app: &mut EffectcraftApp, ui: &mut egui::Ui) -> (Actions, Actions) { // ---- Primary + Solo. ui.horizontal(|ui| { ui.label(RichText::new(tr("Primary:")).color(t.text_dim)); - let r = egui::ComboBox::from_id_salt("eg-primary").selected_text(&comp_name).width(150.0).show_ui(ui, |ui| { + let r = craft_ui::ComboBox::from_id_salt("eg-primary").selected_text(&comp_name).width(150.0).show_ui(ui, |ui| { for (id, _) in project.comps() { let name = project.item(*id).map(|i| i.name.clone()).unwrap_or_default(); if ui.selectable_label(*id == cid, name).clicked() { @@ -374,7 +374,7 @@ fn value_widget(app: &mut EffectcraftApp, ui: &mut egui::Ui, auto: &str, ty: Opt _ => vec![], }; let cur = v.as_enum() as usize; - let r = egui::ComboBox::from_id_salt(("eg-dd", auto)).selected_text(opts.get(cur).cloned().unwrap_or_default()).show_ui(ui, |ui| { + let r = craft_ui::ComboBox::from_id_salt(("eg-dd", auto)).selected_text(opts.get(cur).cloned().unwrap_or_default()).show_ui(ui, |ui| { for (i, o) in opts.iter().enumerate() { if ui.selectable_label(i == cur, o).clicked() { out = Some(json!(i)); @@ -437,7 +437,7 @@ fn value_widget(app: &mut EffectcraftApp, ui: &mut egui::Ui, auto: &str, ty: Opt let fams = effectcraft_engine::text::fonts::families(); let mut d = (**doc).clone(); let mut changed = false; - let r1 = egui::ComboBox::from_id_salt(("eg-font", auto)).selected_text(&d.font).width(110.0).show_ui(ui, |ui| { + let r1 = craft_ui::ComboBox::from_id_salt(("eg-font", auto)).selected_text(&d.font).width(110.0).show_ui(ui, |ui| { for (f, _) in &fams { if ui.selectable_label(*f == d.font, f).clicked() { d.font = f.clone(); @@ -446,7 +446,7 @@ fn value_widget(app: &mut EffectcraftApp, ui: &mut egui::Ui, auto: &str, ty: Opt } }); let styles: Vec = fams.iter().find(|(f, _)| *f == d.font).map(|(_, st)| st.clone()).unwrap_or_default(); - let r2 = egui::ComboBox::from_id_salt(("eg-style", auto)).selected_text(&d.style).width(70.0).show_ui(ui, |ui| { + let r2 = craft_ui::ComboBox::from_id_salt(("eg-style", auto)).selected_text(&d.style).width(70.0).show_ui(ui, |ui| { for st in &styles { if ui.selectable_label(*st == d.style, st).clicked() { d.style = st.clone(); @@ -532,7 +532,7 @@ fn instance(app: &mut EffectcraftApp, ui: &mut egui::Ui, actions: &mut Actions) .filter(|i| matches!(&i.kind, ItemKind::Footage(f) if f.has_video)) .map(|i| (i.id, i.name.clone())) .collect(); - let r = egui::ComboBox::from_id_salt(("eg-media", layer.id.0, c.id)).selected_text(cur_name).show_ui(ui, |ui| { + let r = craft_ui::ComboBox::from_id_salt(("eg-media", layer.id.0, c.id)).selected_text(cur_name).show_ui(ui, |ui| { for (id, name) in &items { if ui.selectable_label(*id == cur, name).clicked() { actions.push(("essential.set".into(), json!({"layer": layer.id.0, "control": c.id, "item": id.0}))); diff --git a/crates/ui-egui/src/panels/forms.rs b/crates/ui-egui/src/panels/forms.rs index 15d9100f8..67128d04d 100644 --- a/crates/ui-egui/src/panels/forms.rs +++ b/crates/ui-egui/src/panels/forms.rs @@ -830,7 +830,7 @@ fn form_grid(ui: &mut egui::Ui, f: &mut Form, regs: &mut Vec<(String, egui::Rect let cur = options.get(*sel).map(|o| o.0.clone()).unwrap_or_default(); // The column keeps its width: a long choice (a GPU adapter's name) ends in an // ellipsis instead of widening the form past the dialog (#978). - egui::ComboBox::from_id_salt(("form", fl.key.as_str())) + craft_ui::ComboBox::from_id_salt(("form", fl.key.as_str())) .selected_text(tr(&cur)) .width(240.0) .truncate() diff --git a/crates/ui-egui/src/panels/key_dialogs.rs b/crates/ui-egui/src/panels/key_dialogs.rs index 73f76aaaa..ab26d700f 100644 --- a/crates/ui-egui/src/panels/key_dialogs.rs +++ b/crates/ui-egui/src/panels/key_dialogs.rs @@ -191,7 +191,7 @@ pub fn velocity(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) { fn combo(ui: &mut egui::Ui, app: &mut EffectcraftApp, id: &str, sel: &mut usize, opts: &[&str], enabled: bool) { ui.add_enabled_ui(enabled, |ui| { // The options are the English source strings; the automation id and label stay English. - let r = egui::ComboBox::from_id_salt(id).width(200.0).selected_text(tr(opts[*sel])).show_ui(ui, |ui| { + let r = craft_ui::ComboBox::from_id_salt(id).width(200.0).selected_text(tr(opts[*sel])).show_ui(ui, |ui| { for (i, o) in opts.iter().enumerate() { ui.selectable_value(sel, i, tr(o)); } diff --git a/crates/ui-egui/src/panels/layer_styles_dialog.rs b/crates/ui-egui/src/panels/layer_styles_dialog.rs index a4af4dae2..14507b868 100644 --- a/crates/ui-egui/src/panels/layer_styles_dialog.rs +++ b/crates/ui-egui/src/panels/layer_styles_dialog.rs @@ -91,7 +91,7 @@ fn control(ui: &mut egui::Ui, p: &Property, v: &KV) -> Option { } (KV::Enum(i), ParamUi::Popup { options }) => { let mut sel = *i as usize; - egui::ComboBox::from_id_salt(("ls-popup", p.uid)).selected_text(options.get(sel).cloned().unwrap_or_default()).show_ui(ui, |ui| { + craft_ui::ComboBox::from_id_salt(("ls-popup", p.uid)).selected_text(options.get(sel).cloned().unwrap_or_default()).show_ui(ui, |ui| { for (k, o) in options.iter().enumerate() { ui.selectable_value(&mut sel, k, o); } diff --git a/crates/ui-egui/src/panels/markers_ui.rs b/crates/ui-egui/src/panels/markers_ui.rs index a696e8ca6..14e774f67 100644 --- a/crates/ui-egui/src/panels/markers_ui.rs +++ b/crates/ui-egui/src/panels/markers_ui.rs @@ -197,7 +197,7 @@ pub fn dialog(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) { ui.label(tr("Label:")); let labels = effectcraft_engine::color::Label::ALL; let cur = labels.get(d.label).copied().unwrap_or_default(); - let r = egui::ComboBox::from_id_salt("dialog.marker.label").selected_text(app.session.prefs.label_name(cur)).show_ui(ui, |ui| { + let r = craft_ui::ComboBox::from_id_salt("dialog.marker.label").selected_text(app.session.prefs.label_name(cur)).show_ui(ui, |ui| { for (i, l) in labels.into_iter().enumerate() { if crate::widgets::label_entry(ui, l, &app.session.prefs.label_name(l), d.label == i, &app.tokens).clicked() { d.label = i; diff --git a/crates/ui-egui/src/panels/primitive_dialog.rs b/crates/ui-egui/src/panels/primitive_dialog.rs index a3483a976..3711c6a80 100644 --- a/crates/ui-egui/src/panels/primitive_dialog.rs +++ b/crates/ui-egui/src/panels/primitive_dialog.rs @@ -58,7 +58,7 @@ pub fn dialog(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) { super::dialogs::modal(ctx, "New 3D Primitive", vec2(420.0, 300.0), t, |ui| { egui::Grid::new("primitive-grid").num_columns(2).spacing([14.0, 9.0]).show(ui, |ui| { ui.label(tr("Type")); - let combo = egui::ComboBox::from_id_salt("primitive-type").selected_text(tr(d.kind.label())).show_ui(ui, |ui| { + let combo = craft_ui::ComboBox::from_id_salt("primitive-type").selected_text(tr(d.kind.label())).show_ui(ui, |ui| { for (i, k) in PrimitiveKind::ALL.into_iter().enumerate() { let item = ui.selectable_value(&mut d.kind, k, tr(k.label())); app.auto.add(&format!("dialog.primitive.typeItem.{i}"), item.rect, k.label()); diff --git a/crates/ui-egui/src/panels/rq_templates.rs b/crates/ui-egui/src/panels/rq_templates.rs index b10381629..753dea50d 100644 --- a/crates/ui-egui/src/panels/rq_templates.rs +++ b/crates/ui-egui/src/panels/rq_templates.rs @@ -60,7 +60,7 @@ pub fn show(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) { ui.label(slot.label()); let cur = templates.default_name(k, slot); let mut pick = cur.clone(); - let r = egui::ComboBox::from_id_salt(("tpl-def", slot.label())).selected_text(&cur).width(300.0).show_ui(ui, |ui| { + let r = craft_ui::ComboBox::from_id_salt(("tpl-def", slot.label())).selected_text(&cur).width(300.0).show_ui(ui, |ui| { for n in &list { ui.selectable_value(&mut pick, n.clone(), n); } @@ -77,7 +77,7 @@ pub fn show(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) { ui.label(egui::RichText::new(tr("Settings")).font(Tokens::semibold(12.5)).color(t.text)); ui.horizontal(|ui| { ui.label(tr("Settings Name:")); - let r = egui::ComboBox::from_id_salt("tpl-select").selected_text(&st.selected).width(300.0).show_ui(ui, |ui| { + let r = craft_ui::ComboBox::from_id_salt("tpl-select").selected_text(&st.selected).width(300.0).show_ui(ui, |ui| { for n in &list { ui.selectable_value(&mut st.selected, n.clone(), n); } diff --git a/crates/ui-egui/src/panels/scriptui_view.rs b/crates/ui-egui/src/panels/scriptui_view.rs index 40096c8c9..242626272 100644 --- a/crates/ui-egui/src/panels/scriptui_view.rs +++ b/crates/ui-egui/src/panels/scriptui_view.rs @@ -214,7 +214,7 @@ fn draw_widget(app: &mut EffectcraftApp, ui: &mut egui::Ui, origin: egui::Pos2, let mut pick: Option = None; ui.add_enabled_ui(w.enabled, |ui| { ui.scope_builder(UiBuilder::new().max_rect(rect), |ui| { - egui::ComboBox::from_id_salt(id).selected_text(cur).width(rect.width() - 8.0).show_ui(ui, |ui| { + craft_ui::ComboBox::from_id_salt(id).selected_text(cur).width(rect.width() - 8.0).show_ui(ui, |ui| { for (i, it) in w.items.iter().enumerate() { if it == "-" { ui.separator(); diff --git a/crates/ui-egui/src/panels/settings.rs b/crates/ui-egui/src/panels/settings.rs index a60272b4b..c285011fe 100644 --- a/crates/ui-egui/src/panels/settings.rs +++ b/crates/ui-egui/src/panels/settings.rs @@ -78,7 +78,7 @@ fn page_ui(app: &mut EffectcraftApp, ui: &mut egui::Ui, page: &Page, cur: &Value ui.horizontal(|ui| { ui.add_sized(vec2(label_w, 18.0), egui::Label::new(tr("Default Output:"))); let shown = if sel.is_empty() { tr("System Default").to_string() } else { sel.clone() }; - let r = egui::ComboBox::from_id_salt(("settings", key)).selected_text(shown).width(260.0).show_ui(ui, |ui| { + let r = craft_ui::ComboBox::from_id_salt(("settings", key)).selected_text(shown).width(260.0).show_ui(ui, |ui| { if ui.selectable_label(sel.is_empty(), tr("System Default")).clicked() { acts.push(Act::Set(key.into(), json!(""))); } @@ -157,7 +157,7 @@ fn page_ui(app: &mut EffectcraftApp, ui: &mut egui::Ui, page: &Page, cur: &Value Kind::Choice(opts) => { let curv = v.as_str().map_or_else(|| v.to_string(), str::to_string); let shown = opts.iter().find(|o| o.1 == curv).map(|o| o.0).unwrap_or(curv.as_str()).to_string(); - egui::ComboBox::from_id_salt(("settings", key)) + craft_ui::ComboBox::from_id_salt(("settings", key)) .selected_text(tr(&shown)) .width(220.0) .show_ui(ui, |ui| { @@ -378,7 +378,7 @@ fn appearance_ui(app: &mut EffectcraftApp, ui: &mut egui::Ui, cur: &Value, t: &T ui.horizontal(|ui| { ui.add_sized(vec2(250.0, 18.0), egui::Label::new(format!("{}:", tr("Appearance Mode"))).truncate()); let shown = APPEARANCE_MODES.iter().find(|(_, v)| *v == mode).map_or(mode.as_str(), |(l, _)| *l); - let r = egui::ComboBox::from_id_salt(("settings", "appearance.appearanceMode")) + let r = craft_ui::ComboBox::from_id_salt(("settings", "appearance.appearanceMode")) .selected_text(tr(shown)) .width(220.0) .show_ui(ui, |ui| { diff --git a/crates/ui-egui/src/panels/shortcut_editor.rs b/crates/ui-egui/src/panels/shortcut_editor.rs index 29bae6c7e..2721c6750 100644 --- a/crates/ui-egui/src/panels/shortcut_editor.rs +++ b/crates/ui-egui/src/panels/shortcut_editor.rs @@ -264,7 +264,7 @@ pub fn show(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) { // ---- preset bar ui.horizontal(|ui| { ui.label(tr("Preset:")); - let r = egui::ComboBox::from_id_salt("sc-preset").selected_text(&active).width(220.0).show_ui(ui, |ui| { + let r = craft_ui::ComboBox::from_id_salt("sc-preset").selected_text(&active).width(220.0).show_ui(ui, |ui| { for n in &presets { if ui.selectable_label(*n == active, n).clicked() { run.push(("shortcuts.preset".into(), json!({"op": "select", "name": n}))); diff --git a/crates/ui-egui/src/panels/text_panels.rs b/crates/ui-egui/src/panels/text_panels.rs index c4e8a2b0b..2a3fb5063 100644 --- a/crates/ui-egui/src/panels/text_panels.rs +++ b/crates/ui-egui/src/panels/text_panels.rs @@ -635,130 +635,131 @@ pub(super) fn font_picker(app: &mut EffectcraftApp, ui: &mut egui::Ui, id: egui: return None; } app.auto.add(&format!("{auto}.toggle"), arrow, "Close font list"); - let mut chosen = None; - let screen = ui.ctx().content_rect(); - let below = screen.bottom() - rect.bottom() - 16.0; - let above = below < 100.0 && rect.top() - screen.top() > below; - let max_height = (if above { rect.top() - screen.top() - 16.0 } else { below }).clamp(28.0, 380.0); - let anchor = if above { rect.left_top() } else { rect.left_bottom() }; - let pivot = if above { Align2::LEFT_BOTTOM } else { Align2::LEFT_TOP }; widgets::note_open_popup_list(ui.ctx()); - let area = egui::Area::new(id.with("area")).order(egui::Order::Foreground).pivot(pivot).fixed_pos(anchor).show(ui.ctx(), |ui| { - egui::Frame::popup(ui.style()).show(ui, |ui| { - ui.set_width(rect.width().max(180.0)); - // Filter row: a checkbox that keeps only the variable fonts. - let filter_h = 22.0; - let row = ui.add_sized([ui.available_width(), filter_h], egui::Button::selectable(false, "")); - let box_rect = Rect::from_min_size(row.rect.min, vec2(18.0, filter_h)); - let label_rect = Rect::from_min_max(pos2(box_rect.max.x + 2.0, row.rect.min.y), row.rect.max); - let boxed = widgets::checkbox(ui, box_rect, only_variable, &t, id.with("variable-check")); - ui.painter().text(label_rect.left_center(), Align2::LEFT_CENTER, tr("Variable fonts only"), Tokens::ui(11.5), t.text); - app.auto.add(&format!("{auto}.variable"), row.rect, "Variable fonts only"); - let toggled = row.clicked() || boxed.clicked(); - if toggled { - only_variable = !only_variable; - ui.data_mut(|d| d.insert_temp(variable_id, only_variable)); - // Typing goes on into the search field. - ui.memory_mut(|m| m.request_focus(search_id)); + let changed = search.changed(); + let mut open = true; + // The search lives in the field; the menu's header is the filter row, its list the fonts. + let menu = craft_ui::combo::Menu::new(ui, &search) + .open(&mut open) + .height(380.0) + .close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside) + .style(egui::style::StyleModifier::default()); + let app_ref = &mut *app; + let header = |ui: &mut egui::Ui| { + ui.set_width(rect.width().max(180.0)); + // Filter row: a checkbox that keeps only the variable fonts. + let filter_h = 22.0; + let row = ui.add_sized([ui.available_width(), filter_h], egui::Button::selectable(false, "")); + let box_rect = Rect::from_min_size(row.rect.min, vec2(18.0, filter_h)); + let label_rect = Rect::from_min_max(pos2(box_rect.max.x + 2.0, row.rect.min.y), row.rect.max); + let boxed = widgets::checkbox(ui, box_rect, only_variable, &t, id.with("variable-check")); + ui.painter().text(label_rect.left_center(), Align2::LEFT_CENTER, tr("Variable fonts only"), Tokens::ui(11.5), t.text); + app_ref.auto.add(&format!("{auto}.variable"), row.rect, "Variable fonts only"); + let toggled = row.clicked() || boxed.clicked(); + if toggled { + only_variable = !only_variable; + ui.data_mut(|d| d.insert_temp(variable_id, only_variable)); + // Typing goes on into the search field. + ui.memory_mut(|m| m.request_focus(search_id)); + } + let query = state.query.trim().to_lowercase(); + let mut seen = std::collections::HashSet::new(); + let filtered: Vec<_> = rows + .iter() + .filter(|r| { + !r.family.is_empty() + && (!only_variable || r.variable) + && (query.is_empty() || ((r.family.to_lowercase().contains(&query) || r.display.to_lowercase().contains(&query)) && seen.insert(&r.family))) + }) + .collect(); + if fresh { + state.selected = filtered.iter().position(|r| r.family == current).unwrap_or(0); + } + if changed || toggled { + state.selected = 0; + } + let last = filtered.len().saturating_sub(1); + state.selected = state.selected.min(last); + if down { + state.selected = state.selected.saturating_add(1).min(last); + } + if up { + state.selected = state.selected.saturating_sub(1); + } + let entered = if enter { filtered.get(state.selected).map(|r| r.family.clone()) } else { None }; + (app_ref, filtered, query.is_empty(), state.selected, changed || toggled, entered) + }; + let list = |ui: &mut egui::Ui, + (app, filtered, all, selected, restart, entered): ( + &mut EffectcraftApp, + Vec<&effectcraft_engine::FontRow>, + bool, + usize, + bool, + Option, + )| { + if restart { + ui.scroll_to_cursor(Some(egui::Align::TOP)); + } + let mut chosen = entered; + if filtered.is_empty() { + let r = ui.label(tr("No matching fonts")); + app.auto.add(&format!("{auto}.empty"), r.rect, "No matching fonts"); + } + let mut was_recent = false; + for (index, r) in filtered.iter().enumerate() { + if all && was_recent && !r.recent { + ui.separator(); } - let query = state.query.trim().to_lowercase(); - let mut seen = std::collections::HashSet::new(); - let filtered: Vec<_> = rows - .iter() - .filter(|r| { - !r.family.is_empty() - && (!only_variable || r.variable) - && (query.is_empty() - || ((r.family.to_lowercase().contains(&query) || r.display.to_lowercase().contains(&query)) && seen.insert(&r.family))) - }) - .collect(); - if fresh { - state.selected = filtered.iter().position(|r| r.family == current).unwrap_or(0); + was_recent = r.recent; + let row_width = ui.available_width(); + let label_width = if preview { (row_width - 100.0).max(40.0) } else { row_width }; + let resp = ui.add_sized([row_width, 24.0], egui::Button::selectable(index == selected, "")); + resp.widget_info(|| egui::WidgetInfo::selected(egui::Role::Button, ui.is_enabled(), index == selected, &r.display)); + let label_rect = Rect::from_min_size(resp.rect.min + vec2(5.0, 0.0), vec2(label_width - 5.0, resp.rect.height())); + ui.painter().with_clip_rect(label_rect.intersect(ui.clip_rect())).text( + label_rect.left_center(), + Align2::LEFT_CENTER, + &r.display, + Tokens::ui(12.0), + t.text, + ); + if ui.is_rect_visible(resp.rect) { + app.auto.add(&format!("{auto}.option.{}", r.family), resp.rect, &r.display); } - if search.changed() || toggled { - state.selected = 0; + if index == selected && (fresh || up || down) { + resp.scroll_to_me(None); } - let last = filtered.len().saturating_sub(1); - state.selected = state.selected.min(last); - if down { - state.selected = state.selected.saturating_add(1).min(last); - } - if up { - state.selected = state.selected.saturating_sub(1); - } - if enter { - chosen = filtered.get(state.selected).map(|r| r.family.clone()); - } - // Give the result list its actual height even when the previous query had - // only one row. A cached tiny Area must not collapse a reopened long list. - let filter_room = filter_h + ui.spacing().item_spacing.y; - let height = (filtered.len() as f32 * (24.0 + ui.spacing().item_spacing.y) + 16.0).clamp(28.0, (max_height - filter_room).max(28.0)); - // A minimum, from below the filter row (a maximum would move the cursor back up). - ui.set_min_height(height); - let mut scroll = egui::ScrollArea::vertical().id_salt(id.with("results")).max_height(height).min_scrolled_height(height); - if search.changed() || toggled { - scroll = scroll.vertical_scroll_offset(0.0); - } - scroll.show(ui, |ui| { - if filtered.is_empty() { - let r = ui.label(tr("No matching fonts")); - app.auto.add(&format!("{auto}.empty"), r.rect, "No matching fonts"); + if preview && ui.is_rect_visible(resp.rect) { + let key = egui::Id::unique(("font-preview", &r.family)); + let lines: std::sync::Arc>> = match ui.data(|d| d.get_temp(key)) { + Some(l) => l, + None => { + let l = std::sync::Arc::new(effectcraft_engine::font_preview(&r.family, 14.0)); + ui.data_mut(|d| d.insert_temp(key, l.clone())); + l + } + }; + // Scaled and placed to fit the row's preview column, and clipped to the row (#577). + let room = Rect::from_min_max(pos2(resp.rect.max.x - 95.0, resp.rect.min.y + 1.0), pos2(resp.rect.max.x - 6.0, resp.rect.max.y - 1.0)); + if let Some((k, o)) = fit_preview(&lines, room, resp.rect.center().y + 5.0) { + let painter = ui.painter().with_clip_rect(resp.rect.intersect(ui.clip_rect())); + for poly in lines.iter() { + let pts: Vec = + poly.iter().filter(|p| p[0].is_finite() && p[1].is_finite()).map(|p| (o + vec2(p[0], p[1]) * k).to_pos2()).collect(); + painter.add(egui::Shape::line(pts, egui::Stroke::new(1.0, t.text_dim))); + } } - let mut was_recent = false; - for (index, r) in filtered.iter().enumerate() { - if query.is_empty() && was_recent && !r.recent { - ui.separator(); - } - was_recent = r.recent; - let row_width = ui.available_width(); - let label_width = if preview { (row_width - 100.0).max(40.0) } else { row_width }; - let resp = ui.add_sized([row_width, 24.0], egui::Button::selectable(index == state.selected, "")); - resp.widget_info(|| egui::WidgetInfo::selected(egui::Role::Button, ui.is_enabled(), index == state.selected, &r.display)); - let label_rect = Rect::from_min_size(resp.rect.min + vec2(5.0, 0.0), vec2(label_width - 5.0, resp.rect.height())); - ui.painter().with_clip_rect(label_rect.intersect(ui.clip_rect())).text( - label_rect.left_center(), - Align2::LEFT_CENTER, - &r.display, - Tokens::ui(12.0), - t.text, - ); - if ui.is_rect_visible(resp.rect) { - app.auto.add(&format!("{auto}.option.{}", r.family), resp.rect, &r.display); - } - if index == state.selected && (fresh || up || down) { - resp.scroll_to_me(None); - } - if preview && ui.is_rect_visible(resp.rect) { - let key = egui::Id::unique(("font-preview", &r.family)); - let lines: std::sync::Arc>> = match ui.data(|d| d.get_temp(key)) { - Some(l) => l, - None => { - let l = std::sync::Arc::new(effectcraft_engine::font_preview(&r.family, 14.0)); - ui.data_mut(|d| d.insert_temp(key, l.clone())); - l - } - }; - // Scaled and placed to fit the row's preview column, and clipped to the row (#577). - let room = Rect::from_min_max(pos2(resp.rect.max.x - 95.0, resp.rect.min.y + 1.0), pos2(resp.rect.max.x - 6.0, resp.rect.max.y - 1.0)); - if let Some((k, o)) = fit_preview(&lines, room, resp.rect.center().y + 5.0) { - let painter = ui.painter().with_clip_rect(resp.rect.intersect(ui.clip_rect())); - for poly in lines.iter() { - let pts: Vec = - poly.iter().filter(|p| p[0].is_finite() && p[1].is_finite()).map(|p| (o + vec2(p[0], p[1]) * k).to_pos2()).collect(); - painter.add(egui::Shape::line(pts, egui::Stroke::new(1.0, t.text_dim))); - } - } - } - if resp.clicked() { - chosen = Some(r.family.clone()); - } - resp.on_hover_text(&r.display); - } - }); - }); - }); - let outside = widgets::pressed_outside(ui.ctx(), &area.response) && !ui.input(|i| i.pointer.interact_pos().is_some_and(|p| rect.contains(p))); - if chosen.is_some() || outside || escape { + } + if resp.clicked() { + chosen = Some(r.family.clone()); + } + resp.on_hover_text(&r.display); + } + chosen + }; + let chosen = menu.show_ui_with_header(header, list).flatten(); + if chosen.is_some() || !open || escape { ui.data_mut(|d| { d.insert_temp(id.with("open"), false); d.remove::(state_id); diff --git a/crates/ui-egui/src/panels/tracker.rs b/crates/ui-egui/src/panels/tracker.rs index 513b3ebaa..f1c5b8b25 100644 --- a/crates/ui-egui/src/panels/tracker.rs +++ b/crates/ui-egui/src/panels/tracker.rs @@ -537,7 +537,7 @@ pub fn options_dialog(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) (LowConfidence::Adapt, "Adapt Feature"), ]; let cur = actions.iter().find(|(a, _)| *a == d.opts.action).map(|(_, l)| *l).unwrap_or("Adapt Feature"); - let r = egui::ComboBox::from_id_salt("track-action") + let r = craft_ui::ComboBox::from_id_salt("track-action") .selected_text(cur) .show_ui(ui, |ui| { for (a, l) in actions { @@ -590,7 +590,7 @@ pub fn target_dialog(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) ui.horizontal(|ui| { ui.label(crate::i18n::tr("Layer:")); let cur = d.target.and_then(|id| layers.iter().find(|(l, _)| *l == id)).map(|(_, n)| n.clone()).unwrap_or_else(|| "None".into()); - let r = egui::ComboBox::from_id_salt("track-target") + let r = craft_ui::ComboBox::from_id_salt("track-target") .selected_text(cur) .width(240.0) .show_ui(ui, |ui| { @@ -623,7 +623,7 @@ pub fn apply_dialog(app: &mut EffectcraftApp, ctx: &egui::Context, t: &Tokens) { ui.horizontal(|ui| { ui.label(crate::i18n::tr("Apply Dimensions:")); let labels = ["X and Y", "X only", "Y only"]; - let r = egui::ComboBox::from_id_salt("track-apply-dims") + let r = craft_ui::ComboBox::from_id_salt("track-apply-dims") .selected_text(labels[d.dims.min(2)]) .show_ui(ui, |ui| { for (i, l) in labels.iter().enumerate() { diff --git a/crates/ui-egui/tests/it/ui_font_search.rs b/crates/ui-egui/tests/it/ui_font_search.rs index 6be69cb7a..122003abe 100644 --- a/crates/ui-egui/tests/it/ui_font_search.rs +++ b/crates/ui-egui/tests/it/ui_font_search.rs @@ -176,3 +176,37 @@ fn font_menu_filters_variable_fonts() { key(&mut h, Key::Escape); assert_eq!(layer_doc(&h.state().session, LayerId(lid)).unwrap().font, "Inter", "filtering changes nothing"); } + +/// The font menu grows back when a query that matched nothing is cleared, while the search +/// field keeps its place and the keyboard; a click on a row then picks that font. +#[test] +fn font_menu_grows_back_after_an_empty_search() { + let mut s = Session::default(); + s.execute("comp.new", json!({"width": 320, "height": 180})).unwrap(); + let lid = s.execute("layer.newText", json!({"text": "Grow", "font": "Inter"})).unwrap()["layer"].as_u64().unwrap(); + let mut h = Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).build_eframe(move |_| EffectcraftApp::new(s)); + h.state_mut().show_panel(PanelKind::Character); + h.run_steps(3); + let rows = |h: &Harness<'_, EffectcraftApp>| h.state().auto.query("character.font.option.").len(); + let search = |h: &Harness<'_, EffectcraftApp>| h.state().auto.find("character.font.search").map(|e| e.rect); + click(&mut h, "character.font"); + h.run_steps(2); + let (full, field) = (rows(&h), search(&h)); + assert!(full > 3, "the open menu shows the fonts: {full} rows"); + let query = "no_such_font_123456789"; + type_text(&mut h, query); + assert_eq!(rows(&h), 0); + assert!(h.state().auto.find("character.font.empty").is_some()); + for pressed in query.chars().flat_map(|_| [true, false]) { + h.input_mut().events.push(Event::Key { key: Key::Backspace, physical_key: None, pressed, repeat: false, modifiers: Modifiers::NONE }); + } + h.run_steps(3); + // (Within a row: the list scrolls to the current font on opening and to the top when cleared.) + assert!(rows(&h) + 1 >= full, "cleared, the menu shows as many fonts as when it opened: {} of {full}", rows(&h)); + assert_eq!(search(&h), field, "the search field stays put"); + // The field still has the keyboard: typing filters again. + type_text(&mut h, "JetBrains"); + assert!(rows(&h) > 0 && h.state().auto.query("character.font.option.").iter().all(|r| r.label.contains("JetBrains"))); + click(&mut h, "character.font.option.JetBrains Mono"); + assert_eq!(layer_doc(&h.state().session, LayerId(lid)).unwrap().font, "JetBrains Mono", "a click on a row picks the font"); +} diff --git a/xtask/src/pdf_deps.rs b/xtask/src/pdf_deps.rs index c26876897..78694c7a6 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 = "512b54355cd1418f4e8c56a3fbd7483c3c274bf3"; +const REV: &str = "92d392cff132eaf53e13afff77f38b5c11b73aa0"; const PACKAGES: &[(&str, &str)] = &[("craft-pdf", "0.1.0"), ("hayro-syntax", "0.7.2"), ("hayro-jbig2", "0.3.0")]; pub fn run(root: &Path) -> Result<(), String> {