G4: Optical margin menu uses shared text flag

Squashed from #4309 onto main.
This commit is contained in:
nrq6993-a11y
2026-10-27 14:36:12 +07:00
committed by bflatastic
parent 76e0e5b0ac
commit 678cff6707
4 changed files with 73 additions and 2 deletions
+1 -2
View File
@@ -40,8 +40,7 @@ pub fn checked(app: &VectorcraftApp, id: &str, p: &Value) -> Option<bool> {
"type.orientation.vertical" | "type.orientation.horizontal" => {
selected_text(false).is_some_and(|t| t.vertical == (id == "type.orientation.vertical"))
}
// Checked when the first type object `selected_text` finds has it on.
"type.opticalMarginAlignment" => selected_text(false).is_some_and(|t| t.optical_margin),
"type.opticalMarginAlignment" => crate::panels::character::shared_text_flag(app, |t| t.optical_margin) == Some(true),
"view.outline" => v.outline,
"view.pixelPreview" => v.pixel_preview,
"view.trimView" => v.trim_view,
+16
View File
@@ -239,6 +239,22 @@ fn optical_margin_alignment_is_a_checked_type_menu_item() {
assert_eq!(checked(&app, "type.orientation.horizontal", &Value::Null), Some(false));
}
/// Type › Optical Margin Alignment menu check uses `shared_text_flag`; mixed values stay unchecked.
#[test]
fn optical_margin_alignment_menu_unchecked_when_shared_flag_none() {
let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), crate::Services::default());
app.run("file.new", json!({})).unwrap();
let off = app.run("text.create", json!({"x": 10, "y": 40, "text": "Off"})).unwrap()["id"].clone();
let on = app.run("text.create", json!({"x": 10, "y": 80, "text": "On"})).unwrap()["id"].clone();
app.run("type.opticalMarginAlignment", json!({"on": true, "id": on})).unwrap();
app.run("select.set", json!({"ids": [off, on]})).unwrap();
let id = "type.opticalMarginAlignment";
assert_eq!(crate::panels::character::shared_text_flag(&app, |t| t.optical_margin), None);
assert_eq!(checked(&app, id, &Value::Null), Some(false));
app.run("select.set", json!({"ids": [on]})).unwrap();
assert_eq!(checked(&app, id, &Value::Null), Some(true));
}
#[test]
fn panel_ids_are_case_insensitive_and_labels_work() {
assert_eq!(normalize_panel("layers"), Some("layers"));
+32
View File
@@ -54,6 +54,38 @@ pub(crate) fn first_selected_text(app: &VectorcraftApp) -> Option<&vectorcraft_d
None
}
/// A boolean on every selected text object (`Some(true)` / `Some(false)`), or `None` when they
/// differ. When the Type tool edits text, only that object counts.
pub(crate) fn shared_text_flag(app: &VectorcraftApp, f: impl Fn(&vectorcraft_doc::TextObject) -> bool) -> Option<bool> {
if let Some((id, _, _)) = text_editing(app) {
return app.session.active().and_then(|st| st.doc.node(id)).and_then(|n| match &n.kind {
NodeKind::Text(t) => Some(f(t)),
_ => None,
});
}
let st = app.session.active()?;
let mut value: Option<bool> = None;
let mut mixed = false;
let mut saw = false;
for node in st.selection.objects.iter().filter_map(|id| st.doc.node(*id)) {
node.walk(&mut |n| {
if let NodeKind::Text(t) = &n.kind {
saw = true;
if mixed {
return;
}
let v = f(t);
match value {
None => value = Some(v),
Some(a) if a == v => {}
Some(_) => mixed = true,
}
}
});
}
if !saw || mixed { None } else { value }
}
/// `f` of the character styles the panels act on: the runs the Type tool's selection covers (the
/// caret's style at a caret), else every run of the selected type, groups' included. `None` (a
/// blank field) where they differ, or with no type.
@@ -488,6 +488,30 @@ fn character_direction_checks_the_direction_of_the_selection() {
assert_eq!(checked(&mut app), ["Default"]);
}
#[test]
fn shared_text_flag_optical_margin_three_object_selections() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({})).unwrap();
let a = app.run("text.create", json!({"x": 10, "y": 40, "text": "a"})).unwrap()["id"].clone();
let b = app.run("text.create", json!({"x": 10, "y": 80, "text": "b"})).unwrap()["id"].clone();
let c = app.run("text.create", json!({"x": 10, "y": 120, "text": "c"})).unwrap()["id"].clone();
app.run("type.opticalMarginAlignment", json!({"on": true, "id": a})).unwrap();
app.run("type.opticalMarginAlignment", json!({"on": false, "id": b})).unwrap();
app.run("type.opticalMarginAlignment", json!({"on": false, "id": c})).unwrap();
app.run("select.set", json!({"ids": [a.clone(), b.clone(), c.clone()]})).unwrap();
assert_eq!(shared_text_flag(&app, |t| t.optical_margin), None);
app.run("type.opticalMarginAlignment", json!({"on": false, "id": a})).unwrap();
app.run("type.opticalMarginAlignment", json!({"on": true, "id": b})).unwrap();
app.run("type.opticalMarginAlignment", json!({"on": true, "id": c})).unwrap();
app.run("select.set", json!({"ids": [a.clone(), b.clone(), c.clone()]})).unwrap();
assert_eq!(shared_text_flag(&app, |t| t.optical_margin), None);
app.run("type.opticalMarginAlignment", json!({"on": true, "id": a})).unwrap();
app.run("type.opticalMarginAlignment", json!({"on": true, "id": b})).unwrap();
app.run("type.opticalMarginAlignment", json!({"on": true, "id": c})).unwrap();
app.run("select.set", json!({"ids": [a, b, c]})).unwrap();
assert_eq!(shared_text_flag(&app, |t| t.optical_margin), Some(true));
}
/// Small Caps, Superscript and Subscript buttons and menu items stay off when the selection mixes them.
#[test]
fn character_mixed_small_caps_and_position_show_off() {