mirror of
https://github.com/storytold/vectorcraft.git
synced 2026-11-01 21:57:12 -06:00
Merge pull request #5063 from dotCooCoo/fix/character-panel-mixed-toggles
Type: Character panel toggles show off for a mixed selection, and text.setFormat checks on/off values
This commit is contained in:
@@ -318,6 +318,22 @@ fn spacing_param(p: &Value, key: &str, c: &str) -> Result<Option<[Option<f64>; 3
|
||||
fn set_format(s: &mut Session, p: &Value) -> Result<Value> {
|
||||
const C: &str = "text.setFormat";
|
||||
let ids = super::typecmd::text_targets(s, p, C)?;
|
||||
check_switches(
|
||||
p,
|
||||
&[
|
||||
"underline",
|
||||
"strikethrough",
|
||||
"allCaps",
|
||||
"proportionalMetrics",
|
||||
"tateChuYoko",
|
||||
"standardVerticalRoman",
|
||||
"noBreak",
|
||||
"hyphenate",
|
||||
"hyphenateCapitalized",
|
||||
"romanHangingPunctuation",
|
||||
],
|
||||
C,
|
||||
)?;
|
||||
let num = |k: &str| p.get(k).and_then(Value::as_f64);
|
||||
let flag = |k: &str| p.get(k).and_then(Value::as_bool);
|
||||
let kerning = match p.get("kerning") {
|
||||
|
||||
@@ -325,6 +325,16 @@ fn type_commands_refuse_a_number_or_boolean_given_as_text() {
|
||||
("text.setRangeStyle", json!({"id": id.0, "allCaps": 1}), "allCaps"),
|
||||
("text.setRangeStyle", json!({"id": id.0, "noBreak": "true"}), "noBreak"),
|
||||
("text.setRangeStyle", json!({"id": id.0, "size": 30, "tateChuYoko": 1}), "tateChuYoko"),
|
||||
("text.setFormat", json!({"ids": [id.0], "underline": "true"}), "underline"),
|
||||
("text.setFormat", json!({"ids": [id.0], "strikethrough": 1}), "strikethrough"),
|
||||
("text.setFormat", json!({"ids": [id.0], "allCaps": "false"}), "allCaps"),
|
||||
("text.setFormat", json!({"ids": [id.0], "proportionalMetrics": 1}), "proportionalMetrics"),
|
||||
("text.setFormat", json!({"ids": [id.0], "tateChuYoko": "true"}), "tateChuYoko"),
|
||||
("text.setFormat", json!({"ids": [id.0], "standardVerticalRoman": "true"}), "standardVerticalRoman"),
|
||||
("text.setFormat", json!({"ids": [id.0], "rotation": 10, "noBreak": 1}), "noBreak"),
|
||||
("text.setFormat", json!({"ids": [id.0], "hyphenate": "false"}), "hyphenate"),
|
||||
("text.setFormat", json!({"ids": [id.0], "hyphenateCapitalized": 0}), "hyphenateCapitalized"),
|
||||
("text.setFormat", json!({"ids": [id.0], "romanHangingPunctuation": "true"}), "romanHangingPunctuation"),
|
||||
("text.getRange", json!({"id": id.0, "effective": "true"}), "effective"),
|
||||
("type.opticalMarginAlignment", json!({"on": "true"}), "on"),
|
||||
("type.systemLayout", json!({"on": "true"}), "on"),
|
||||
@@ -338,12 +348,13 @@ fn type_commands_refuse_a_number_or_boolean_given_as_text() {
|
||||
assert!(wrong.is_empty(), "not refused: {wrong:#?}");
|
||||
let style = json!({"id": id.0, "size": 30, "underline": true, "tracking": null, "start": null, "noBreak": true});
|
||||
s.execute("text.setRangeStyle", &style).unwrap();
|
||||
s.execute("text.setFormat", &json!({"ids": [id.0], "strikethrough": true, "hyphenate": null})).unwrap();
|
||||
s.execute("type.size.increase", &json!({"by": null})).unwrap();
|
||||
s.execute("type.opticalMarginAlignment", &json!({"on": true})).unwrap();
|
||||
s.execute("type.ruby", &json!({"text": "a", "size": null, "overhang": false})).unwrap();
|
||||
let t = obj(&s, id);
|
||||
let st = &t.runs[0].style;
|
||||
assert!(st.underline && st.no_break && t.optical_margin && st.size > 30.0, "{st:?}");
|
||||
assert!(st.underline && st.strikethrough && st.no_break && t.optical_margin && st.size > 30.0, "{st:?}");
|
||||
assert!(!t.ruby.is_empty() && t.ruby.iter().all(|r| !r.overhang && r.size_percent == 50.0), "{:?}", t.ruby);
|
||||
}
|
||||
|
||||
|
||||
@@ -419,11 +419,12 @@ pub(crate) fn highlight(app: &mut VectorcraftApp, field: Field) -> Result<Value,
|
||||
}
|
||||
|
||||
/// `type.underline` (Cmd+Shift+U): what the Underline button does, on the text the panels act on.
|
||||
/// Returns whether the text is now underlined.
|
||||
/// Returns whether the text is now underlined: off only when every character was underlined.
|
||||
pub(crate) fn toggle_underline(app: &mut VectorcraftApp) -> Result<Value, String> {
|
||||
let (s, _) = text_style(app).ok_or("select some type first")?;
|
||||
format(app, json!({"underline": !s.underline}));
|
||||
Ok(json!(!s.underline))
|
||||
text_style(app).ok_or("select some type first")?;
|
||||
let on = shared(app, |s| s.underline) != Some(true);
|
||||
format(app, json!({"underline": on}));
|
||||
Ok(json!(on))
|
||||
}
|
||||
|
||||
/// `text.setRangeStyle` with `p` on the range the Type tool has selected (the whole text when
|
||||
@@ -946,15 +947,19 @@ fn attributes(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
}
|
||||
ui.add_space(4.0);
|
||||
ui.horizontal(|ui| {
|
||||
// On only when every character the panel acts on has it; a mixed selection turns it on.
|
||||
let all_caps = shared(app, |s| s.all_caps) == Some(true);
|
||||
let small = shared(app, |s| s.small_caps.is_some()) == Some(true);
|
||||
let position = shared(app, |s| s.position).map(|p| p.id()).unwrap_or("");
|
||||
let underline = shared(app, |s| s.underline) == Some(true);
|
||||
let strike = shared(app, |s| s.strikethrough) == Some(true);
|
||||
for (kind, tip, on, change) in [
|
||||
(Glyph::AllCaps, tl!("All Caps"), s.all_caps, json!({"allCaps": !s.all_caps})),
|
||||
(Glyph::AllCaps, tl!("All Caps"), all_caps, json!({"allCaps": !all_caps})),
|
||||
(Glyph::SmallCaps, tl!("Small Caps"), small, json!({"smallCaps": !small})),
|
||||
(Glyph::Super, tl!("Superscript"), position == "superscript", position_toggle(position, "superscript")),
|
||||
(Glyph::Sub, tl!("Subscript"), position == "subscript", position_toggle(position, "subscript")),
|
||||
(Glyph::Underline, tl!("Underline"), s.underline, json!({"underline": !s.underline})),
|
||||
(Glyph::Strike, tl!("Strikethrough"), s.strikethrough, json!({"strikethrough": !s.strikethrough})),
|
||||
(Glyph::Underline, tl!("Underline"), underline, json!({"underline": !underline})),
|
||||
(Glyph::Strike, tl!("Strikethrough"), strike, json!({"strikethrough": !strike})),
|
||||
] {
|
||||
if style_toggle(ui, kind, tip, on) {
|
||||
format(app, change);
|
||||
@@ -1093,8 +1098,7 @@ fn digits_items(app: &mut VectorcraftApp, ui: &mut Ui, digits: Option<vectorcraf
|
||||
}
|
||||
|
||||
pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
let st = text_style(app);
|
||||
let has = st.is_some();
|
||||
let has = text_style(app).is_some();
|
||||
let hidden: bool = pstate(ui.ctx(), "ch-hide-options");
|
||||
if menu_item(ui, if hidden { tl!("Show Options") } else { tl!("Hide Options") }, true, false) {
|
||||
set_pstate(ui.ctx(), "ch-hide-options", !hidden);
|
||||
@@ -1109,11 +1113,12 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
set_pstate(ui.ctx(), FONT_HEIGHT_OPTIONS, !heights);
|
||||
}
|
||||
ui.separator();
|
||||
let s = st.map(|x| x.0);
|
||||
// A check item is checked when every character the panel acts on has it; choosing it with a
|
||||
// mixed selection turns it on.
|
||||
for (label, key, on) in [
|
||||
(tl!("All Caps"), "allCaps", s.as_ref().is_some_and(|s| s.all_caps)),
|
||||
(tl!("Underline"), "underline", s.as_ref().is_some_and(|s| s.underline)),
|
||||
(tl!("Strikethrough"), "strikethrough", s.as_ref().is_some_and(|s| s.strikethrough)),
|
||||
(tl!("All Caps"), "allCaps", shared(app, |s| s.all_caps) == Some(true)),
|
||||
(tl!("Underline"), "underline", shared(app, |s| s.underline) == Some(true)),
|
||||
(tl!("Strikethrough"), "strikethrough", shared(app, |s| s.strikethrough) == Some(true)),
|
||||
] {
|
||||
if menu_item(ui, label, has, on) {
|
||||
format(app, json!({key: !on}));
|
||||
@@ -1134,7 +1139,7 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
// Character Alignment: where characters smaller than the largest on their line line up, with
|
||||
// the East Asian options.
|
||||
if app.session.prefs.show_east_asian_options {
|
||||
let align = s.as_ref().map(|s| s.char_align);
|
||||
let align = shared(app, |s| s.char_align);
|
||||
ui.add_enabled_ui(has, |ui| {
|
||||
// Indented like the items beside it (their check column).
|
||||
ui.menu_button(format!(" {}", tl!("Character Alignment")), |ui| {
|
||||
@@ -1154,12 +1159,12 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
});
|
||||
});
|
||||
// Proportional Metrics: full-width glyphs on the font's proportional widths (`palt`, `vpal`).
|
||||
let proportional = s.as_ref().is_some_and(|s| s.proportional_metrics);
|
||||
let proportional = shared(app, |s| s.proportional_metrics) == Some(true);
|
||||
if menu_item(ui, tl!("Proportional Metrics"), has, proportional) {
|
||||
format(app, json!({"proportionalMetrics": !proportional}));
|
||||
}
|
||||
// Tate-chu-yoko: the selected characters stand side by side across one em of a vertical column.
|
||||
let tcy = s.as_ref().is_some_and(|s| s.tate_chu_yoko);
|
||||
let tcy = shared(app, |s| s.tate_chu_yoko) == Some(true);
|
||||
if menu_item(ui, tl!("Tate-chu-yoko"), has, tcy) {
|
||||
format(app, json!({"tateChuYoko": !tcy}));
|
||||
}
|
||||
@@ -1175,7 +1180,7 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
// Digits: the numeral system the selected characters' digits are drawn in, with the Middle
|
||||
// Eastern (Indic) options, as Paragraph Direction is in the Paragraph panel.
|
||||
if app.session.prefs.show_indic_options {
|
||||
let digits = s.as_ref().map(|s| s.digits);
|
||||
let digits = shared(app, |s| s.digits);
|
||||
ui.add_enabled_ui(has, |ui| {
|
||||
ui.menu_button(format!(" {}", tl!("Digits")), |ui| digits_items(app, ui, digits));
|
||||
});
|
||||
@@ -1185,7 +1190,7 @@ pub fn menu(app: &mut VectorcraftApp, ui: &mut Ui) {
|
||||
ui.separator();
|
||||
}
|
||||
// No Break: the selected characters stay on one line.
|
||||
let no_break = s.as_ref().is_some_and(|s| s.no_break);
|
||||
let no_break = shared(app, |s| s.no_break) == Some(true);
|
||||
if menu_item(ui, tl!("No Break"), has, no_break) {
|
||||
format(app, json!({"noBreak": !no_break}));
|
||||
}
|
||||
|
||||
@@ -415,9 +415,17 @@ fn bold_and_italic_pick_the_familys_own_faces() {
|
||||
}
|
||||
|
||||
/// From one frame of the panel: a point inside each of the Small Caps, Superscript and
|
||||
/// Subscript buttons, and which of them are drawn on. The two buttons with a "1" are
|
||||
/// Superscript (the "1" up) and Subscript; Small Caps is the button before them.
|
||||
/// Subscript buttons, and which of them are drawn on.
|
||||
fn script_buttons(ctx: &egui::Context, app: &mut VectorcraftApp) -> ([egui::Pos2; 3], [bool; 3]) {
|
||||
let (points, on) = style_buttons(ctx, app);
|
||||
([points[1], points[2], points[3]], [on[1], on[2], on[3]])
|
||||
}
|
||||
|
||||
/// From one frame of the panel: a point inside each of the All Caps, Small Caps, Superscript,
|
||||
/// Subscript, Underline and Strikethrough buttons, and which of them are drawn on. The two buttons
|
||||
/// with a "1" are Superscript (the "1" up) and Subscript; the others sit one button apart from them
|
||||
/// in that order.
|
||||
fn style_buttons(ctx: &egui::Context, app: &mut VectorcraftApp) -> ([egui::Pos2; 6], [bool; 6]) {
|
||||
let mut out = ctx.run_ui(egui::RawInput::default(), |ui| show(app, ui));
|
||||
out.textures_delta.clear();
|
||||
fn walk(s: &egui::Shape, on: egui::Color32, ones: &mut Vec<egui::Pos2>, lit: &mut Vec<egui::Rect>) {
|
||||
@@ -437,7 +445,8 @@ fn script_buttons(ctx: &egui::Context, app: &mut VectorcraftApp) -> ([egui::Pos2
|
||||
// Each "1" is drawn 6 points right of its button's center and 5 points above or below it.
|
||||
let y = (sup.y + sub.y) / 2.0;
|
||||
let (sup, sub) = (egui::pos2(sup.x - 6.0, y), egui::pos2(sub.x - 6.0, y));
|
||||
let points = [sup - vec2(sub.x - sup.x, 0.0), sup, sub];
|
||||
let step = vec2(sub.x - sup.x, 0.0);
|
||||
let points = [sup - step * 2.0, sup - step, sup, sub, sub + step, sub + step * 2.0];
|
||||
(points, points.map(|p| lit.iter().any(|r| r.contains(p))))
|
||||
}
|
||||
|
||||
@@ -802,6 +811,131 @@ fn character_mixed_small_caps_and_position_show_off() {
|
||||
assert_eq!(checked(&mut app), [false, false, false]);
|
||||
}
|
||||
|
||||
/// The All Caps, Underline and Strikethrough buttons are drawn off when only some of the selected
|
||||
/// characters have them, and a click turns them on for every character.
|
||||
#[test]
|
||||
fn mixed_all_caps_underline_and_strikethrough_buttons_show_off_and_turn_on() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 300, "height": 200})).unwrap();
|
||||
let id = app.run("text.create", json!({"x": 20, "y": 50, "text": "AB", "size": 12})).unwrap()["id"].as_u64().unwrap();
|
||||
app.run("text.setRangeStyle", json!({"id": id, "start": 0, "end": 1, "allCaps": true, "underline": true, "strikethrough": true})).unwrap();
|
||||
app.run("select.set", json!({"ids": [id]})).unwrap();
|
||||
let flags = |app: &VectorcraftApp| match &app.session.active().unwrap().doc.node(NodeId(id)).unwrap().kind {
|
||||
NodeKind::Text(t) => t.runs.iter().map(|r| [r.style.all_caps, r.style.underline, r.style.strikethrough]).collect::<Vec<_>>(),
|
||||
_ => panic!("text"),
|
||||
};
|
||||
let ctx = egui::Context::default();
|
||||
let (at, lit) = style_buttons(&ctx, &mut app);
|
||||
assert_eq!([lit[0], lit[4], lit[5]], [false; 3]);
|
||||
for (i, button) in [0, 4, 5].into_iter().enumerate() {
|
||||
super::super::tests_appearance::click(&ctx, &mut app, at[button], show);
|
||||
assert!(flags(&app).iter().all(|f| f[i]), "button {button}: {:?}", flags(&app));
|
||||
assert!(style_buttons(&ctx, &mut app).1[button], "button {button}");
|
||||
}
|
||||
}
|
||||
|
||||
/// `type.underline` does what the Underline button does: with only some of the selected characters
|
||||
/// underlined it underlines all of them, and with all of them underlined it turns underlining off.
|
||||
#[test]
|
||||
fn underline_command_underlines_a_mixed_selection() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.run("file.new", json!({"width": 300, "height": 200})).unwrap();
|
||||
let id = app.run("text.create", json!({"x": 20, "y": 50, "text": "AB", "size": 12})).unwrap()["id"].as_u64().unwrap();
|
||||
app.run("text.setRangeStyle", json!({"id": id, "start": 0, "end": 1, "underline": true})).unwrap();
|
||||
app.run("select.set", json!({"ids": [id]})).unwrap();
|
||||
let underlined = |app: &VectorcraftApp| match &app.session.active().unwrap().doc.node(NodeId(id)).unwrap().kind {
|
||||
NodeKind::Text(t) => t.runs.iter().map(|r| r.style.underline).collect::<Vec<_>>(),
|
||||
_ => panic!("text"),
|
||||
};
|
||||
assert_eq!(super::toggle_underline(&mut app), Ok(json!(true)));
|
||||
assert!(underlined(&app).iter().all(|u| *u), "{:?}", underlined(&app));
|
||||
assert_eq!(super::toggle_underline(&mut app), Ok(json!(false)));
|
||||
assert!(underlined(&app).iter().all(|u| !*u), "{:?}", underlined(&app));
|
||||
}
|
||||
|
||||
/// The panel menu's All Caps, Underline, Strikethrough, Proportional Metrics, Tate-chu-yoko and
|
||||
/// No Break are unchecked when only some of the selected characters have them, and choosing one
|
||||
/// turns it on for every character.
|
||||
#[test]
|
||||
fn mixed_check_items_show_unchecked_and_turn_on() {
|
||||
type Attribute = fn(&CharStyle) -> bool;
|
||||
let items: [(&str, &str, Attribute); 6] = [
|
||||
("All Caps", "allCaps", |s| s.all_caps),
|
||||
("Underline", "underline", |s| s.underline),
|
||||
("Strikethrough", "strikethrough", |s| s.strikethrough),
|
||||
("Proportional Metrics", "proportionalMetrics", |s| s.proportional_metrics),
|
||||
("Tate-chu-yoko", "tateChuYoko", |s| s.tate_chu_yoko),
|
||||
("No Break", "noBreak", |s| s.no_break),
|
||||
];
|
||||
let mut wrong = vec![];
|
||||
for (label, key, has) in items {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.session.prefs.show_east_asian_options = true;
|
||||
app.run("file.new", json!({"width": 300, "height": 200})).unwrap();
|
||||
let id = app.run("text.create", json!({"x": 20, "y": 50, "text": "AB"})).unwrap()["id"].as_u64().unwrap();
|
||||
app.run("text.setRangeStyle", json!({"id": id, "start": 0, "end": 1, key: true})).unwrap();
|
||||
app.run("select.set", json!({"ids": [id]})).unwrap();
|
||||
let on = |app: &VectorcraftApp| match &app.session.active().unwrap().doc.node(NodeId(id)).unwrap().kind {
|
||||
NodeKind::Text(t) => t.runs.iter().map(|r| has(&r.style)).collect::<Vec<_>>(),
|
||||
_ => panic!("text"),
|
||||
};
|
||||
assert_eq!(on(&app), [true, false], "{label}");
|
||||
let ctx = egui::Context::default();
|
||||
let row = |app: &mut VectorcraftApp| {
|
||||
let texts = super::super::tests_appearance::frame_events(&ctx, app, vec![], menu);
|
||||
texts.into_iter().find(|(t, _)| t.trim_start_matches(['✓', ' ']) == label).unwrap_or_else(|| panic!("{label} missing"))
|
||||
};
|
||||
let (text, at) = row(&mut app);
|
||||
super::super::tests_appearance::click(&ctx, &mut app, at.center(), menu);
|
||||
let shown = row(&mut app).0;
|
||||
if text.starts_with('✓') || !on(&app).iter().all(|o| *o) || !shown.starts_with('✓') {
|
||||
wrong.push(format!("{label}: shown as {text:?}, then {:?} and {shown:?}", on(&app)));
|
||||
}
|
||||
}
|
||||
assert!(wrong.is_empty(), "{wrong:#?}");
|
||||
}
|
||||
|
||||
/// The panel menu's Character Alignment and Digits submenus check no item when the selected
|
||||
/// characters differ, and check the value they share.
|
||||
#[test]
|
||||
fn character_alignment_and_digits_check_nothing_for_a_mixed_selection() {
|
||||
let mut app = VectorcraftApp::new(Session::new(), Default::default());
|
||||
app.session.prefs.show_east_asian_options = true;
|
||||
app.session.prefs.show_indic_options = true;
|
||||
app.run("file.new", json!({"width": 300, "height": 200})).unwrap();
|
||||
let id = app.run("text.create", json!({"x": 20, "y": 50, "text": "A1"})).unwrap()["id"].as_u64().unwrap();
|
||||
app.run("text.setRangeStyle", json!({"id": id, "start": 0, "end": 1, "charAlign": "emBoxTop", "digits": "hindi"})).unwrap();
|
||||
app.run("select.set", json!({"ids": [id]})).unwrap();
|
||||
let press = |pos: egui::Pos2, pressed: bool| egui::Event::PointerButton {
|
||||
pos,
|
||||
button: egui::PointerButton::Primary,
|
||||
pressed,
|
||||
modifiers: Default::default(),
|
||||
};
|
||||
let ticked = |texts: &[(String, egui::Rect)]| texts.iter().filter_map(|(t, _)| t.strip_prefix("✓ ")).map(str::to_string).collect::<Vec<_>>();
|
||||
// The items checked in the submenu `name` opens (not in the panel menu itself), in a fresh
|
||||
// context, so that no other submenu is open.
|
||||
let checked = |app: &mut VectorcraftApp, name: &str| {
|
||||
let ctx = egui::Context::default();
|
||||
crate::theme::install_fonts(&ctx);
|
||||
let mut frame = |events: Vec<egui::Event>| super::super::tests_appearance::frame_events(&ctx, app, events, menu);
|
||||
let texts = frame(vec![]);
|
||||
let before = ticked(&texts);
|
||||
let sub = texts.iter().find(|(t, _)| t.trim() == name).unwrap_or_else(|| panic!("{name} missing from {texts:?}")).1.center();
|
||||
for e in [egui::Event::PointerMoved(sub), press(sub, true), press(sub, false)] {
|
||||
frame(vec![e]);
|
||||
}
|
||||
let texts = frame(vec![]);
|
||||
assert!(texts.iter().any(|(t, _)| t.contains("Roman Baseline") || t.contains("Hindi")), "{name} did not open: {texts:?}");
|
||||
ticked(&texts).into_iter().filter(|t| !before.contains(t)).collect::<Vec<_>>()
|
||||
};
|
||||
let mixed = (checked(&mut app, "Character Alignment"), checked(&mut app, "Digits"));
|
||||
assert_eq!(mixed, (vec![], vec![]));
|
||||
app.run("text.setRangeStyle", json!({"id": id, "charAlign": "emBoxTop", "digits": "hindi"})).unwrap();
|
||||
let same = (checked(&mut app, "Character Alignment"), checked(&mut app, "Digits"));
|
||||
assert_eq!(same, (vec!["Em Box Top/Right".to_string()], vec!["Hindi".to_string()]));
|
||||
}
|
||||
|
||||
/// At a caret in text the Type tool edits, with no characters selected, the Character
|
||||
/// Direction items are disabled: a click on one leaves the direction of every character as it was.
|
||||
#[test]
|
||||
|
||||
+2
-1
@@ -1155,7 +1155,8 @@ These commands refuse a string, a number or another value that is not true or fa
|
||||
`` `closed` must be true or false ``, naming the key, and leave the document and its undo history as they were:
|
||||
`clockwise` in `shape.spiral`; `closed` in `shape.arc` and `path.create`; `vertical` and `placeholder` in `text.create`
|
||||
and `text.createInPath`; `underline`, `strikethrough`, `allCaps`, `proportionalMetrics`, `tateChuYoko`, `standardVerticalRoman` and `noBreak`
|
||||
in `text.setRangeStyle`; `effective` in `text.getRange`; `on` in `type.opticalMarginAlignment`, `type.systemLayout`
|
||||
in `text.setRangeStyle` and `text.setFormat`, which also checks `hyphenate`, `hyphenateCapitalized` and
|
||||
`romanHangingPunctuation`; `effective` in `text.getRange`; `on` in `type.opticalMarginAlignment`, `type.systemLayout`
|
||||
and `type.fractionalWidths`; `remove` and `overhang` in `type.ruby`; `expandInstances` in `symbol.delete`; `alt` in
|
||||
`symbol.spray` and `symbol.adjust`; `spot` and `global` in `swatch.new` and `swatch.edit`; `unlink` in
|
||||
`swatch.delete`; `fromArtwork`, `toGlobal` and `includeTints` in `swatch.newGroup`; `global` and `selection` in
|
||||
|
||||
@@ -892,6 +892,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur
|
||||
The Tabs panel menu's Clear All Tabs (`text.tabs.clear`), Delete Tab and Repeat Tab (`text.tabs.set`) can be chosen while the panel's ruler shows tab stops. Delete Tab removes the stop selected on the ruler. Repeat Tab replaces the stops after the selected stop with up to 20 copies of it. The copies are spaced by the selected stop's distance from the stop before it, or from the paragraph's left indent when it is the first stop, and no copy is placed at 2,000 pt or beyond. Clear All Tabs removes every stop, so the default stops every ½ inch apply again.
|
||||
- **Interface languages, vertical type:** Preferences ▸ User Interface (or VectorCraft ▸ Language on macOS) picks Automatic (the system locale) or a registered language, persisted as `interfaceLanguage`. Translations come from per-language catalogs (`i18n/*.tsv`): Traditional and Simplified Chinese, Japanese, German (every `de-*` locale), Spanish (neutral international Spanish, for every `es-*` locale), French (every `fr-*` locale), Italian (every `it-*` locale), Russian (every `ru-*` locale) and Ukrainian (every `uk-*` locale), Catalan (every `ca-*` locale) and Polish (every `pl-*` locale; AI-drafted, native review pending) cover the whole interface, Czech every menu label; Czech, German, Greek, Spanish, French, Hungarian, Italian, Japanese, Korean, Vietnamese, Polish, Brazilian Portuguese, Russian, Simplified Chinese, Traditional Chinese, Swedish, Ukrainian and Finnish also translate status-bar and error messages as they are shown (`@msg` templates, every message literal tested; the messages themselves stay English for agents); untranslated text stays English. Japanese and Chinese glyphs come from the craft-fonts build input (release builds) or the installed fonts, Czech, German, Spanish, French, Italian, Russian and Ukrainian ones from the bundled UI fonts. The web app follows the browser's languages (or `?lang=`). The Basic toolbar's Type button holds Vertical Type in its flyout. Vertical Type, Vertical Area Type and Vertical Type on a Path create vertical text (columns right to left, upright CJK glyphs and marks with `vert` alternates, Latin and longer numbers on their side, two- and three-digit numbers set as tate-chu-yoko in one em of the column (kept whole when justified), upright glyphs centred on the column (tracked or justified too), point type's anchor on the first column's centre line); kinsoku applies to horizontal and vertical Japanese (Paragraph panel › Kinsoku Set, `text.setFormat {kinsoku}`: Hard, the default, keeps closing marks, iteration marks, the prolonged sound mark and small kana off the line start and opening brackets off its end; Soft lets 々, ー and small kana start a line as JLREQ's level 3 rules do; None turns it off), and justified Japanese lines spread their room between characters (not inside Latin words); Paragraph panel › Mojikumi Set (shown with Preferences › Type › Show East Asian Options, `text.setFormat {mojikumi}`): None or Line-end Punctuation Half Width (new type's default, as in Illustrator; older documents and imported text keep None) shares one half-em space between consecutive punctuation, sets a closing mark ending a line half width and an opening bracket starting a line flush with its start (a paragraph's first line at its first-line indent, JIS X 4051's principle in JLREQ 3.1.5), and puts a quarter em between Japanese and Latin letters or digits; Paragraph panel menu › Top-to-Top / Bottom-to-Bottom Leading measures leading from em box top to top (area type's first line touches the frame) or from baseline to baseline (new type in a Japanese interface starts top to top, with em box centre character alignment, #432); Paragraph panel menu › Burasagari None / Regular / Force (`text.setFormat {burasagari: "none"|"standard"|"forced"}`) hangs an East Asian comma or full stop (、。,.、。) ending an area type line outside it, Regular (new type's default) when it doesn't fit and Force always (the rest of the line then fills the measure; closing brackets and Latin punctuation don't hang); Character panel menu › Character Alignment (with the East Asian options; `text.setFormat` or `text.setRangeStyle {charAlign}`) lines characters smaller than the largest on their line up on the Roman baseline, the em box top/right, centre or bottom/left, or the ICF (ideographic character face) top/right or bottom/left, from the font's BASE table or, without one, the average ink box of some ideographs and kana; Proportional Metrics (the same menu; `{proportionalMetrics}`) sets full-width glyphs on the font's proportional widths (OpenType `palt`) in horizontal type and upright glyphs on its proportional heights (`vpal`, shaped top to bottom; fonts without vertical metrics keep their cells) in vertical type, and Line-end Punctuation Half Width takes nothing more off the punctuation they re-spaced; Type ▸ Type Orientation switches existing text; caret, selection, hit testing and arrow keys follow the writing direction. Vertical text exports to PDF as real text and to SVG as outlines. The interface is available in Polish (`pl`): menus, panels, dialogs and messages, with one/few/many plurals; AI-drafted, native review pending (#1498).
|
||||
Character panel menu › Standard Vertical Roman Alignment (with the East Asian options; `text.setFormat` or `text.setRangeStyle {standardVerticalRoman}`, off by default) stands the selected Latin letters, digits and other half-width characters upright one below the other in vertical type instead of rotating them ([formatting Asian characters](https://helpx.adobe.com/illustrator/using/formatting-asian-characters.html)); spaces keep their width, tate-chu-yoko blocks stay across the column, and Character Style Options gives it its own checkbox beside Character Alignment.
|
||||
The Character panel's All Caps, Underline and Strikethrough buttons and its menu's All Caps, Underline, Strikethrough, Proportional Metrics, Tate-chu-yoko and No Break show off while only some of the selected characters have the attribute, and choosing one then turns it on for all of them, as `type.underline` (Cmd+Shift+U) does for underlining; the Character Alignment and Digits submenus check no item while the selected characters differ. `text.setFormat` refuses a value that is not true or false for its on/off attributes, as `text.setRangeStyle` does.
|
||||
- **Transparency:** opacity masks (clip/invert/disable/link), exported as SVG `<mask>` and PDF soft masks. The constant alpha soft mask that PDF and `.ai` export draw a CMYK page with transparency through is white, so applications that read every soft mask by its luminosity show the page too (#1359, part of #1286).
|
||||
Isolation mode and opacity mask editing exclude each other ([transparency and blending modes](https://web.archive.org/web/20230102045151/https://helpx.adobe.com/illustrator/using/transparency-blending-modes.html)): in isolation mode a click or Alt-click on the mask thumbnail does not edit the mask, and Make Mask is off for one object; while a mask is edited, Enter Isolation Mode and Object › Perspective › Edit Text are off, the Contextual Task Bar leaves out Isolate, and Quick Actions leave out Isolate Group.
|
||||
Pattern editing mode and opacity mask editing exclude each other, by a rule of VectorCraft itself: in pattern editing mode a click or Alt-click on the mask thumbnail does not edit the mask, and Make Mask is off for one object; while a mask is edited, Object › Pattern › Make and Edit Pattern are off.
|
||||
|
||||
Reference in New Issue
Block a user