diff --git a/ROADMAP.md b/ROADMAP.md index 7a77546c6..a9d976c30 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -28,7 +28,8 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope - Footnotes: Type › Insert Footnote (caret moves into the note), Document Footnote Options (numbering style incl. symbols, start/restart per page/spread/section, prefix/suffix, reference position/character style, paragraph style, separator, spacing, first baseline, rule above); notes composed at the bottom of the referencing column with the body text making room; edited in place on the canvas; rendered, exported to PDF (real text) and IDML (InDesign's structure, verified opening in InDesign 2026) and imported from IDML; `footnote.*` commands. - Tabs panel (Type › Tabs, ⇧⌘T): alignment buttons, X, Leader and Align On fields, a ruler to add, move and delete stops and drag the first-line, left and right indents, Clear All, Delete Tab, Repeat Tab and Reset Indents; it opens over the selected text frame with its ruler on the text column. Paragraph Style Options › Tabs edits a style's stops. Commands: `type.tabs.get`, `type.tabs.add`, `type.tabs.move`, `type.tabs.change`, `type.tabs.remove`, `type.tabs.clear`, `type.tabs.repeat` (with `style` for a paragraph style). - Paragraph Rules: Rule Above / Rule Below (on, weight, colour, tint, column or text width, offset, left/right indent) set in Type › Paragraph Rules…, the Paragraph panel menu and Paragraph Style Options › Paragraph Rules; inherited style values shown, and `type.para` / `style.paragraph.edit` change only the rule fields named. -- Paragraph Style Options lists its sections in InDesign's order. Advanced Character Formats, OpenType Features, Underline Options and Strikethrough Options are the Character Style Options sections; Paragraph Border and Paragraph Shading (on, colour, tint, per-side offsets and border weights), Keep Options (keep with next, keep lines together, start paragraph), Span Columns (single, span, split with gutters) and Bullets and Numbering (list type, bullet and text after, list, format, number, Start At or Continue from Previous Number, label indents) edit the existing paragraph attributes. +- Paragraph Style Options lists its sections in InDesign's order. Advanced Character Formats, OpenType Features, Underline Options and Strikethrough Options are the Character Style Options sections; Paragraph Border and Paragraph Shading (on, colour, tint, per-side offsets and border weights), Keep Options (keep with next, keep lines together, start paragraph), Span Columns (single, span, split with gutters, least space before and after the span or split) and Bullets and Numbering (list type and level, bullet with a glyph grid to pick it from, bullet font, text after, list, format, number, Start At or Continue from Previous Number, Restart Numbers at This Level After, the label's character style, alignment, indents and tab position) edit the paragraph attributes; the same sections open for the selected paragraphs from the Paragraph panel menu (Paragraph Borders and Shading…, Span Columns…, Bullets and Numbering…). +- Lists have levels 1–9: each level numbers on its own and restarts after a higher level (any, or a given one), `^1`…`^9` in the number text are the levels' current numbers, and named lists keep their levels across stories. A bullet or number is left-aligned at the first line's start, centred on it or ends at it; the tab position is a stop for the text after it. Span and split paragraphs keep at least their space before and after from the column text. All of these round-trip through IDML (`NumberingLevel`, `NumberingRestartPolicies`, `BulletsCharacterStyle`, `NumberingCharacterStyle`, `BulletsAlignment`, `NumberingAlignment`, `BulletsFont`, `SpanColumnMinSpaceBefore`/`After`); the tab position is not written to IDML. - Cross-references and text anchors: Insert Cross-Reference (paragraph by style, or text anchor), 11 formats with InDesign's building blocks (full/partial paragraph, paragraph text/number, page number, anchor name, chapter, file name) plus user formats; resolved live at composition — never out of date, no Update step; unresolved destinations show `??`; PDF link annotations to the destination page; IDML export/import as InDesign cross-reference sources, hyperlinks, text destinations and formats (verified in InDesign 2026); `xref.*` / `anchor.*` commands. - Index: page references (topic from the selection or up to 4 levels, sort keys; current page, to end of story, next n paragraphs, suppressed, See / See also), Generate / Update Index (section headings, nested or run-in, page ranges merged, Index Title / Section Head / Level 1–4 styles); IDML export/import as InDesign topics, page references and topic cross-references (verified in InDesign 2026); `index.*` commands. - Text clipboard keeps formatting (character/paragraph formats, footnotes, cross-references, index markers, tables) and falls back to plain text when the system clipboard changed; Paste without Formatting (⇧⌘V); Change Case (UPPERCASE, lowercase, Title Case, Sentence case — formatting kept); Type › Insert Special Character / White Space / Break Character submenus. @@ -72,7 +73,7 @@ DesignCraft aims at full Adobe InDesign parity — and to be better: faster, ope - Nested line styles (`nestedLineStyles` paragraph attribute, Paragraph Style Options › Drop Caps and Nested Styles, IDML AllNestedLineStyles): the first lines restyle until the line breaks settle. - User dictionary hyphenation exceptions (`hyphenation.addException/removeException/list`, Edit ▸ Spelling ▸ User Dictionary): `ex~am~ple` breaks only at `~`, a plain word never hyphenates. - Frame Fitting Options (`object.fittingOptions`, Object ▸ Fitting ▸ Frame Fitting Options…): Auto-Fit refits on resize, fitting, Align From reference point, crop amounts; IDML FrameFittingOption. -- Paragraph borders (per-side weights, colour, tint, offsets) and shading offsets, drawn per column part of split paragraphs (also when overset); Paragraph panel controls; IDML ParagraphBorder*/ParagraphShading*Offset. +- Paragraph borders and shading, drawn per column part of split paragraphs (also when overset) once the lines are in place: border per-side weights, stroke type (built-in types and the document's stroke styles), cap, join, colour, tint, gap colour and tint, corner size and shape per corner, per-side offsets, column or text width, top edge (ascent, baseline, em box, leading) and bottom edge (descent, baseline, em box), a closed box in each part of a split paragraph when asked, and one box for consecutive paragraphs with the same settings; shading has the same corners, offsets, edges and width, Clip to Frame, and Do not Print or Export (shown on screen only). Paragraph Borders and Shading… dialog (Paragraph panel menu, Type menu); IDML ParagraphBorder*/ParagraphShading*/MergeConsecutiveParaBorders. - Font menu: search, favourites (stars, Show Favorites Only; `favoriteFonts` preference) and each family previewed in its own face. Western (and other-script) families come first, then Japanese, Simplified Chinese, Traditional Chinese and Korean ones, each group alphabetical and set off by a separator; the group comes from the font's own data (`meta` languages, family names in a CJK language, OS/2 code pages, else what it covers), read once (installed fonts by the font scan, without loading them). CJK families show their native names, the English name on hover; Preferences › Type › Show Font Names in English (`showFontNamesInEnglish`) shows English names throughout. Search matches either name; documents store the English name. Every family list (Character panel, Control bar, Properties, style dialogs, Find Font, Glyphs) follows this order. - Application preferences (engine `Prefs`) now persist across launches (desktop: prefs.json beside ui.json). - View ▸ Fit Selection in Window (⌥⌘=) and Find/Change ▸ Object (`find.objects`, `find.changeObjects`: by fill, stroke, weight, opacity, kind, layer, label). diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index 5c8f251aa..e3d9df68c 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -1132,6 +1132,14 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) { "characterStyleOptions" => crate::i18n::tr(&app.ui.language, "Character Style Options"), "footnoteOptions" => crate::i18n::tr(&app.ui.language, "Footnote Options"), "paragraphRules" => crate::i18n::tr(&app.ui.language, "Paragraph Rules"), + "paragraphLocal" => crate::i18n::tr( + &app.ui.language, + match d.s("section").as_str() { + "span" => "Span Columns", + "bullets" => "Bullets and Numbering", + _ => "Paragraph Borders and Shading", + }, + ), "insertXref" => crate::i18n::tr(&app.ui.language, "New Cross-Reference"), "findFont" => crate::i18n::tr(&app.ui.language, "Find/Replace Font"), "polygonSettings" => crate::i18n::tr(&app.ui.language, "Polygon Settings"), @@ -1393,6 +1401,7 @@ pub fn show(app: &mut DesignApp, ctx: &egui::Context) { let current = d.fields.get("current").cloned().unwrap_or_default(); paragraph_rules(app, ui, &mut d, "", ¤t); } + "paragraphLocal" => paragraph_local(app, ui, &mut d), "insertXref" => insert_xref(app, ui, &mut d), "findFont" => find_font(app, ui, &mut d), "colorPicker" => color_picker(ui, &mut d), @@ -1855,6 +1864,13 @@ pub fn confirm(app: &mut DesignApp) -> Result { } Ok(r) } + "paragraphLocal" => { + let attrs: Map = d.fields.iter().filter_map(|(k, v)| k.strip_prefix("p.").map(|a| (a.to_string(), v.clone()))).collect(); + if attrs.is_empty() { + return Ok(Value::Null); + } + app.run("type.para", json!({"attrs": attrs})) + } "paragraphRules" => { let attrs = rule_edits(&d, ""); if attrs.is_empty() { @@ -2621,7 +2637,12 @@ fn paragraph_style_options(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialo "shading" => border_shading(&app.ui.language, ui, d, &pv, &doc, false), "keep" => keep_options(&app.ui.language, ui, d, &pv), "span" => span_columns(&app.ui.language, ui, d, &pv, &doc), - "bullets" => bullets_and_numbering(&app.ui.language, ui, d, &pv, &doc), + "bullets" => { + let (char_styles, families, fonts, scope) = ListUi::new_parts(app); + let lu = + ListUi { char_styles: &char_styles, families: &families, fonts: &fonts, scope, family: &cp.font_family, style: &cp.font_style }; + bullets_and_numbering(&app.ui.language, ui, d, &pv, &doc, &lu); + } _ => { egui::Grid::new("psg").num_columns(2).spacing([8.0, 6.0]).show(ui, |ui| { crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Style Name:")); @@ -2840,42 +2861,174 @@ fn swatch_menu(ui: &mut egui::Ui, doc: &designcraft_doc::Document, id: impl egui picked } -/// Paragraph Style Options › Paragraph Border / Paragraph Shading: on or off, colour, tint and the -/// offsets past the text; the border also has a stroke weight per side. +/// Built-in stroke types of the paragraph border's Type menu (serialized `StrokeType`s). +const BORDER_TYPES: [&str; 11] = + ["solid", "dashed", "dotted", "thickThin", "thinThick", "thinThin", "thickThick", "thinThickThin", "thickThinThick", "wavy", "hashed"]; + +/// The border's stroke type: a built-in type or one of the document's stroke styles. +fn border_type_menu(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Value, doc: &designcraft_doc::Document) { + use designcraft_doc::StrokeType; + let cur: StrokeType = serde_json::from_value(para_value(d, pv, "borderType")).unwrap_or_default(); + let shown = match &cur { + 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| { + 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::(ty.clone()) else { continue }; + let on = std::mem::discriminant(&t) == std::mem::discriminant(&cur); + if ui.selectable_label(on, crate::rtl::widget(ui, crate::i18n::tr(lang, t.label()))).clicked() && !on { + para_set(d, "borderType", ty); + } + } + for st in &doc.stroke_styles { + let on = matches!(&cur, StrokeType::Style { name } if *name == st.name); + if ui.selectable_label(on, &st.name).clicked() { + para_set(d, "borderType", json!({"kind": "style", "name": st.name})); + } + } + }); +} + +/// Corner size and shape of a border or shading (`key` holds top left, top right, bottom right, +/// bottom left); an edited corner stores all four. +fn para_corners(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Value, key: &str, units: Unit) { + use designcraft_geom::corners::{Corner, CornerShape}; + let mut corners: [Corner; 4] = serde_json::from_value(para_value(d, pv, key)).unwrap_or_default(); + let mut changed = false; + egui::Grid::new(("pscorners", key)).num_columns(3).spacing([8.0, 6.0]).show(ui, |ui| { + for (i, label) in ["Top Left:", "Top Right:", "Bottom Right:", "Bottom Left:"].into_iter().enumerate() { + let Some(c) = corners.get_mut(i) else { continue }; + crate::rtl::label(ui, crate::i18n::tr(lang, label)); + if let Some(v) = crate::widgets::measure(ui, &format!("ps{key}{i}"), Some(c.size), units, 70.0) { + 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)) + .selected_text(crate::rtl::widget(ui, crate::i18n::tr(lang, c.shape.label()))) + .width(130.0) + .show_ui(ui, |ui| { + for shape in CornerShape::ALL { + if ui.selectable_label(c.shape == shape, crate::rtl::widget(ui, crate::i18n::tr(lang, shape.label()))).clicked() { + c.shape = shape; + changed = true; + } + } + }); + ui.end_row(); + } + }); + if changed { + para_set(d, key, serde_json::to_value(corners).unwrap_or_default()); + } +} + +/// The colour and tint rows of a border, its gap or shading (`keys`: colour, tint), in a grid. +fn para_color_tint( + ui: &mut egui::Ui, + d: &mut Dialog, + pv: &Value, + doc: &designcraft_doc::Document, + keys: (&str, &str), + labels: (&str, &str), + enabled: bool, +) { + let (color_key, tint_key) = keys; + crate::rtl::label(ui, labels.0); + let color = para_value(d, pv, color_key).as_str().unwrap_or("").to_string(); + if let Some(w) = ui.add_enabled_ui(enabled, |ui| swatch_menu(ui, doc, ("pscolor", color_key), &color, 160.0)).inner { + para_set(d, color_key, json!(w)); + } + ui.end_row(); + crate::rtl::label(ui, labels.1); + let tint = para_value(d, pv, tint_key).as_f64().map(|t| t * 100.0); + if let Some(v) = ui.add_enabled_ui(enabled, |ui| crate::widgets::number(ui, &format!("ps{tint_key}"), tint, "%", 60.0, 0)).inner { + para_set(d, tint_key, json!(if v.is_finite() { (v / 100.0).clamp(0.0, 1.0) } else { 1.0 })); + } + ui.end_row(); +} + +/// Paragraph Style Options › Paragraph Border / Paragraph Shading (and the Paragraph Borders and +/// Shading dialog): on or off, the stroke (border) or colour (shading), corners, offsets, the +/// edges and width the box follows, and the options for split and consecutive paragraphs. fn border_shading(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Value, doc: &designcraft_doc::Document, border: bool) { let key = |field: &str| format!("{}{field}", if border { "border" } else { "shading" }); let on_key = key("On"); para_check(ui, d, pv, &on_key, crate::i18n::tr(lang, if border { "Border" } else { "Shading" })); let on = para_value(d, pv, &on_key).as_bool().unwrap_or(false); let units = doc.settings.horizontal_units; - ui.add_space(6.0); - ui.add_enabled_ui(on, |ui| { - if border { - crate::rtl::label(ui, egui::RichText::new(crate::i18n::tr(lang, "Stroke")).font(semibold(12.0))); - para_sides(lang, ui, d, pv, &key("Weights"), &|ui: &mut egui::Ui, id: &str, v: Option| { - crate::widgets::number(ui, id, v, " pt", 60.0, 2).map(|w| w.clamp(0.0, 1000.0)) + let head = |ui: &mut egui::Ui, t: &str| { + ui.add_space(4.0); + crate::rtl::label(ui, egui::RichText::new(crate::i18n::tr(lang, t)).font(semibold(12.0))); + }; + ui.add_space(4.0); + egui::ScrollArea::vertical().id_salt(("psbsscroll", border)).max_height(330.0).show(ui, |ui| { + ui.add_enabled_ui(on, |ui| { + if border { + head(ui, "Stroke"); + para_sides(lang, ui, d, pv, &key("Weights"), &|ui: &mut egui::Ui, id: &str, v: Option| { + crate::widgets::number(ui, id, v, " pt", 60.0, 2).map(|w| if w.is_finite() { w.clamp(0.0, 1000.0) } else { 0.0 }) + }); + ui.add_space(4.0); + } else { + head(ui, "Color"); + } + egui::Grid::new(("psbs", border)).num_columns(2).spacing([8.0, 6.0]).show(ui, |ui| { + if border { + crate::rtl::label(ui, crate::i18n::tr(lang, "Type:")); + border_type_menu(lang, ui, d, pv, doc); + ui.end_row(); + } + let (color, tint) = (crate::i18n::tr(lang, "Color:"), crate::i18n::tr(lang, "Tint:")); + para_color_tint(ui, d, pv, doc, (&key("Color"), &key("Tint")), (color, tint), true); + if border { + // The gap colour shows between the dashes, dots or stripes. + let solid = para_value(d, pv, "borderType").get("kind").and_then(Value::as_str).is_none_or(|k| k == "solid"); + let (gap, gap_tint) = (crate::i18n::tr(lang, "Gap Color:"), crate::i18n::tr(lang, "Gap Tint:")); + para_color_tint(ui, d, pv, doc, ("borderGapColor", "borderGapTint"), (gap, gap_tint), !solid); + crate::rtl::label(ui, crate::i18n::tr(lang, "Cap:")); + para_choice(lang, ui, d, pv, "borderCap", &[("butt", "Butt"), ("round", "Round"), ("projecting", "Projecting")]); + ui.end_row(); + crate::rtl::label(ui, crate::i18n::tr(lang, "Join:")); + para_choice(lang, ui, d, pv, "borderJoin", &[("miter", "Miter"), ("round", "Round"), ("bevel", "Bevel")]); + ui.end_row(); + } + }); + head(ui, "Corner Size and Shape"); + para_corners(lang, ui, d, pv, &key("Corners"), units); + head(ui, "Offsets"); + para_sides(lang, ui, d, pv, &key("Offsets"), &|ui: &mut egui::Ui, id: &str, v: Option| { + crate::widgets::measure(ui, id, v, units, 60.0) }); ui.add_space(4.0); - } - egui::Grid::new(("psbs", border)).num_columns(2).spacing([8.0, 6.0]).show(ui, |ui| { - crate::rtl::label(ui, crate::i18n::tr(lang, "Color:")); - let color_key = key("Color"); - let color = para_value(d, pv, &color_key).as_str().unwrap_or("").to_string(); - if let Some(w) = swatch_menu(ui, doc, ("pscolor", border), &color, 160.0) { - para_set(d, &color_key, json!(w)); + egui::Grid::new(("psbsedges", border)).num_columns(2).spacing([8.0, 6.0]).show(ui, |ui| { + crate::rtl::label(ui, crate::i18n::tr(lang, "Top Edge:")); + para_choice( + lang, + ui, + d, + pv, + &key("Top"), + &[("ascent", "Ascent"), ("baseline", "Baseline"), ("emBox", "Em Box Top"), ("leading", "Leading")], + ); + ui.end_row(); + crate::rtl::label(ui, crate::i18n::tr(lang, "Bottom Edge:")); + para_choice(lang, ui, d, pv, &key("Bottom"), &[("descent", "Descent"), ("baseline", "Baseline"), ("emBox", "Em Box Bottom")]); + ui.end_row(); + crate::rtl::label(ui, crate::i18n::tr(lang, "Width:")); + para_choice(lang, ui, d, pv, &key("Width"), &[("column", "Column"), ("text", "Text")]); + ui.end_row(); + }); + ui.add_space(4.0); + if border { + para_check(ui, d, pv, "borderDisplayIfSplits", crate::i18n::tr(lang, "Display Border if Paragraph Splits Across Frames/Columns")); + para_check(ui, d, pv, "borderMerge", crate::i18n::tr(lang, "Merge Consecutive Borders and Shading")); + } else { + para_check(ui, d, pv, "shadingClip", crate::i18n::tr(lang, "Clip to Frame")); + para_check(ui, d, pv, "shadingNonprinting", crate::i18n::tr(lang, "Do not Print or Export")); } - ui.end_row(); - crate::rtl::label(ui, crate::i18n::tr(lang, "Tint:")); - let tint_key = key("Tint"); - let tint = para_value(d, pv, &tint_key).as_f64().map(|t| t * 100.0); - if let Some(v) = crate::widgets::number(ui, &format!("ps{tint_key}"), tint, "%", 60.0, 0) { - para_set(d, &tint_key, json!((v / 100.0).clamp(0.0, 1.0))); - } - ui.end_row(); }); - ui.add_space(4.0); - crate::rtl::label(ui, egui::RichText::new(crate::i18n::tr(lang, "Offsets")).font(semibold(12.0))); - para_sides(lang, ui, d, pv, &key("Offsets"), &|ui: &mut egui::Ui, id: &str, v: Option| crate::widgets::measure(ui, id, v, units, 60.0)); }); } @@ -2932,8 +3085,9 @@ fn keep_options(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Value) { }); } -/// Paragraph Style Options › Span Columns: one column, spanning columns (0 = all), or split into -/// sub-columns with inside and outside gutters. +/// Paragraph Style Options › Span Columns (and the Span Columns dialog): one column, spanning +/// columns (0 = all), or split into sub-columns with inside and outside gutters; the least space +/// before and after a spanning or split paragraph. fn span_columns(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Value, doc: &designcraft_doc::Document) { use designcraft_doc::SpanColumns; let cur: SpanColumns = serde_json::from_value(para_value(d, pv, "spanColumns")).unwrap_or_default(); @@ -2977,11 +3131,26 @@ fn span_columns(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Value, doc: next = Some(SpanColumns::Split(if c.is_finite() { c.round().clamp(2.0, 20.0) as u32 } else { 2 })); } ui.end_row(); - for (label, key) in [("Inside Gutter:", "splitInsideGutter"), ("Outside Gutter:", "splitOutsideGutter")] { - crate::rtl::label(ui, crate::i18n::tr(lang, label)); - para_measure(ui, d, pv, key, units); - ui.end_row(); - } + } + } + let v_units = doc.settings.vertical_units; + let spaces = match cur { + SpanColumns::Single => None, + SpanColumns::Span(_) => Some(("Space Before Span:", "Space After Span:")), + SpanColumns::Split(_) => Some(("Space Before Split:", "Space After Split:")), + }; + if let Some((before, after)) = spaces { + for (label, key) in [(before, "spanSpaceBefore"), (after, "spanSpaceAfter")] { + crate::rtl::label(ui, crate::i18n::tr(lang, label)); + para_measure(ui, d, pv, key, v_units); + ui.end_row(); + } + } + if matches!(cur, SpanColumns::Split(_)) { + for (label, key) in [("Inside Gutter:", "splitInsideGutter"), ("Outside Gutter:", "splitOutsideGutter")] { + crate::rtl::label(ui, crate::i18n::tr(lang, label)); + para_measure(ui, d, pv, key, units); + ui.end_row(); } } }); @@ -3014,86 +3183,283 @@ fn list_text(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Value, label: & ui.end_row(); } -/// Paragraph Style Options › Bullets and Numbering: the list type; the bullet and the text after -/// it, or the list, number style, number text and where numbering starts; then the indents that -/// place the label. -fn bullets_and_numbering(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Value, doc: &designcraft_doc::Document) { - let units = doc.settings.horizontal_units; - let kind = para_value(d, pv, "listType").as_str().unwrap_or("none").to_string(); - egui::Grid::new("psbn").num_columns(2).spacing([8.0, 6.0]).show(ui, |ui| { - crate::rtl::label(ui, crate::i18n::tr(lang, "List Type:")); - para_choice(lang, ui, d, pv, "listType", &[("none", "None"), ("bullets", "Bullets"), ("numbers", "Numbers")]); - ui.end_row(); - match kind.as_str() { - "bullets" => { - crate::rtl::label(ui, crate::i18n::tr(lang, "Bullet Character:")); - ui.horizontal(|ui| { - let cur = para_value(d, pv, "bulletChar").as_str().unwrap_or("").to_string(); - for b in ["\u{2022}", "\u{2013}", "\u{2014}", "\u{00BB}", "*"] { - if ui.add_sized([24.0, 20.0], egui::Button::selectable(cur == b, b)).clicked() { - para_set(d, "bulletChar", json!(b)); - } - } - let mut s = cur; - if ui.add(egui::TextEdit::singleline(&mut s).desired_width(36.0)).changed() { - para_set(d, "bulletChar", json!(s)); - } - }); - ui.end_row(); - list_text(lang, ui, d, pv, "Text After:", "listSeparator"); +/// What the Bullets and Numbering section offers besides the paragraph's attributes: the +/// document's character styles, the font families for the bullet, the fonts, and the paragraph's +/// font (the bullet's when it has none of its own). +struct ListUi<'a> { + char_styles: &'a [String], + families: &'a [String], + fonts: &'a designcraft_fonts::ScopedFonts<'static>, + scope: u32, + family: &'a str, + style: &'a str, +} + +impl ListUi<'_> { + fn new_parts(app: &DesignApp) -> (Vec, Vec, designcraft_fonts::ScopedFonts<'static>, u32) { + let styles = app.session.active().map(|s| s.doc.styles.character.iter().map(|c| c.name.clone()).collect()).unwrap_or_default(); + let families = crate::panels::font_menu(app).into_iter().map(|f| f.family).collect(); + (styles, families, crate::panels::fonts(app), crate::panels::font_scope(app)) + } +} + +/// 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| { + 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)) + { + if ui.selectable_label(n == cur, crate::rtl::widget(ui, crate::i18n::style_name(lang, n))).clicked() { + para_set(d, key, json!(n)); + } } - "numbers" => { - crate::rtl::label(ui, crate::i18n::tr(lang, "List:")); - 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| { - 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)); - } - } - }); - ui.end_row(); - crate::rtl::label(ui, crate::i18n::tr(lang, "Format:")); - para_choice(lang, ui, d, pv, "numberStyle", &NUMBER_STYLES); - ui.end_row(); - list_text(lang, ui, d, pv, "Number:", "numberExpression"); - crate::rtl::label(ui, crate::i18n::tr(lang, "Mode:")); - 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| { - if ui - .selectable_label(start.is_none(), crate::rtl::widget(ui, crate::i18n::tr(lang, "Continue from Previous Number"))) - .clicked() - { - para_set(d, "startAt", Value::Null); - } - if ui.selectable_label(start.is_some(), crate::rtl::widget(ui, crate::i18n::tr(lang, "Start At"))).clicked() - && start.is_none() - { - para_set(d, "startAt", json!(1)); - } - }); - if let Some(n) = start - && let Some(v) = crate::widgets::number(ui, "psstartat", Some(n as f64), "", 60.0, 0) - { - para_set(d, "startAt", json!(if v.is_finite() { v.round().clamp(0.0, 1_000_000.0) as u32 } else { 1 })); - } - }); - ui.end_row(); + }, + ); +} + +/// The bullet character: common bullets, a field, and the font's glyphs in a grid to pick from. +fn bullet_char(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Value, lu: &ListUi<'_>) { + crate::rtl::label(ui, crate::i18n::tr(lang, "Bullet Character:")); + ui.horizontal(|ui| { + let cur = para_value(d, pv, "bulletChar").as_str().unwrap_or("").to_string(); + for b in ["\u{2022}", "\u{2013}", "\u{2014}", "\u{00BB}", "*"] { + if ui.add_sized([24.0, 20.0], egui::Button::selectable(cur == b, b)).clicked() { + para_set(d, "bulletChar", json!(b)); } - _ => {} } - if kind != "none" { - for (label, key) in [("Left Indent:", "leftIndent"), ("First Line Indent:", "firstLineIndent")] { - crate::rtl::label(ui, crate::i18n::tr(lang, label)); - para_measure(ui, d, pv, key, units); - ui.end_row(); - } + let mut s = cur; + if ui.add(egui::TextEdit::singleline(&mut s).desired_width(36.0)).changed() { + para_set(d, "bulletChar", json!(s)); + } + let open = d.b("bulletGlyphs"); + if ui.selectable_label(open, crate::rtl::widget(ui, crate::i18n::tr(lang, "Glyphs…"))).clicked() { + d.fields.insert("bulletGlyphs".into(), json!(!open)); } }); + ui.end_row(); + if d.b("bulletGlyphs") { + ui.label(""); + ui.vertical(|ui| { + ui.set_width(300.0); + let font = para_value(d, pv, "bulletFont").as_str().unwrap_or("").to_string(); + let (family, style) = if font.is_empty() { + (lu.family.to_string(), lu.style.to_string()) + } else { + let st = para_value(d, pv, "bulletFontStyle").as_str().unwrap_or("").to_string(); + let st = if st.is_empty() { lu.fonts.styles(&font).into_iter().next().unwrap_or_else(|| "Regular".into()) } else { st }; + (font, st) + }; + let mut q = d.s("bulletGlyphQuery"); + if ui + .add( + egui::TextEdit::singleline(&mut q) + .hint_text(crate::rtl::widget(ui, crate::i18n::tr(lang, "Search: character or U+code"))) + .desired_width(300.0), + ) + .changed() + { + d.fields.insert("bulletGlyphQuery".into(), json!(q)); + } + if let (Some(c), _) = crate::panels::glyphs::grid(ui, lu.fonts, lu.scope, &family, &style, &q, 160.0) { + para_set(d, "bulletChar", json!(c.to_string())); + } + }); + ui.end_row(); + } +} + +/// The bullet's font family and style ("" = the paragraph's). +fn bullet_font(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Value, lu: &ListUi<'_>) { + 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( + ui, + |ui| { + if ui.selectable_label(font.is_empty(), inherit).clicked() { + para_set(d, "bulletFont", json!("")); + para_set(d, "bulletFontStyle", json!("")); + } + for f in lu.families { + if ui.selectable_label(*f == font, f).clicked() { + para_set(d, "bulletFont", json!(f)); + para_set(d, "bulletFontStyle", json!(lu.fonts.styles(f).into_iter().next().unwrap_or_default())); + } + } + }, + ); + ui.end_row(); + 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| { + for st in lu.fonts.styles(&font) { + if ui.selectable_label(st == style, &st).clicked() { + para_set(d, "bulletFontStyle", json!(st)); + } + } + }); + }); + ui.end_row(); +} + +/// Paragraph Style Options › Bullets and Numbering (and the Bullets and Numbering dialog): the +/// list type and level; the bullet, its font and the text after it, or the list, number style, +/// number text and where numbering starts and restarts; the label's character style; then where +/// the label goes (alignment, indents and the tab position of the text after it). +fn bullets_and_numbering(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Value, doc: &designcraft_doc::Document, lu: &ListUi<'_>) { + let units = doc.settings.horizontal_units; + let kind = para_value(d, pv, "listType").as_str().unwrap_or("none").to_string(); + let level = para_value(d, pv, "listLevel").as_u64().unwrap_or(1).clamp(1, designcraft_doc::LIST_LEVELS as u64) as u32; + egui::ScrollArea::vertical().id_salt("psbnscroll").max_height(340.0).show(ui, |ui| { + egui::Grid::new("psbn").num_columns(2).spacing([8.0, 6.0]).show(ui, |ui| { + crate::rtl::label(ui, crate::i18n::tr(lang, "List Type:")); + para_choice(lang, ui, d, pv, "listType", &[("none", "None"), ("bullets", "Bullets"), ("numbers", "Numbers")]); + ui.end_row(); + if kind != "none" { + crate::rtl::label(ui, crate::i18n::tr(lang, "Level:")); + para_count(ui, d, pv, "listLevel", (1, designcraft_doc::LIST_LEVELS as u32)); + ui.end_row(); + } + match kind.as_str() { + "bullets" => { + bullet_char(lang, ui, d, pv, lu); + bullet_font(lang, ui, d, pv, lu); + list_text(lang, ui, d, pv, "Text After:", "listSeparator"); + crate::rtl::label(ui, crate::i18n::tr(lang, "Character Style:")); + list_char_style(lang, ui, d, pv, "bulletCharStyle", lu.char_styles); + ui.end_row(); + } + "numbers" => { + crate::rtl::label(ui, crate::i18n::tr(lang, "List:")); + 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| { + 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)); + } + } + }); + ui.end_row(); + crate::rtl::label(ui, crate::i18n::tr(lang, "Format:")); + para_choice(lang, ui, d, pv, "numberStyle", &NUMBER_STYLES); + ui.end_row(); + list_text(lang, ui, d, pv, "Number:", "numberExpression"); + crate::rtl::label(ui, crate::i18n::tr(lang, "Character Style:")); + list_char_style(lang, ui, d, pv, "numberCharStyle", lu.char_styles); + ui.end_row(); + crate::rtl::label(ui, crate::i18n::tr(lang, "Mode:")); + 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| { + if ui + .selectable_label(start.is_none(), crate::rtl::widget(ui, crate::i18n::tr(lang, "Continue from Previous Number"))) + .clicked() + { + para_set(d, "startAt", Value::Null); + } + if ui.selectable_label(start.is_some(), crate::rtl::widget(ui, crate::i18n::tr(lang, "Start At"))).clicked() + && start.is_none() + { + para_set(d, "startAt", json!(1)); + } + }); + if let Some(n) = start + && let Some(v) = crate::widgets::number(ui, "psstartat", Some(n as f64), "", 60.0, 0) + { + para_set(d, "startAt", json!(if v.is_finite() { v.round().clamp(0.0, 1_000_000.0) as u32 } else { 1 })); + } + }); + ui.end_row(); + // Restart Numbers at This Level After: a higher level (any, or one of them). + ui.label(""); + ui.add_enabled_ui(level > 1, |ui| { + ui.horizontal(|ui| { + 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| { + 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(); + } + _ => {} + } + if kind != "none" { + crate::rtl::label(ui, crate::i18n::tr(lang, "Alignment:")); + let key = if kind == "bullets" { "bulletAlign" } else { "numberAlign" }; + para_choice(lang, ui, d, pv, key, &[("left", "Left"), ("center", "Center"), ("right", "Right")]); + ui.end_row(); + for (label, key) in [("Left Indent:", "leftIndent"), ("First Line Indent:", "firstLineIndent")] { + crate::rtl::label(ui, crate::i18n::tr(lang, label)); + para_measure(ui, d, pv, key, units); + ui.end_row(); + } + // Tab Position: none, or a stop for the text after the label. + crate::rtl::label(ui, crate::i18n::tr(lang, "Tab Position:")); + ui.horizontal(|ui| { + let tab = para_value(d, pv, "listTab").as_f64(); + let mut on = tab.is_some(); + if ui.checkbox(&mut on, "").changed() { + let left = para_value(d, pv, "leftIndent").as_f64().filter(|v| v.is_finite() && *v > 0.0).unwrap_or(18.0); + para_set(d, "listTab", if on { json!(left) } else { Value::Null }); + } + if let Some(t) = tab + && let Some(v) = crate::widgets::measure(ui, "pslisttab", Some(t), units, 80.0) + { + para_set(d, "listTab", json!(if v.is_finite() { v.max(0.0) } else { 0.0 })); + } + }); + ui.end_row(); + } + }); + }); +} + +/// Paragraph Borders and Shading, Span Columns, and Bullets and Numbering for the selected +/// paragraphs: the Paragraph Style Options sections over the paragraphs' attributes (`current`); +/// OK applies the edits (`p.`) as local formatting. +fn paragraph_local(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog) { + let Some(doc) = app.session.active().map(|s| s.doc.clone()) else { return }; + let pv = d.fields.get("current").cloned().unwrap_or_default(); + let lang = app.ui.language.clone(); + ui.set_min_width(480.0); + match d.s("section").as_str() { + "span" => span_columns(&lang, ui, d, &pv, &doc), + "bullets" => { + let (char_styles, families, fonts, scope) = ListUi::new_parts(app); + let (family, style) = (d.s("family"), d.s("style")); + let lu = ListUi { char_styles: &char_styles, families: &families, fonts: &fonts, scope, family: &family, style: &style }; + bullets_and_numbering(&lang, ui, d, &pv, &doc, &lu); + } + section => { + let border = section != "shading"; + ui.horizontal(|ui| { + for (tab, label) in [("border", "Border"), ("shading", "Shading")] { + if ui.selectable_label(section == tab, crate::rtl::widget(ui, crate::i18n::tr(&lang, label))).clicked() { + d.fields.insert("section".into(), json!(tab)); + } + } + }); + ui.separator(); + border_shading(&lang, ui, d, &pv, &doc, border); + } + } } /// The section list on the left of the style options dialogs. diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index 7f4ba0f4a..acb0ec593 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -2892,6 +2892,99 @@ const TABLE: &[(&str, [&str; 5])] = &[ ("Numbers", ["Nummerierung", "Numéros", "Números", "自動番号", "编号"]), ("Bullet Character:", ["Aufzählungszeichen:", "Caractère de puce:", "Carácter de viñeta:", "箇条書き記号:", "项目符号字符:"]), ("Text After:", ["Text danach:", "Texte après:", "Texto después:", "後続テキスト:", "后续文本:"]), + ("Gap Tint:", ["Lückenfarbton:", "Teinte de l'espace:", "Matiz del hueco:", "すき間の濃淡:", "间隙色调:"]), + ( + "Corner Size and Shape", + ["Eckengröße und -form", "Taille et forme des angles", "Tamaño y forma de esquinas", "角のサイズとシェイプ", "角大小和形状"], + ), + ("Top Left:", ["Oben links:", "En haut à gauche:", "Superior izquierda:", "左上:", "左上:"]), + ("Top Right:", ["Oben rechts:", "En haut à droite:", "Superior derecha:", "右上:", "右上:"]), + ("Bottom Right:", ["Unten rechts:", "En bas à droite:", "Inferior derecha:", "右下:", "右下:"]), + ("Bottom Left:", ["Unten links:", "En bas à gauche:", "Inferior izquierda:", "左下:", "左下:"]), + ("Rounded", ["Abgerundet", "Arrondi", "Redondeado", "角丸", "圆角"]), + ("Inverse Rounded", ["Umgekehrt abgerundet", "Arrondi inversé", "Redondeado inverso", "逆角丸", "反向圆角"]), + ("Inset", ["Eingezogen", "En retrait", "Hundido", "内側に切り欠き", "内凹"]), + ("Fancy", ["Verziert", "Fantaisie", "Decorativo", "装飾", "花式"]), + ("Top Edge:", ["Obere Kante:", "Bord supérieur:", "Borde superior:", "上端:", "上边缘:"]), + ("Bottom Edge:", ["Untere Kante:", "Bord inférieur:", "Borde inferior:", "下端:", "下边缘:"]), + ("Em Box Top", ["Geviert oben", "Haut du cadratin", "Parte superior del cuadratín", "仮想ボディの上", "全角框顶部"]), + ("Descent", ["Unterlänge", "Jambage inférieur", "Descendente", "ディセント", "下伸部"]), + ("Em Box Bottom", ["Geviert unten", "Bas du cadratin", "Parte inferior del cuadratín", "仮想ボディの下", "全角框底部"]), + ( + "Display Border if Paragraph Splits Across Frames/Columns", + [ + "Rahmen anzeigen, wenn der Absatz auf Rahmen/Spalten verteilt ist", + "Afficher le contour si le paragraphe est réparti sur plusieurs blocs/colonnes", + "Mostrar borde si el párrafo se divide entre marcos/columnas", + "段落がフレームや段をまたぐときに囲み罫を表示", + "段落跨框架/栏拆分时显示边框", + ], + ), + ( + "Merge Consecutive Borders and Shading", + [ + "Aufeinanderfolgende Rahmen und Schattierungen zusammenführen", + "Fusionner les contours et trames consécutifs", + "Combinar bordes y sombreados consecutivos", + "連続する囲み罫と背景色を結合", + "合并连续的边框和底纹", + ], + ), + ("Clip to Frame", ["Auf Rahmen beschneiden", "Découper selon le bloc", "Recortar al marco", "フレームでクリップ", "剪切到框架"]), + ( + "Do not Print or Export", + ["Nicht drucken oder exportieren", "Ne pas imprimer ni exporter", "No imprimir ni exportar", "印刷・書き出ししない", "不打印或导出"], + ), + ("Space Before Span:", ["Abstand vor Überspannung:", "Espace avant l'étendue:", "Espacio antes de abarcar:", "段抜き前のアキ:", "跨栏前间距:"]), + ( + "Space After Span:", + ["Abstand nach Überspannung:", "Espace après l'étendue:", "Espacio después de abarcar:", "段抜き後のアキ:", "跨栏后间距:"], + ), + ("Space Before Split:", ["Abstand vor Teilung:", "Espace avant la division:", "Espacio antes de dividir:", "段分割前のアキ:", "拆分前间距:"]), + ("Space After Split:", ["Abstand nach Teilung:", "Espace après la division:", "Espacio después de dividir:", "段分割後のアキ:", "拆分后间距:"]), + ("Level:", ["Ebene:", "Niveau:", "Nivel:", "レベル:", "级别:"]), + ("Glyphs…", ["Glyphen…", "Glyphes…", "Glifos…", "字形…", "字形…"]), + ("(Paragraph Font)", ["(Absatzschrift)", "(Police du paragraphe)", "(Fuente del párrafo)", "(段落のフォント)", "(段落字体)"]), + ( + "Restart Numbers at This Level After:", + [ + "Nummerierung auf dieser Ebene neu beginnen nach:", + "Recommencer la numérotation à ce niveau après:", + "Reiniciar números en este nivel después de:", + "このレベルの番号を振り直すきっかけ:", + "在此级别后重新编号:", + ], + ), + ( + "Any Previous Level", + ["Einer beliebigen höheren Ebene", "Tout niveau précédent", "Cualquier nivel anterior", "任意の上位レベル", "任何上一级别"], + ), + ("Tab Position:", ["Tabulatorposition:", "Position de la tabulation:", "Posición de tabulación:", "タブ位置:", "制表符位置:"]), + ( + "Paragraph Borders and Shading…", + [ + "Absatzrahmen und -schattierung…", + "Contours et trames de paragraphe…", + "Bordes y sombreado de párrafo…", + "段落囲み罫と段落背景色…", + "段落边框和底纹…", + ], + ), + ( + "Paragraph Borders and Shading", + [ + "Absatzrahmen und -schattierung", + "Contours et trames de paragraphe", + "Bordes y sombreado de párrafo", + "段落囲み罫と段落背景色", + "段落边框和底纹", + ], + ), + ("Span Columns…", ["Spalten überspannen…", "Étendre sur les colonnes…", "Abarcar columnas…", "段抜きと段分割…", "跨栏…"]), + ( + "Bullets and Numbering…", + ["Aufzählungszeichen und Nummerierung…", "Puces et numérotation…", "Viñetas y numeración…", "箇条書き記号と番号付け…", "项目符号和编号…"], + ), ("List:", ["Liste:", "Liste:", "Lista:", "リスト:", "列表:"]), ("[Default]", ["[Standard]", "[Par défaut]", "[Predeterminado]", "[デフォルト]", "[默认]"]), ("Number:", ["Zahl:", "Numéro:", "Número:", "番号:", "编号:"]), diff --git a/crates/ui-egui/src/i18n/ar.rs b/crates/ui-egui/src/i18n/ar.rs index f39830cfa..54b3ae45c 100644 --- a/crates/ui-egui/src/i18n/ar.rs +++ b/crates/ui-egui/src/i18n/ar.rs @@ -1513,6 +1513,39 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Numbers", "أرقام"), ("Bullet Character:", "حرف التعداد النقطي:"), ("Text After:", "النص بعده:"), + ("Gap Tint:", "تدرج الفجوة:"), + ("Corner Size and Shape", "حجم الزوايا وشكلها"), + ("Top Left:", "أعلى اليسار:"), + ("Top Right:", "أعلى اليمين:"), + ("Bottom Right:", "أسفل اليمين:"), + ("Bottom Left:", "أسفل اليسار:"), + ("Rounded", "مستدير"), + ("Inverse Rounded", "مستدير معكوس"), + ("Inset", "داخلي"), + ("Fancy", "مزخرف"), + ("Top Edge:", "الحافة العلوية:"), + ("Bottom Edge:", "الحافة السفلية:"), + ("Em Box Top", "أعلى مربع em"), + ("Descent", "الانحدار السفلي"), + ("Em Box Bottom", "أسفل مربع em"), + ("Display Border if Paragraph Splits Across Frames/Columns", "إظهار الحد إذا انقسمت الفقرة عبر الإطارات/الأعمدة"), + ("Merge Consecutive Borders and Shading", "دمج الحدود والتظليل المتتالية"), + ("Clip to Frame", "قص حسب الإطار"), + ("Do not Print or Export", "عدم الطباعة أو التصدير"), + ("Space Before Span:", "المسافة قبل الامتداد:"), + ("Space After Span:", "المسافة بعد الامتداد:"), + ("Space Before Split:", "المسافة قبل التقسيم:"), + ("Space After Split:", "المسافة بعد التقسيم:"), + ("Level:", "المستوى:"), + ("Glyphs…", "الحروف الرسومية…"), + ("(Paragraph Font)", "(خط الفقرة)"), + ("Restart Numbers at This Level After:", "إعادة الترقيم في هذا المستوى بعد:"), + ("Any Previous Level", "أي مستوى سابق"), + ("Tab Position:", "موضع الجدولة:"), + ("Paragraph Borders and Shading…", "حدود الفقرة وتظليلها…"), + ("Paragraph Borders and Shading", "حدود الفقرة وتظليلها"), + ("Span Columns…", "امتداد الأعمدة…"), + ("Bullets and Numbering…", "التعداد النقطي والرقمي…"), ("List:", "القائمة:"), ("[Default]", "[افتراضي]"), ("Number:", "الرقم:"), diff --git a/crates/ui-egui/src/i18n/it.rs b/crates/ui-egui/src/i18n/it.rs index 6fc4af654..b82b7b615 100644 --- a/crates/ui-egui/src/i18n/it.rs +++ b/crates/ui-egui/src/i18n/it.rs @@ -1536,6 +1536,39 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Numbers", "Numeri"), ("Bullet Character:", "Carattere punto elenco:"), ("Text After:", "Testo dopo:"), + ("Gap Tint:", "Tinta spazio:"), + ("Corner Size and Shape", "Dimensione e forma angoli"), + ("Top Left:", "In alto a sinistra:"), + ("Top Right:", "In alto a destra:"), + ("Bottom Right:", "In basso a destra:"), + ("Bottom Left:", "In basso a sinistra:"), + ("Rounded", "Arrotondato"), + ("Inverse Rounded", "Arrotondato inverso"), + ("Inset", "Rientrato"), + ("Fancy", "Decorativo"), + ("Top Edge:", "Bordo superiore:"), + ("Bottom Edge:", "Bordo inferiore:"), + ("Em Box Top", "Parte superiore del quadratone"), + ("Descent", "Discendente"), + ("Em Box Bottom", "Parte inferiore del quadratone"), + ("Display Border if Paragraph Splits Across Frames/Columns", "Mostra bordo se il paragrafo si divide tra cornici/colonne"), + ("Merge Consecutive Borders and Shading", "Unisci bordi e ombreggiature consecutivi"), + ("Clip to Frame", "Ritaglia alla cornice"), + ("Do not Print or Export", "Non stampare né esportare"), + ("Space Before Span:", "Spazio prima dell'estensione:"), + ("Space After Span:", "Spazio dopo l'estensione:"), + ("Space Before Split:", "Spazio prima della divisione:"), + ("Space After Split:", "Spazio dopo la divisione:"), + ("Level:", "Livello:"), + ("Glyphs…", "Glifi…"), + ("(Paragraph Font)", "(Font del paragrafo)"), + ("Restart Numbers at This Level After:", "Riavvia numerazione a questo livello dopo:"), + ("Any Previous Level", "Qualsiasi livello precedente"), + ("Tab Position:", "Posizione tabulazione:"), + ("Paragraph Borders and Shading…", "Bordi e ombreggiatura paragrafo…"), + ("Paragraph Borders and Shading", "Bordi e ombreggiatura paragrafo"), + ("Span Columns…", "Estendi su colonne…"), + ("Bullets and Numbering…", "Elenchi puntati e numerati…"), ("List:", "Elenco:"), ("[Default]", "[Predefinito]"), ("Number:", "Numero:"), diff --git a/crates/ui-egui/src/i18n/ja.rs b/crates/ui-egui/src/i18n/ja.rs index 34f61c675..30d3b1145 100644 --- a/crates/ui-egui/src/i18n/ja.rs +++ b/crates/ui-egui/src/i18n/ja.rs @@ -1697,6 +1697,40 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Numbers", "自動番号"), ("Bullet Character:", "箇条書き記号:"), ("Text After:", "後続テキスト:"), + ("Gap Tint:", "すき間の濃淡:"), + ("Corner Size and Shape", "角のサイズとシェイプ"), + ("Top Left:", "左上:"), + ("Top Right:", "右上:"), + ("Bottom Right:", "右下:"), + ("Bottom Left:", "左下:"), + ("Rounded", "角丸"), + ("Inverse Rounded", "逆角丸"), + ("Inset", "内側に切り欠き"), + ("Fancy", "装飾"), + ("Top Edge:", "上端:"), + ("Bottom Edge:", "下端:"), + ("Em Box Top", "仮想ボディの上"), + ("Descent", "ディセント"), + ("Em Box Bottom", "仮想ボディの下"), + ("Display Border if Paragraph Splits Across Frames/Columns", "段落がフレームや段をまたぐときに囲み罫を表示"), + ("Merge Consecutive Borders and Shading", "連続する囲み罫と背景色を結合"), + ("Clip to Frame", "フレームでクリップ"), + ("Do not Print or Export", "印刷・書き出ししない"), + ("Space Before Span:", "段抜き前のアキ:"), + ("Space After Span:", "段抜き後のアキ:"), + ("Space Before Split:", "段分割前のアキ:"), + ("Space After Split:", "段分割後のアキ:"), + ("Level:", "レベル:"), + ("Glyphs…", "字形…"), + ("(Paragraph Font)", "(段落のフォント)"), + ("Restart Numbers at This Level After:", "このレベルの番号を振り直すきっかけ:"), + ("Any Previous Level", "任意の上位レベル"), + ("Tab Position:", "タブ位置:"), + ("Paragraph Borders and Shading…", "段落囲み罫と段落背景色…"), + ("Paragraph Borders and Shading", "段落囲み罫と段落背景色"), + ("Span Columns…", "段抜きと段分割…"), + ("Bullets and Numbering…", "箇条書き記号と番号付け…"), + ("Tint:", "濃淡:"), ("List:", "リスト:"), ("[Default]", "[デフォルト]"), ("Number:", "番号:"), diff --git a/crates/ui-egui/src/i18n/pt_br.rs b/crates/ui-egui/src/i18n/pt_br.rs index d9604d616..4b70d5efd 100644 --- a/crates/ui-egui/src/i18n/pt_br.rs +++ b/crates/ui-egui/src/i18n/pt_br.rs @@ -1118,6 +1118,39 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Numbers", "Números"), ("Bullet Character:", "Caractere de marcador:"), ("Text After:", "Texto após:"), + ("Gap Tint:", "Tonalidade do espaço:"), + ("Corner Size and Shape", "Tamanho e forma dos cantos"), + ("Top Left:", "Superior esquerdo:"), + ("Top Right:", "Superior direito:"), + ("Bottom Right:", "Inferior direito:"), + ("Bottom Left:", "Inferior esquerdo:"), + ("Rounded", "Arredondado"), + ("Inverse Rounded", "Arredondado inverso"), + ("Inset", "Recuado"), + ("Fancy", "Decorativo"), + ("Top Edge:", "Borda superior:"), + ("Bottom Edge:", "Borda inferior:"), + ("Em Box Top", "Topo do quadratim"), + ("Descent", "Descendente"), + ("Em Box Bottom", "Base do quadratim"), + ("Display Border if Paragraph Splits Across Frames/Columns", "Exibir borda se o parágrafo se dividir entre quadros/colunas"), + ("Merge Consecutive Borders and Shading", "Mesclar bordas e sombreamentos consecutivos"), + ("Clip to Frame", "Recortar no quadro"), + ("Do not Print or Export", "Não imprimir nem exportar"), + ("Space Before Span:", "Espaço antes da extensão:"), + ("Space After Span:", "Espaço após a extensão:"), + ("Space Before Split:", "Espaço antes da divisão:"), + ("Space After Split:", "Espaço após a divisão:"), + ("Level:", "Nível:"), + ("Glyphs…", "Glifos…"), + ("(Paragraph Font)", "(Fonte do parágrafo)"), + ("Restart Numbers at This Level After:", "Reiniciar números neste nível após:"), + ("Any Previous Level", "Qualquer nível anterior"), + ("Tab Position:", "Posição da tabulação:"), + ("Paragraph Borders and Shading…", "Bordas e sombreamento do parágrafo…"), + ("Paragraph Borders and Shading", "Bordas e sombreamento do parágrafo"), + ("Span Columns…", "Abranger colunas…"), + ("Bullets and Numbering…", "Marcadores e numeração…"), ("List:", "Lista:"), ("[Default]", "[Padrão]"), ("Number:", "Número:"), diff --git a/crates/ui-egui/src/i18n/uk.rs b/crates/ui-egui/src/i18n/uk.rs index a8e78dfef..df9e0f722 100644 --- a/crates/ui-egui/src/i18n/uk.rs +++ b/crates/ui-egui/src/i18n/uk.rs @@ -1499,6 +1499,39 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("Numbers", "Нумерація"), ("Bullet Character:", "Символ маркера:"), ("Text After:", "Текст після:"), + ("Gap Tint:", "Відтінок проміжку:"), + ("Corner Size and Shape", "Розмір і форма кутів"), + ("Top Left:", "Угорі ліворуч:"), + ("Top Right:", "Угорі праворуч:"), + ("Bottom Right:", "Унизу праворуч:"), + ("Bottom Left:", "Унизу ліворуч:"), + ("Rounded", "Заокруглений"), + ("Inverse Rounded", "Обернено заокруглений"), + ("Inset", "Вдавлений"), + ("Fancy", "Декоративний"), + ("Top Edge:", "Верхній край:"), + ("Bottom Edge:", "Нижній край:"), + ("Em Box Top", "Верх кегельної площадки"), + ("Descent", "Нижній виносний елемент"), + ("Em Box Bottom", "Низ кегельної площадки"), + ("Display Border if Paragraph Splits Across Frames/Columns", "Показувати рамку, якщо абзац розбито між фреймами/колонками"), + ("Merge Consecutive Borders and Shading", "Об’єднувати послідовні рамки й заливки"), + ("Clip to Frame", "Обтинати за фреймом"), + ("Do not Print or Export", "Не друкувати й не експортувати"), + ("Space Before Span:", "Відступ перед охопленням:"), + ("Space After Span:", "Відступ після охоплення:"), + ("Space Before Split:", "Відступ перед розділенням:"), + ("Space After Split:", "Відступ після розділення:"), + ("Level:", "Рівень:"), + ("Glyphs…", "Гліфи…"), + ("(Paragraph Font)", "(Шрифт абзацу)"), + ("Restart Numbers at This Level After:", "Починати нумерацію на цьому рівні заново після:"), + ("Any Previous Level", "Будь-якого попереднього рівня"), + ("Tab Position:", "Позиція табуляції:"), + ("Paragraph Borders and Shading…", "Рамки й заливка абзацу…"), + ("Paragraph Borders and Shading", "Рамки й заливка абзацу"), + ("Span Columns…", "Охоплення колонок…"), + ("Bullets and Numbering…", "Маркери та нумерація…"), ("List:", "Список:"), ("[Default]", "[Типово]"), ("Number:", "Номер:"), diff --git a/crates/ui-egui/src/menus.rs b/crates/ui-egui/src/menus.rs index 325ac0ed8..f0bc06337 100644 --- a/crates/ui-egui/src/menus.rs +++ b/crates/ui-egui/src/menus.rs @@ -68,6 +68,14 @@ pub const UI_COMMANDS: &[(&str, &str, Option<&str>, &str)] = &[ None, "{rule?: ruleAbove|ruleBelow} — Rule Above / Rule Below of the selected paragraphs (type.para)", ), + ( + "app.paragraphBordersDialog", + "Paragraph Borders and Shading…", + None, + "{tab?: border|shading} — border and shading of the selected paragraphs (type.para)", + ), + ("app.spanColumnsDialog", "Span Columns…", None, "{} — span or split columns of the selected paragraphs (type.para)"), + ("app.bulletsNumberingDialog", "Bullets and Numbering…", None, "{} — bullets or numbers of the selected paragraphs (type.para)"), ("app.findFontDialog", "Find/Replace Font…", None, "{} — fonts used (missing ones flagged) and replacing them"), ("app.insertXrefDialog", "Insert Cross-Reference…", None, "{} — New Cross-Reference dialog (paragraph or text anchor, format)"), ("app.deleteAllGuides", "Delete All Guides on Spread", None, "{} — the spread in view"), @@ -378,6 +386,9 @@ pub const MENUS: &[(&str, &[&str])] = &[ "cmd:type.alignRight", "cmd:type.justify", "ui:app.paragraphRulesDialog", + "ui:app.paragraphBordersDialog", + "ui:app.spanColumnsDialog", + "ui:app.bulletsNumberingDialog", "-", "cmd:type.bold", "cmd:type.italic", @@ -946,6 +957,23 @@ pub fn run_ui(app: &mut DesignApp, id: &str, p: &Value) -> Option { + let Some(a) = crate::panels::text_attrs(app) else { return Some(Err("select text or a text frame".into())) }; + let section = match id { + "app.spanColumnsDialog" => "span", + "app.bulletsNumberingDialog" => "bullets", + _ if p.get("tab").and_then(Value::as_str) == Some("shading") => "shading", + _ => "border", + }; + let fields = json!({ + "section": section, + "current": a["para"], + "family": a["chars"]["fontFamily"], + "style": a["chars"]["fontStyle"], + }); + app.ui.dialog = Some(crate::dialogs::Dialog::new("paragraphLocal", fields)); + Ok(Value::Null) + } "app.footnoteOptionsDialog" => { let Some(st) = app.session.active() else { return Some(Err("no document open".into())) }; let o = serde_json::to_value(&st.doc.footnote_options).unwrap_or_default(); diff --git a/crates/ui-egui/src/panels/glyphs.rs b/crates/ui-egui/src/panels/glyphs.rs index 3fb9d00eb..d72ffd2d2 100644 --- a/crates/ui-egui/src/panels/glyphs.rs +++ b/crates/ui-egui/src/panels/glyphs.rs @@ -1,4 +1,5 @@ -//! Glyphs panel: every character of a font in a grid; click to insert it at the text cursor. +//! Glyphs panel: every character of a font in a grid; click to insert it at the text cursor. The +//! grid also picks a list's bullet character. use egui::{Sense, vec2}; use serde_json::json; @@ -50,16 +51,7 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) { .hint_text(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Search: character or U+code"))) .desired_width(f32::INFINITY), ); - let face = db.face(&st.family, &st.style); let q = st.query.trim().to_string(); - let code = q.strip_prefix("U+").or_else(|| q.strip_prefix("u+")).and_then(|h| u32::from_str_radix(h, 16).ok()); - let mut chars: Vec = face.chars().into_iter().map(|(c, _)| c).filter(|c| !c.is_control()).collect(); - chars.sort(); - chars.dedup(); - if !q.is_empty() { - chars.retain(|c| Some(*c as u32) == code || q.contains(*c) || format!("{:04X}", *c as u32).contains(&q.to_ascii_uppercase())); - } - chars.truncate(MAX); let mut insert: Option = None; if !st.recent.is_empty() { ui.horizontal_wrapped(|ui| { @@ -71,15 +63,61 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) { } }); } + let (picked, count) = grid(ui, &db, scope, &st.family, &st.style, &q, 320.0); + insert = insert.or(picked); + if let Some(c) = insert { + match app.run("text.insert", json!({"text": c.to_string(), "raw": true})) { + Ok(_) => { + st.recent.retain(|x| *x != c); + st.recent.insert(0, c); + st.recent.truncate(12); + } + Err(e) => app.status(format!("Glyphs: {e}")), + } + } + crate::rtl::label( + ui, + egui::RichText::new( + crate::i18n::tr(&app.ui.language, "{count} glyphs — click to insert at the text cursor").replace("{count}", &count.to_string()), + ) + .size(10.5) + .color(t.text_dim), + ); + ui.data_mut(|d| d.insert_temp(id, st)); +} + +/// The characters of font `family` `style` in a grid (those matching `query`, a character or +/// U+code, when it isn't empty), at most `max_height` tall and scrolling: the character clicked, +/// and how many are shown. +pub fn grid( + ui: &mut egui::Ui, + db: &designcraft_fonts::ScopedFonts<'_>, + scope: u32, + family: &str, + style: &str, + query: &str, + max_height: f32, +) -> (Option, usize) { + let t = Tokens::get(ui.ctx()); + let face = db.face(family, style); + let q = query.trim(); + let code = q.strip_prefix("U+").or_else(|| q.strip_prefix("u+")).and_then(|h| u32::from_str_radix(h, 16).ok()); + let mut chars: Vec = face.chars().into_iter().map(|(c, _)| c).filter(|c| !c.is_control()).collect(); + chars.sort(); + chars.dedup(); + if !q.is_empty() { + chars.retain(|c| Some(*c as u32) == code || q.contains(*c) || format!("{:04X}", *c as u32).contains(&q.to_ascii_uppercase())); + } + chars.truncate(MAX); let cols = ((ui.available_width() / CELL).floor() as u32).max(1); let ppp = ui.ctx().pixels_per_point(); let cell_px = (CELL * ppp).round() as u32; let ink = t.text_strong; - let key = egui::Id::new(("glyph_grid", &st.family, &st.style, scope, &q, cols, cell_px, ink.to_array())); + let key = egui::Id::new(("glyph_grid", family, style, scope, q, cols, cell_px, ink.to_array())); let tex: egui::TextureHandle = match ui.data(|d| d.get_temp::(key)) { Some(t) => t, None => { - let img = designcraft_render::glyphs::glyph_grid(&db, &st.family, &st.style, &chars, cols, cell_px, ink.to_array()); + let img = designcraft_render::glyphs::glyph_grid(db, family, style, &chars, cols, cell_px, ink.to_array()); let ci = egui::ColorImage::from_rgba_premultiplied([img.width as usize, img.height as usize], &img.pixels); let h = ui.ctx().load_texture("glyph_grid", ci, egui::TextureOptions::LINEAR); ui.data_mut(|d| d.insert_temp(key, h.clone())); @@ -87,7 +125,8 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) { } }; let rows = (chars.len() as u32).div_ceil(cols); - egui::ScrollArea::vertical().max_height(320.0).show(ui, |ui| { + let mut picked = None; + egui::ScrollArea::vertical().id_salt(("glyph_scroll", family, style)).max_height(max_height).show(ui, |ui| { let size = vec2(cols as f32 * CELL, rows as f32 * CELL); let (r, resp) = ui.allocate_exact_size(size, Sense::click()); ui.painter().rect_filled(r, 0.0, t.input); @@ -115,26 +154,8 @@ pub fn show(app: &mut DesignApp, ui: &mut egui::Ui) { if resp.clicked() && let Some(c) = resp.interact_pointer_pos().and_then(cell_at) { - insert = Some(c); + picked = Some(c); } }); - if let Some(c) = insert { - match app.run("text.insert", json!({"text": c.to_string(), "raw": true})) { - Ok(_) => { - st.recent.retain(|x| *x != c); - st.recent.insert(0, c); - st.recent.truncate(12); - } - Err(e) => app.status(format!("Glyphs: {e}")), - } - } - crate::rtl::label( - ui, - egui::RichText::new( - crate::i18n::tr(&app.ui.language, "{count} glyphs — click to insert at the text cursor").replace("{count}", &chars.len().to_string()), - ) - .size(10.5) - .color(t.text_dim), - ); - ui.data_mut(|d| d.insert_temp(id, st)); + (picked, chars.len()) } diff --git a/crates/ui-egui/src/panels/properties.rs b/crates/ui-egui/src/panels/properties.rs index 49cb920f2..c2fc9a675 100644 --- a/crates/ui-egui/src/panels/properties.rs +++ b/crates/ui-egui/src/panels/properties.rs @@ -1412,9 +1412,16 @@ pub fn paragraph_panel(app: &mut DesignApp, ui: &mut egui::Ui) { crate::rtl::label(ui, egui::RichText::new(crate::i18n::tr(&app.ui.language, "Paragraph")).strong()); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { crate::menus::menu_button(ui, "☰", |ui| { - if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, "Paragraph Rules…"))).clicked() { - let _ = app.run("app.paragraphRulesDialog", json!({})); - ui.close(); + for (label, id) in [ + ("Paragraph Rules…", "app.paragraphRulesDialog"), + ("Paragraph Borders and Shading…", "app.paragraphBordersDialog"), + ("Span Columns…", "app.spanColumnsDialog"), + ("Bullets and Numbering…", "app.bulletsNumberingDialog"), + ] { + if ui.button(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, label))).clicked() { + let _ = app.run(id, json!({})); + ui.close(); + } } }); });