mirror of
https://github.com/storytold/designcraft.git
synced 2026-11-14 14:22:39 +00:00
Merge pull request #2647 from dotCooCoo/feat/non-rectangular-frame-inset
Text Frame Options: a non-rectangular frame shows one Inset field
This commit is contained in:
16 files changed
+201
-13
No files matched your search
@@ -1005,6 +1005,28 @@ fn selected_frame_options(app: &DesignApp) -> Option<designcraft_doc::TextFrameO
|
||||
st.doc.item(fid)?.text_frame().map(|t| t.options.clone())
|
||||
}
|
||||
|
||||
/// Whether every selected text frame (the selected items, or the frame of the caret) has an
|
||||
/// outline that is not its own axis-aligned rectangle: an oval, a polygon, rounded or other corner
|
||||
/// options. Its text keeps one inset from the outline, as compose sets it; type on a path doesn't.
|
||||
/// A selection that mixes them with rectangular frames shows the four sides, so each frame keeps
|
||||
/// the sides left as shown.
|
||||
fn selected_frame_is_shaped(app: &DesignApp) -> bool {
|
||||
let Some(st) = app.session.active() else { return false };
|
||||
let shaped = |fid: designcraft_doc::ItemId| -> Option<bool> {
|
||||
let item = st.doc.item(fid)?;
|
||||
if item.text_frame()?.options.path.is_some() {
|
||||
return Some(false);
|
||||
}
|
||||
let bp = if item.corners.is_none() { item.path.to_bezpath() } else { designcraft_geom::corners::apply(&item.path, &item.corners) };
|
||||
Some(designcraft_compose::FrameOutline::new(&bp, 0.0).is_some())
|
||||
};
|
||||
if st.selection.items.is_empty() {
|
||||
let Some(fid) = st.selection.text.and_then(|t| t.frame) else { return false };
|
||||
return shaped(fid).unwrap_or(false);
|
||||
}
|
||||
st.selection.items.iter().all(|&fid| shaped(fid).unwrap_or(false))
|
||||
}
|
||||
|
||||
/// Object › Text Frame Options shows the selected frame's options. `_shown` keeps what it showed,
|
||||
/// so OK sends only the fields the user changed. Fields given when the dialog opened count as
|
||||
/// changed.
|
||||
@@ -1032,7 +1054,7 @@ fn seed_text_frame_options(app: &DesignApp, d: &mut Dialog) {
|
||||
};
|
||||
designcraft_doc::FrameBaselineGrid { start: g.start, increment: g.increment, relative_to }
|
||||
});
|
||||
let shown = json!({
|
||||
let mut shown = json!({
|
||||
"columns": o.columns, "gutter": fm(o.gutter),
|
||||
"columnsKind": kind(serde_json::to_value(o.columns_kind).unwrap_or_default()), "columnWidth": fm(width),
|
||||
"balanceColumns": o.balance_columns, "ignoreWrap": o.ignore_wrap,
|
||||
@@ -1051,6 +1073,12 @@ fn seed_text_frame_options(app: &DesignApp, d: &mut Dialog) {
|
||||
"customGrid": o.baseline_grid.is_some(), "gridStart": fm(grid.start), "gridIncrement": fm(grid.increment),
|
||||
"gridRelativeTo": grid.relative_to.name(),
|
||||
});
|
||||
// A non-rectangular frame shows one Inset (its top inset, the one kept from the outline).
|
||||
let shaped = selected_frame_is_shaped(app);
|
||||
if shaped && let Value::Object(m) = &mut shown {
|
||||
m.insert("inset".into(), fm(o.inset[0]));
|
||||
}
|
||||
d.fields.insert("_shaped".into(), json!(shaped));
|
||||
if let Value::Object(m) = &shown {
|
||||
for (k, v) in m {
|
||||
d.fields.entry(k.clone()).or_insert_with(|| v.clone());
|
||||
@@ -1078,9 +1106,11 @@ fn text_frame_options_dialog(app: &DesignApp, ui: &mut egui::Ui, d: &mut Dialog)
|
||||
}
|
||||
ui.set_min_width(560.0);
|
||||
ui.horizontal_top(|ui| {
|
||||
// A fixed height: the separator would otherwise stretch the dialog to the window.
|
||||
ui.set_min_height(330.0);
|
||||
ui.set_max_height(330.0);
|
||||
// A fixed height: the separator would otherwise stretch the dialog to the window. A
|
||||
// non-rectangular frame's Inset row needs one more row.
|
||||
let height = if d.b("_shaped") { 360.0 } else { 330.0 };
|
||||
ui.set_min_height(height);
|
||||
ui.set_max_height(height);
|
||||
section_list(
|
||||
&lang,
|
||||
ui,
|
||||
@@ -1218,8 +1248,9 @@ fn text_frame_preview(app: &mut DesignApp, d: &mut Dialog) {
|
||||
}
|
||||
|
||||
/// Text Frame Options › General, as InDesign's: Columns (kind, number and width, gutter and
|
||||
/// maximum, Balance Columns), Inset Spacing (top and bottom, the link, left and right), Vertical
|
||||
/// Justification (align, paragraph spacing limit) and Ignore Text Wrap.
|
||||
/// maximum, Balance Columns), Inset Spacing (top and bottom, the link, left and right; one Inset
|
||||
/// for a non-rectangular frame), Vertical Justification (align, paragraph spacing limit) and
|
||||
/// Ignore Text Wrap.
|
||||
fn general(lang: &str, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
let tr = |s: &'static str| crate::i18n::tr(lang, s);
|
||||
let (cap, field, cap2) = (64.0, 110.0, 70.0);
|
||||
@@ -1243,8 +1274,10 @@ fn general(lang: &str, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
// Fixed Number: each column's width, from the frame, the insets and the gutter.
|
||||
let frame = d.fields.get("_frameWidth").and_then(Value::as_f64);
|
||||
let n = d.n("columns").unwrap_or(1.0).max(1.0);
|
||||
// A non-rectangular frame keeps its sides until its one Inset is changed.
|
||||
let (left, right) = if d.b("_shaped") && edited(d, "inset") { ("inset", "inset") } else { ("insetLeft", "insetRight") };
|
||||
let width = frame.map(|w| {
|
||||
let inner = w - d.m("insetLeft").unwrap_or(0.0) - d.m("insetRight").unwrap_or(0.0);
|
||||
let inner = w - d.m(left).unwrap_or(0.0) - d.m(right).unwrap_or(0.0);
|
||||
((inner - d.m("gutter").unwrap_or(0.0) * (n - 1.0)) / n).max(0.0)
|
||||
});
|
||||
let _ = crate::widgets::NumField::measure("tfo_auto_width", width, d.unit).width(field).spinner().enabled(false).show(ui);
|
||||
@@ -1262,6 +1295,9 @@ fn general(lang: &str, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
check(ui, d, "balanceColumns", tr("Balance Columns"));
|
||||
});
|
||||
framed(ui, Some(tr("Inset Spacing")), |ui| {
|
||||
// A non-rectangular frame: the four sides are dimmed and one Inset sets the distance kept
|
||||
// from the outline.
|
||||
let shaped = d.b("_shaped");
|
||||
// While linked, a side typed in sets all four ("Make all settings the same").
|
||||
let sides = ["insetTop", "insetLeft", "insetBottom", "insetRight"];
|
||||
let before: Vec<String> = sides.iter().map(|k| d.s(k)).collect();
|
||||
@@ -1270,24 +1306,32 @@ fn general(lang: &str, ui: &mut egui::Ui, d: &mut Dialog) {
|
||||
{
|
||||
ui.horizontal(|ui| {
|
||||
caption_at(ui, tr(l1), cap);
|
||||
spin_measure(ui, d, k1, field, true);
|
||||
spin_measure(ui, d, k1, field, !shaped);
|
||||
ui.add_space(8.0);
|
||||
if r == 0 {
|
||||
let linked = d.b("insetLinked");
|
||||
let tip = tr("Make all settings the same");
|
||||
if crate::widgets::icon_toggle_sized(ui, if linked { "link" } else { "link-broken" }, linked, tip, egui::vec2(22.0, 21.0))
|
||||
.clicked()
|
||||
{
|
||||
let toggle = ui.add_enabled_ui(!shaped, |ui| {
|
||||
crate::widgets::icon_toggle_sized(ui, if linked { "link" } else { "link-broken" }, linked, tip, egui::vec2(22.0, 21.0))
|
||||
});
|
||||
if toggle.inner.clicked() {
|
||||
d.fields.insert("insetLinked".into(), json!(!linked));
|
||||
}
|
||||
} else {
|
||||
ui.add_space(22.0 + ui.spacing().item_spacing.x);
|
||||
}
|
||||
caption_at(ui, tr(l2), cap2 - 30.0);
|
||||
spin_measure(ui, d, k2, field, true);
|
||||
spin_measure(ui, d, k2, field, !shaped);
|
||||
});
|
||||
}
|
||||
if d.b("insetLinked")
|
||||
if shaped {
|
||||
ui.horizontal(|ui| {
|
||||
caption_at(ui, tr("Inset:"), cap);
|
||||
spin_measure(ui, d, "inset", field, true);
|
||||
});
|
||||
}
|
||||
if !shaped
|
||||
&& d.b("insetLinked")
|
||||
&& let Some(k) = sides.iter().zip(&before).find(|(k, b)| d.s(k) != **b).map(|(k, _)| *k)
|
||||
{
|
||||
let v = d.fields.get(k).cloned().unwrap_or_default();
|
||||
|
||||
@@ -1559,6 +1559,136 @@ fn text_frame_options_set_a_custom_baseline_grid_relative_to() {
|
||||
assert_eq!(grid(&app), None);
|
||||
}
|
||||
|
||||
/// A non-rectangular frame (an oval, or a rectangle with rounded corners) shows one Inset, the
|
||||
/// distance kept from its outline, and OK sends it as one value for all four sides. A rectangle
|
||||
/// keeps the four side fields.
|
||||
#[test]
|
||||
fn text_frame_options_set_one_inset_for_a_non_rectangular_frame() {
|
||||
let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
app.run("file.new", json!({})).unwrap();
|
||||
let oval = app.run("frame.create", json!({"rect": [36, 36, 336, 236], "shape": "ellipse", "content": "text", "text": "Hello"})).unwrap();
|
||||
let oval = designcraft_doc::ItemId(oval["id"].as_u64().unwrap());
|
||||
let rect = app.run("frame.create", json!({"rect": [36, 300, 336, 500], "content": "text", "text": "Hello"})).unwrap();
|
||||
let rect = designcraft_doc::ItemId(rect["id"].as_u64().unwrap());
|
||||
app.run("object.textFrameOptions", json!({"ids": [oval.0, rect.0], "inset": [6, 2, 3, 4]})).unwrap();
|
||||
let opts = |app: &DesignApp, id| app.session.doc().unwrap().doc.item(id).unwrap().text_frame().unwrap().options.clone();
|
||||
let open = |app: &mut DesignApp, id: designcraft_doc::ItemId| {
|
||||
app.run("selection.set", json!({"ids": [id.0]})).unwrap();
|
||||
let mut d = Dialog::new("textFrameOptions", json!({}));
|
||||
seed_text_frame_options(app, &mut d);
|
||||
d
|
||||
};
|
||||
// The oval shows its top inset as the one Inset.
|
||||
let d = open(&mut app, oval);
|
||||
assert!(d.b("_shaped"));
|
||||
assert_eq!(d.s("inset"), "0p6");
|
||||
// OK without edits changes nothing, and adds no undo step.
|
||||
let undo = app.session.doc().unwrap().history.undo.len();
|
||||
app.ui.dialog = Some(d);
|
||||
assert_eq!(confirm(&mut app).unwrap(), Value::Null);
|
||||
assert_eq!(app.session.doc().unwrap().history.undo.len(), undo);
|
||||
// Inset set to 1p0: all four sides take it.
|
||||
let mut d = open(&mut app, oval);
|
||||
d.fields.insert("inset".into(), json!("1p0"));
|
||||
app.ui.dialog = Some(d);
|
||||
confirm(&mut app).unwrap();
|
||||
assert_eq!(opts(&app, oval).inset, [12.0; 4]);
|
||||
assert_eq!(app.session.doc().unwrap().history.undo.len(), undo + 1, "one undo step");
|
||||
// A value that isn't a measure is refused and the frame keeps its inset.
|
||||
let mut d = open(&mut app, oval);
|
||||
d.fields.insert("inset".into(), json!("not a measure"));
|
||||
app.ui.dialog = Some(d);
|
||||
assert!(confirm(&mut app).unwrap_err().to_string().contains("the inset is not a measure"));
|
||||
assert_eq!(opts(&app, oval).inset, [12.0; 4]);
|
||||
// A rectangle shows the four sides; with rounded corners it is not rectangular either.
|
||||
let d = open(&mut app, rect);
|
||||
assert!(!d.b("_shaped") && !d.fields.contains_key("inset"), "a rectangle shows the four sides");
|
||||
app.run("object.cornerOptions", json!({"ids": [rect.0], "shape": "rounded", "size": 12})).unwrap();
|
||||
let d = open(&mut app, rect);
|
||||
assert!(d.b("_shaped"));
|
||||
assert_eq!(d.s("inset"), "0p6");
|
||||
}
|
||||
|
||||
/// A selection that mixes a non-rectangular frame with a rectangle shows the four sides, even with
|
||||
/// the oval first, and a changed side leaves the rectangle's other sides as they were.
|
||||
#[test]
|
||||
fn text_frame_options_show_four_sides_for_a_mixed_selection() {
|
||||
let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
app.run("file.new", json!({})).unwrap();
|
||||
let oval = app.run("frame.create", json!({"rect": [36, 36, 336, 236], "shape": "ellipse", "content": "text", "text": "Hello"})).unwrap();
|
||||
let oval = designcraft_doc::ItemId(oval["id"].as_u64().unwrap());
|
||||
let rect = app.run("frame.create", json!({"rect": [36, 300, 336, 500], "content": "text", "text": "Hello"})).unwrap();
|
||||
let rect = designcraft_doc::ItemId(rect["id"].as_u64().unwrap());
|
||||
app.run("object.textFrameOptions", json!({"ids": [oval.0, rect.0], "inset": [6, 2, 3, 4]})).unwrap();
|
||||
let opts = |app: &DesignApp, id| app.session.doc().unwrap().doc.item(id).unwrap().text_frame().unwrap().options.clone();
|
||||
app.run("selection.set", json!({"ids": [oval.0, rect.0]})).unwrap();
|
||||
let mut d = Dialog::new("textFrameOptions", json!({}));
|
||||
seed_text_frame_options(&app, &mut d);
|
||||
assert!(!d.b("_shaped") && !d.fields.contains_key("inset"), "a mixed selection shows the four sides");
|
||||
d.fields.insert("insetTop".into(), json!("1p0"));
|
||||
app.ui.dialog = Some(d);
|
||||
confirm(&mut app).unwrap();
|
||||
assert_eq!(opts(&app, rect).inset, [12.0, 2.0, 3.0, 4.0]);
|
||||
assert_eq!(opts(&app, oval).inset, [12.0, 2.0, 3.0, 4.0]);
|
||||
// Two non-rectangular frames still show the one Inset.
|
||||
app.run("object.cornerOptions", json!({"ids": [rect.0], "shape": "rounded", "size": 12})).unwrap();
|
||||
let mut d = Dialog::new("textFrameOptions", json!({}));
|
||||
seed_text_frame_options(&app, &mut d);
|
||||
assert!(d.b("_shaped"));
|
||||
}
|
||||
|
||||
/// The dialog dims Top, Left, Bottom, Right and the link for a non-rectangular frame and adds an
|
||||
/// Inset field; for a rectangle it shows no Inset field and the sides stay editable.
|
||||
#[test]
|
||||
fn text_frame_options_dims_the_sides_for_a_non_rectangular_frame() {
|
||||
use egui_kittest::{
|
||||
Harness,
|
||||
kittest::{NodeT, Queryable},
|
||||
};
|
||||
for shaped in [true, false] {
|
||||
let mut app = DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
app.run("file.new", json!({})).unwrap();
|
||||
let shape = if shaped { "ellipse" } else { "rectangle" };
|
||||
let r = app.run("frame.create", json!({"rect": [36, 36, 336, 236], "shape": shape, "content": "text", "text": "Hello"})).unwrap();
|
||||
app.run("selection.set", json!({"ids": [r["id"]]})).unwrap();
|
||||
app.ui.dialog = Some(Dialog::new("textFrameOptions", json!({})));
|
||||
// The General tab's Columns and Align dropdowns have no accessible name yet.
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1000.0, 800.0)).with_accessibility_check(false).build_ui_state(
|
||||
|ui, app: &mut DesignApp| {
|
||||
let key = egui::Id::unique("text_frame_inset_test_fonts");
|
||||
if ui.data(|d| d.get_temp::<bool>(key)).is_none() {
|
||||
crate::theme::install_fonts(ui.ctx(), "");
|
||||
ui.data_mut(|d| d.insert_temp(key, true));
|
||||
return;
|
||||
}
|
||||
show(app, ui.ctx());
|
||||
},
|
||||
app,
|
||||
);
|
||||
h.run_steps(3);
|
||||
for side in ["insetTop", "insetLeft", "insetBottom", "insetRight"] {
|
||||
let label = format!("Increase tfo_{side}");
|
||||
assert_eq!(h.get_by_label(&label).accesskit_node().is_disabled(), shaped, "{shape}: {side}");
|
||||
}
|
||||
assert_eq!(h.query_by_label("Inset:").is_some(), shaped, "{shape}: the Inset caption");
|
||||
assert_eq!(h.query_by_label("Increase tfo_inset").is_some(), shaped, "{shape}: the Inset field");
|
||||
// The General tab fits in the dialog's fixed height: the buttons stay put on another tab.
|
||||
let (wrap, cancel) = (h.get_by_label("Ignore Text Wrap").rect().bottom(), h.get_by_label("Cancel").rect().top());
|
||||
assert!(wrap < cancel, "{shape}: Ignore Text Wrap ends at {wrap}, below the top of Cancel at {cancel}");
|
||||
h.get_by_label("Auto-Size").click();
|
||||
h.run_steps(3);
|
||||
assert_eq!(h.get_by_label("Cancel").rect().top(), cancel, "{shape}: the buttons stay put on another tab");
|
||||
h.get_by_label("General").click();
|
||||
h.run_steps(3);
|
||||
// The link toggles only for a rectangle.
|
||||
let linked = |h: &Harness<'_, DesignApp>| h.state().ui.dialog.as_ref().unwrap().b("insetLinked");
|
||||
let before = linked(&h);
|
||||
h.get_by_label("Make all settings the same").click();
|
||||
h.run_steps(3);
|
||||
assert_eq!(linked(&h) != before, !shaped, "{shape}: the link");
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn text_frame_options_change_only_what_was_edited() {
|
||||
let mut app = crate::DesignApp::new(designcraft_engine::Session::new(), crate::Services::default());
|
||||
|
||||
@@ -1861,6 +1861,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Keep Options", "خيارات الإبقاء"),
|
||||
("Span Columns", "امتداد الأعمدة"),
|
||||
("Top:", "أعلى:"),
|
||||
("Inset:", "الإزاحة الداخلية:"),
|
||||
("Left:", "يسار:"),
|
||||
("Bottom:", "أسفل:"),
|
||||
("Right:", "يمين:"),
|
||||
|
||||
@@ -1916,6 +1916,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Third Space", "Třetina čtverčíku"),
|
||||
("Title Case", "Velká Počáteční Písmena"),
|
||||
("Top:", "Nahoře:"),
|
||||
("Inset:", "Odsazení:"),
|
||||
("Trademark Symbol", "Symbol ochranné známky"),
|
||||
("Transform Again Individually", "Transformovat znovu jednotlivě"),
|
||||
("Transform Sequence Again", "Transformovat posloupnost znovu"),
|
||||
|
||||
@@ -1866,6 +1866,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Third Space", "Drittelgeviert"),
|
||||
("Title Case", "Wortanfänge groß"),
|
||||
("Top:", "Oben:"),
|
||||
("Inset:", "Versatz:"),
|
||||
("Trademark Symbol", "Markensymbol"),
|
||||
("Transform Again Individually", "Erneut einzeln transformieren"),
|
||||
("Transform Sequence Again", "Abfolge erneut transformieren"),
|
||||
|
||||
@@ -1730,6 +1730,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Top Margin", "Felső margó"),
|
||||
("Top of Page", "Oldal teteje"),
|
||||
("Top:", "Fent:"),
|
||||
("Inset:", "Belső margó:"),
|
||||
("Track Changes", "Változások követése"),
|
||||
("Track Changes in All Stories", "Változások követése minden szövegfolyamban"),
|
||||
("Tracking", "Betűköz"),
|
||||
|
||||
@@ -1921,6 +1921,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Keep Options", "Opzioni di unione"),
|
||||
("Span Columns", "Estendi su colonne"),
|
||||
("Top:", "Superiore:"),
|
||||
("Inset:", "Rientro:"),
|
||||
("Left:", "Sinistra:"),
|
||||
("Bottom:", "Inferiore:"),
|
||||
("Right:", "Destra:"),
|
||||
|
||||
@@ -2059,6 +2059,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Third Space", "三分スペース"),
|
||||
("Title Case", "各単語の先頭を大文字に"),
|
||||
("Top:", "上:"),
|
||||
("Inset:", "フレーム内マージン:"),
|
||||
("Trademark Symbol", "商標記号"),
|
||||
("Transform Again Individually", "個別に再変形"),
|
||||
("Transform Sequence Again", "一連の変形を再実行"),
|
||||
|
||||
@@ -1957,6 +1957,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Third Space", "მესამედი შორისი"),
|
||||
("Title Case", "ყოველი სიტყვა მთავრულით"),
|
||||
("Top:", "ზემოთ:"),
|
||||
("Inset:", "შეწევა:"),
|
||||
("Trademark Symbol", "სავაჭრო ნიშნის სიმბოლო"),
|
||||
("Transform Again Individually", "გარდაქმნის ცალ-ცალკე გამეორება"),
|
||||
("Transform Sequence Again", "გარდაქმნების მიმდევრობის გამეორება"),
|
||||
|
||||
@@ -1891,6 +1891,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Third Space", "Trzecia firetu"),
|
||||
("Title Case", "Wielkie Litery Wyrazów"),
|
||||
("Top:", "Góra:"),
|
||||
("Inset:", "Margines wewnętrzny:"),
|
||||
("Trademark Symbol", "Symbol znaku towarowego"),
|
||||
("Transform Again Individually", "Przekształć ponownie osobno"),
|
||||
("Transform Sequence Again", "Powtórz sekwencję przekształceń"),
|
||||
|
||||
@@ -1468,6 +1468,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Keep Options", "Opções de manter"),
|
||||
("Span Columns", "Abranger colunas"),
|
||||
("Top:", "Superior:"),
|
||||
("Inset:", "Recuo:"),
|
||||
("Left:", "Esquerda:"),
|
||||
("Bottom:", "Inferior:"),
|
||||
("Right:", "Direita:"),
|
||||
|
||||
@@ -1901,6 +1901,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Third Space", "Треть шпации"),
|
||||
("Title Case", "Каждое слово с прописной"),
|
||||
("Top:", "Сверху:"),
|
||||
("Inset:", "Отступ:"),
|
||||
("Trademark Symbol", "Знак товарного знака"),
|
||||
("Transform Again Individually", "Повторить трансформирование по отдельности"),
|
||||
("Transform Sequence Again", "Повторить последовательность трансформирования"),
|
||||
|
||||
@@ -65,6 +65,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Gutter:", "Spaltmellanrum:"),
|
||||
("Fit Guides to:", "Anpassa stödlinjer till:"),
|
||||
("Top:", "Överkant:"),
|
||||
("Inset:", "Infällning:"),
|
||||
("Bottom:", "Nederkant:"),
|
||||
("Inside:", "Inre:"),
|
||||
("Outside:", "Yttre:"),
|
||||
|
||||
@@ -479,6 +479,7 @@ pub(super) const TABLE: &[(&str, [&str; 5])] = &[
|
||||
("Gutter:", ["Steg:", "Gouttière :", "Medianil:", "間隔:", "栏间距:"]),
|
||||
("Fit Guides to:", ["Hilfslinien einpassen in:", "Ajuster les repères à :", "Ajustar guías a:", "ガイドの適用先:", "参考线适合:"]),
|
||||
("Top:", ["Oben:", "Haut :", "Superior:", "天:", "上:"]),
|
||||
("Inset:", ["Versatz:", "Marge intérieure :", "Margen interior:", "フレーム内マージン :", "内边距:"]),
|
||||
("Bottom:", ["Unten:", "Bas :", "Inferior:", "地:", "下:"]),
|
||||
("Inside:", ["Innen:", "Intérieur :", "Interior:", "ノド:", "内:"]),
|
||||
("Outside:", ["Außen:", "Extérieur :", "Exterior:", "小口:", "外:"]),
|
||||
|
||||
@@ -1888,6 +1888,7 @@ pub(super) const TABLE: &[(&str, &str)] = &[
|
||||
("Keep Options", "Параметри утримання"),
|
||||
("Span Columns", "Охоплення колонок"),
|
||||
("Top:", "Зверху:"),
|
||||
("Inset:", "Відступ:"),
|
||||
("Left:", "Ліворуч:"),
|
||||
("Bottom:", "Знизу:"),
|
||||
("Right:", "Праворуч:"),
|
||||
|
||||
@@ -353,6 +353,7 @@ It is evidence of breadth: each item exists. It is not a claim of InDesign-level
|
||||
- Auto-size (Text Frame Options › Auto-Size, `object.textFrameOptions {autoSize: off|heightOnly|widthOnly|heightAndWidth|heightAndWidthProportionally, autoSizeReference, useMinimumHeight, minimumHeight, useMinimumWidth, minimumWidth, noLineBreaks}`): the frame fits its text exactly after every edit (typing, pasting, style changes, option changes) in the same undo step (opening a file keeps the frame sizes it gives); it grows and shrinks around the reference point, which stays put in spread space for rotated frames too, never below the minimum height/width and never past 10,000 pt. Width Only and No Line Breaks set each paragraph on one line; Height and Width without No Line Breaks keeps the width; Keep Proportions scales both sides until the text fits. IDML keeps all the options (`AutoSizing…`, `…ForAutoSizing` attributes). Not covered: frames of a thread other than the last, vertical frames, type on a path, and multi-column balancing in Keep Proportions.
|
||||
- Balance Columns (Text Frame Options › Balance Columns, `object.textFrameOptions {balanceColumns}`, IDML `VerticalBalanceColumns`): in the frame the story ends in, the columns after the last explicit column break fill to about the same depth (the last may be shorter by a line per column); keep options hold, spanning and split paragraphs keep their own balancing, vertical justification then applies to each column as usual, and vertical frames balance their bands. A frame the text runs on from, or that is overset, fills normally.
|
||||
- Text Frame Options dialog, laid out as InDesign's (General, Column Rules, Baseline Options, Auto-Size; framed Columns, Inset Spacing and Vertical Justification groups with spinner fields; Fixed Number shows each column's width as the fields change). Preview shows the edits on the page until OK (one undo step) or Cancel. An invalid value (such as a gutter that isn't a measure) takes the preview off the page, so the frame shows as it was, and the dialog's status line gives the reason; OK refuses the value too. Preview stays checked and shows the edits again once the value is valid. Vertical Justification › Paragraph Spacing Limit (`object.textFrameOptions {paragraphSpacingLimit}`, IDML `VerticalThreshold`): with Justify, the extra space goes between paragraphs up to the limit, the rest between lines.
|
||||
- Text Frame Options › Inset Spacing for a non-rectangular frame (an oval, a polygon, a frame with corner options; when every selected text frame is one, a mixed selection keeps the four sides): Top, Left, Bottom, Right and the link are dimmed, and one Inset field sets the distance kept between the text and the frame's outline (`object.textFrameOptions {inset: <number>}`, all four sides).
|
||||
- Place a page of an IDML or DesignCraft document (`file.place {layoutPage}`): its objects, stories, styles and swatches as one group (INDD files are not readable).
|
||||
- Articles panel (`article.new/add/remove/options/delete/list`): reading order and content of EPUB and HTML exports.
|
||||
- Object ▸ Clipping Path (`object.clippingPath {type: alpha|edges, threshold, tolerance}`): traces the placed image and makes the outline the frame. Clipping paths saved in IDML (Detect Edges, Alpha Channel, Photoshop Path, User-Modified Path; Invert) hide the image outside the path on screen and in PDF and survive IDML export; `object.clippingPathOptions` reports, inverts or removes one (Object ▸ Clipping Path ▸ None).
|
||||
|
||||
Reference in new issue
Block a user