mirror of
https://github.com/storytold/designcraft.git
synced 2026-10-26 11:16:50 -06:00
Merge feat/style-sections-cjk: CJK sections in Paragraph and Character Style Options
Conflict: crates/engine/src/cmd/style.rs: both params notes in the paragraph style edit spec (startAt null, kinsoku null); create_para with next's set_json_over, continue_numbering and the branch's explicit_default_kinsoku; edit_para calls both helpers; both tests Conflict: crates/ui-egui/src/dialogs.rs: next's section arms (advanced to bullets) and the branch's CJK section arm; next's paragraph helpers and paragraph_local before the branch's section_list (cjk flag, scrolling list; doc comment stays removed as on next) The branch's "Lines:" row in the shared i18n table is dropped: next already has the key (drop caps).
This commit is contained in:
@@ -95,7 +95,7 @@ pub fn specs() -> Vec<CommandSpec> {
|
||||
"Paragraph Style Options…",
|
||||
[],
|
||||
None,
|
||||
"{name, rename?, basedOn?, nextStyle?, para?: {… ruleAbove?/ruleBelow?: only the named rule fields change; startAt?: null = continue numbering}, chars?: {…}}",
|
||||
"{name, rename?, basedOn?, nextStyle?, para?: {… ruleAbove?/ruleBelow?: only the named rule fields change; startAt?: null = continue numbering; kinsoku?: set | null (default rules)}, chars?: {…}}",
|
||||
has_doc,
|
||||
edit_para
|
||||
),
|
||||
@@ -440,6 +440,7 @@ fn create_para(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let base = str_param(p, "name").unwrap_or("Paragraph Style 1").to_string();
|
||||
let mut para: ParaAttrs = attrs(p.get("para"), |a: &mut ParaAttrs, k, v| a.set_json_over(k, v, &designcraft_doc::ParaProps::default()))?;
|
||||
continue_numbering(p.get("para"), &mut para);
|
||||
explicit_default_kinsoku(&mut para, p.get("para"));
|
||||
let mut chars: CharAttrs = attrs(p.get("chars"), |a: &mut CharAttrs, k, v| a.set_json(k, v))?;
|
||||
if p.get("fromSelection").and_then(Value::as_bool).unwrap_or(false)
|
||||
&& let Ok(cur) = s.execute("type.selectionAttrs", &json!({}))
|
||||
@@ -470,6 +471,14 @@ fn create_para(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
})
|
||||
}
|
||||
|
||||
/// `kinsoku: null` sets the composer's default kinsoku rules, also over a Based On style's set.
|
||||
/// For the other attributes `null` leaves the style's value as it is.
|
||||
fn explicit_default_kinsoku(para: &mut ParaAttrs, given: Option<&Value>) {
|
||||
if given.and_then(|o| o.get("kinsoku")).is_some_and(Value::is_null) {
|
||||
para.kinsoku = Some(None);
|
||||
}
|
||||
}
|
||||
|
||||
#[allow(non_snake_case)]
|
||||
fn ParaProps_to_attrs(p: &designcraft_doc::ParaProps) -> ParaAttrs {
|
||||
designcraft_doc::ParaProps::common([p])
|
||||
@@ -494,6 +503,7 @@ fn edit_para(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
let (current, _) = d.styles.resolve_para_style(&name);
|
||||
let mut para: ParaAttrs = attrs(p.get("para"), |a: &mut ParaAttrs, k, v| a.set_json_over(k, v, ¤t))?;
|
||||
continue_numbering(p.get("para"), &mut para);
|
||||
explicit_default_kinsoku(&mut para, p.get("para"));
|
||||
let st = d.styles_mut().para_mut(&name).ok_or_else(|| bad("style.paragraph.edit", format!("no style `{name}`")))?;
|
||||
st.para.merge(¶);
|
||||
st.chars.merge(&chars);
|
||||
@@ -1216,6 +1226,22 @@ mod color_tests {
|
||||
assert_eq!(s.doc().unwrap().doc.styles.para("Fresh").unwrap().para.start_at, Some(None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn null_kinsoku_sets_the_default_rules_over_a_based_on_set() {
|
||||
let mut s = Session::new();
|
||||
s.execute("file.new", &json!({})).unwrap();
|
||||
let hard = serde_json::to_value(designcraft_doc::cjk::Kinsoku::named("HardKinsoku").unwrap()).unwrap();
|
||||
s.execute("style.paragraph.create", &json!({"name": "Base", "para": {"kinsoku": hard}})).unwrap();
|
||||
s.execute("style.paragraph.create", &json!({"name": "Body", "basedOn": "Base", "para": {"kinsoku": hard}})).unwrap();
|
||||
s.execute("style.paragraph.edit", &json!({"name": "Body", "para": {"kinsoku": null}})).unwrap();
|
||||
let styles = s.doc().unwrap().doc.styles.clone();
|
||||
assert_eq!(styles.para("Body").unwrap().para.kinsoku, Some(None));
|
||||
assert_eq!(styles.resolve_para_style("Body").0.kinsoku, None, "Base's set no longer applies");
|
||||
// An edit without the attribute keeps it.
|
||||
s.execute("style.paragraph.edit", &json!({"name": "Body", "para": {"rensuuji": false}})).unwrap();
|
||||
assert_eq!(s.doc().unwrap().doc.styles.para("Body").unwrap().para.kinsoku, Some(None));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn new_character_style_checks_its_based_on() {
|
||||
let mut s = Session::new();
|
||||
|
||||
@@ -8,6 +8,8 @@ use serde_json::{Map, Value, json};
|
||||
use crate::DesignApp;
|
||||
use crate::theme::semibold;
|
||||
|
||||
mod style_cjk;
|
||||
|
||||
#[derive(Clone, Debug, Serialize)]
|
||||
pub struct Dialog {
|
||||
pub id: String,
|
||||
@@ -2280,6 +2282,7 @@ fn paragraph_style_options(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialo
|
||||
&app.ui.language,
|
||||
ui,
|
||||
d,
|
||||
crate::cjk_features(app),
|
||||
&[
|
||||
("general", "General"),
|
||||
("chars", "Basic Character Formats"),
|
||||
@@ -2708,6 +2711,9 @@ fn paragraph_style_options(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialo
|
||||
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);
|
||||
}
|
||||
s if style_cjk::is_section(s) => {
|
||||
style_cjk::section(&app.ui.language, ui, d, s, &CharFields { base: &cv, sparse: false }, Some(&pv), &doc)
|
||||
}
|
||||
_ => {
|
||||
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:"));
|
||||
@@ -3572,14 +3578,18 @@ fn paragraph_local(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
}
|
||||
}
|
||||
|
||||
fn section_list(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, sections: &[(&str, &str)]) {
|
||||
fn section_list(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, cjk: bool, sections: &[(&str, &str)]) {
|
||||
let sections = style_cjk::sections(cjk, d.id == "paragraphStyleOptions", sections);
|
||||
ui.vertical(|ui| {
|
||||
ui.set_width(170.0);
|
||||
for (id, label) in sections {
|
||||
if ui.selectable_label(d.s("section") == *id, crate::rtl::widget(ui, crate::i18n::tr(lang, label))).clicked() {
|
||||
d.fields.insert("section".into(), json!(id));
|
||||
// The dialog has a fixed height; a long list scrolls.
|
||||
egui::ScrollArea::vertical().id_salt("style_sections").auto_shrink([false, true]).show(ui, |ui| {
|
||||
for (id, label) in §ions {
|
||||
if ui.selectable_label(d.s("section") == *id, crate::rtl::widget(ui, crate::i18n::tr(lang, label))).clicked() {
|
||||
d.fields.insert("section".into(), json!(id));
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
@@ -3652,6 +3662,7 @@ fn character_style_options(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialo
|
||||
&lang,
|
||||
ui,
|
||||
d,
|
||||
crate::cjk_features(app),
|
||||
&[
|
||||
("general", "General"),
|
||||
("chars", "Basic Character Formats"),
|
||||
@@ -3670,6 +3681,7 @@ fn character_style_options(app: &mut DesignApp, ui: &mut egui::Ui, d: &mut Dialo
|
||||
"openType" => open_type_features(&lang, ui, d, &cf),
|
||||
"underline" => line_options(&lang, ui, d, &cf, &doc, "underline"),
|
||||
"strikethrough" => line_options(&lang, ui, d, &cf, &doc, "strikethrough"),
|
||||
s if style_cjk::is_section(s) => style_cjk::section(&lang, ui, d, s, &cf, None, &doc),
|
||||
_ => {
|
||||
egui::Grid::new("csg").num_columns(2).spacing([8.0, 6.0]).show(ui, |ui| {
|
||||
crate::rtl::label(ui, crate::i18n::tr(&lang, "Style Name:"));
|
||||
|
||||
@@ -0,0 +1,570 @@
|
||||
//! The CJK sections of Paragraph Style Options and Character Style Options, listed while Show CJK
|
||||
//! Features is on ([`crate::cjk_features`]): Tate-chu-yoko, Kenten, Japanese Composition, Grid
|
||||
//! and Warichu settings. They edit `c.<attr>` and `p.<attr>` fields like the other sections, and
|
||||
//! OK applies them with the dialog's style edit command.
|
||||
|
||||
use designcraft_doc::cjk::Kinsoku;
|
||||
use serde_json::{Value, json};
|
||||
|
||||
use super::{CharFields, Dialog, char_check, char_choice, char_number};
|
||||
|
||||
struct Section {
|
||||
id: &'static str,
|
||||
label: &'static str,
|
||||
/// Absent from Character Style Options.
|
||||
para_only: bool,
|
||||
/// Listed after Export Tagging.
|
||||
after_export: bool,
|
||||
}
|
||||
|
||||
const SECTIONS: &[Section] = &[
|
||||
Section { id: "cjk.tcy", label: "Tate-chu-yoko Settings", para_only: false, after_export: false },
|
||||
Section { id: "cjk.kenten", label: "Kenten Settings", para_only: false, after_export: false },
|
||||
Section { id: "cjk.composition", label: "Japanese Composition Settings", para_only: true, after_export: false },
|
||||
Section { id: "cjk.grid", label: "Grid Settings", para_only: true, after_export: false },
|
||||
Section { id: "cjk.warichu", label: "Warichu Settings", para_only: false, after_export: true },
|
||||
];
|
||||
|
||||
/// The section list of a style options dialog: `base` with the CJK sections when `cjk` is on.
|
||||
/// They go before Export Tagging, except Warichu Settings, which follows it; without Export
|
||||
/// Tagging they all go last.
|
||||
pub(super) fn sections<'a>(cjk: bool, para: bool, base: &[(&'a str, &'a str)]) -> Vec<(&'a str, &'a str)> {
|
||||
if !cjk {
|
||||
return base.to_vec();
|
||||
}
|
||||
let shown = move |after: bool| SECTIONS.iter().filter(move |s| (para || !s.para_only) && s.after_export == after).map(|s| (s.id, s.label));
|
||||
let mut out = Vec::with_capacity(base.len() + SECTIONS.len());
|
||||
let mut placed = false;
|
||||
for s in base {
|
||||
if s.0 == "export" && !placed {
|
||||
out.extend(shown(false));
|
||||
out.push(*s);
|
||||
out.extend(shown(true));
|
||||
placed = true;
|
||||
} else {
|
||||
out.push(*s);
|
||||
}
|
||||
}
|
||||
if !placed {
|
||||
out.extend(shown(false));
|
||||
out.extend(shown(true));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
pub(super) fn is_section(id: &str) -> bool {
|
||||
SECTIONS.iter().any(|s| s.id == id)
|
||||
}
|
||||
|
||||
/// Draws CJK section `id`. `para` holds the paragraph style's resolved paragraph attributes; it
|
||||
/// is `None` in Character Style Options, which have no paragraph-only sections.
|
||||
pub(super) fn section(
|
||||
lang: &str,
|
||||
ui: &mut egui::Ui,
|
||||
d: &mut Dialog,
|
||||
id: &str,
|
||||
cf: &CharFields,
|
||||
para: Option<&Value>,
|
||||
doc: &designcraft_doc::Document,
|
||||
) {
|
||||
match (id, para) {
|
||||
("cjk.tcy", _) => tate_chu_yoko(lang, ui, d, cf),
|
||||
("cjk.kenten", _) => kenten(lang, ui, d, cf),
|
||||
("cjk.warichu", _) => warichu(lang, ui, d, cf),
|
||||
("cjk.composition", Some(pv)) => japanese_composition(lang, ui, d, cf, pv, doc),
|
||||
("cjk.grid", Some(pv)) => grid_settings(lang, ui, d, pv),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
|
||||
/// Tate-chu-yoko Settings: the switch and the run's offsets.
|
||||
fn tate_chu_yoko(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, cf: &CharFields) {
|
||||
char_check(ui, d, cf, "tateChuYoko", crate::i18n::tr(lang, "Tate-Chu-Yoko"));
|
||||
ui.add_space(4.0);
|
||||
egui::Grid::new("cjktcy").num_columns(2).spacing([8.0, 6.0]).show(ui, |ui| {
|
||||
crate::rtl::label(ui, crate::i18n::tr(lang, "Up/Down Position:"));
|
||||
char_number(ui, d, cf, "tateChuYokoYOffset", " pt", 1.0, (-1000.0, 1000.0));
|
||||
ui.end_row();
|
||||
crate::rtl::label(ui, crate::i18n::tr(lang, "Left/Right Position:"));
|
||||
char_number(ui, d, cf, "tateChuYokoXOffset", " pt", 1.0, (-1000.0, 1000.0));
|
||||
ui.end_row();
|
||||
});
|
||||
}
|
||||
|
||||
/// The preset kenten marks: (label, character). The sesame dot is the empty character, which the
|
||||
/// composer draws as U+FE45 and IDML export writes as `KentenSesameDot`.
|
||||
pub(super) const KENTEN: &[(&str, &str)] = &[
|
||||
("Sesame Dot", ""),
|
||||
("White Sesame Dot", "\u{FE46}"),
|
||||
("Bullseye", "\u{25C9}"),
|
||||
("Black Circle", "\u{25CF}"),
|
||||
("Small Black Circle", "\u{2022}"),
|
||||
("Double Circle", "\u{25CE}"),
|
||||
("Black Triangle", "\u{25B2}"),
|
||||
("White Triangle", "\u{25B3}"),
|
||||
("White Circle", "\u{25CB}"),
|
||||
("Small White Circle", "\u{25E6}"),
|
||||
];
|
||||
|
||||
/// Dialog-only: Custom was picked, so the Character field is editable even while it holds a
|
||||
/// preset mark.
|
||||
const KENTEN_CUSTOM: &str = "cjk.kentenCustom";
|
||||
|
||||
fn kenten_preset(character: &str) -> Option<&'static (&'static str, &'static str)> {
|
||||
let character = if character == "\u{FE45}" { "" } else { character };
|
||||
KENTEN.iter().find(|k| k.1 == character)
|
||||
}
|
||||
|
||||
/// Kenten Type picks: `None` switches kenten off, `Some(mark)` sets that mark.
|
||||
pub(super) fn pick_kenten(d: &mut Dialog, cf: &CharFields, mark: Option<&str>) {
|
||||
d.fields.remove(KENTEN_CUSTOM);
|
||||
match mark {
|
||||
Some(m) => {
|
||||
cf.set(d, "kenten", json!(true));
|
||||
cf.set(d, "kentenCharacter", json!(m));
|
||||
}
|
||||
None => {
|
||||
cf.set(d, "kenten", json!(false));
|
||||
// IDML export writes any stored mark as a custom kenten, even with kenten off.
|
||||
if cf.get(d, "kentenCharacter").as_str().is_some_and(|c| !c.is_empty()) {
|
||||
cf.set(d, "kentenCharacter", json!(""));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Kenten Settings: the mark (a preset or a custom character).
|
||||
fn kenten(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, cf: &CharFields) {
|
||||
let on = cf.get(d, "kenten").as_bool();
|
||||
let character = cf.get(d, "kentenCharacter").as_str().unwrap_or("").to_string();
|
||||
let preset = kenten_preset(&character);
|
||||
let custom = on == Some(true) && (d.b(KENTEN_CUSTOM) || preset.is_none());
|
||||
let shown = match on {
|
||||
None => String::new(),
|
||||
Some(false) => crate::i18n::tr(lang, "None").to_string(),
|
||||
Some(true) if custom => crate::i18n::tr(lang, "Custom").to_string(),
|
||||
Some(true) => preset.map_or(String::new(), |p| crate::i18n::tr(lang, p.0).to_string()),
|
||||
};
|
||||
egui::Grid::new("cjkkenten").num_columns(2).spacing([8.0, 6.0]).show(ui, |ui| {
|
||||
crate::rtl::label(ui, crate::i18n::tr(lang, "Kenten Type:"));
|
||||
egui::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() {
|
||||
d.fields.remove(KENTEN_CUSTOM);
|
||||
cf.set(d, "kenten", Value::Null);
|
||||
cf.set(d, "kentenCharacter", Value::Null);
|
||||
}
|
||||
if ui.selectable_label(on == Some(false), crate::rtl::widget(ui, crate::i18n::tr(lang, "None"))).clicked() {
|
||||
pick_kenten(d, cf, None);
|
||||
}
|
||||
for (label, mark) in KENTEN {
|
||||
let selected = on == Some(true) && !custom && preset.is_some_and(|p| p.1 == *mark);
|
||||
if ui.selectable_label(selected, crate::rtl::widget(ui, crate::i18n::tr(lang, label))).clicked() {
|
||||
pick_kenten(d, cf, Some(*mark));
|
||||
}
|
||||
}
|
||||
if ui.selectable_label(custom, crate::rtl::widget(ui, crate::i18n::tr(lang, "Custom"))).clicked() {
|
||||
d.fields.insert(KENTEN_CUSTOM.into(), json!(true));
|
||||
cf.set(d, "kenten", json!(true));
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
crate::rtl::label(ui, crate::i18n::tr(lang, "Character:"));
|
||||
let mut text = if custom { character.clone() } else { String::new() };
|
||||
if ui.add_enabled(custom, egui::TextEdit::singleline(&mut text).desired_width(40.0)).changed() {
|
||||
// One mark: the character typed last replaces the one before it.
|
||||
let mark: String = text.chars().last().map(String::from).unwrap_or_default();
|
||||
cf.set(d, "kentenCharacter", json!(mark));
|
||||
}
|
||||
ui.end_row();
|
||||
});
|
||||
}
|
||||
|
||||
/// A whole-number attribute (stored as an integer: the model's fields are `u32`).
|
||||
fn char_count(ui: &mut egui::Ui, d: &mut Dialog, cf: &CharFields, key: &str, range: (f64, f64)) {
|
||||
let v = cf.get(d, key).as_f64();
|
||||
let field = crate::widgets::NumField::number(key, v, "", 0).width(80.0).range(range.0, range.1);
|
||||
let edit = if cf.sparse { field.show_or_clear(ui) } else { field.show(ui).map(Some) };
|
||||
if let Some(n) = edit {
|
||||
cf.set(d, key, n.map_or(Value::Null, |n| json!(n.round().max(range.0).min(range.1) as u64)));
|
||||
}
|
||||
}
|
||||
|
||||
/// Warichu Settings: the switch, lines, size, spacing, alignment and line break minimums.
|
||||
fn warichu(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, cf: &CharFields) {
|
||||
char_check(ui, d, cf, "warichu", crate::i18n::tr(lang, "Warichu"));
|
||||
ui.add_space(4.0);
|
||||
egui::Grid::new("cjkwarichu").num_columns(2).spacing([8.0, 6.0]).show(ui, |ui| {
|
||||
crate::rtl::label(ui, crate::i18n::tr(lang, "Lines:"));
|
||||
char_count(ui, d, cf, "warichuLines", (2.0, 16.0));
|
||||
ui.end_row();
|
||||
crate::rtl::label(ui, crate::i18n::tr(lang, "Warichu Size:"));
|
||||
char_number(ui, d, cf, "warichuSize", "%", 1.0, (1.0, 1000.0));
|
||||
ui.end_row();
|
||||
crate::rtl::label(ui, crate::i18n::tr(lang, "Line Spacing:"));
|
||||
char_number(ui, d, cf, "warichuLineSpacing", " pt", 1.0, (-1000.0, 1000.0));
|
||||
ui.end_row();
|
||||
crate::rtl::label(ui, crate::i18n::tr(lang, "Alignment:"));
|
||||
char_choice(
|
||||
lang,
|
||||
ui,
|
||||
d,
|
||||
cf,
|
||||
"warichuAlignment",
|
||||
&[
|
||||
(json!("auto"), "Auto"),
|
||||
(json!("left"), "Left"),
|
||||
(json!("center"), "Center"),
|
||||
(json!("right"), "Right"),
|
||||
(json!("justify"), "Justify"),
|
||||
],
|
||||
);
|
||||
ui.end_row();
|
||||
});
|
||||
ui.add_space(6.0);
|
||||
crate::rtl::label(ui, egui::RichText::new(crate::i18n::tr(lang, "Line Break Options")).font(crate::theme::semibold(12.0)));
|
||||
egui::Grid::new("cjkwarichubreak").num_columns(2).spacing([8.0, 6.0]).show(ui, |ui| {
|
||||
crate::rtl::label(ui, crate::i18n::tr(lang, "Characters Before Break:"));
|
||||
char_count(ui, d, cf, "warichuCharsBeforeBreak", (0.0, 100.0));
|
||||
ui.end_row();
|
||||
crate::rtl::label(ui, crate::i18n::tr(lang, "Characters After Break:"));
|
||||
char_count(ui, d, cf, "warichuCharsAfterBreak", (0.0, 100.0));
|
||||
ui.end_row();
|
||||
});
|
||||
}
|
||||
|
||||
/// A paragraph attribute as shown: the dialog's `p.<key>` edit, else the resolved value.
|
||||
fn para_value(d: &Dialog, pv: &Value, key: &str) -> Value {
|
||||
d.fields.get(&format!("p.{key}")).cloned().unwrap_or_else(|| pv.get(key).cloned().unwrap_or(Value::Null))
|
||||
}
|
||||
|
||||
/// 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| {
|
||||
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));
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn para_check(ui: &mut egui::Ui, d: &mut Dialog, pv: &Value, key: &str, label: &str) {
|
||||
let mut on = para_value(d, pv, key).as_bool().unwrap_or(false);
|
||||
if ui.checkbox(&mut on, crate::rtl::widget(ui, label)).changed() {
|
||||
d.fields.insert(format!("p.{key}"), json!(on));
|
||||
}
|
||||
}
|
||||
|
||||
/// The built-in kinsoku sets ([`Kinsoku::named`]) and their labels.
|
||||
const BUILT_IN_KINSOKU: &[(&str, &str)] = &[
|
||||
("HardKinsoku", "Hard Kinsoku"),
|
||||
("SoftKinsoku", "Soft Kinsoku"),
|
||||
("SimplifiedChineseKinsoku", "Simplified Chinese Kinsoku"),
|
||||
("TraditionalChineseKinsoku", "Traditional Chinese Kinsoku"),
|
||||
("KoreanKinsoku", "Korean Kinsoku"),
|
||||
];
|
||||
|
||||
/// The Kinsoku Set menu: no kinsoku, the built-in sets, then every other named set the document's
|
||||
/// paragraph styles and paragraphs use (the model keeps each set by value, not in a list).
|
||||
pub(super) fn kinsoku_sets(doc: &designcraft_doc::Document) -> Vec<Kinsoku> {
|
||||
let mut sets = vec![Kinsoku::default()];
|
||||
sets.extend(BUILT_IN_KINSOKU.iter().filter_map(|(name, _)| Kinsoku::named(name)));
|
||||
let used = doc.styles.paragraph.iter().map(|s| &s.para.kinsoku).chain(doc.stories.values().flat_map(|s| s.paras.iter().map(|p| &p.para.kinsoku)));
|
||||
for k in used.flatten().flatten() {
|
||||
if !k.name.is_empty() && !sets.iter().any(|s| s.name == k.name) {
|
||||
sets.push(k.clone());
|
||||
}
|
||||
}
|
||||
sets
|
||||
}
|
||||
|
||||
fn kinsoku_label(lang: &str, name: &str) -> String {
|
||||
if name.is_empty() {
|
||||
return crate::i18n::tr(lang, "None").to_string();
|
||||
}
|
||||
BUILT_IN_KINSOKU.iter().find(|k| k.0 == name).map_or_else(|| name.to_string(), |k| crate::i18n::tr(lang, k.1).to_string())
|
||||
}
|
||||
|
||||
/// IDML's names for "no mojikumi set".
|
||||
fn mojikumi_value(v: &str) -> &str {
|
||||
if matches!(v, "Nothing" | "None") { "" } else { v }
|
||||
}
|
||||
|
||||
/// The Mojikumi Set menu as (value, label): none (empty label), the document's sets, and the
|
||||
/// `current` value when it names a set the document doesn't define (kept as imported).
|
||||
pub(super) fn mojikumi_sets(doc: &designcraft_doc::Document, current: &str) -> Vec<(String, String)> {
|
||||
let mut out = vec![(String::new(), String::new())];
|
||||
for t in &doc.styles.mojikumi_tables {
|
||||
let v = format!("MojikumiTable/{}", t.name);
|
||||
if !out.iter().any(|o| o.0 == v) {
|
||||
out.push((v, t.name.clone()));
|
||||
}
|
||||
}
|
||||
let current = mojikumi_value(current);
|
||||
if !current.is_empty() && !out.iter().any(|o| o.0 == current) {
|
||||
out.push((current.to_string(), current.trim_start_matches("MojikumiTable/").trim_start_matches("$ID/").to_string()));
|
||||
}
|
||||
out
|
||||
}
|
||||
|
||||
/// Japanese Composition Settings: kinsoku, hanging, bunri-kinshi, mojikumi, the leading model,
|
||||
/// rensuuji and line-end ideographic spaces.
|
||||
fn japanese_composition(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, cf: &CharFields, pv: &Value, doc: &designcraft_doc::Document) {
|
||||
egui::Grid::new("cjkcomp").num_columns(2).spacing([8.0, 6.0]).show(ui, |ui| {
|
||||
crate::rtl::label(ui, crate::i18n::tr(lang, "Kinsoku Set:"));
|
||||
let sets = kinsoku_sets(doc);
|
||||
// `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| {
|
||||
// `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);
|
||||
}
|
||||
for k in &sets {
|
||||
if ui.selectable_label(name.as_deref() == Some(k.name.as_str()), crate::rtl::widget(ui, kinsoku_label(lang, &k.name))).clicked() {
|
||||
if let Ok(v) = serde_json::to_value(k) {
|
||||
d.fields.insert("p.kinsoku".into(), v);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
crate::rtl::label(ui, crate::i18n::tr(lang, "Kinsoku Type:"));
|
||||
let kind = para_value(d, pv, "kinsokuType");
|
||||
let kind = match kind.as_str().unwrap_or("") {
|
||||
"KinsokuPushInFirst" => "",
|
||||
k => k,
|
||||
}
|
||||
.to_string();
|
||||
para_choice(
|
||||
lang,
|
||||
ui,
|
||||
d,
|
||||
"kinsokuType",
|
||||
&kind,
|
||||
&[
|
||||
("", "Push In First"),
|
||||
("KinsokuPushOutFirst", "Push Out First"),
|
||||
("KinsokuPushOutOnly", "Push Out Only"),
|
||||
("KinsokuPrioritizeAdjustmentAmount", "Prioritize Adjustment Amount"),
|
||||
],
|
||||
);
|
||||
ui.end_row();
|
||||
crate::rtl::label(ui, crate::i18n::tr(lang, "Hanging Type:"));
|
||||
let hang = para_value(d, pv, "kinsokuHang").as_str().unwrap_or("none").to_string();
|
||||
para_choice(lang, ui, d, "kinsokuHang", &hang, &[("none", "None"), ("regular", "Standard"), ("force", "Force")]);
|
||||
ui.end_row();
|
||||
});
|
||||
para_check(ui, d, pv, "bunriKinshi", crate::i18n::tr(lang, "Bunri-Kinshi"));
|
||||
ui.add_space(4.0);
|
||||
egui::Grid::new("cjkcomp2").num_columns(2).spacing([8.0, 6.0]).show(ui, |ui| {
|
||||
crate::rtl::label(ui, crate::i18n::tr(lang, "Mojikumi Set:"));
|
||||
let raw = para_value(d, pv, "mojikumi");
|
||||
let current = mojikumi_value(raw.as_str().unwrap_or("")).to_string();
|
||||
let sets = mojikumi_sets(doc, ¤t);
|
||||
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| {
|
||||
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));
|
||||
}
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
crate::rtl::label(ui, crate::i18n::tr(lang, "Leading Model:"));
|
||||
// Labelled by where the composer measures the leading from (`cjk_line_reference` in
|
||||
// compose): `akiAbove` from the em box top, `akiBelow` from its bottom.
|
||||
let mut opts = vec![
|
||||
(json!("akiAbove"), "Em Box Top/Right"),
|
||||
(json!("center"), "Em Box Center"),
|
||||
(json!("roman"), "Roman Baseline"),
|
||||
(json!("akiBelow"), "Em Box Bottom/Left"),
|
||||
];
|
||||
// Kept from IDML; not offered otherwise.
|
||||
if cf.get(d, "leadingModel") == json!("centerDown") {
|
||||
opts.push((json!("centerDown"), "Em Box Center (Down)"));
|
||||
}
|
||||
char_choice(lang, ui, d, cf, "leadingModel", &opts);
|
||||
ui.end_row();
|
||||
});
|
||||
para_check(ui, d, pv, "rensuuji", crate::i18n::tr(lang, "Rensuuji"));
|
||||
para_check(ui, d, pv, "treatIdeographicSpaceAsSpace", crate::i18n::tr(lang, "Absorb Ideographic Space at Line End"));
|
||||
}
|
||||
|
||||
/// Grid Settings: whether lines align to the baseline grid, and only the first line.
|
||||
fn grid_settings(lang: &str, ui: &mut egui::Ui, d: &mut Dialog, pv: &Value) {
|
||||
use designcraft_doc::GridAlign;
|
||||
let cur: GridAlign = serde_json::from_value(para_value(d, pv, "gridAlign")).unwrap_or_default();
|
||||
let set = |d: &mut Dialog, g: GridAlign| {
|
||||
if let Ok(v) = serde_json::to_value(g) {
|
||||
d.fields.insert("p.gridAlign".into(), v);
|
||||
}
|
||||
};
|
||||
egui::Grid::new("cjkgrid").num_columns(2).spacing([8.0, 6.0]).show(ui, |ui| {
|
||||
crate::rtl::label(ui, crate::i18n::tr(lang, "Grid Alignment:"));
|
||||
let on = cur != GridAlign::None;
|
||||
let shown = crate::i18n::tr(lang, if on { "Roman Baseline" } else { "None" });
|
||||
egui::ComboBox::from_id_salt("cjkgridalign").selected_text(crate::rtl::widget(ui, shown)).width(220.0).show_ui(ui, |ui| {
|
||||
if ui.selectable_label(!on, crate::rtl::widget(ui, crate::i18n::tr(lang, "None"))).clicked() {
|
||||
set(d, GridAlign::None);
|
||||
}
|
||||
if ui.selectable_label(on, crate::rtl::widget(ui, crate::i18n::tr(lang, "Roman Baseline"))).clicked() && !on {
|
||||
set(d, GridAlign::AllLines);
|
||||
}
|
||||
});
|
||||
ui.end_row();
|
||||
});
|
||||
let mut first = cur == GridAlign::FirstLineOnly;
|
||||
let check = egui::Checkbox::new(&mut first, crate::rtl::widget(ui, crate::i18n::tr(lang, "Align Only First Line to Grid")));
|
||||
if ui.add_enabled(cur != GridAlign::None, check).changed() {
|
||||
set(d, if first { GridAlign::FirstLineOnly } else { GridAlign::AllLines });
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::DesignApp;
|
||||
|
||||
/// A document with paragraph style Body on its text and character style Emphasis.
|
||||
fn app_with_text() -> DesignApp {
|
||||
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
app.run("file.new", json!({})).unwrap();
|
||||
app.run("style.paragraph.create", json!({"name": "Body"})).unwrap();
|
||||
app.run("style.character.create", json!({"name": "Emphasis", "chars": {"fontStyle": "Italic"}})).unwrap();
|
||||
let r = app.run("frame.create", json!({"rect": [72, 72, 300, 200], "content": "text", "text": "漢字かな"})).unwrap();
|
||||
app.run("text.select", json!({"story": r["story"], "anchor": 0, "focus": 2})).unwrap();
|
||||
app.run("style.paragraph.apply", json!({"name": "Body"})).unwrap();
|
||||
app
|
||||
}
|
||||
|
||||
/// The text painted by the open dialog once it has settled.
|
||||
fn painted(app: &mut DesignApp, ctx: &egui::Context) -> Vec<String> {
|
||||
fn collect(shape: &egui::Shape, out: &mut Vec<String>) {
|
||||
match shape {
|
||||
egui::Shape::Text(t) => out.push(t.galley.job.text.trim().to_string()),
|
||||
egui::Shape::Vec(shapes) => shapes.iter().for_each(|s| collect(s, out)),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
let screen = egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(1400.0, 900.0));
|
||||
let mut texts = Vec::new();
|
||||
for _ in 0..4 {
|
||||
let time = ctx.input(|i| i.time) + 1.0 / 60.0;
|
||||
let mut output = ctx.run_ui(egui::RawInput { screen_rect: Some(screen), time: Some(time), ..Default::default() }, |ui| {
|
||||
crate::dialogs::show(app, ui.ctx());
|
||||
});
|
||||
output.textures_delta.clear();
|
||||
texts.clear();
|
||||
for s in &output.shapes {
|
||||
collect(&s.shape, &mut texts);
|
||||
}
|
||||
}
|
||||
texts
|
||||
}
|
||||
|
||||
/// The list model, not painted labels: the section list scrolls, so lower rows may be clipped.
|
||||
#[test]
|
||||
fn cjk_sections_follow_show_cjk_features() {
|
||||
let mut app = app_with_text();
|
||||
let base = [("general", "General"), ("export", "Export Tagging")];
|
||||
let ids = |app: &DesignApp, para: bool| sections(crate::cjk_features(app), para, &base).into_iter().map(|s| s.0).collect::<Vec<_>>();
|
||||
app.run("prefs.set", json!({"cjkFeatures": true})).unwrap();
|
||||
assert_eq!(ids(&app, true), ["general", "cjk.tcy", "cjk.kenten", "cjk.composition", "cjk.grid", "export", "cjk.warichu"]);
|
||||
assert_eq!(ids(&app, false), ["general", "cjk.tcy", "cjk.kenten", "export", "cjk.warichu"]);
|
||||
assert_eq!(
|
||||
sections(true, false, &[("general", "General")]).into_iter().map(|s| s.0).collect::<Vec<_>>(),
|
||||
["general", "cjk.tcy", "cjk.kenten", "cjk.warichu"]
|
||||
);
|
||||
app.run("prefs.set", json!({"cjkFeatures": false})).unwrap();
|
||||
assert_eq!(ids(&app, true), ["general", "export"]);
|
||||
assert_eq!(ids(&app, false), ["general", "export"]);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn cjk_sections_draw() {
|
||||
let mut app = app_with_text();
|
||||
app.run("prefs.set", json!({"cjkFeatures": true})).unwrap();
|
||||
let ctx = egui::Context::default();
|
||||
for lang in ["", "ja", "ar"] {
|
||||
app.run("app.language", json!({"lang": lang})).unwrap();
|
||||
for s in SECTIONS {
|
||||
for fields in [
|
||||
json!({"id": "paragraphStyleOptions", "name": "Body"}),
|
||||
json!({"id": "paragraphStyleOptions", "new": true}),
|
||||
json!({"id": "characterStyleOptions", "name": "Emphasis"}),
|
||||
json!({"id": "characterStyleOptions", "name": "Emphasis", "c.kenten": true, "c.kentenCharacter": "※", "c.warichuLines": null}),
|
||||
] {
|
||||
let id = fields["id"].as_str().unwrap().to_string();
|
||||
let mut f = fields.clone();
|
||||
f["section"] = json!(s.id);
|
||||
app.ui.dialog = Some(Dialog::new(&id, f));
|
||||
painted(&mut app, &ctx);
|
||||
assert!(app.ui.dialog.is_some(), "{lang} {id} {} stays open", s.id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn kenten_and_japanese_composition_reach_the_style_and_its_paragraph() {
|
||||
let mut app = app_with_text();
|
||||
app.run("prefs.set", json!({"cjkFeatures": true})).unwrap();
|
||||
if let Some(st) = app.session.active_mut() {
|
||||
let table = designcraft_doc::cjk::MojikumiTable { name: "Body Text".into(), ..Default::default() };
|
||||
std::sync::Arc::make_mut(&mut st.doc).styles_mut().mojikumi_tables.push(table);
|
||||
}
|
||||
let doc = app.session.active().unwrap().doc.clone();
|
||||
let hard = kinsoku_sets(&doc).into_iter().find(|k| k.name == "HardKinsoku").unwrap();
|
||||
let moji = mojikumi_sets(&doc, "").into_iter().find(|s| s.1 == "Body Text").unwrap().0;
|
||||
assert_eq!(moji, "MojikumiTable/Body Text");
|
||||
|
||||
let ctx = egui::Context::default();
|
||||
app.ui.dialog = Some(Dialog::new("paragraphStyleOptions", json!({"name": "Body", "section": "cjk.kenten"})));
|
||||
painted(&mut app, &ctx);
|
||||
{
|
||||
let d = app.ui.dialog.as_mut().unwrap();
|
||||
pick_kenten(d, &CharFields { base: &Value::Null, sparse: false }, Some("\u{25CF}"));
|
||||
d.fields.insert("section".into(), json!("cjk.composition"));
|
||||
d.fields.insert("p.kinsoku".into(), serde_json::to_value(&hard).unwrap());
|
||||
d.fields.insert("p.mojikumi".into(), json!(moji));
|
||||
d.fields.insert("p.kinsokuHang".into(), json!("regular"));
|
||||
}
|
||||
painted(&mut app, &ctx);
|
||||
crate::dialogs::confirm(&mut app).unwrap();
|
||||
|
||||
let doc = app.session.active().unwrap().doc.clone();
|
||||
let st = doc.styles.para("Body").unwrap();
|
||||
assert_eq!(st.para.kinsoku, Some(Some(hard)));
|
||||
assert_eq!(st.para.mojikumi.as_deref(), Some("MojikumiTable/Body Text"));
|
||||
assert_eq!(st.para.kinsoku_hang, Some(designcraft_doc::cjk::KinsokuHang::Regular));
|
||||
assert_eq!((st.chars.kenten, st.chars.kenten_character.as_deref()), (Some(true), Some("\u{25CF}")));
|
||||
let a = app.run("type.selectionAttrs", json!({})).unwrap();
|
||||
assert_eq!(a["para"]["kinsoku"]["name"], "HardKinsoku", "the paragraph follows its style");
|
||||
assert_eq!(a["para"]["mojikumi"], "MojikumiTable/Body Text");
|
||||
assert_eq!(a["chars"]["kenten"], true);
|
||||
assert_eq!(a["chars"]["kentenCharacter"], "\u{25CF}");
|
||||
|
||||
// Default Rules (`null`) replaces the set.
|
||||
app.ui.dialog = Some(Dialog::new("paragraphStyleOptions", json!({"name": "Body", "section": "cjk.composition", "p.kinsoku": null})));
|
||||
painted(&mut app, &ctx);
|
||||
crate::dialogs::confirm(&mut app).unwrap();
|
||||
assert_eq!(app.session.active().unwrap().doc.styles.para("Body").unwrap().para.kinsoku, Some(None));
|
||||
assert!(app.run("type.selectionAttrs", json!({})).unwrap()["para"]["kinsoku"].is_null());
|
||||
|
||||
// A character style sets only the kenten attributes.
|
||||
app.ui.dialog = Some(Dialog::new("characterStyleOptions", json!({"name": "Emphasis", "section": "cjk.kenten"})));
|
||||
painted(&mut app, &ctx);
|
||||
{
|
||||
let d = app.ui.dialog.as_mut().unwrap();
|
||||
let base = json!({});
|
||||
pick_kenten(d, &CharFields { base: &base, sparse: true }, Some(""));
|
||||
}
|
||||
crate::dialogs::confirm(&mut app).unwrap();
|
||||
let st = app.session.active().unwrap().doc.styles.char_style("Emphasis").cloned().unwrap();
|
||||
assert_eq!((st.chars.kenten, st.chars.kenten_character.as_deref()), (Some(true), Some("")));
|
||||
}
|
||||
}
|
||||
@@ -90,6 +90,103 @@ const TABLE: &[(&str, [&str; 5])] = &[
|
||||
("Ruby…", ["Ruby …", "Ruby…", "Ruby…", "ルビ…", "旁注…"]),
|
||||
("Kenten", ["Kenten", "Kenten", "Kenten", "圏点", "圈点"]),
|
||||
("Warichu", ["Warichu", "Warichu", "Warichu", "割り注", "割注"]),
|
||||
// Style Options: CJK sections.
|
||||
("Tate-chu-yoko Settings", ["Tate-Chu-Yoko-Einstellungen", "Paramètres Tate-Chu-Yoko", "Ajustes de Tate-Chu-Yoko", "縦中横設定", "纵中横设置"]),
|
||||
("Kenten Settings", ["Kenten-Einstellungen", "Paramètres Kenten", "Ajustes de Kenten", "圏点設定", "圈点设置"]),
|
||||
(
|
||||
"Japanese Composition Settings",
|
||||
["Japanischer Satz", "Paramètres de composition japonaise", "Ajustes de composición japonesa", "日本語文字組版", "日文排版设置"],
|
||||
),
|
||||
("Grid Settings", ["Rastereinstellungen", "Paramètres de grille", "Ajustes de cuadrícula", "グリッド設定", "网格设置"]),
|
||||
("Warichu Settings", ["Warichu-Einstellungen", "Paramètres Warichu", "Ajustes de Warichu", "割注設定", "割注设置"]),
|
||||
("Up/Down Position:", ["Position oben/unten:", "Position haut/bas:", "Posición arriba/abajo:", "上下位置:", "上下位置:"]),
|
||||
("Left/Right Position:", ["Position links/rechts:", "Position gauche/droite:", "Posición izquierda/derecha:", "左右位置:", "左右位置:"]),
|
||||
("Kenten Type:", ["Kenten-Art:", "Type de kenten:", "Tipo de kenten:", "圏点種類:", "圈点类型:"]),
|
||||
("Sesame Dot", ["Sesampunkt", "Point sésame", "Punto de sésamo", "ゴマ", "芝麻点"]),
|
||||
("White Sesame Dot", ["Weißer Sesampunkt", "Point sésame blanc", "Punto de sésamo blanco", "白ゴマ", "空心芝麻点"]),
|
||||
("Bullseye", ["Zielscheibe", "Cible", "Diana", "蛇の目", "靶心"]),
|
||||
("Black Circle", ["Schwarzer Kreis", "Cercle noir", "Círculo negro", "黒丸", "实心圆"]),
|
||||
("Small Black Circle", ["Kleiner schwarzer Kreis", "Petit cercle noir", "Círculo negro pequeño", "小さい黒丸", "小实心圆"]),
|
||||
("Double Circle", ["Doppelkreis", "Double cercle", "Círculo doble", "二重丸", "双圆"]),
|
||||
("Black Triangle", ["Schwarzes Dreieck", "Triangle noir", "Triángulo negro", "黒三角", "实心三角"]),
|
||||
("White Triangle", ["Weißes Dreieck", "Triangle blanc", "Triángulo blanco", "白三角", "空心三角"]),
|
||||
("White Circle", ["Weißer Kreis", "Cercle blanc", "Círculo blanco", "白丸", "空心圆"]),
|
||||
("Small White Circle", ["Kleiner weißer Kreis", "Petit cercle blanc", "Círculo blanco pequeño", "小さい白丸", "小空心圆"]),
|
||||
("Character:", ["Zeichen:", "Caractère:", "Carácter:", "文字:", "字符:"]),
|
||||
("Kinsoku Set:", ["Kinsoku-Satz:", "Jeu kinsoku:", "Conjunto kinsoku:", "禁則処理セット:", "避头尾集:"]),
|
||||
("Default Rules", ["Standardregeln", "Règles par défaut", "Reglas predeterminadas", "既定の規則", "默认规则"]),
|
||||
("Hard Kinsoku", ["Hartes Kinsoku", "Kinsoku strict", "Kinsoku estricto", "強い禁則", "严格避头尾"]),
|
||||
("Soft Kinsoku", ["Weiches Kinsoku", "Kinsoku souple", "Kinsoku flexible", "弱い禁則", "宽松避头尾"]),
|
||||
(
|
||||
"Simplified Chinese Kinsoku",
|
||||
["Kinsoku für vereinfachtes Chinesisch", "Kinsoku chinois simplifié", "Kinsoku de chino simplificado", "簡体中国語禁則", "简体中文避头尾"],
|
||||
),
|
||||
(
|
||||
"Traditional Chinese Kinsoku",
|
||||
["Kinsoku für traditionelles Chinesisch", "Kinsoku chinois traditionnel", "Kinsoku de chino tradicional", "繁体中国語禁則", "繁体中文避头尾"],
|
||||
),
|
||||
("Korean Kinsoku", ["Koreanisches Kinsoku", "Kinsoku coréen", "Kinsoku coreano", "韓国語禁則", "韩文避头尾"]),
|
||||
("Kinsoku Type:", ["Kinsoku-Methode:", "Type de kinsoku:", "Tipo de kinsoku:", "禁則調整方式:", "避头尾调整方式:"]),
|
||||
("Push In First", ["Zuerst einziehen", "Rentrer d'abord", "Primero hacia dentro", "追い込み優先", "先推入"]),
|
||||
("Push Out First", ["Zuerst hinausschieben", "Repousser d'abord", "Primero hacia fuera", "追い出し優先", "先推出"]),
|
||||
("Push Out Only", ["Nur hinausschieben", "Repousser uniquement", "Solo hacia fuera", "追い出しのみ", "仅推出"]),
|
||||
(
|
||||
"Prioritize Adjustment Amount",
|
||||
["Anpassungsbetrag bevorzugen", "Priorité à la valeur d'ajustement", "Priorizar la cantidad de ajuste", "調整量を優先", "优先调整量"],
|
||||
),
|
||||
("Hanging Type:", ["Hängende Interpunktion:", "Ponctuation suspendue:", "Puntuación colgante:", "ぶら下がり方法:", "悬挂方式:"]),
|
||||
("Force", ["Erzwingen", "Forcer", "Forzar", "強制", "强制"]),
|
||||
("Bunri-Kinshi", ["Bunri-Kinshi", "Bunri-Kinshi", "Bunri-Kinshi", "分離禁止処理", "分离禁止"]),
|
||||
("Mojikumi Set:", ["Mojikumi-Satz:", "Jeu mojikumi:", "Conjunto mojikumi:", "文字組み:", "标点挤压集:"]),
|
||||
("Leading Model:", ["Zeilenabstandsmodell:", "Modèle d'interlignage:", "Modelo de interlineado:", "行送りの基準位置:", "行距基准位置:"]),
|
||||
(
|
||||
"Em Box Top/Right",
|
||||
["Geviert oben/rechts", "Haut/droite du cadratin", "Parte superior/derecha del cuadratín", "仮想ボディの上/右", "全角字框上/右"],
|
||||
),
|
||||
("Em Box Center", ["Geviert Mitte", "Centre du cadratin", "Centro del cuadratín", "仮想ボディの中央", "全角字框居中"]),
|
||||
(
|
||||
"Em Box Bottom/Left",
|
||||
["Geviert unten/links", "Bas/gauche du cadratin", "Parte inferior/izquierda del cuadratín", "仮想ボディの下/左", "全角字框下/左"],
|
||||
),
|
||||
(
|
||||
"Em Box Center (Down)",
|
||||
["Geviert Mitte (unten)", "Centre du cadratin (bas)", "Centro del cuadratín (abajo)", "仮想ボディの中央(下)", "全角字框居中(下)"],
|
||||
),
|
||||
("Roman Baseline", ["Lateinische Grundlinie", "Ligne de base romaine", "Línea de base romana", "欧文ベースライン", "罗马字基线"]),
|
||||
("Rensuuji", ["Rensuuji", "Rensuuji", "Rensuuji", "連数字処理", "连数字处理"]),
|
||||
(
|
||||
"Absorb Ideographic Space at Line End",
|
||||
[
|
||||
"Ideografisches Leerzeichen am Zeilenende absorbieren",
|
||||
"Absorber l'espace idéographique en fin de ligne",
|
||||
"Absorber el espacio ideográfico al final de línea",
|
||||
"全角スペースを行末吸収",
|
||||
"行尾吸收全角空格",
|
||||
],
|
||||
),
|
||||
("Grid Alignment:", ["Am Raster ausrichten:", "Alignement sur la grille:", "Alineación con cuadrícula:", "グリッド揃え:", "网格对齐:"]),
|
||||
(
|
||||
"Align Only First Line to Grid",
|
||||
[
|
||||
"Nur erste Zeile am Raster ausrichten",
|
||||
"Aligner uniquement la première ligne sur la grille",
|
||||
"Alinear solo la primera línea con la cuadrícula",
|
||||
"1 行目のみグリッドに揃える",
|
||||
"仅首行对齐网格",
|
||||
],
|
||||
),
|
||||
("Warichu Size:", ["Warichu-Größe:", "Taille warichu:", "Tamaño de warichu:", "割注サイズ:", "割注大小:"]),
|
||||
("Line Spacing:", ["Zeilenabstand:", "Interligne:", "Interlineado:", "行の間隔:", "行间距:"]),
|
||||
("Auto", ["Auto", "Auto", "Automático", "自動", "自动"]),
|
||||
("Line Break Options", ["Umbruchoptionen", "Options de saut de ligne", "Opciones de salto de línea", "改行オプション", "换行选项"]),
|
||||
(
|
||||
"Characters Before Break:",
|
||||
["Zeichen vor dem Umbruch:", "Caractères avant le saut:", "Caracteres antes del salto:", "1 行目の最小文字数:", "换行前最少字符数:"],
|
||||
),
|
||||
(
|
||||
"Characters After Break:",
|
||||
["Zeichen nach dem Umbruch:", "Caractères après le saut:", "Caracteres después del salto:", "最終行の最小文字数:", "换行后最少字符数:"],
|
||||
),
|
||||
("Font", ["Schriftart", "Police", "Fuente", "フォント", "字体"]),
|
||||
("Size", ["Schriftgrad", "Corps", "Tamaño", "サイズ", "大小"]),
|
||||
("Character", ["Zeichen", "Caractère", "Carácter", "文字", "字符"]),
|
||||
|
||||
@@ -49,6 +49,58 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Tate-Chu-Yoko", "縦中横"),
|
||||
("Ruby…", "ルビ…"),
|
||||
("Kenten", "圏点"),
|
||||
("Tate-chu-yoko Settings", "縦中横設定"),
|
||||
("Kenten Settings", "圏点設定"),
|
||||
("Japanese Composition Settings", "日本語文字組版"),
|
||||
("Grid Settings", "グリッド設定"),
|
||||
("Warichu Settings", "割注設定"),
|
||||
("Up/Down Position:", "上下位置:"),
|
||||
("Left/Right Position:", "左右位置:"),
|
||||
("Kenten Type:", "圏点種類:"),
|
||||
("Sesame Dot", "ゴマ"),
|
||||
("White Sesame Dot", "白ゴマ"),
|
||||
("Bullseye", "蛇の目"),
|
||||
("Black Circle", "黒丸"),
|
||||
("Small Black Circle", "小さい黒丸"),
|
||||
("Double Circle", "二重丸"),
|
||||
("Black Triangle", "黒三角"),
|
||||
("White Triangle", "白三角"),
|
||||
("White Circle", "白丸"),
|
||||
("Small White Circle", "小さい白丸"),
|
||||
("Character:", "文字:"),
|
||||
("Kinsoku Set:", "禁則処理セット:"),
|
||||
("Default Rules", "既定の規則"),
|
||||
("Hard Kinsoku", "強い禁則"),
|
||||
("Soft Kinsoku", "弱い禁則"),
|
||||
("Simplified Chinese Kinsoku", "簡体中国語禁則"),
|
||||
("Traditional Chinese Kinsoku", "繁体中国語禁則"),
|
||||
("Korean Kinsoku", "韓国語禁則"),
|
||||
("Kinsoku Type:", "禁則調整方式:"),
|
||||
("Push In First", "追い込み優先"),
|
||||
("Push Out First", "追い出し優先"),
|
||||
("Push Out Only", "追い出しのみ"),
|
||||
("Prioritize Adjustment Amount", "調整量を優先"),
|
||||
("Hanging Type:", "ぶら下がり方法:"),
|
||||
("Force", "強制"),
|
||||
("Bunri-Kinshi", "分離禁止処理"),
|
||||
("Mojikumi Set:", "文字組み:"),
|
||||
("Leading Model:", "行送りの基準位置:"),
|
||||
("Em Box Top/Right", "仮想ボディの上/右"),
|
||||
("Em Box Center", "仮想ボディの中央"),
|
||||
("Em Box Bottom/Left", "仮想ボディの下/左"),
|
||||
("Em Box Center (Down)", "仮想ボディの中央(下)"),
|
||||
("Roman Baseline", "欧文ベースライン"),
|
||||
("Rensuuji", "連数字処理"),
|
||||
("Absorb Ideographic Space at Line End", "全角スペースを行末吸収"),
|
||||
("Grid Alignment:", "グリッド揃え:"),
|
||||
("Align Only First Line to Grid", "1 行目のみグリッドに揃える"),
|
||||
("Lines:", "行数:"),
|
||||
("Warichu Size:", "割注サイズ:"),
|
||||
("Line Spacing:", "行の間隔:"),
|
||||
("Auto", "自動"),
|
||||
("Line Break Options", "改行オプション"),
|
||||
("Characters Before Break:", "1 行目の最小文字数:"),
|
||||
("Characters After Break:", "最終行の最小文字数:"),
|
||||
("Font", "フォント"),
|
||||
("Size", "サイズ"),
|
||||
("Character", "文字"),
|
||||
|
||||
Reference in New Issue
Block a user