fix(ui): dropdown menus grow back after a search with no matches (#1917)

* fix(ui): font menus grow back after a search with no matches

Every combo box is craft_ui::ComboBox (craft-libs pinned at 62bb0e1d), whose
menu follows its contents' height while open (egui#5225); clippy.toml bans
egui::ComboBox.

The Font menu splits into a header (search field, filters) and a list
(font_menu_header, font_menu_list). Font fields show it in a
craft_ui::combo::Menu and the style dialogs' Font Family combo boxes in
ComboBox::show_ui_with_header, so only the rows scroll and the search field
stays in place. The blank and Parent's Font rows sit in the header, above
the search.

* chore(deps): craft-libs 60d60d48 (dropdown fixes)

* chore(deps): craft-libs 4d7b82f7 (dropdown fixes)

* chore(deps): craft-libs 92d392cf (#387 merged)

* chore(xtask): pdf-deps expects craft-libs 92d392cf
This commit is contained in:
aiongg
2026-10-27 10:27:32 -04:00
committed by GitHub
parent 50650f74d7
commit d77fdf2d4c
26 changed files with 450 additions and 327 deletions
Generated
+19 -19
View File
@@ -1004,7 +1004,7 @@ dependencies = [
[[package]]
name = "craft-atomic"
version = "0.1.0"
source = "git+https://github.com/storytold/craft-libs?rev=b926b5c03e256c86ca10934f58d03f628810b5b7#b926b5c03e256c86ca10934f58d03f628810b5b7"
source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0"
dependencies = [
"rustix 1.1.5",
]
@@ -1012,7 +1012,7 @@ dependencies = [
[[package]]
name = "craft-cms"
version = "0.1.0"
source = "git+https://github.com/storytold/craft-libs?rev=b926b5c03e256c86ca10934f58d03f628810b5b7#b926b5c03e256c86ca10934f58d03f628810b5b7"
source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0"
dependencies = [
"rayon",
]
@@ -1020,17 +1020,17 @@ dependencies = [
[[package]]
name = "craft-docpath"
version = "0.1.0"
source = "git+https://github.com/storytold/craft-libs?rev=b926b5c03e256c86ca10934f58d03f628810b5b7#b926b5c03e256c86ca10934f58d03f628810b5b7"
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=b926b5c03e256c86ca10934f58d03f628810b5b7#b926b5c03e256c86ca10934f58d03f628810b5b7"
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=b926b5c03e256c86ca10934f58d03f628810b5b7#b926b5c03e256c86ca10934f58d03f628810b5b7"
source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0"
dependencies = [
"fixed_decimal",
"icu_decimal",
@@ -1053,7 +1053,7 @@ dependencies = [
[[package]]
name = "craft-keymap"
version = "0.1.0"
source = "git+https://github.com/storytold/craft-libs?rev=b926b5c03e256c86ca10934f58d03f628810b5b7#b926b5c03e256c86ca10934f58d03f628810b5b7"
source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0"
dependencies = [
"craft-i18n",
"craft-ui",
@@ -1065,7 +1065,7 @@ dependencies = [
[[package]]
name = "craft-pdf"
version = "0.1.0"
source = "git+https://github.com/storytold/craft-libs?rev=b926b5c03e256c86ca10934f58d03f628810b5b7#b926b5c03e256c86ca10934f58d03f628810b5b7"
source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0"
dependencies = [
"bytemuck",
"hayro",
@@ -1078,7 +1078,7 @@ dependencies = [
[[package]]
name = "craft-prepress"
version = "0.1.0"
source = "git+https://github.com/storytold/craft-libs?rev=b926b5c03e256c86ca10934f58d03f628810b5b7#b926b5c03e256c86ca10934f58d03f628810b5b7"
source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0"
dependencies = [
"craft-cms",
"craft-pdf",
@@ -1091,7 +1091,7 @@ dependencies = [
[[package]]
name = "craft-print"
version = "0.1.0"
source = "git+https://github.com/storytold/craft-libs?rev=b926b5c03e256c86ca10934f58d03f628810b5b7#b926b5c03e256c86ca10934f58d03f628810b5b7"
source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0"
dependencies = [
"craft-winprint-gdi",
"miniz_oxide 0.9.1",
@@ -1104,7 +1104,7 @@ dependencies = [
[[package]]
name = "craft-ui"
version = "0.1.0"
source = "git+https://github.com/storytold/craft-libs?rev=b926b5c03e256c86ca10934f58d03f628810b5b7#b926b5c03e256c86ca10934f58d03f628810b5b7"
source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0"
dependencies = [
"craft-fonts-build",
"craft-i18n",
@@ -1125,7 +1125,7 @@ dependencies = [
[[package]]
name = "craft-update"
version = "0.1.0"
source = "git+https://github.com/storytold/craft-libs?rev=b926b5c03e256c86ca10934f58d03f628810b5b7#b926b5c03e256c86ca10934f58d03f628810b5b7"
source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0"
dependencies = [
"chrono",
"dunce",
@@ -1150,7 +1150,7 @@ dependencies = [
[[package]]
name = "craft-window"
version = "0.1.0"
source = "git+https://github.com/storytold/craft-libs?rev=b926b5c03e256c86ca10934f58d03f628810b5b7#b926b5c03e256c86ca10934f58d03f628810b5b7"
source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0"
dependencies = [
"egui",
"objc2 0.6.5",
@@ -2635,7 +2635,7 @@ dependencies = [
[[package]]
name = "hayro"
version = "0.7.1"
source = "git+https://github.com/storytold/craft-libs?rev=b926b5c03e256c86ca10934f58d03f628810b5b7#b926b5c03e256c86ca10934f58d03f628810b5b7"
source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0"
dependencies = [
"bytemuck",
"hayro-interpret",
@@ -2655,7 +2655,7 @@ checksum = "9f4d0e94ddd48749f06bbe4e5389fb9799a0c45bcaf00495042076ef05e3241a"
[[package]]
name = "hayro-cmap"
version = "0.1.0"
source = "git+https://github.com/storytold/craft-libs?rev=b926b5c03e256c86ca10934f58d03f628810b5b7#b926b5c03e256c86ca10934f58d03f628810b5b7"
source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0"
dependencies = [
"brotli",
"hayro-postscript",
@@ -2664,7 +2664,7 @@ dependencies = [
[[package]]
name = "hayro-interpret"
version = "0.7.0"
source = "git+https://github.com/storytold/craft-libs?rev=b926b5c03e256c86ca10934f58d03f628810b5b7#b926b5c03e256c86ca10934f58d03f628810b5b7"
source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0"
dependencies = [
"bitflags 2.13.2",
"hayro-cmap",
@@ -2682,7 +2682,7 @@ dependencies = [
[[package]]
name = "hayro-jbig2"
version = "0.3.0"
source = "git+https://github.com/storytold/craft-libs?rev=b926b5c03e256c86ca10934f58d03f628810b5b7#b926b5c03e256c86ca10934f58d03f628810b5b7"
source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0"
dependencies = [
"fearless_simd",
"hayro-ccitt",
@@ -2700,12 +2700,12 @@ dependencies = [
[[package]]
name = "hayro-postscript"
version = "0.1.0"
source = "git+https://github.com/storytold/craft-libs?rev=b926b5c03e256c86ca10934f58d03f628810b5b7#b926b5c03e256c86ca10934f58d03f628810b5b7"
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=b926b5c03e256c86ca10934f58d03f628810b5b7#b926b5c03e256c86ca10934f58d03f628810b5b7"
source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0"
dependencies = [
"flate2",
"hayro-ccitt",
@@ -3300,7 +3300,7 @@ dependencies = [
[[package]]
name = "krilla"
version = "0.8.2"
source = "git+https://github.com/storytold/craft-libs?rev=b926b5c03e256c86ca10934f58d03f628810b5b7#b926b5c03e256c86ca10934f58d03f628810b5b7"
source = "git+https://github.com/storytold/craft-libs?rev=92d392cff132eaf53e13afff77f38b5c11b73aa0#92d392cff132eaf53e13afff77f38b5c11b73aa0"
dependencies = [
"base64 0.22.1",
"bumpalo",
+17 -17
View File
@@ -12,12 +12,12 @@ repository = "https://github.com/storytold/designcraft"
[workspace.dependencies]
# Shared reader and writer; publish the immutable revision before this consumer.
craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "b926b5c03e256c86ca10934f58d03f628810b5b7", version = "=0.1.0", default-features = false }
craft-pdf = { git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0", version = "=0.1.0", default-features = false }
# Shared keyboard bindings: chords, overrides, conflicts, egui dispatch, the Keyboard Shortcuts dialog.
craft-keymap = { git = "https://github.com/storytold/craft-libs", rev = "b926b5c03e256c86ca10934f58d03f628810b5b7", version = "=0.1.0", features = ["egui", "ui"] }
craft-keymap = { git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0", version = "=0.1.0", features = ["egui", "ui"] }
# Separations, ink coverage and the ink manager; same revision as craft-pdf and the krilla patch.
craft-prepress = { git = "https://github.com/storytold/craft-libs", rev = "b926b5c03e256c86ca10934f58d03f628810b5b7", version = "=0.1.0", features = ["flatten"] }
craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "b926b5c03e256c86ca10934f58d03f628810b5b7", features = ["egui"] }
craft-prepress = { git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0", version = "=0.1.0", features = ["flatten"] }
craft-update = { git = "https://github.com/storytold/craft-libs", version = "=0.1.0", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0", features = ["egui"] }
# internal
designcraft-geom = { path = "crates/geom" }
designcraft-color = { path = "crates/color" }
@@ -80,20 +80,20 @@ zip = { version = "3", default-features = false, features = ["deflate"] }
indd = { git = "https://github.com/storytold/indd-utils", branch = "main" }
# Which paths stored in documents may be followed (craft-libs).
craft-docpath = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "b926b5c03e256c86ca10934f58d03f628810b5b7" }
craft-docpath = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0" }
# Atomic file replacement for saves and exports (craft-libs).
craft-atomic = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "b926b5c03e256c86ca10934f58d03f628810b5b7" }
craft-atomic = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0" }
# File › Print through the system print spooler (craft-libs).
craft-print = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "b926b5c03e256c86ca10934f58d03f628810b5b7" }
craft-print = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0" }
# ICC colour management: working spaces, transforms, black point compensation (craft-libs).
craft-cms = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "b926b5c03e256c86ca10934f58d03f628810b5b7" }
craft-cms = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0" }
# ui
# Shared frontend components; keep this dependency above the engine boundary.
craft-ui = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "b926b5c03e256c86ca10934f58d03f628810b5b7", features = ["serde"] }
craft-ui = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0", features = ["serde"] }
# The application bar as the window's title bar (craft-libs).
craft-window = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "b926b5c03e256c86ca10934f58d03f628810b5b7", features = ["egui"] }
craft-window = { version = "0.1.0", git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0", features = ["egui"] }
egui = "0.37"
egui_extras = { version = "0.37", default-features = false, features = ["svg"] }
eframe = { version = "0.37", default-features = false }
@@ -131,16 +131,16 @@ debug = 0
# Opt-in cancelable macOS termination on the pinned winit 0.30 line.
[patch.crates-io]
krilla = { git = "https://github.com/storytold/craft-libs", rev = "b926b5c03e256c86ca10934f58d03f628810b5b7", version = "=0.8.2" }
krilla = { git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0", version = "=0.8.2" }
winit = { git = "https://github.com/storytold/winit.git", rev = "a517b3846ee8457d84ee4975110bfda6797c44dc" }
# The hayro crates from craft-libs' vendor/ directory (pdfcraft's, plus the plate mode and decode
# limits craft-pdf and craft-prepress need); same revision as craft-pdf.
hayro = { git = "https://github.com/storytold/craft-libs", rev = "b926b5c03e256c86ca10934f58d03f628810b5b7", version = "=0.7.1" }
hayro-interpret = { git = "https://github.com/storytold/craft-libs", rev = "b926b5c03e256c86ca10934f58d03f628810b5b7", version = "=0.7.0" }
hayro-syntax = { git = "https://github.com/storytold/craft-libs", rev = "b926b5c03e256c86ca10934f58d03f628810b5b7", version = "=0.7.2" }
hayro-jbig2 = { git = "https://github.com/storytold/craft-libs", rev = "b926b5c03e256c86ca10934f58d03f628810b5b7", version = "=0.3.0" }
hayro-cmap = { git = "https://github.com/storytold/craft-libs", rev = "b926b5c03e256c86ca10934f58d03f628810b5b7", version = "=0.1.0" }
hayro-postscript = { git = "https://github.com/storytold/craft-libs", rev = "b926b5c03e256c86ca10934f58d03f628810b5b7", version = "=0.1.0" }
hayro = { git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0", version = "=0.7.1" }
hayro-interpret = { git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0", version = "=0.7.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" }
hayro-cmap = { git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0", version = "=0.1.0" }
hayro-postscript = { git = "https://github.com/storytold/craft-libs", rev = "92d392cff132eaf53e13afff77f38b5c11b73aa0", version = "=0.1.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" }
+3
View File
@@ -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)" },
]
+4 -4
View File
@@ -38,7 +38,7 @@ fn page_preset_and_orientation(app: &mut DesignApp, ui: &mut egui::Ui, lang: &st
let landscape = size.0 > size.1;
ui.vertical(|ui| {
let cur = panels::properties::preset_name(size.0, size.1);
egui::ComboBox::from_id_salt("ctl_page_preset").selected_text(crate::rtl::widget(ui, crate::i18n::tr(lang, cur))).width(130.0).show_ui(
craft_ui::ComboBox::from_id_salt("ctl_page_preset").selected_text(crate::rtl::widget(ui, crate::i18n::tr(lang, cur))).width(130.0).show_ui(
ui,
|ui| {
for (name, w, h) in panels::properties::PAGE_PRESETS {
@@ -105,7 +105,7 @@ fn control_page(app: &mut DesignApp, ui: &mut egui::Ui) {
ui.vertical(|ui| {
caption(ui, crate::i18n::tr(&lang, "Liquid Page Rule:"));
let label = panels::interactive::liquid_rule_label(pg.liquid);
egui::ComboBox::from_id_salt("ctl_page_rule").selected_text(crate::rtl::widget(ui, crate::i18n::tr(&lang, label))).width(150.0).show_ui(
craft_ui::ComboBox::from_id_salt("ctl_page_rule").selected_text(crate::rtl::widget(ui, crate::i18n::tr(&lang, label))).width(150.0).show_ui(
ui,
|ui| {
for (r, l) in panels::interactive::LIQUID_RULES {
@@ -368,7 +368,7 @@ fn control_character(
let cs = attrs.as_ref().and_then(|a| a["characterStyle"].as_str()).unwrap_or(designcraft_doc::NO_CHAR_STYLE).to_string();
let names: Vec<String> = app.session.active().map(|d| d.doc.styles.character.iter().map(|c| c.name.clone()).collect()).unwrap_or_default();
let mut pick = None;
egui::ComboBox::from_id_salt("ctl_char_style")
craft_ui::ComboBox::from_id_salt("ctl_char_style")
.selected_text(crate::rtl::widget(ui, crate::i18n::style_name(&app.ui.language, &cs)))
.width(150.0)
.show_ui(ui, |ui| {
@@ -382,7 +382,7 @@ fn control_character(
let _ = app.run("style.character.apply", json!({"name": n}));
}
let lang = chars["language"].as_str().unwrap_or("English: USA").to_string();
egui::ComboBox::from_id_salt("ctl_language").selected_text(crate::rtl::widget(ui, tr(&lang))).width(150.0).show_ui(ui, |ui| {
craft_ui::ComboBox::from_id_salt("ctl_language").selected_text(crate::rtl::widget(ui, tr(&lang))).width(150.0).show_ui(ui, |ui| {
egui::ScrollArea::vertical().max_height(320.0).show(ui, |ui| crate::panels::properties::language_menu(app, ui, &lang));
});
});
+1 -1
View File
@@ -223,7 +223,7 @@ fn controls(ui: &mut egui::Ui, v: &mut View, lang: &str) {
}
ui.separator();
ui.label(tr(lang, "Sort"));
egui::ComboBox::from_id_salt("credits_sort").selected_text(tr(lang, v.key.label().0)).show_ui(ui, |ui| {
craft_ui::ComboBox::from_id_salt("credits_sort").selected_text(tr(lang, v.key.label().0)).show_ui(ui, |ui| {
for k in SortKey::ALL {
if ui.selectable_label(v.key == k, tr(lang, k.label().0)).clicked() {
v.key = k;
+106 -94
View File
@@ -314,7 +314,7 @@ fn epub_export(app: &crate::DesignApp, ui: &mut egui::Ui, d: &mut Dialog) {
d.fields.insert("mode".into(), json!("style"));
}
let cur = d.s("style");
egui::ComboBox::from_id_salt("epub_split_style").selected_text(crate::rtl::widget(ui, &cur)).width(180.0).show_ui(ui, |ui| {
craft_ui::ComboBox::from_id_salt("epub_split_style").selected_text(crate::rtl::widget(ui, &cur)).width(180.0).show_ui(ui, |ui| {
for name in &styles {
if ui.selectable_label(cur == *name, crate::rtl::widget(ui, name)).clicked() {
d.fields.insert("style".into(), json!(name));
@@ -343,7 +343,7 @@ fn print_dialog(app: &crate::DesignApp, ui: &mut egui::Ui, d: &mut Dialog) {
if printers.is_empty() {
crate::rtl::label(ui, egui::RichText::new(crate::i18n::tr(&app.ui.language, "System default")).italics());
} else {
egui::ComboBox::from_id_salt("printer").selected_text(&cur).width(220.0).show_ui(ui, |ui| {
craft_ui::ComboBox::from_id_salt("printer").selected_text(&cur).width(220.0).show_ui(ui, |ui| {
for p in &printers {
if ui.selectable_label(*p == cur, p).clicked() {
d.fields.insert("printer".into(), json!(p));
@@ -423,7 +423,7 @@ fn pdf_export(app: &crate::DesignApp, ui: &mut egui::Ui, d: &mut Dialog) {
ui.horizontal(|ui| {
crate::rtl::label(ui, crate::i18n::tr(lang, "Preset:"));
let cur = d.s("preset");
egui::ComboBox::from_id_salt("pdf_preset").selected_text(crate::rtl::widget(ui, crate::i18n::tr(lang, &cur))).width(200.0).show_ui(
craft_ui::ComboBox::from_id_salt("pdf_preset").selected_text(crate::rtl::widget(ui, crate::i18n::tr(lang, &cur))).width(200.0).show_ui(
ui,
|ui| {
for p in PDF_EXPORT_PRESETS {
@@ -1508,7 +1508,7 @@ fn document_setup(app: &crate::DesignApp, ui: &mut egui::Ui, d: &mut Dialog) ->
setup_row(ui, rtl, |ui| {
setup_caption(ui, tr("Intent:"), label_w);
let cur = d.s("intent");
egui::ComboBox::from_id_salt("ds_intent")
craft_ui::ComboBox::from_id_salt("ds_intent")
.selected_text(crate::rtl::widget(
ui,
tr(match cur.as_str() {
@@ -1542,7 +1542,7 @@ fn document_setup(app: &crate::DesignApp, ui: &mut egui::Ui, d: &mut Dialog) ->
setup_row(ui, rtl, |ui| {
setup_caption(ui, tr("Binding:"), label_w);
let right_to_left = d.s("binding") == "rightToLeft";
egui::ComboBox::from_id_salt("ds_binding")
craft_ui::ComboBox::from_id_salt("ds_binding")
.selected_text(crate::rtl::widget(ui, tr(if right_to_left { "Right to Left" } else { "Left to Right" })))
.width(150.0)
.show_ui(ui, |ui| {
@@ -1563,7 +1563,7 @@ fn document_setup(app: &crate::DesignApp, ui: &mut egui::Ui, d: &mut Dialog) ->
setup_group(ui, |ui| {
setup_row(ui, rtl, |ui| {
crate::rtl::label(ui, egui::RichText::new(tr("Page Size:")).color(t.text_strong));
egui::ComboBox::from_id_salt("ds_preset").selected_text(crate::rtl::widget(ui, &preset)).width(260.0).show_ui(ui, |ui| {
craft_ui::ComboBox::from_id_salt("ds_preset").selected_text(crate::rtl::widget(ui, &preset)).width(260.0).show_ui(ui, |ui| {
for p in designcraft_doc::build::PRESETS {
if ui.selectable_label(tr(p.name) == preset, crate::rtl::widget(ui, tr(p.name))).clicked() {
// Keep the orientation; the preset's size in the document's units.
@@ -1915,7 +1915,7 @@ fn preferences(app: &crate::DesignApp, ui: &mut egui::Ui, d: &mut Dialog, max_he
ui.horizontal(|ui| {
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "On Screen:"));
let rich = d.b("richBlack");
egui::ComboBox::from_id_salt("pref_black")
craft_ui::ComboBox::from_id_salt("pref_black")
.selected_text(crate::rtl::widget(
ui,
crate::i18n::tr(
@@ -1935,7 +1935,7 @@ fn preferences(app: &crate::DesignApp, ui: &mut egui::Ui, d: &mut Dialog, max_he
ui.horizontal(|ui| {
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Printing / Exporting:"));
let rich = d.b("richBlackOutput");
egui::ComboBox::from_id_salt("pref_black_out")
craft_ui::ComboBox::from_id_salt("pref_black_out")
.selected_text(crate::rtl::widget(
ui,
crate::i18n::tr(
@@ -1985,7 +1985,7 @@ fn preferences(app: &crate::DesignApp, ui: &mut egui::Ui, d: &mut Dialog, max_he
ui.end_row();
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Relative To:"));
let cur = d.s("bg.relativeTo");
egui::ComboBox::from_id_salt("bg_rel")
craft_ui::ComboBox::from_id_salt("bg_rel")
.selected_text(crate::rtl::widget(
ui,
crate::i18n::tr(&app.ui.language, if cur == "topMargin" { "Top Margin" } else { "Top of Page" }),
@@ -2066,7 +2066,7 @@ fn preferences(app: &crate::DesignApp, ui: &mut egui::Ui, d: &mut Dialog, max_he
let cur = d.s("displayQuality");
ui.horizontal(|ui| {
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Default View:"));
egui::ComboBox::from_id_salt("pref_dq")
craft_ui::ComboBox::from_id_salt("pref_dq")
.selected_text(match cur.as_str() {
"fast" => crate::i18n::tr(&app.ui.language, "Fast"),
"typical" => crate::i18n::tr(&app.ui.language, "Typical"),
@@ -2088,7 +2088,7 @@ fn preferences(app: &crate::DesignApp, ui: &mut egui::Ui, d: &mut Dialog, max_he
let cur = d.fields.get("rulerOrigin").and_then(Value::as_str).unwrap_or("spread").to_string();
const ORIGINS: [(&str, &str); 3] = [("spread", "Spread"), ("page", "Page"), ("spine", "Spine")];
let shown = ORIGINS.iter().find(|o| o.0 == cur).map_or("Spread", |o| o.1);
egui::ComboBox::from_id_salt("rulerOrigin")
craft_ui::ComboBox::from_id_salt("rulerOrigin")
.selected_text(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, shown)))
.width(140.0)
.show_ui(ui, |ui| {
@@ -2102,7 +2102,7 @@ fn preferences(app: &crate::DesignApp, ui: &mut egui::Ui, d: &mut Dialog, max_he
for (key, label) in [("horizontalUnits", "Horizontal:"), ("verticalUnits", "Vertical:")] {
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, label));
let cur: Unit = d.fields.get(key).and_then(|v| serde_json::from_value(v.clone()).ok()).unwrap_or_default();
egui::ComboBox::from_id_salt(key)
craft_ui::ComboBox::from_id_salt(key)
.selected_text(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, cur.label())))
.width(140.0)
.show_ui(ui, |ui| {
@@ -2137,17 +2137,18 @@ fn preferences(app: &crate::DesignApp, ui: &mut egui::Ui, d: &mut Dialog, max_he
crate::rtl::label(ui, egui::RichText::new(crate::i18n::tr(&app.ui.language, "Interface Language")).font(semibold(12.0)));
// Each language by its own name, so the list reads the same in every interface language.
let current = crate::i18n::LANGUAGES.iter().find(|(code, _)| *code == app.ui.language).map_or("English", |(_, name)| *name);
egui::ComboBox::from_id_salt("pref_language").width(180.0).height(400.0).selected_text(crate::rtl::widget(ui, current)).show_ui(
ui,
|ui| {
craft_ui::ComboBox::from_id_salt("pref_language")
.width(180.0)
.height(400.0)
.selected_text(crate::rtl::widget(ui, current))
.show_ui(ui, |ui| {
for (code, name) in crate::i18n::picker_languages() {
if ui.selectable_label(code == app.ui.language, crate::rtl::widget(ui, name)).clicked() {
// Applied by `show` right after this frame, which has `&mut DesignApp`.
d.fields.insert("applyLanguage".into(), json!(code));
}
}
},
);
});
ui.add_space(6.0);
crate::rtl::label(ui, egui::RichText::new(crate::i18n::tr(&app.ui.language, "When Scaling")).font(semibold(12.0)));
check(ui, d, "scaleStrokes", crate::i18n::tr(&app.ui.language, "Include Stroke Weight"));
@@ -2392,7 +2393,7 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
for (key, label, any) in [("objFill", "Find fill:", "(any)"), ("objChangeFill", "Change fill to:", "(unchanged)")] {
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, label));
let cur = d.s(key);
egui::ComboBox::from_id_salt(key)
craft_ui::ComboBox::from_id_salt(key)
.selected_text(if cur.is_empty() { crate::i18n::tr(&app.ui.language, any) } else { cur.as_str() })
.width(180.0)
.show_ui(ui, |ui| {
@@ -2412,7 +2413,7 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
}
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Kind:"));
let cur = d.s("objKind");
egui::ComboBox::from_id_salt("objKind")
craft_ui::ComboBox::from_id_salt("objKind")
.selected_text(if cur.is_empty() { crate::i18n::tr(&app.ui.language, "(any)") } else { cur.as_str() })
.show_ui(ui, |ui| {
for v in ["", "text", "graphic", "shape", "line", "group"] {
@@ -2461,7 +2462,7 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
ui.end_row();
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Search:"));
let cur = d.s("scope");
egui::ComboBox::from_id_salt("fcscope")
craft_ui::ComboBox::from_id_salt("fcscope")
.selected_text(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, &cur)))
.show_ui(ui, |ui| {
for v in ["document", "story", "selection"] {
@@ -2571,7 +2572,7 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
"media" => crate::i18n::tr(&app.ui.language, "Media"),
_ => crate::i18n::tr(&app.ui.language, "Crop"),
};
egui::ComboBox::from_id_salt("pdf_crop").selected_text(crate::rtl::widget(ui, label(&cur))).show_ui(ui, |ui| {
craft_ui::ComboBox::from_id_salt("pdf_crop").selected_text(crate::rtl::widget(ui, label(&cur))).show_ui(ui, |ui| {
for v in designcraft_doc::PdfCrop::ALL.map(designcraft_doc::PdfCrop::name) {
if ui.selectable_label(label(&cur) == label(v), label(v)).clicked() {
d.fields.insert("crop".into(), json!(v));
@@ -2608,7 +2609,7 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
for (key, label, kind) in [("rgb", "RGB:", "rgb"), ("cmyk", "CMYK:", "cmyk")] {
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, label));
let cur = d.s(key);
egui::ComboBox::from_id_salt(("ws", key)).selected_text(&cur).width(260.0).show_ui(ui, |ui| {
craft_ui::ComboBox::from_id_salt(("ws", key)).selected_text(&cur).width(260.0).show_ui(ui, |ui| {
for pr in profiles.iter().filter(|p| p["kind"] == kind) {
let name = pr["name"].as_str().unwrap_or("");
if ui.selectable_label(name == cur, name).clicked() {
@@ -2620,7 +2621,7 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
}
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Intent:"));
let cur = d.s("intent");
egui::ComboBox::from_id_salt("ws_intent")
craft_ui::ComboBox::from_id_salt("ws_intent")
.selected_text(crate::rtl::widget(
ui,
crate::i18n::tr(&app.ui.language, designcraft_color::cms::Intent::parse(&cur).map_or("", |i| i.label())),
@@ -2731,7 +2732,7 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
} else {
mapped.clone()
};
egui::ComboBox::from_id_salt(("imp_map", w))
craft_ui::ComboBox::from_id_salt(("imp_map", w))
.selected_text(crate::rtl::widget(ui, &shown))
.width(170.0)
.show_ui(ui, |ui| {
@@ -2852,7 +2853,7 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
};
ui.horizontal(|ui| {
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Fitting:"));
egui::ComboBox::from_id_salt("ff_fit").selected_text(crate::rtl::widget(ui, label(&cur))).width(200.0).show_ui(
craft_ui::ComboBox::from_id_salt("ff_fit").selected_text(crate::rtl::widget(ui, label(&cur))).width(200.0).show_ui(
ui,
|ui| {
for v in ["none", "fillProportionally", "fitProportionally", "fitContentToFrame", "centerContent"] {
@@ -2910,7 +2911,7 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) {
"vcard" => crate::i18n::tr(&app.ui.language, "Business Card"),
_ => crate::i18n::tr(&app.ui.language, "Web Hyperlink"),
};
egui::ComboBox::from_id_salt("qr_type").selected_text(crate::rtl::widget(ui, label(&cur))).width(160.0).show_ui(
craft_ui::ComboBox::from_id_salt("qr_type").selected_text(crate::rtl::widget(ui, label(&cur))).width(160.0).show_ui(
ui,
|ui| {
for v in ["url", "text", "sms", "email", "vcard"] {
@@ -4143,7 +4144,7 @@ fn paragraph_style_options(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialo
ui.horizontal(|ui| {
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Tag:"));
let cur = d.s("x.tag");
egui::ComboBox::from_id_salt("export_tag")
craft_ui::ComboBox::from_id_salt("export_tag")
.selected_text(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, &cur)))
.show_ui(ui, |ui| {
for t in ["[Automatic]", "p", "h1", "h2", "h3", "h4", "h5", "h6", "blockquote", "pre", "li", "figcaption", "aside", "div"]
@@ -4167,7 +4168,7 @@ fn paragraph_style_options(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialo
egui::Grid::new("psi").num_columns(2).spacing([8.0, 6.0]).show(ui, |ui| {
let alignment_label = crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Alignment:"));
let a: designcraft_doc::Align = serde_json::from_value(cur(d, "p.align", &pv["align"])).unwrap_or_default();
egui::ComboBox::from_id_salt("psalign")
craft_ui::ComboBox::from_id_salt("psalign")
.selected_text(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, a.label())))
.width(240.0)
.show_ui(ui, |ui| {
@@ -4201,7 +4202,7 @@ fn paragraph_style_options(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialo
let grid_label = crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Align to Grid:"));
let g: GridAlign = serde_json::from_value(cur(d, "p.gridAlign", &pv["gridAlign"])).unwrap_or_default();
let shown = GRID_ALIGN.iter().find(|o| o.0 == g).map_or("", |o| o.1);
egui::ComboBox::from_id_salt("psgridalign")
craft_ui::ComboBox::from_id_salt("psgridalign")
.selected_text(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, shown)))
.width(240.0)
.show_ui(ui, |ui| {
@@ -4265,7 +4266,7 @@ fn paragraph_style_options(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialo
.map(str::to_string)
.or(style.based_on.clone())
.unwrap_or_else(|| "[No Paragraph Style]".into());
let combo = egui::ComboBox::from_id_salt("psbased")
let combo = craft_ui::ComboBox::from_id_salt("psbased")
.selected_text(crate::rtl::widget(ui, crate::i18n::style_name(&app.ui.language, &based)))
.width(220.0)
.show_ui(ui, |ui| {
@@ -4372,7 +4373,7 @@ fn delete_paragraph_style(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog
d.fields.insert("replaceWith".into(), json!(designcraft_doc::BASIC_PARAGRAPH));
}
let replace = d.s("replaceWith");
egui::ComboBox::from_id_salt("del_para_replace")
craft_ui::ComboBox::from_id_salt("del_para_replace")
.selected_text(crate::rtl::widget(ui, crate::i18n::style_name(&app.ui.language, &replace)))
.width(260.0)
.show_ui(ui, |ui| {
@@ -4506,7 +4507,7 @@ fn para_measure(ui: &mut egui::Ui, d: &mut Dialog, pv: &Value, key: &str, unit:
fn para_choice(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Value, key: &str, opts: &[(&str, &str)]) {
let cur = para_value(d, pv, key);
let shown = opts.iter().find(|o| cur == o.0).map_or("", |o| crate::i18n::tr(lang, o.1));
egui::ComboBox::from_id_salt(("pschoice", key)).selected_text(crate::rtl::widget(ui, shown)).width(200.0).show_ui(ui, |ui| {
craft_ui::ComboBox::from_id_salt(("pschoice", key)).selected_text(crate::rtl::widget(ui, shown)).width(200.0).show_ui(ui, |ui| {
for (v, label) in opts {
if ui.selectable_label(cur == *v, crate::rtl::widget(ui, crate::i18n::tr(lang, label))).clicked() {
para_set(d, key, json!(v));
@@ -4543,7 +4544,7 @@ fn para_sides(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Value, key: &s
/// picked.
fn swatch_menu(ui: &mut egui::Ui, doc: &designcraft_doc::Document, id: impl egui::AsIdSalt, current: &str, width: f32) -> Option<String> {
let mut picked = None;
egui::ComboBox::from_id_salt(id).selected_text(current).width(width).show_ui(ui, |ui| {
craft_ui::ComboBox::from_id_salt(id).selected_text(current).width(width).show_ui(ui, |ui| {
for w in doc.swatches.iter().filter(|w| !w.hidden) {
let (c, g) = crate::widgets::swatch_colors(doc, &w.name, 1.0);
ui.horizontal(|ui| {
@@ -4570,7 +4571,7 @@ fn border_type_menu(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Value, d
StrokeType::Style { name } => name.clone(),
k => crate::i18n::tr(lang, k.label()).to_string(),
};
egui::ComboBox::from_id_salt("psbordertype").selected_text(crate::rtl::widget(ui, shown)).width(160.0).show_ui(ui, |ui| {
craft_ui::ComboBox::from_id_salt("psbordertype").selected_text(crate::rtl::widget(ui, shown)).width(160.0).show_ui(ui, |ui| {
for k in BORDER_TYPES {
let ty = if k == "dashed" { json!({"kind": k, "pattern": []}) } else { json!({"kind": k}) };
let Ok(t) = serde_json::from_value::<StrokeType>(ty.clone()) else { continue };
@@ -4602,7 +4603,7 @@ fn para_corners(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Value, key:
c.size = if v.is_finite() { v.clamp(0.0, 10_000.0) } else { 0.0 };
changed = true;
}
egui::ComboBox::from_id_salt(("pscornershape", key, i))
craft_ui::ComboBox::from_id_salt(("pscornershape", key, i))
.selected_text(crate::rtl::widget(ui, crate::i18n::tr(lang, c.shape.label())))
.width(130.0)
.show_ui(ui, |ui| {
@@ -4761,7 +4762,7 @@ fn drop_cap_fields(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Value, cn
crate::rtl::label(ui, crate::i18n::tr(lang, "Character Style:"));
let style = field_or(d, "p.dropCapStyle", &pv["dropCapStyle"]).as_str().unwrap_or("").to_string();
let style = if style.is_empty() { designcraft_doc::NO_CHAR_STYLE.to_string() } else { style };
egui::ComboBox::from_id_salt("ps_dropcap_style")
craft_ui::ComboBox::from_id_salt("ps_dropcap_style")
.selected_text(crate::rtl::widget(ui, crate::i18n::style_name(lang, &style)))
.width(160.0)
.show_ui(ui, |ui| {
@@ -4812,19 +4813,19 @@ fn nested_style_rows(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Value,
fn nested_style_row(lang: &str, ui: &mut egui::Ui, ns: &mut Value, i: usize, cnames: &[String]) -> (bool, bool) {
let mut changed = false;
let style = ns["style"].as_str().unwrap_or("[None]").to_string();
egui::ComboBox::from_id_salt(("ns_style", i)).selected_text(crate::rtl::widget(ui, crate::i18n::style_name(lang, &style))).width(110.0).show_ui(
ui,
|ui| {
craft_ui::ComboBox::from_id_salt(("ns_style", i))
.selected_text(crate::rtl::widget(ui, crate::i18n::style_name(lang, &style)))
.width(110.0)
.show_ui(ui, |ui| {
for c in cnames {
if ui.selectable_label(*c == style, crate::rtl::widget(ui, crate::i18n::style_name(lang, c))).clicked() {
ns["style"] = json!(c);
changed = true;
}
}
},
);
});
let through = ns["through"].as_bool().unwrap_or(true);
egui::ComboBox::from_id_salt(("ns_thr", i))
craft_ui::ComboBox::from_id_salt(("ns_thr", i))
.selected_text(crate::rtl::widget(ui, crate::i18n::tr(lang, if through { "through" } else { "up to" })))
.width(70.0)
.show_ui(ui, |ui| {
@@ -4846,7 +4847,7 @@ fn nested_style_row(lang: &str, ui: &mut egui::Ui, ns: &mut Value, i: usize, cna
count_value(ui, ns, &mut changed);
}
let kind = ns["until"]["kind"].as_str().unwrap_or("words").to_string();
egui::ComboBox::from_id_salt(("ns_until", i))
craft_ui::ComboBox::from_id_salt(("ns_until", i))
.selected_text(crate::rtl::widget(
ui,
if crate::i18n::category_first(lang) { format!("{}:", crate::i18n::tr(lang, &kind)) } else { crate::i18n::tr(lang, &kind).to_owned() },
@@ -4910,17 +4911,17 @@ fn nested_line_style_rows(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Va
fn nested_line_style_row(lang: &str, ui: &mut egui::Ui, l: &mut Value, i: usize, cnames: &[String]) -> (bool, bool) {
let mut changed = false;
let style = l["style"].as_str().unwrap_or("").to_string();
egui::ComboBox::from_id_salt(("nls_style", i)).selected_text(crate::rtl::widget(ui, crate::i18n::style_name(lang, &style))).width(130.0).show_ui(
ui,
|ui| {
craft_ui::ComboBox::from_id_salt(("nls_style", i))
.selected_text(crate::rtl::widget(ui, crate::i18n::style_name(lang, &style)))
.width(130.0)
.show_ui(ui, |ui| {
for c in cnames {
if ui.selectable_label(*c == style, crate::rtl::widget(ui, crate::i18n::style_name(lang, c))).clicked() {
l["style"] = json!(c);
changed = true;
}
}
},
);
});
crate::rtl::label(
ui,
if crate::i18n::category_first(lang) { format!("{}:", crate::i18n::tr(lang, "lines")) } else { crate::i18n::tr(lang, "for").to_owned() },
@@ -4953,7 +4954,7 @@ fn grep_styles(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Value, cnames
ui.horizontal(|ui| {
crate::rtl::label(ui, crate::i18n::tr(lang, "Apply Style:"));
let style = g["style"].as_str().unwrap_or("").to_string();
egui::ComboBox::from_id_salt(("gs_style", i))
craft_ui::ComboBox::from_id_salt(("gs_style", i))
.selected_text(crate::rtl::widget(ui, crate::i18n::style_name(lang, &style)))
.width(110.0)
.show_ui(ui, |ui| {
@@ -5068,7 +5069,7 @@ fn span_columns(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Value, doc:
// "Span Columns" names both the section and this option, which some languages word differently.
let kind_label = |k: &'static str| -> &'static str { crate::i18n::tr_context(lang, k, "paragraphLayout") };
let shown = kinds.iter().find(|k| same(&k.1, &cur)).map_or("", |k| kind_label(k.0));
egui::ComboBox::from_id_salt("psspankind").selected_text(crate::rtl::widget(ui, shown)).width(200.0).show_ui(ui, |ui| {
craft_ui::ComboBox::from_id_salt("psspankind").selected_text(crate::rtl::widget(ui, shown)).width(200.0).show_ui(ui, |ui| {
for (label, v) in kinds {
if ui.selectable_label(same(&v, &cur), crate::rtl::widget(ui, kind_label(label))).clicked() && !same(&v, &cur) {
next = Some(v);
@@ -5081,7 +5082,7 @@ fn span_columns(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Value, doc:
SpanColumns::Span(n) => {
crate::rtl::label(ui, crate::i18n::tr(lang, "Columns:"));
let shown = if n == 0 { crate::i18n::tr(lang, "All").to_string() } else { n.to_string() };
egui::ComboBox::from_id_salt("psspann").selected_text(crate::rtl::widget(ui, shown)).width(80.0).show_ui(ui, |ui| {
craft_ui::ComboBox::from_id_salt("psspann").selected_text(crate::rtl::widget(ui, shown)).width(80.0).show_ui(ui, |ui| {
if ui.selectable_label(n == 0, crate::rtl::widget(ui, crate::i18n::tr(lang, "All"))).clicked() {
next = Some(SpanColumns::Span(0));
}
@@ -5174,9 +5175,10 @@ impl ListUi<'_> {
/// A character style for a bullet or number (`key`), [None] first.
fn list_char_style(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Value, key: &str, styles: &[String]) {
let cur = para_value(d, pv, key).as_str().unwrap_or(designcraft_doc::NO_CHAR_STYLE).to_string();
egui::ComboBox::from_id_salt(("pslistcs", key)).selected_text(crate::rtl::widget(ui, crate::i18n::style_name(lang, &cur))).width(200.0).show_ui(
ui,
|ui| {
craft_ui::ComboBox::from_id_salt(("pslistcs", key))
.selected_text(crate::rtl::widget(ui, crate::i18n::style_name(lang, &cur)))
.width(200.0)
.show_ui(ui, |ui| {
for n in std::iter::once(designcraft_doc::NO_CHAR_STYLE)
.chain(styles.iter().map(String::as_str).filter(|n| *n != designcraft_doc::NO_CHAR_STYLE))
{
@@ -5184,8 +5186,7 @@ fn list_char_style(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Value, ke
para_set(d, key, json!(n));
}
}
},
);
});
}
/// The bullet character: common bullets, a field, and the font's glyphs in a grid to pick from.
@@ -5244,7 +5245,7 @@ fn bullet_font(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Value, lu: &L
let font = para_value(d, pv, "bulletFont").as_str().unwrap_or("").to_string();
let inherit = crate::i18n::tr(lang, "(Paragraph Font)");
crate::rtl::label(ui, crate::i18n::tr(lang, "Font Family:"));
egui::ComboBox::from_id_salt("psbulletfont").selected_text(if font.is_empty() { inherit } else { font.as_str() }).width(200.0).show_ui(
craft_ui::ComboBox::from_id_salt("psbulletfont").selected_text(if font.is_empty() { inherit } else { font.as_str() }).width(200.0).show_ui(
ui,
|ui| {
if ui.selectable_label(font.is_empty(), inherit).clicked() {
@@ -5263,7 +5264,7 @@ fn bullet_font(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Value, lu: &L
crate::rtl::label(ui, crate::i18n::tr(lang, "Font Style:"));
let style = para_value(d, pv, "bulletFontStyle").as_str().unwrap_or("").to_string();
ui.add_enabled_ui(!font.is_empty(), |ui| {
egui::ComboBox::from_id_salt("psbulletstyle").selected_text(style.as_str()).width(200.0).show_ui(ui, |ui| {
craft_ui::ComboBox::from_id_salt("psbulletstyle").selected_text(style.as_str()).width(200.0).show_ui(ui, |ui| {
for st in lu.fonts.styles(&font) {
if ui.selectable_label(st == style, &st).clicked() {
para_set(d, "bulletFontStyle", json!(st));
@@ -5306,7 +5307,7 @@ fn bullets_and_numbering(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Val
let list = para_value(d, pv, "listName").as_str().unwrap_or("").to_string();
// "" is the story's own list.
let name = |n: &str| if n.is_empty() { crate::i18n::tr(lang, "[Default]").to_string() } else { n.to_string() };
egui::ComboBox::from_id_salt("pslist").selected_text(name(&list)).width(200.0).show_ui(ui, |ui| {
craft_ui::ComboBox::from_id_salt("pslist").selected_text(name(&list)).width(200.0).show_ui(ui, |ui| {
for n in std::iter::once("").chain(doc.settings.lists.iter().map(|l| l.name.as_str())) {
if ui.selectable_label(n == list, name(n)).clicked() {
para_set(d, "listName", json!(n));
@@ -5325,7 +5326,7 @@ fn bullets_and_numbering(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Val
let start = para_value(d, pv, "startAt").as_u64();
ui.horizontal(|ui| {
let shown = crate::i18n::tr(lang, if start.is_some() { "Start At" } else { "Continue from Previous Number" });
egui::ComboBox::from_id_salt("psmode").selected_text(crate::rtl::widget(ui, shown)).width(200.0).show_ui(ui, |ui| {
craft_ui::ComboBox::from_id_salt("psmode").selected_text(crate::rtl::widget(ui, shown)).width(200.0).show_ui(ui, |ui| {
if ui
.selectable_label(start.is_none(), crate::rtl::widget(ui, crate::i18n::tr(lang, "Continue from Previous Number")))
.clicked()
@@ -5352,16 +5353,16 @@ fn bullets_and_numbering(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Val
para_check(ui, d, pv, "restartNumbers", crate::i18n::tr(lang, "Restart Numbers at This Level After:"));
let after = para_value(d, pv, "restartAfterLevel").as_u64().unwrap_or(0) as u32;
let label = |n: u32| if n == 0 { crate::i18n::tr(lang, "Any Previous Level").to_string() } else { n.to_string() };
egui::ComboBox::from_id_salt("psrestartafter").selected_text(crate::rtl::widget(ui, label(after))).width(140.0).show_ui(
ui,
|ui| {
craft_ui::ComboBox::from_id_salt("psrestartafter")
.selected_text(crate::rtl::widget(ui, label(after)))
.width(140.0)
.show_ui(ui, |ui| {
for n in 0..level {
if ui.selectable_label(n == after, crate::rtl::widget(ui, label(n))).clicked() {
para_set(d, "restartAfterLevel", json!(n));
}
}
},
);
});
});
});
ui.end_row();
@@ -5497,7 +5498,7 @@ fn character_style_general(
text_field_named(ui, d, "rename", 220.0, crate::i18n::tr(lang, "Style Name"));
ui.end_row();
crate::rtl::label(ui, crate::i18n::tr(lang, "Based On:"));
let combo = egui::ComboBox::from_id_salt("csbased")
let combo = craft_ui::ComboBox::from_id_salt("csbased")
.selected_text(crate::rtl::widget(ui, crate::i18n::style_name(lang, based)))
.width(220.0)
.show_ui(ui, |ui| {
@@ -5656,7 +5657,7 @@ fn char_number(ui: &mut egui::Ui, d: &mut Dialog, cf: &CharFields, key: &str, su
fn char_choice(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, cf: &CharFields, key: &str, opts: &[(Value, &str)]) {
let cur = cf.get(d, key);
let shown = opts.iter().find(|o| o.0 == cur).map_or("", |o| crate::i18n::tr(lang, o.1));
egui::ComboBox::from_id_salt(("cs", key)).selected_text(crate::rtl::widget(ui, shown)).width(200.0).show_ui(ui, |ui| {
craft_ui::ComboBox::from_id_salt(("cs", key)).selected_text(crate::rtl::widget(ui, shown)).width(200.0).show_ui(ui, |ui| {
if cf.sparse && ui.selectable_label(cur.is_null(), " ").clicked() {
cf.set(d, key, Value::Null);
}
@@ -5703,22 +5704,29 @@ fn basic_character_formats(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialo
crate::rtl::label(ui, crate::i18n::tr(lang, "Font Family:"));
let fam = cf.get(d, "fontFamily").as_str().unwrap_or("").to_string();
let shown = if fam.is_empty() { "—".to_string() } else { crate::panels::font_label(app, &menu, &fam) };
egui::ComboBox::from_id_salt("psfam")
let picked = craft_ui::ComboBox::from_id_salt("psfam")
.selected_text(shown)
.width(200.0)
.height(440.0)
.close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside)
.show_ui(ui, |ui| {
if cf.sparse && ui.selectable_label(fam.is_empty(), " ").clicked() {
cf.set(d, "fontFamily", Value::Null);
}
if let Some(f) = crate::panels::font_menu_body(app, ui, &menu, &fam, 200.0) {
cf.set(d, "fontFamily", json!(f.family));
if let Some(style) = f.style {
cf.set(d, "fontStyle", json!(style));
.show_ui_with_header(
ui,
|ui| {
if cf.sparse && ui.selectable_label(fam.is_empty(), " ").clicked() {
cf.set(d, "fontFamily", Value::Null);
}
}
});
crate::panels::font_menu_header(app, ui, &menu, &fam, 200.0)
},
crate::panels::font_menu_list,
)
.inner
.flatten();
if let Some(f) = picked {
cf.set(d, "fontFamily", json!(f.family));
if let Some(style) = f.style {
cf.set(d, "fontStyle", json!(style));
}
}
ui.end_row();
crate::rtl::label(ui, crate::i18n::tr(lang, "Font Style:"));
let sty = cf.get(d, "fontStyle").as_str().unwrap_or("").to_string();
@@ -5729,7 +5737,7 @@ fn basic_character_formats(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialo
cf.set(d, "fontStyle", if s.trim().is_empty() { Value::Null } else { json!(s) });
}
} else {
egui::ComboBox::from_id_salt("pssty").selected_text(&sty).width(200.0).show_ui(ui, |ui| {
craft_ui::ComboBox::from_id_salt("pssty").selected_text(&sty).width(200.0).show_ui(ui, |ui| {
if cf.sparse && ui.selectable_label(sty.is_empty(), " ").clicked() {
cf.set(d, "fontStyle", Value::Null);
}
@@ -5837,7 +5845,7 @@ fn advanced_character_formats(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, cf:
let labels: Vec<String> = crate::panels::properties::DIGIT_OPTIONS.iter().map(|o| crate::panels::properties::digit_label(lang, *o)).collect();
let cur = cf.get(d, "digits");
let shown = crate::panels::properties::DIGIT_OPTIONS.iter().position(|o| json!(o.0) == cur).map_or(String::new(), |i| labels[i].clone());
egui::ComboBox::from_id_salt(("cs", "digits")).selected_text(crate::rtl::widget(ui, &shown)).width(200.0).show_ui(ui, |ui| {
craft_ui::ComboBox::from_id_salt(("cs", "digits")).selected_text(crate::rtl::widget(ui, &shown)).width(200.0).show_ui(ui, |ui| {
if cf.sparse && ui.selectable_label(cur.is_null(), " ").clicked() {
cf.set(d, "digits", Value::Null);
}
@@ -5913,14 +5921,18 @@ fn open_type_features(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, cf: &CharFi
crate::rtl::label(ui, crate::i18n::tr(lang, "Figure Style:"));
let fig = otf::figures(&list);
let shown = otf::FIGURES.iter().find(|f| f.0 == fig).map_or("", |f| f.1);
egui::ComboBox::from_id_salt("csfig").selected_text(crate::rtl::widget(ui, crate::i18n::tr(lang, shown))).width(180.0).show_ui(ui, |ui| {
for (id, label, ..) in otf::FIGURES {
if ui.selectable_label(fig == *id, crate::rtl::widget(ui, crate::i18n::tr(lang, label))).clicked() && otf::set_figures(&mut list, id)
{
changed = true;
craft_ui::ComboBox::from_id_salt("csfig").selected_text(crate::rtl::widget(ui, crate::i18n::tr(lang, shown))).width(180.0).show_ui(
ui,
|ui| {
for (id, label, ..) in otf::FIGURES {
if ui.selectable_label(fig == *id, crate::rtl::widget(ui, crate::i18n::tr(lang, label))).clicked()
&& otf::set_figures(&mut list, id)
{
changed = true;
}
}
}
});
},
);
});
ui.add_space(4.0);
crate::rtl::label(ui, crate::i18n::tr(lang, "Stylistic Sets"));
@@ -5995,7 +6007,7 @@ fn delete_character_style(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog
d.fields.insert("replaceWith".into(), json!(designcraft_doc::NO_CHAR_STYLE));
}
let replace = d.s("replaceWith");
egui::ComboBox::from_id_salt("del_char_replace")
craft_ui::ComboBox::from_id_salt("del_char_replace")
.selected_text(crate::rtl::widget(ui, crate::i18n::style_name(&app.ui.language, &replace)))
.width(260.0)
.show_ui(ui, |ui| {
@@ -6010,7 +6022,7 @@ fn delete_character_style(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog
fn combo(ui: &mut egui::Ui, d: &mut Dialog, key: &str, opts: &[(&str, &str)]) {
let cur = d.s(key);
let shown = opts.iter().find(|o| o.0 == cur).map_or(cur.as_str(), |o| o.1).to_string();
egui::ComboBox::from_id_salt(key).selected_text(crate::rtl::widget(ui, &shown)).width(170.0).show_ui(ui, |ui| {
craft_ui::ComboBox::from_id_salt(key).selected_text(crate::rtl::widget(ui, &shown)).width(170.0).show_ui(ui, |ui| {
for (v, label) in opts {
if ui.selectable_label(cur == *v, crate::rtl::widget(ui, *label)).clicked() {
d.fields.insert(key.into(), json!(v));
@@ -6351,7 +6363,7 @@ fn paragraph_rules(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog, prefix: &
};
ui.horizontal(|ui| {
let shown = RULES.iter().find(|r| r.0 == which).map_or(which, |r| r.1);
egui::ComboBox::from_id_salt("rule_which").selected_text(crate::rtl::widget(ui, crate::i18n::tr(lang, shown))).width(130.0).show_ui(
craft_ui::ComboBox::from_id_salt("rule_which").selected_text(crate::rtl::widget(ui, crate::i18n::tr(lang, shown))).width(130.0).show_ui(
ui,
|ui| {
for (v, label) in RULES {
@@ -6387,7 +6399,7 @@ fn paragraph_rules(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog, prefix: &
}
crate::rtl::label(ui, crate::i18n::tr(lang, "Width:"));
let column = rule["columnWidth"].as_bool().unwrap_or(true);
egui::ComboBox::from_id_salt(("rule_width", which))
craft_ui::ComboBox::from_id_salt(("rule_width", which))
.selected_text(crate::rtl::widget(ui, crate::i18n::tr(lang, if column { "Column" } else { "Text" })))
.width(130.0)
.show_ui(ui, |ui| {
@@ -6794,7 +6806,7 @@ fn toc_params(app: &DesignApp, d: &Dialog) -> Value {
fn toc_style_combo(ui: &mut egui::Ui, salt: &str, styles: &[String], auto: &str, cur: &mut String) -> bool {
let shown = if cur.is_empty() { auto.to_string() } else { cur.clone() };
let mut changed = false;
egui::ComboBox::from_id_salt(("toc_style", salt)).selected_text(shown).width(200.0).show_ui(ui, |ui| {
craft_ui::ComboBox::from_id_salt(("toc_style", salt)).selected_text(shown).width(200.0).show_ui(ui, |ui| {
if ui.selectable_label(cur.is_empty(), auto).clicked() {
cur.clear();
changed = true;
@@ -6885,7 +6897,7 @@ fn toc_dialog(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog) {
crate::rtl::label(ui, tr("Page Number:"));
let choices = [("after", "After Entry"), ("before", "Before Entry"), ("none", "No Page Number")];
let cur = choices.iter().find(|(v, _)| *v == r.page_number).map_or("After Entry", |(_, l)| l);
egui::ComboBox::from_id_salt("toc_page_number").selected_text(tr(cur)).width(200.0).show_ui(ui, |ui| {
craft_ui::ComboBox::from_id_salt("toc_page_number").selected_text(tr(cur)).width(200.0).show_ui(ui, |ui| {
for (v, l) in choices {
if ui.selectable_label(r.page_number == v, tr(l)).clicked() {
r.page_number = v.into();
@@ -6904,7 +6916,7 @@ fn toc_dialog(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog) {
ui.checkbox(&mut r.sort, tr("Sort Entries in Alphabetical Order"));
ui.end_row();
crate::rtl::label(ui, tr("Level:"));
egui::ComboBox::from_id_salt("toc_level").selected_text(r.level.to_string()).width(60.0).show_ui(ui, |ui| {
craft_ui::ComboBox::from_id_salt("toc_level").selected_text(r.level.to_string()).width(60.0).show_ui(ui, |ui| {
for l in 1..=9u8 {
if ui.selectable_label(r.level == l, l.to_string()).clicked() {
r.level = l;
+6 -7
View File
@@ -130,7 +130,7 @@ pub fn swatch_options(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog) {
if kind == "color" {
caption(ui, lang, "Color Type:");
let cur = d.s("colorType");
let combo = egui::ComboBox::from_id_salt("swatch_color_type")
let combo = craft_ui::ComboBox::from_id_salt("swatch_color_type")
.selected_text(crate::rtl::widget(ui, tr(lang, if cur == "spot" { "Spot" } else { "Process" })))
.show_ui(ui, |ui| {
for (v, l) in [("process", "Process"), ("spot", "Spot")] {
@@ -149,9 +149,9 @@ pub fn swatch_options(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog) {
"gray" => "Gray",
_ => "CMYK",
};
let combo = egui::ComboBox::from_id_salt("swatch_color_mode").selected_text(crate::rtl::widget(ui, tr(lang, label(&cur)))).show_ui(
ui,
|ui| {
let combo = craft_ui::ComboBox::from_id_salt("swatch_color_mode")
.selected_text(crate::rtl::widget(ui, tr(lang, label(&cur))))
.show_ui(ui, |ui| {
for m in ["cmyk", "rgb", "gray"] {
if ui.selectable_label(cur == m, crate::rtl::widget(ui, tr(lang, label(m)))).clicked()
&& cur != m
@@ -160,8 +160,7 @@ pub fn swatch_options(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog) {
set_color(&mut d.fields, c, Some(m));
}
}
},
);
});
let _ = crate::widgets::name_combo(combo, tr(lang, "Color Mode"));
ui.end_row();
match d.s("mode").as_str() {
@@ -283,7 +282,7 @@ pub fn layer_options(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog) {
ui.end_row();
caption(ui, lang, "Color:");
ui.horizontal(|ui| {
let combo = egui::ComboBox::from_id_salt("layer_color")
let combo = craft_ui::ComboBox::from_id_salt("layer_color")
.selected_text(crate::rtl::widget(ui, tr(lang, named.unwrap_or("Custom"))))
.width(140.0)
.show_ui(ui, |ui| {
+29 -22
View File
@@ -143,33 +143,40 @@ fn font_fields(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog, cf: &Char
};
crate::rtl::label(ui, tr(lang, "Font Family:"));
ui.add_enabled_ui(enabled, |ui| {
egui::ComboBox::from_id_salt(("cjkfam", fam_key))
let picked = craft_ui::ComboBox::from_id_salt(("cjkfam", fam_key))
.selected_text(shown)
.width(200.0)
.height(440.0)
.close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside)
.show_ui(ui, |ui| {
if cf.sparse && ui.selectable_label(raw.is_null(), " ").clicked() {
cf.set(d, fam_key, Value::Null);
cf.set(d, style_key, Value::Null);
}
if ui.selectable_label(!raw.is_null() && fam.is_empty(), crate::rtl::widget(ui, parent.as_str())).clicked() {
cf.set(d, fam_key, json!(""));
cf.set(d, style_key, json!(""));
}
if let Some(f) = crate::panels::font_menu_body(app, ui, &menu, &fam, 200.0) {
cf.set(d, fam_key, json!(f.family));
if let Some(style) = f.style {
cf.set(d, style_key, json!(style));
.show_ui_with_header(
ui,
|ui| {
if cf.sparse && ui.selectable_label(raw.is_null(), " ").clicked() {
cf.set(d, fam_key, Value::Null);
cf.set(d, style_key, Value::Null);
}
}
});
if ui.selectable_label(!raw.is_null() && fam.is_empty(), crate::rtl::widget(ui, parent.as_str())).clicked() {
cf.set(d, fam_key, json!(""));
cf.set(d, style_key, json!(""));
}
crate::panels::font_menu_header(app, ui, &menu, &fam, 200.0)
},
crate::panels::font_menu_list,
)
.inner
.flatten();
if let Some(f) = picked {
cf.set(d, fam_key, json!(f.family));
if let Some(style) = f.style {
cf.set(d, style_key, json!(style));
}
}
});
ui.end_row();
crate::rtl::label(ui, tr(lang, "Font Style:"));
let sty = cf.get(d, style_key).as_str().unwrap_or("").to_string();
ui.add_enabled_ui(enabled && !fam.is_empty(), |ui| {
egui::ComboBox::from_id_salt(("cjksty", style_key)).selected_text(&sty).width(200.0).show_ui(ui, |ui| {
craft_ui::ComboBox::from_id_salt(("cjksty", style_key)).selected_text(&sty).width(200.0).show_ui(ui, |ui| {
for s in fonts.styles(&fam) {
if ui.selectable_label(s == sty, &s).clicked() {
cf.set(d, style_key, json!(s));
@@ -527,7 +534,7 @@ fn kenten(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog, cf: &CharField
Some(false) => tr(lang, "None").to_string(),
Some(true) => tr(lang, kenten_label(kind)).to_string(),
};
egui::ComboBox::from_id_salt("cjkkententype").selected_text(crate::rtl::widget(ui, shown)).width(200.0).show_ui(ui, |ui| {
craft_ui::ComboBox::from_id_salt("cjkkententype").selected_text(crate::rtl::widget(ui, shown)).width(200.0).show_ui(ui, |ui| {
if cf.sparse && ui.selectable_label(on.is_none(), " ").clicked() {
for key in ["kenten", "kentenKind", "kentenCharacter"] {
cf.set(d, key, Value::Null);
@@ -654,7 +661,7 @@ fn para_value(d: &Dialog, pv: &Value, key: &str) -> Value {
/// A paragraph attribute from `opts` (value, label); `current` is the value shown.
fn para_choice(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, key: &str, current: &str, opts: &[(&str, &str)]) {
let shown = opts.iter().find(|o| o.0 == current).map_or("", |o| crate::i18n::tr(lang, o.1));
egui::ComboBox::from_id_salt(("cjkp", key)).selected_text(crate::rtl::widget(ui, shown)).width(220.0).show_ui(ui, |ui| {
craft_ui::ComboBox::from_id_salt(("cjkp", key)).selected_text(crate::rtl::widget(ui, shown)).width(220.0).show_ui(ui, |ui| {
for (v, label) in opts {
if ui.selectable_label(*v == current, crate::rtl::widget(ui, crate::i18n::tr(lang, label))).clicked() {
d.fields.insert(format!("p.{key}"), json!(v));
@@ -731,7 +738,7 @@ fn japanese_composition(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, cf: &Char
// `null`: no set given, so the composer's default rules apply.
let name = para_value(d, pv, "kinsoku").get("name").and_then(Value::as_str).map(str::to_string);
let shown = name.as_deref().map_or_else(|| crate::i18n::tr(lang, "Default Rules").to_string(), |n| kinsoku_label(lang, n));
egui::ComboBox::from_id_salt("cjkkinsoku").selected_text(crate::rtl::widget(ui, shown)).width(220.0).show_ui(ui, |ui| {
craft_ui::ComboBox::from_id_salt("cjkkinsoku").selected_text(crate::rtl::widget(ui, shown)).width(220.0).show_ui(ui, |ui| {
// `style.paragraph.edit` takes `null` as the default rules, also over a Based On set.
if ui.selectable_label(name.is_none(), crate::rtl::widget(ui, crate::i18n::tr(lang, "Default Rules"))).clicked() {
d.fields.insert("p.kinsoku".into(), Value::Null);
@@ -780,7 +787,7 @@ fn japanese_composition(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, cf: &Char
let sets = mojikumi_sets(doc, &current);
let label = |l: &str| if l.is_empty() { crate::i18n::tr(lang, "None").to_string() } else { l.to_string() };
let shown = sets.iter().find(|s| s.0 == current).map_or_else(String::new, |s| label(s.1.as_str()));
egui::ComboBox::from_id_salt("cjkmojikumi").selected_text(crate::rtl::widget(ui, shown)).width(220.0).show_ui(ui, |ui| {
craft_ui::ComboBox::from_id_salt("cjkmojikumi").selected_text(crate::rtl::widget(ui, shown)).width(220.0).show_ui(ui, |ui| {
for (v, l) in &sets {
if ui.selectable_label(*v == current, crate::rtl::widget(ui, label(l.as_str()))).clicked() {
d.fields.insert("p.mojikumi".into(), json!(v));
@@ -834,7 +841,7 @@ fn grid_settings(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Value) {
grid("cjkgrid").show(ui, |ui| {
crate::rtl::label(ui, tr(lang, "Grid Alignment:"));
let shown = if on { REFERENCES.iter().find(|r| r.0 == reference).map_or("", |r| r.1) } else { "None" };
egui::ComboBox::from_id_salt("cjkgridalign").selected_text(crate::rtl::widget(ui, tr(lang, shown))).width(220.0).show_ui(ui, |ui| {
craft_ui::ComboBox::from_id_salt("cjkgridalign").selected_text(crate::rtl::widget(ui, tr(lang, shown))).width(220.0).show_ui(ui, |ui| {
if ui.selectable_label(!on, crate::rtl::widget(ui, tr(lang, "None"))).clicked() {
set(d, GridAlign::None);
}
+8 -8
View File
@@ -250,7 +250,7 @@ fn swatch(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog, path: &[&str]) {
.session
.active()
.map_or_else(Vec::new, |st| st.doc.swatches.iter().filter(|w| !w.hidden || w.name == cur).map(|w| w.name.clone()).collect());
egui::ComboBox::from_id_salt(("fx_swatch", path.join("."))).selected_text(&cur).width(150.0).show_ui(ui, |ui| {
craft_ui::ComboBox::from_id_salt(("fx_swatch", path.join("."))).selected_text(&cur).width(150.0).show_ui(ui, |ui| {
for n in names {
if ui.selectable_label(n == cur, &n).clicked() {
set(d, path, json!(n));
@@ -299,7 +299,7 @@ fn page_ui(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog, page: &str) {
grid("t").show(ui, |ui| {
row(ui, "Mode:");
let cur = d.fields.get("blend").and_then(Value::as_str).unwrap_or("Normal").to_string();
egui::ComboBox::from_id_salt("fx_blend").selected_text(tr(&cur)).width(150.0).show_ui(ui, |ui| {
craft_ui::ComboBox::from_id_salt("fx_blend").selected_text(tr(&cur)).width(150.0).show_ui(ui, |ui| {
for m in designcraft_color::BlendMode::ALL {
if ui.selectable_label(m.label() == cur, tr(m.label())).clicked() {
d.fields.insert("blend".into(), json!(m.label()));
@@ -367,7 +367,7 @@ fn page_ui(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog, page: &str) {
serde_json::to_value(m).ok().and_then(|v| v.as_str().map(str::to_string)).unwrap_or_default()
};
let shown = designcraft_color::BlendMode::ALL.iter().find(|m| label(m) == cur).map_or("Screen", |m| m.label());
egui::ComboBox::from_id_salt("fx_glow_mode").selected_text(tr(shown)).width(150.0).show_ui(ui, |ui| {
craft_ui::ComboBox::from_id_salt("fx_glow_mode").selected_text(tr(shown)).width(150.0).show_ui(ui, |ui| {
for m in designcraft_color::BlendMode::ALL {
if ui.selectable_label(label(&m) == cur, tr(m.label())).clicked() {
set(d, &["innerGlow", "mode"], json!(label(&m)));
@@ -395,7 +395,7 @@ fn page_ui(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog, page: &str) {
ui.end_row();
row(ui, "Technique:");
let cur = d.fields.get("fx").and_then(|f| f["innerGlow"]["technique"].as_str()).unwrap_or("softer").to_string();
egui::ComboBox::from_id_salt("fx_technique").selected_text(tr(if cur == "precise" { "Precise" } else { "Softer" })).show_ui(
craft_ui::ComboBox::from_id_salt("fx_technique").selected_text(tr(if cur == "precise" { "Precise" } else { "Softer" })).show_ui(
ui,
|ui| {
for (v, l) in [("softer", "Softer"), ("precise", "Precise")] {
@@ -409,7 +409,7 @@ fn page_ui(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog, page: &str) {
row(ui, "Source:");
let mut center = d.fields.get("fx").is_some_and(|f| f["innerGlow"]["center"].as_bool().unwrap_or(false));
let label = |c: bool| tr(if c { "Center" } else { "Edge" });
egui::ComboBox::from_id_salt("fx_source").selected_text(label(center)).show_ui(ui, |ui| {
craft_ui::ComboBox::from_id_salt("fx_source").selected_text(label(center)).show_ui(ui, |ui| {
for c in [false, true] {
if ui.selectable_value(&mut center, c, label(c)).clicked() {
set(d, &["innerGlow", "center"], json!(c));
@@ -490,7 +490,7 @@ fn page_ui(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog, page: &str) {
"rounded" => "Rounded",
_ => "Diffused",
};
egui::ComboBox::from_id_salt("fx_corners").selected_text(tr(shown)).show_ui(ui, |ui| {
craft_ui::ComboBox::from_id_salt("fx_corners").selected_text(tr(shown)).show_ui(ui, |ui| {
for (v, l) in [("sharp", "Sharp"), ("rounded", "Rounded"), ("diffused", "Diffused")] {
if ui.selectable_label(cur == v, tr(l)).clicked() {
set(d, &["featherOptions", "corners"], json!(v));
@@ -539,7 +539,7 @@ fn page_ui(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog, page: &str) {
row(ui, "Type:");
let mut radial = d.fields.get("fx").is_some_and(|f| f["gradientFeather"]["radial"].as_bool().unwrap_or(false));
let label = |r: bool| tr(if r { "Radial" } else { "Linear" });
egui::ComboBox::from_id_salt("fx_gtype").selected_text(label(radial)).show_ui(ui, |ui| {
craft_ui::ComboBox::from_id_salt("fx_gtype").selected_text(label(radial)).show_ui(ui, |ui| {
for r in [false, true] {
if ui.selectable_value(&mut radial, r, label(r)).clicked() {
set(d, &["gradientFeather", "radial"], json!(r));
@@ -573,7 +573,7 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog) {
crate::rtl::label(ui, tr("Settings for:"));
// Stroke, Fill and Text levels aren't in the document model yet: the object only.
ui.add_enabled_ui(false, |ui| {
egui::ComboBox::from_id_salt("fx_level").selected_text(tr("Object")).width(110.0).show_ui(ui, |_| {});
craft_ui::ComboBox::from_id_salt("fx_level").selected_text(tr("Object")).width(110.0).show_ui(ui, |_| {});
});
});
ui.add_space(6.0);
+10 -7
View File
@@ -771,14 +771,17 @@ fn details(app: &mut DesignApp, ui: &mut Ui, d: &mut Dialog) {
row(ui, rtl, |ui| {
cell(ui, rtl, tr("Width"), |ui| spin(ui, d, "width", COLUMN_W - 8.0, Step::Measure(1.0)));
cell(ui, rtl, tr("Units"), |ui| {
egui::ComboBox::from_id_salt("nd_units").selected_text(crate::rtl::widget(ui, tr(u.label()))).width(COLUMN_W - 8.0).show_ui(ui, |ui| {
// InDesign's eight; a preset in another unit keeps it on the list.
for v in NEW_DOCUMENT_UNITS.into_iter().chain((!NEW_DOCUMENT_UNITS.contains(&u)).then_some(u)) {
if ui.selectable_label(v == u, crate::rtl::widget(ui, tr(v.label()))).clicked() && v != u {
set_units(d, v);
craft_ui::ComboBox::from_id_salt("nd_units").selected_text(crate::rtl::widget(ui, tr(u.label()))).width(COLUMN_W - 8.0).show_ui(
ui,
|ui| {
// InDesign's eight; a preset in another unit keeps it on the list.
for v in NEW_DOCUMENT_UNITS.into_iter().chain((!NEW_DOCUMENT_UNITS.contains(&u)).then_some(u)) {
if ui.selectable_label(v == u, crate::rtl::widget(ui, tr(v.label()))).clicked() && v != u {
set_units(d, v);
}
}
}
});
},
);
});
});
row(ui, rtl, |ui| {
+21 -13
View File
@@ -3,7 +3,7 @@
pub mod cell_stroke;
pub mod conditions;
mod font_menu;
pub use font_menu::{FontPick, SAMPLES as FONT_SAMPLES, font_menu_body};
pub use font_menu::{FONT_MENU_HEIGHT, FontMenu, FontPick, SAMPLES as FONT_SAMPLES, font_menu_header, font_menu_list};
pub mod datamerge;
pub mod glyphs;
pub mod interactive;
@@ -383,7 +383,8 @@ pub fn swatch_picker(
Some(resp)
}
/// A font family combo box showing `current` by its menu name, with [`font_menu_body`] as its menu.
/// A font family combo box showing `current` by its menu name, with the Font menu
/// ([`font_menu_header`]) as its menu.
/// Returns the font picked.
pub fn font_combo(
app: &mut DesignApp,
@@ -394,15 +395,20 @@ pub fn font_combo(
) -> Option<FontPick> {
let menu = font_menu(app);
let shown = if current.is_empty() { "—".to_string() } else { font_label(app, &menu, current) };
// A field and a plain popup rather than egui's combo box: its own scroll area would keep the
// menu at the size it last had (a family opening, a filter clearing, wouldn't show).
let field = ui.push_id(id_salt, |ui| crate::widgets::dropdown(ui, &shown, width)).inner;
font_popup(app, &field, &menu, current)
font_popup(app, ui, &field, &menu, current)
}
/// [`font_menu_body`] in a popup under `field` (a font field the caller draws), which screen
/// readers hear as a combo box with the current family as its value. Returns the font picked.
pub fn font_popup(app: &mut DesignApp, field: &egui::Response, menu: &[designcraft_fonts::FamilyInfo], current: &str) -> Option<FontPick> {
/// The Font menu ([`font_menu_header`]) in a menu under `field` (a font field the caller draws in
/// `ui`), which screen readers hear as a combo box with the current family as its value. Returns
/// the font picked.
pub fn font_popup(
app: &mut DesignApp,
ui: &egui::Ui,
field: &egui::Response,
menu: &[designcraft_fonts::FamilyInfo],
current: &str,
) -> Option<FontPick> {
let shown = if current.is_empty() { "—".to_string() } else { font_label(app, menu, current) };
field.widget_info(|| {
let mut info = egui::WidgetInfo::new(egui::Role::ComboBox);
@@ -411,10 +417,12 @@ pub fn font_popup(app: &mut DesignApp, field: &egui::Response, menu: &[designcra
info.label = Some(crate::i18n::tr(&app.ui.language, "Font Family").to_owned());
info
});
egui::Popup::menu(field)
let width = field.rect.width();
craft_ui::combo::Menu::new(ui, field)
.height(FONT_MENU_HEIGHT)
.close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside)
.show(|ui| font_menu_body(app, ui, menu, current, field.rect.width()))
.and_then(|r| r.inner)
.show_ui_with_header(|ui| font_menu_header(app, ui, menu, current, width), font_menu_list)
.flatten()
}
/// The style a pick sets: the one picked, else the family's Regular (else its first).
@@ -454,7 +462,7 @@ pub fn style_shown(current: &str) -> String {
pub fn font_style_picker(app: &mut DesignApp, ui: &mut egui::Ui, family: &str, current: &str, width: f32) {
let styles = fonts(app).styles(family);
let mut pick = None;
let combo = egui::ComboBox::from_id_salt("font_style").selected_text(style_shown(current)).width(width).show_ui(ui, |ui| {
let combo = craft_ui::ComboBox::from_id_salt("font_style").selected_text(style_shown(current)).width(width).show_ui(ui, |ui| {
for s in &styles {
if ui.selectable_label(s == current, s).clicked() {
pick = Some(s.clone());
@@ -472,7 +480,7 @@ pub fn para_style_picker(app: &mut DesignApp, ui: &mut egui::Ui, current: &str,
let mut pick = None;
let shown = crate::i18n::style_name(&app.ui.language, current);
let label = if overridden { format!("{shown}+") } else { shown.to_string() };
let combo = egui::ComboBox::from_id_salt("para_style").selected_text(crate::rtl::widget(ui, label)).width(width).show_ui(ui, |ui| {
let combo = craft_ui::ComboBox::from_id_salt("para_style").selected_text(crate::rtl::widget(ui, label)).width(width).show_ui(ui, |ui| {
for n in names.iter().filter(|n| *n != designcraft_doc::NO_PARA_STYLE) {
if ui.selectable_label(n == current, crate::rtl::widget(ui, crate::i18n::style_name(&app.ui.language, n))).clicked() {
pick = Some(n.clone());
+1 -1
View File
@@ -192,7 +192,7 @@ pub fn stroke_fields(app: &mut DesignApp, ui: &mut Ui, id: &str, v: &Value) -> O
ui.end_row();
caption(ui, tr(&lang, "Type:"));
let shown = type_label(&lang, &v["type"]);
let combo = egui::ComboBox::from_id_salt((id, "type")).selected_text(crate::rtl::widget(ui, &shown)).width(140.0).show_ui(ui, |ui| {
let combo = craft_ui::ComboBox::from_id_salt((id, "type")).selected_text(crate::rtl::widget(ui, &shown)).width(140.0).show_ui(ui, |ui| {
for kind in TYPES {
let text = tr(&lang, builtin(kind).label());
if ui.selectable_label(shown == text, crate::rtl::widget(ui, text)).clicked() {
+1 -1
View File
@@ -197,7 +197,7 @@ fn select_source(app: &mut DesignApp) {
fn choice(ui: &mut egui::Ui, id: &str, current: &str, options: &[(&str, &str)], lang: &str, name: &str) -> Option<String> {
let label = options.iter().find(|(value, _)| *value == current).map(|(_, text)| tr(lang, text)).unwrap_or(current);
let mut picked = None;
let combo = egui::ComboBox::from_id_salt(id).selected_text(label).width(ui.available_width().min(200.0)).show_ui(ui, |ui| {
let combo = craft_ui::ComboBox::from_id_salt(id).selected_text(label).width(ui.available_width().min(200.0)).show_ui(ui, |ui| {
for (value, text) in options {
if ui.selectable_label(*value == current, tr(lang, text)).clicked() {
picked = Some((*value).to_string());
+141 -113
View File
@@ -297,13 +297,41 @@ fn clock(p: &egui::Painter, r: Rect, color: Color32) {
/// one. A click on a family picks it (its Regular style), on one of its styles that style; the
/// arrow opens a family, the star marks a favourite and ≈ shows similar fonts, all keeping the menu
/// open (it closes on a click outside only).
pub fn font_menu_body(app: &mut DesignApp, ui: &mut Ui, menu: &[FamilyInfo], current: &str, width: f32) -> Option<FontPick> {
///
/// [`font_menu_header`] lays out the search field and the filters, [`font_menu_list`] the rows
/// under them: the header and list of a [`craft_ui::combo::Menu`] or [`craft_ui::ComboBox`], so
/// only the rows scroll. Rows a field adds above the search (a blank "no font" row) go in the
/// header before [`font_menu_header`].
pub struct FontMenu<'a> {
app: &'a mut DesignApp,
menu: &'a [FamilyInfo],
current: &'a str,
st: MenuState,
rows: Vec<Row>,
highlighted_row: Option<usize>,
loading: bool,
sample: String,
row_h: f32,
sample_size: f32,
down: bool,
up: bool,
escape: bool,
pick: Option<FontPick>,
}
/// The Font menu's maximum height, its search field and filters included.
pub const FONT_MENU_HEIGHT: f32 = 500.0;
const STATE_ID: &str = "font_menu_state";
/// The Font menu's search field and filters, for the header of a menu whose list is
/// [`font_menu_list`]. See [`FontMenu`].
pub fn font_menu_header<'a>(app: &'a mut DesignApp, ui: &mut Ui, menu: &'a [FamilyInfo], current: &'a str, width: f32) -> FontMenu<'a> {
let lang = app.ui.language.clone();
let tr = |s: &'static str| crate::i18n::tr(&lang, s);
let (fonts, scope) = (super::fonts(app), super::font_scope(app));
let english = app.session.prefs.show_font_names_in_english;
let mut favs = app.session.prefs.favorite_fonts.clone();
let state_id = egui::Id::unique("font_menu_state");
let favs = app.session.prefs.favorite_fonts.clone();
let state_id = egui::Id::unique(STATE_ID);
let mut st: MenuState = ui.data(|d| d.get_temp(state_id)).unwrap_or_default();
let frame = ui.ctx().cumulative_frame_nr();
if st.frame.is_none_or(|f| f.saturating_add(1) < frame) {
@@ -315,7 +343,6 @@ pub fn font_menu_body(app: &mut DesignApp, ui: &mut Ui, menu: &[FamilyInfo], cur
let t = Tokens::get(ui.ctx());
ui.set_width(width.max(560.0));
let mut pick: Option<FontPick> = None;
let mut favs_changed = false;
// The keys the menu uses are its own: a dialog under it never sees Return or Escape.
let (down, up, enter, escape) = ui.input_mut(|i| {
let mut key = |k| i.consume_key(egui::Modifiers::NONE, k);
@@ -506,125 +533,126 @@ pub fn font_menu_body(app: &mut DesignApp, ui: &mut Ui, menu: &[FamilyInfo], cur
};
}
FontMenu { app, menu, current, st, rows, highlighted_row, loading, sample, row_h, sample_size, down, up, escape, pick }
}
/// The Font menu's rows, under [`font_menu_header`]. Returns the font picked.
pub fn font_menu_list(ui: &mut Ui, m: FontMenu<'_>) -> Option<FontPick> {
let FontMenu { app, menu, current, mut st, rows, highlighted_row, loading, sample, row_h, sample_size, down, up, escape, mut pick } = m;
let lang = app.ui.language.clone();
let tr = |s: &'static str| crate::i18n::tr(&lang, s);
let (fonts, scope) = (super::fonts(app), super::font_scope(app));
let english = app.session.prefs.show_font_names_in_english;
let mut favs = app.session.prefs.favorite_fonts.clone();
let state_id = egui::Id::unique(STATE_ID);
let t = Tokens::get(ui.ctx());
let mut favs_changed = false;
let mut toggle_expand = None;
let mut toggle_fav = None;
let mut similar = None;
// A popup only offers last frame's height: ask for the rows' own (a family just opened, a
// filter just cleared), up to the list's limit.
let list_h: f32 = rows
.iter()
.map(|r| match r {
Row::Family { .. } => row_h + ui.spacing().item_spacing.y,
Row::Style { .. } => row_h.min(30.0) + ui.spacing().item_spacing.y,
Row::Separator => 8.0,
})
.sum();
egui::ScrollArea::vertical().max_height(420.0).min_scrolled_height(list_h.clamp(24.0, 420.0)).auto_shrink([false, true]).show(ui, |ui| {
if loading {
crate::rtl::label(ui, egui::RichText::new(tr("Loading fonts…")).color(t.text_dim));
} else if rows.is_empty() {
crate::rtl::label(ui, egui::RichText::new(tr("No fonts match")).color(t.text_dim));
}
for (i, row) in rows.iter().enumerate() {
let (family, style, recent) = match row {
Row::Separator => {
ui.separator();
continue;
}
Row::Family { info, recent } => (info, None, *recent),
Row::Style { family, style } => {
let Some(info) = menu.iter().find(|f| f.family == *family) else { continue };
(info, Some(style.as_str()), false)
}
};
let f = &family.family;
let (r, mut resp) =
ui.allocate_exact_size(vec2(ui.available_width(), if style.is_some() { row_h.min(30.0) } else { row_h }), Sense::click());
let highlighted = highlighted_row == Some(i);
if highlighted && (down || up) {
ui.scroll_to_rect(r, None);
}
if !ui.is_rect_visible(r) {
if loading {
crate::rtl::label(ui, egui::RichText::new(tr("Loading fonts…")).color(t.text_dim));
} else if rows.is_empty() {
crate::rtl::label(ui, egui::RichText::new(tr("No fonts match")).color(t.text_dim));
}
for (i, row) in rows.iter().enumerate() {
let (family, style, recent) = match row {
Row::Separator => {
ui.separator();
continue;
}
let label = match style {
Some(s) => s.to_string(),
None => family.label(english).to_string(),
};
let is_current = style.is_none() && f == current;
resp.widget_info(|| egui::WidgetInfo::selected(egui::Role::Button, true, is_current, &label));
if style.is_none() && label != *f {
resp = resp.on_hover_text(f);
Row::Family { info, recent } => (info, None, *recent),
Row::Style { family, style } => {
let Some(info) = menu.iter().find(|f| f.family == *family) else { continue };
(info, Some(style.as_str()), false)
}
// Over the row or one of its buttons (the star, the arrow, ≈ are on top of it).
let hovered = resp.contains_pointer();
if is_current {
ui.painter().rect_filled(r, 0.0, t.row_selected);
} else if highlighted || hovered {
ui.painter().rect_filled(r, 0.0, t.hover);
};
let f = &family.family;
let (r, mut resp) = ui.allocate_exact_size(vec2(ui.available_width(), if style.is_some() { row_h.min(30.0) } else { row_h }), Sense::click());
let highlighted = highlighted_row == Some(i);
if highlighted && (down || up) {
ui.scroll_to_rect(r, None);
}
if !ui.is_rect_visible(r) {
continue;
}
let label = match style {
Some(s) => s.to_string(),
None => family.label(english).to_string(),
};
let is_current = style.is_none() && f == current;
resp.widget_info(|| egui::WidgetInfo::selected(egui::Role::Button, true, is_current, &label));
if style.is_none() && label != *f {
resp = resp.on_hover_text(f);
}
// Over the row or one of its buttons (the star, the arrow, ≈ are on top of it).
let hovered = resp.contains_pointer();
if is_current {
ui.painter().rect_filled(r, 0.0, t.row_selected);
} else if highlighted || hovered {
ui.painter().rect_filled(r, 0.0, t.hover);
}
let p = ui.painter_at(r);
let m = meta(ui, &fonts, scope, f);
let cy = r.center().y;
let mut handled = false;
if style.is_none() {
// Favourite star.
let star = Rect::from_center_size(pos2(r.min.x + 10.0, cy), vec2(16.0, 16.0));
let fav = favs.contains(f);
let sr = ui.interact(star, ui.scope_id().with(("fav", f, recent)), Sense::click());
paint_star(&p, star.center(), 6.5, fav, if fav { t.accent } else { t.text_dim });
if sr.clicked() {
toggle_fav = Some(f.clone());
handled = true;
}
let p = ui.painter_at(r);
let m = meta(ui, &fonts, scope, f);
let cy = r.center().y;
let mut handled = false;
if style.is_none() {
// Favourite star.
let star = Rect::from_center_size(pos2(r.min.x + 10.0, cy), vec2(16.0, 16.0));
let fav = favs.contains(f);
let sr = ui.interact(star, ui.scope_id().with(("fav", f, recent)), Sense::click());
paint_star(&p, star.center(), 6.5, fav, if fav { t.accent } else { t.text_dim });
if sr.clicked() {
toggle_fav = Some(f.clone());
// The arrow opens a family of several styles.
if m.styles.len() > 1 && !recent {
let arrow = Rect::from_center_size(pos2(r.min.x + 28.0, cy), vec2(16.0, 16.0));
let ar = ui.interact(arrow, ui.scope_id().with(("open", f)), Sense::click());
ar.widget_info(|| {
egui::WidgetInfo::labeled(egui::Role::Button, true, tr(if st.expanded.contains(f) { "Hide Styles" } else { "Show Styles" }))
});
let open = st.expanded.contains(f);
crate::icons::paint(&p, arrow.shrink(2.0), if open { "chevron-down" } else { "chevron-right" }, t.icon);
if ar.clicked() {
toggle_expand = Some(f.clone());
handled = true;
}
// The arrow opens a family of several styles.
if m.styles.len() > 1 && !recent {
let arrow = Rect::from_center_size(pos2(r.min.x + 28.0, cy), vec2(16.0, 16.0));
let ar = ui.interact(arrow, ui.scope_id().with(("open", f)), Sense::click());
ar.widget_info(|| {
egui::WidgetInfo::labeled(egui::Role::Button, true, tr(if st.expanded.contains(f) { "Hide Styles" } else { "Show Styles" }))
});
let open = st.expanded.contains(f);
crate::icons::paint(&p, arrow.shrink(2.0), if open { "chevron-down" } else { "chevron-right" }, t.icon);
if ar.clicked() {
toggle_expand = Some(f.clone());
handled = true;
}
}
}
// Name (and how many styles), then the sample in the face, then the format.
let name_x = r.min.x + if style.is_some() { 48.0 } else { 40.0 };
let name = if style.is_none() && m.styles.len() > 1 { format!("{label} ({})", m.styles.len()) } else { label.clone() };
p.text(pos2(name_x, cy), Align2::LEFT_CENTER, &name, FontId::proportional(12.5), t.text);
let preview_x = r.min.x + (r.width() * 0.42).max(190.0);
let icons_x = r.max.x - 52.0;
let face_style = style.map_or_else(|| regular(&m.styles), str::to_string);
let tex = preview(ui, &fonts, scope, f, &face_style, &sample, sample_size, t.text);
let size = tex.size_vec2() / ui.ctx().pixels_per_point();
let shown = Rect::from_min_size(pos2(preview_x, cy - size.y / 2.0), size);
let clip = Rect::from_min_max(pos2(preview_x, r.min.y), pos2(icons_x - 6.0, r.max.y));
ui.painter_at(clip).image(tex.id(), shown, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE);
// Show Similar Fonts (on hover), and the format.
if style.is_none() && (hovered || st.similar.as_deref() == Some(f.as_str())) {
let sim = Rect::from_center_size(pos2(icons_x + 12.0, cy), vec2(18.0, 18.0));
let sr = ui.interact(sim, ui.scope_id().with(("similar", f, recent)), Sense::click());
sr.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, tr("Show Similar Fonts")));
p.text(sim.center(), Align2::CENTER_CENTER, "≈", FontId::proportional(15.0), if sr.hovered() { t.text_strong } else { t.icon });
if sr.on_hover_text(tr("Show Similar Fonts")).clicked() {
similar = Some(f.clone());
handled = true;
}
}
if style.is_none() {
let badge = Rect::from_center_size(pos2(r.max.x - 16.0, cy), vec2(20.0, 16.0));
paint_format(&p, badge, m.kind.format, t.text_dim, t.accent);
ui.interact(badge, ui.scope_id().with(("format", f, recent)), Sense::hover()).on_hover_text(tr(format_name(m.kind.format)));
}
if resp.clicked() && !handled {
pick = Some(FontPick { family: f.clone(), style: style.map(str::to_string) });
}
}
});
// Name (and how many styles), then the sample in the face, then the format.
let name_x = r.min.x + if style.is_some() { 48.0 } else { 40.0 };
let name = if style.is_none() && m.styles.len() > 1 { format!("{label} ({})", m.styles.len()) } else { label.clone() };
p.text(pos2(name_x, cy), Align2::LEFT_CENTER, &name, FontId::proportional(12.5), t.text);
let preview_x = r.min.x + (r.width() * 0.42).max(190.0);
let icons_x = r.max.x - 52.0;
let face_style = style.map_or_else(|| regular(&m.styles), str::to_string);
let tex = preview(ui, &fonts, scope, f, &face_style, &sample, sample_size, t.text);
let size = tex.size_vec2() / ui.ctx().pixels_per_point();
let shown = Rect::from_min_size(pos2(preview_x, cy - size.y / 2.0), size);
let clip = Rect::from_min_max(pos2(preview_x, r.min.y), pos2(icons_x - 6.0, r.max.y));
ui.painter_at(clip).image(tex.id(), shown, Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)), Color32::WHITE);
// Show Similar Fonts (on hover), and the format.
if style.is_none() && (hovered || st.similar.as_deref() == Some(f.as_str())) {
let sim = Rect::from_center_size(pos2(icons_x + 12.0, cy), vec2(18.0, 18.0));
let sr = ui.interact(sim, ui.scope_id().with(("similar", f, recent)), Sense::click());
sr.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, tr("Show Similar Fonts")));
p.text(sim.center(), Align2::CENTER_CENTER, "≈", FontId::proportional(15.0), if sr.hovered() { t.text_strong } else { t.icon });
if sr.on_hover_text(tr("Show Similar Fonts")).clicked() {
similar = Some(f.clone());
handled = true;
}
}
if style.is_none() {
let badge = Rect::from_center_size(pos2(r.max.x - 16.0, cy), vec2(20.0, 16.0));
paint_format(&p, badge, m.kind.format, t.text_dim, t.accent);
ui.interact(badge, ui.scope_id().with(("format", f, recent)), Sense::hover()).on_hover_text(tr(format_name(m.kind.format)));
}
if resp.clicked() && !handled {
pick = Some(FontPick { family: f.clone(), style: style.map(str::to_string) });
}
}
if let Some(f) = toggle_expand
&& !st.expanded.remove(&f)
{
@@ -301,3 +301,66 @@ fn font_fields_in_panels_and_dialogs_search() {
assert_eq!(h.state().app.ui.dialog.as_ref().map(|d| d.id.clone()), dialog_id, "{what}: Escape in the menu leaves the dialog open");
}
}
/// The font rows the open menu shows under its search field `search`: how many, and the bottom
/// of the lowest.
fn rows_shown(h: &Harness<'static, Window>, search: egui::Rect) -> (usize, f32) {
let families: Vec<String> = crate::panels::font_menu(&h.state().app).iter().map(|f| f.label(true).to_string()).collect();
let rows: Vec<egui::Rect> = h
.query_all_by(|n| n.role() == egui::accesskit::Role::Button && n.label().is_some_and(|l| families.contains(&l)))
.map(|n| n.rect())
.filter(|r| r.min.y > search.max.y)
.collect();
(rows.len(), rows.iter().map(|r| r.max.y).fold(f32::MIN, f32::max))
}
/// Opens the font menu of the field at `at`, searches for a font that doesn't exist and clears
/// the search: the menu grows back to the rows it showed, with the search field where it was.
/// The menu lists every family by its English name: the test UI has no CJK font for a system
/// CJK family's native name.
fn menu_regrows_after_a_search_with_no_matches(h: &mut Harness<'static, Window>, at: egui::Rect, what: &str) {
h.state_mut().app.session.prefs.show_font_names_in_english = true;
click_at(h, at.center());
h.run_steps(4);
let focused = |h: &Harness<'static, Window>| h.query_by(|n| n.role() == egui::accesskit::Role::TextInput && n.is_focused()).map(|n| n.rect());
let field = focused(h).unwrap_or_else(|| panic!("{what}: the menu opens with the search field focused"));
let (count, bottom) = rows_shown(h, field);
assert!(count > 1, "{what}: the menu lists the fonts: {count}");
let query = "zzqqxxnomatch";
h.event(egui::Event::Text(query.into()));
h.run_steps(4);
assert!(h.query_by_label("No fonts match").is_some(), "{what}: nothing matches");
assert_eq!(rows_shown(h, field).0, 0, "{what}: no rows while nothing matches");
for _ in query.chars() {
h.key_press(egui::Key::Backspace);
}
h.run_steps(4);
assert_eq!(search(h), "", "{what}: the search is cleared");
let again = focused(h).unwrap_or_else(|| panic!("{what}: the menu is still open"));
assert!((again.min.y - field.min.y).abs() < 0.5, "{what}: the search field stays at {}, not {}", field.min.y, again.min.y);
let (count_again, bottom_again) = rows_shown(h, again);
assert_eq!(count_again, count, "{what}: the menu shows as many rows as before the search");
assert!((bottom_again - bottom).abs() < 1.0, "{what}: the menu grows back to {bottom}, not {bottom_again}");
}
#[test]
fn the_font_field_menu_grows_back_after_a_search_with_no_matches() {
let (mut h, _) = typing();
let at = field(&h, designcraft_fonts::DEFAULT_FAMILY);
menu_regrows_after_a_search_with_no_matches(&mut h, at, "Control bar");
}
/// Paragraph Style Options › Basic Character Formats' Font Family combo box. The CJK style
/// fields' combo box hosts the same menu the same way.
#[test]
fn the_style_dialog_font_menu_grows_back_after_a_search_with_no_matches() {
let app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
let mut h = test_window::open(app, vec2(1440.0, 900.0));
let section = json!({"name": designcraft_doc::BASIC_PARAGRAPH, "section": "chars"});
h.state_mut().app.ui.dialog = Some(crate::dialogs::Dialog::new("paragraphStyleOptions", section));
h.run_steps(4);
let at = *fields(&h, designcraft_fonts::DEFAULT_FAMILY).first().expect("the dialog's font field is shown");
menu_regrows_after_a_search_with_no_matches(&mut h, at, "Paragraph Style Options");
}
+1 -1
View File
@@ -38,7 +38,7 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
st.style = f.style.clone().or_else(|| db.styles(&f.family).into_iter().next()).unwrap_or_else(|| "Regular".into());
st.family = f.family;
}
let combo = egui::ComboBox::from_id_salt("glyph_style").selected_text(&st.style).width(80.0).show_ui(ui, |ui| {
let combo = craft_ui::ComboBox::from_id_salt("glyph_style").selected_text(&st.style).width(80.0).show_ui(ui, |ui| {
for s in db.styles(&st.family) {
if ui.selectable_label(s == st.style, &s).clicked() {
st.style = s;
+4 -4
View File
@@ -365,7 +365,7 @@ pub fn buttons(app: &mut DesignApp, ui: &mut egui::Ui) {
let mut pick = None;
ui.horizontal(|ui| {
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "On Release"));
let combo = egui::ComboBox::from_id_salt("button_action")
let combo = craft_ui::ComboBox::from_id_salt("button_action")
.selected_text(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, labels.iter().find(|l| l.0 == kind).map_or("", |l| l.1))))
.show_ui(ui, |ui| {
for (k, l) in labels {
@@ -426,7 +426,7 @@ pub fn buttons(app: &mut DesignApp, ui: &mut egui::Ui) {
let cur = ff.as_ref().map(|f| f.kind);
ui.horizontal(|ui| {
crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Form Field"));
let combo = egui::ComboBox::from_id_salt("form_kind")
let combo = craft_ui::ComboBox::from_id_salt("form_kind")
.selected_text(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, kinds.iter().find(|k| k.0 == cur).map_or("", |k| k.1))))
.show_ui(ui, |ui| {
for (k, l) in kinds {
@@ -539,7 +539,7 @@ pub fn liquid(app: &mut DesignApp, ui: &mut egui::Ui) {
crate::widgets::caption(ui, &format!("{} {}", crate::i18n::tr(&lang, "Page:"), d.page_name(page)));
});
});
let combo = egui::ComboBox::from_id_salt("liquid_rule")
let combo = craft_ui::ComboBox::from_id_salt("liquid_rule")
.selected_text(crate::rtl::widget(ui, crate::i18n::tr(&lang, liquid_rule_label(pg.liquid))))
.width(190.0)
.show_ui(ui, |ui| {
@@ -669,7 +669,7 @@ pub fn transitions(app: &mut DesignApp, ui: &mut egui::Ui) {
use designcraft_doc::TransitionKind as K;
crate::rtl::label(ui, egui::RichText::new(format!("{} {}", crate::i18n::tr(&app.ui.language, "Spread"), si + 1)).strong());
let mut kind = cur.map(|c| c.kind);
let combo = egui::ComboBox::from_id_salt("transition_kind")
let combo = craft_ui::ComboBox::from_id_salt("transition_kind")
.selected_text(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, kind.map_or("None", K::label))))
.show_ui(ui, |ui| {
ui.selectable_value(&mut kind, None, crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "None")));
@@ -66,7 +66,7 @@ pub fn stroke_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
StrokeType::Style { name } => name.clone(),
k => k.label().to_string(),
};
let combo = egui::ComboBox::from_id_salt("stype")
let combo = craft_ui::ComboBox::from_id_salt("stype")
.selected_text(crate::rtl::widget(
ui,
if matches!(st.kind, StrokeType::Style { .. }) { &shown } else { crate::i18n::tr(&app.ui.language, &shown) },
@@ -106,7 +106,7 @@ pub fn stroke_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
ui.end_row();
for (key, label, cur, flip) in [("start", "Start/End:", st.start, true), ("end", "", st.end, false)] {
caption(ui, crate::i18n::tr(&app.ui.language, label));
let combo = egui::ComboBox::from_id_salt(key)
let combo = craft_ui::ComboBox::from_id_salt(key)
.selected_text(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, cur.label())))
.width(140.0)
.show_ui(ui, |ui| {
@@ -305,7 +305,7 @@ fn wrap_combo(
ui.horizontal(|ui| {
caption(ui, crate::i18n::tr(&app.ui.language, caption_text));
let shown = opts.iter().chain(shown_only).find(|o| o.0 == cur).map_or(cur, |o| o.1).to_string();
let combo = egui::ComboBox::from_id_salt(salt)
let combo = craft_ui::ComboBox::from_id_salt(salt)
.selected_text(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, &shown)))
.width(170.0)
.show_ui(ui, |ui| {
@@ -468,7 +468,7 @@ pub fn effects_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
B::Luminosity,
];
ui.horizontal(|ui| {
let combo = egui::ComboBox::from_id_salt("blend")
let combo = craft_ui::ComboBox::from_id_salt("blend")
.selected_text(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, it.blend.label())))
.width(130.0)
.show_ui(ui, |ui| {
+1 -1
View File
@@ -179,7 +179,7 @@ pub(super) fn character_section(app: &mut DesignApp, ui: &mut Ui) {
egui::FontId::proportional(11.5),
t.text,
);
if let Some(f) = crate::panels::font_popup(app, &resp, &menu, &fam) {
if let Some(f) = crate::panels::font_popup(app, ui, &resp, &menu, &fam) {
crate::panels::apply_font(app, &f);
}
ui.add_space(1.0);
@@ -16,7 +16,7 @@ pub fn character_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
let lang = c["language"].as_str().unwrap_or("English: USA").to_string();
ui.horizontal(|ui| {
caption(ui, crate::i18n::tr(&app.ui.language, "Language"));
let combo = egui::ComboBox::from_id_salt("char_language")
let combo = craft_ui::ComboBox::from_id_salt("char_language")
.selected_text(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, &lang)))
.width(170.0)
.show_ui(ui, |ui| language_menu(app, ui, &lang));
@@ -30,7 +30,7 @@ pub fn character_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
.iter()
.find(|o| o.0 == digits)
.map_or_else(|| digit_label(&app.ui.language, DIGIT_OPTIONS[0]), |o| digit_label(&app.ui.language, *o));
let combo = egui::ComboBox::from_id_salt("char_digits").selected_text(crate::rtl::widget(ui, cur)).width(150.0).show_ui(ui, |ui| {
let combo = craft_ui::ComboBox::from_id_salt("char_digits").selected_text(crate::rtl::widget(ui, cur)).width(150.0).show_ui(ui, |ui| {
for o in DIGIT_OPTIONS {
if ui.selectable_label(o.0 == digits, crate::rtl::widget(ui, digit_label(&app.ui.language, o))).clicked() {
let _ = app.run("type.char", json!({"attrs": {"digits": o.0}}));
+2 -2
View File
@@ -401,7 +401,7 @@ pub fn color_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
}
ui.label(egui::RichText::new(&name).size(11.0).color(t.text_dim));
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
let combo = egui::ComboBox::from_id_salt("color_mode")
let combo = craft_ui::ComboBox::from_id_salt("color_mode")
.width(64.0)
.selected_text(match mode {
ColorMode::Cmyk => "CMYK",
@@ -741,7 +741,7 @@ pub fn gradient_panel(app: &mut DesignApp, ui: &mut egui::Ui) {
crate::widgets::caption_w(ui, crate::i18n::tr(&lang, "Type:"), 52.0);
let shown = if is_gradient { crate::i18n::tr(&lang, g.kind.label()) } else { "" };
let combo =
egui::ComboBox::from_id_salt("grad_type").selected_text(crate::rtl::widget(ui, shown)).width(104.0).show_ui(ui, |ui| {
craft_ui::ComboBox::from_id_salt("grad_type").selected_text(crate::rtl::widget(ui, shown)).width(104.0).show_ui(ui, |ui| {
for (k, v) in [("Linear", "linear"), ("Radial", "radial")] {
if ui
.selectable_label(is_gradient && g.kind.label() == k, crate::rtl::widget(ui, crate::i18n::tr(&lang, k)))
+2 -2
View File
@@ -69,7 +69,7 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
for (label, kind, names, cur) in [("Table Style", "table", tnames, tcur), ("Cell Style", "cell", cnames, ccur)] {
ui.horizontal(|ui| {
caption(ui, crate::i18n::tr(&app.ui.language, label));
let combo = egui::ComboBox::from_id_salt(("tp_style", kind)).selected_text(&cur).width(130.0).show_ui(ui, |ui| {
let combo = craft_ui::ComboBox::from_id_salt(("tp_style", kind)).selected_text(&cur).width(130.0).show_ui(ui, |ui| {
for n in &names {
if ui.selectable_label(*n == cur, n).clicked() {
run(app, &format!("style.{kind}.apply"), json!({"name": n, "table": tid}));
@@ -126,7 +126,7 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) {
let row = rows.get(r0).cloned().unwrap_or(Value::Null);
let exactly = row["mode"] == "exactly";
caption(ui, crate::i18n::tr(&app.ui.language, "Row Height"));
let combo = egui::ComboBox::from_id_salt("tp_rowmode")
let combo = craft_ui::ComboBox::from_id_salt("tp_rowmode")
.selected_text(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, if exactly { "Exactly" } else { "At Least" })))
.width(70.0)
.show_ui(ui, |ui| {
+1 -1
View File
@@ -105,7 +105,7 @@ fn text(ui: &mut Ui, d: &mut Dialog, key: &str, width: f32) {
fn style_menu(ui: &mut Ui, d: &mut Dialog, key: &str) {
let current: NumberStyle = d.fields.get(key).and_then(|v| serde_json::from_value(v.clone()).ok()).unwrap_or_default();
let shown = STYLES.iter().find(|(s, _)| *s == current).map_or(STYLES[0].1, |(_, l)| l);
egui::ComboBox::from_id_salt(("section_style", key)).selected_text(shown).width(150.0).show_ui(ui, |ui| {
craft_ui::ComboBox::from_id_salt(("section_style", key)).selected_text(shown).width(150.0).show_ui(ui, |ui| {
for (style, label) in STYLES {
if ui.selectable_label(style == current, label).clicked() {
d.fields.insert(key.into(), json!(style));
+1 -1
View File
@@ -145,7 +145,7 @@ fn text_controls(app: &mut DesignApp, ui: &mut Ui, in_text: bool) {
let menu = panels::font_menu(app);
let shown = if fam.is_empty() { "—".to_string() } else { panels::font_label(app, &menu, &fam) };
let field = widgets::dropdown(ui, &shown, 150.0);
if let Some(f) = panels::font_popup(app, &field, &menu, &fam) {
if let Some(f) = panels::font_popup(app, ui, &field, &menu, &fam) {
panels::apply_font(app, &f);
}
let styles = fonts.styles(&fam);
+1 -1
View File
@@ -1,7 +1,7 @@
# Shared PDF backend
DesignCraft pins `craft-pdf`, patched Krilla 0.8.2 and the six Hayro reader crates (with
`hayro-cmap` and `hayro-postscript`) to craft-libs revision `b926b5c03e256c86ca10934f58d03f628810b5b7`. `craft-ui`,
`hayro-cmap` and `hayro-postscript`) to craft-libs revision `92d392cff132eaf53e13afff77f38b5c11b73aa0`. `craft-ui`,
`craft-print` and the other craft-libs crates are pinned to the same revision. The reader migration is reviewed on top of the separate writer change.
## Writing
+1 -1
View File
@@ -5,7 +5,7 @@ use std::process::Command;
use serde_json::Value;
const REPO: &str = "https://github.com/storytold/craft-libs";
const REV: &str = "b926b5c03e256c86ca10934f58d03f628810b5b7";
const REV: &str = "92d392cff132eaf53e13afff77f38b5c11b73aa0";
const PACKAGES: &[(&str, &str)] = &[
("craft-pdf", "0.1.0"),
("krilla", "0.8.2"),