From 88b3be01362e21b7c2d25ea7c41b5ff6d04cb8da Mon Sep 17 00:00:00 2001 From: aiongg Date: Tue, 6 Oct 2026 19:52:41 -0400 Subject: [PATCH] UI: drop cap fields, selection over drop caps --- crates/ui-egui/src/canvas.rs | 9 +++++ crates/ui-egui/src/dialogs.rs | 45 +++++++++++++++++++++++-- crates/ui-egui/src/i18n.rs | 6 ++++ crates/ui-egui/src/i18n/ar.rs | 6 ++++ crates/ui-egui/src/panels/properties.rs | 11 ++++++ 5 files changed, 74 insertions(+), 3 deletions(-) diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index e1562c602..98a30f0d4 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -1004,6 +1004,15 @@ fn draw_text_selection( let x1 = if e == l.range.end && range.end > l.range.end { l.end_x.max(x0 + 3.0) } else { compose::caret_x(l, e) }; quads.push(quad(x0, x1)); } + // A selected drop cap is highlighted down to its baseline. + if let Some(dc) = l.drop_cap { + let sel = l.glyphs.iter().filter(|g| g.len > 0 && g.byte < dc.end && g.byte >= s && g.byte < e); + let (a, b) = sel.fold((f64::INFINITY, f64::NEG_INFINITY), |(a, b), g| (a.min(g.x), b.max(g.x + g.adv))); + if a < b { + let r = dc.rect; + quads.push([Point::new(a, r.y0), Point::new(b, r.y0), Point::new(b, r.y1), Point::new(a, r.y1)]); + } + } for (gi, g) in l.glyphs.iter().enumerate() { if g.visible && g.len > 0 && g.byte >= s && g.byte < e { glyphs.push((li, gi)); diff --git a/crates/ui-egui/src/dialogs.rs b/crates/ui-egui/src/dialogs.rs index bfa83d4a9..35deb1081 100644 --- a/crates/ui-egui/src/dialogs.rs +++ b/crates/ui-egui/src/dialogs.rs @@ -1771,6 +1771,35 @@ fn paragraph_style_options(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialo }); } "nested" => { + crate::rtl::label(ui, egui::RichText::new(crate::i18n::tr(&app.ui.language, "Drop Caps")).font(semibold(12.0))); + egui::Grid::new("psdc").num_columns(2).spacing([8.0, 6.0]).show(ui, |ui| { + for (label, key, max) in [ + ("Lines:", "dropCapLines", designcraft_compose::MAX_DROP_CAP_LINES), + ("Characters:", "dropCapChars", designcraft_compose::MAX_DROP_CAP_CHARS), + ] { + crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, label)); + let v = cur(d, &format!("p.{key}"), &pv[key]).as_f64(); + if let Some(n) = crate::widgets::number(ui, &format!("ps{key}"), v, "", 60.0, 0) { + d.fields.insert(format!("p.{key}"), json!((n.max(0.0) as u64).min(max as u64))); + } + ui.end_row(); + } + crate::rtl::label(ui, crate::i18n::tr(&app.ui.language, "Character Style:")); + let style = cur(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") + .selected_text(crate::rtl::widget(ui, crate::i18n::style_name(&app.ui.language, &style))) + .width(160.0) + .show_ui(ui, |ui| { + for c in &cnames { + if ui.selectable_label(*c == style, crate::rtl::widget(ui, crate::i18n::style_name(&app.ui.language, c))).clicked() { + d.fields.insert("p.dropCapStyle".into(), json!(c)); + } + } + }); + ui.end_row(); + }); + ui.add_space(8.0); crate::rtl::label(ui, egui::RichText::new(crate::i18n::tr(&app.ui.language, "Nested Styles")).font(semibold(12.0))); let mut list: Vec = cur(d, "p.nestedStyles", &pv["nestedStyles"]).as_array().cloned().unwrap_or_default(); let mut changed = false; @@ -1814,9 +1843,19 @@ fn paragraph_style_options(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialo .selected_text(crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, &kind))) .width(100.0) .show_ui(ui, |ui| { - for k in - ["sentences", "words", "characters", "letters", "digits", "tab", "forcedLineBreak", "emSpace", "enSpace", "chars"] - { + for k in [ + "sentences", + "words", + "characters", + "letters", + "digits", + "tab", + "forcedLineBreak", + "emSpace", + "enSpace", + "dropcap", + "chars", + ] { if ui.selectable_label(k == kind, crate::rtl::widget(ui, crate::i18n::tr(&app.ui.language, k))).clicked() { ns["until"] = if k == "chars" { json!({"kind": "chars", "chars": ":"}) } else { json!({"kind": k}) }; changed = true; diff --git a/crates/ui-egui/src/i18n.rs b/crates/ui-egui/src/i18n.rs index 60fae75f3..a70bbe22a 100644 --- a/crates/ui-egui/src/i18n.rs +++ b/crates/ui-egui/src/i18n.rs @@ -1891,6 +1891,12 @@ const TABLE: &[(&str, [&str; 5])] = &[ ("forcedLineBreak", ["forcedLineBreak", "forcedLineBreak", "forcedLineBreak", "forcedLineBreak", "强制换行符"]), ("emSpace", ["emSpace", "emSpace", "emSpace", "emSpace", "全角空格"]), ("enSpace", ["enSpace", "enSpace", "enSpace", "enSpace", "半角空格"]), + ("dropcap", ["Initiale", "lettrine", "capitular", "ドロップキャップ", "首字下沉"]), + ("Drop Caps", ["Initialen", "Lettrines", "Capitulares", "ドロップキャップ", "首字下沉"]), + ("Lines:", ["Zeilen:", "Lignes :", "Líneas:", "行数:", "行数:"]), + ("Characters:", ["Zeichen:", "Caractères :", "Caracteres:", "文字数:", "字符数:"]), + ("Drop Lines", ["Initialzeilen", "Lignes lettrine", "Líneas capitular", "ドロップ行数", "下沉行数"]), + ("Drop Chars", ["Initialzeichen", "Car. lettrine", "Car. capitular", "ドロップ文字数", "下沉字符数"]), ("chars", ["chars", "chars", "chars", "chars", "指定字符"]), ("[Automatic]", ["[Automatic]", "[Automatic]", "[Automatic]", "[Automatic]", "[自动]"]), ("Tracking:", ["Tracking:", "Tracking:", "Tracking:", "Tracking:", "字距:"]), diff --git a/crates/ui-egui/src/i18n/ar.rs b/crates/ui-egui/src/i18n/ar.rs index 03f97a62b..3180bedea 100644 --- a/crates/ui-egui/src/i18n/ar.rs +++ b/crates/ui-egui/src/i18n/ar.rs @@ -317,6 +317,12 @@ pub(super) const TABLE: &[(&str, &str)] = &[ ("forcedLineBreak", "فاصل سطر إجباري"), ("emSpace", "مسافة em"), ("enSpace", "مسافة en"), + ("dropcap", "الحرف الاستهلالي"), + ("Drop Caps", "الأحرف الاستهلالية"), + ("Lines:", "الأسطر:"), + ("Characters:", "الأحرف:"), + ("Drop Lines", "أسطر الاستهلال"), + ("Drop Chars", "أحرف الاستهلال"), ("chars", "محارف محددة"), ("[Automatic]", "[تلقائي]"), ("Size:", "الحجم:"), diff --git a/crates/ui-egui/src/panels/properties.rs b/crates/ui-egui/src/panels/properties.rs index bf3f0b3af..817215226 100644 --- a/crates/ui-egui/src/panels/properties.rs +++ b/crates/ui-egui/src/panels/properties.rs @@ -1452,6 +1452,17 @@ pub fn paragraph_panel(app: &mut DesignApp, ui: &mut egui::Ui) { } ui.end_row(); } + // Drop cap: number of lines and characters. + for (label, key, max, id) in [ + ("Drop Lines", "dropCapLines", designcraft_compose::MAX_DROP_CAP_LINES, "ppdl"), + ("Drop Chars", "dropCapChars", designcraft_compose::MAX_DROP_CAP_CHARS, "ppdc"), + ] { + caption(ui, crate::i18n::tr(&app.ui.language, label)); + if let Some(v) = number(ui, id, p[key].as_f64(), "", 64.0, 0) { + let _ = app.run("type.para", json!({"attrs": {key: (v.max(0.0) as u64).min(max as u64)}})); + } + } + ui.end_row(); }); ui.horizontal(|ui| { let mut h = p["hyphenate"].as_bool().unwrap_or(true);