refactor(ui-egui): move panels/character.rs tests to panels/character/tests.rs

This commit is contained in:
aiongg
2026-10-26 18:18:17 +00:00
parent 36b6c513f7
commit 4d174fd4a6
2 changed files with 633 additions and 634 deletions
+1 -634
View File
@@ -948,637 +948,4 @@ fn reset_panel(app: &mut VectorcraftApp) {
}
#[cfg(test)]
mod tests {
use super::*;
use vectorcraft_engine::Session;
#[test]
fn scaled_dimensions_do_not_show_false_mixed_values_from_roundoff() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 300, "height": 200})).unwrap();
let a = app.run("text.create", json!({"x": 20, "y": 50, "text": "Axis", "size": 12})).unwrap()["id"].clone();
app.run("object.scale", json!({"sx": 120, "sy": 110})).unwrap();
let b = app.run("text.create", json!({"x": 20, "y": 90, "text": "Label", "size": 24})).unwrap()["id"].clone();
app.run("select.set", json!({"ids": [a, b]})).unwrap();
app.run("text.setStyle", json!({"size": 7, "leading": 9})).unwrap();
app.run("text.setFormat", json!({"hScale": 130, "baselineShift": 3})).unwrap();
for (actual, expected) in [
(shared_effective(&app, |s| s.size), 7.0),
(shared_effective(&app, CharStyle::effective_leading), 9.0),
(shared_effective(&app, |s| s.h_scale), 130.0),
(shared_effective(&app, |s| s.baseline_shift), 3.0),
] {
assert!((actual.expect("equal dimensions must not be mixed") - expected).abs() < 1e-8);
}
}
/// A final empty paragraph counts: with the object selected, its Auto Leading differing from
/// the paragraph before it shows Leading as mixed.
#[test]
fn leading_field_counts_the_final_empty_paragraph() {
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": "One\n", "size": 10})).unwrap()["id"].clone();
app.run("select.set", json!({"ids": [id]})).unwrap();
assert!((shared_leading(&app).unwrap() - 12.0).abs() < 1e-9);
app.run("text.setFormat", json!({"autoLeading": 200, "start": 4, "end": 4})).unwrap();
assert_eq!(shared_leading(&app), None, "the empty last paragraph differs");
}
/// Auto leading shows the leading the paragraph's Justification › Auto Leading sets.
#[test]
fn leading_field_shows_the_paragraph_auto_leading() {
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": "One\nTwo", "size": 10})).unwrap()["id"].clone();
app.run("select.set", json!({"ids": [id]})).unwrap();
assert!((shared_leading(&app).unwrap() - 12.0).abs() < 1e-9);
app.run("text.setFormat", json!({"autoLeading": 200, "start": 5, "end": 5})).unwrap();
assert_eq!(shared_leading(&app), None, "the paragraphs differ");
assert_eq!(shared(&app, |s| s.leading.is_none()), Some(true), "still Auto");
app.run("text.setFormat", json!({"autoLeading": 200})).unwrap();
assert!((shared_leading(&app).unwrap() - 20.0).abs() < 1e-9);
app.run("text.setStyle", json!({"leading": 15})).unwrap();
assert!((shared_leading(&app).unwrap() - 15.0).abs() < 1e-9, "a set leading is its own");
}
#[test]
fn scaled_collapsed_text_keeps_nondimensional_controls_available() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 300, "height": 200})).unwrap();
app.run("text.create", json!({"x": 20, "y": 50, "text": "Axis", "size": 12})).unwrap();
app.run("object.transform", json!({"matrix": [0, 0, 0, 0, 0, 0]})).unwrap();
assert!(text_style(&app).is_some());
assert_eq!(shared(&app, |s| s.tracking), Some(0.0));
assert_eq!(shared_effective(&app, |s| s.size), None);
app.run("text.setStyle", json!({"tracking": 40})).unwrap();
app.run("text.setFormat", json!({"leftIndent": 4})).unwrap();
assert_eq!(shared(&app, |s| s.tracking), Some(40.0));
assert_eq!(text_style(&app).unwrap().1.left_indent, 4.0);
let ctx = egui::Context::default();
let texts = super::super::tests_appearance::frame_events(&ctx, &mut app, vec![], show);
assert!(!texts.iter().any(|(s, _)| s == "No text selected"));
}
#[test]
fn scaled_character_fields_show_document_sizes_and_mixed_group_values() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 300, "height": 200})).unwrap();
let a = app.run("text.create", json!({"x": 20, "y": 50, "text": "Axis", "size": 12})).unwrap()["id"].clone();
app.run("object.scale", json!({"sx": 200})).unwrap();
assert_eq!(shared_effective(&app, |s| s.size), Some(24.0));
assert_eq!(text_style(&app).unwrap().0.size, 12.0, "style definitions keep stored attributes");
let b = app.run("text.create", json!({"x": 20, "y": 90, "text": "Label", "size": 24})).unwrap()["id"].clone();
app.run("select.set", json!({"ids": [a, b]})).unwrap();
app.run("object.group", json!({})).unwrap();
assert_eq!(shared_effective(&app, |s| s.size), Some(24.0), "different local sizes, same displayed size");
app.run("object.scale", json!({"sx": 200, "sy": 100})).unwrap();
assert_eq!(shared_effective(&app, |s| s.h_scale), Some(200.0));
app.run("text.setStyle", json!({"size": 10})).unwrap();
assert_eq!(shared_effective(&app, |s| s.size), Some(10.0));
app.run("text.setFormat", json!({"hScale": 100})).unwrap();
assert_eq!(shared_effective(&app, |s| s.h_scale), Some(100.0));
}
#[test]
fn scaled_range_fields_follow_the_type_tool_selection() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 300, "height": 200})).unwrap();
app.session.select_tool("type", Default::default()).unwrap();
let id = app.run("text.create", json!({"x": 20, "y": 50, "text": "Axis label", "size": 12})).unwrap()["id"].clone();
app.session.apply_actions(vec![vectorcraft_tools::Action::Notify("text.editNew".into())]).unwrap();
assert!(text_editing(&app).is_some());
app.run("object.scale", json!({"sx": 200})).unwrap();
app.run("text.setRangeStyle", json!({"id": id, "start": 0, "end": 4, "size": 10})).unwrap();
app.session.set_tool_option("selectAll", &json!(true));
assert_eq!(shared_effective(&app, |s| s.size), None);
app.session.set_tool_option("select", &json!({"start": 0, "end": 4}));
assert_eq!(shared_effective(&app, |s| s.size), Some(10.0));
assert_eq!(text_style(&app).unwrap().0.size, 5.0);
}
/// The Kerning field at a caret kerns that pair, not the whole text (#2217).
#[test]
fn kerning_at_a_caret_sets_the_pair_not_the_whole_text() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 300, "height": 200})).unwrap();
app.session.select_tool("type", Default::default()).unwrap();
let id = app.run("text.create", json!({"x": 20, "y": 50, "text": "AVAX", "size": 12})).unwrap()["id"].clone();
app.session.apply_actions(vec![vectorcraft_tools::Action::Notify("text.editNew".into())]).unwrap();
app.session.set_tool_option("select", &json!({"start": 2, "end": 2}));
set_kerning(&mut app, json!(-50));
let kerning = |app: &VectorcraftApp, at: usize| {
let st = app.session.active().unwrap();
let Some(NodeKind::Text(t)) = st.doc.node(NodeId(id.as_u64().unwrap())).map(|n| &n.kind) else { panic!("text") };
vectorcraft_text::edit::style_at(&t.runs, at + 1).kerning // the character at `at`
};
assert_eq!(kerning(&app, 1), Some(-50.0), "the character before the caret");
assert_eq!(kerning(&app, 3), None, "the rest is untouched");
assert_eq!(kerning(&app, 0), None);
// A selected range still takes it whole.
app.session.set_tool_option("select", &json!({"start": 2, "end": 4}));
set_kerning(&mut app, json!(30));
assert_eq!((kerning(&app, 1), kerning(&app, 2), kerning(&app, 3)), (Some(-50.0), Some(30.0), Some(30.0)));
}
/// A variable font gets the Variable Font button, which opens one slider per axis named as the
/// font names it; a drag along one sets that axis of the selected type in one undo step.
#[test]
fn variable_fonts_get_a_slider_per_axis() {
use vectorcraft_text::test_fonts::{TWO_AXIS_FAMILY, two_axis_font};
let db = vectorcraft_text::FontDb::global();
db.add_font(two_axis_font().unwrap());
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 300, "height": 200})).unwrap();
let plain = app.run("text.create", json!({"x": 20, "y": 50, "text": "Static"})).unwrap()["id"].clone();
let id = app.run("text.create", json!({"x": 20, "y": 120, "text": "lIn", "font": TWO_AXIS_FAMILY})).unwrap()["id"].clone();
app.run("select.set", json!({"ids": [plain]})).unwrap();
assert!(!crate::tests_labels::painted_text(&mut app, show).contains("Variable Font"), "a static font");
app.run("select.set", json!({"ids": [id]})).unwrap();
assert!(crate::tests_labels::painted_text(&mut app, show).contains("Variable Font"));
let face = db.face(TWO_AXIS_FAMILY, "Regular").unwrap();
let text = crate::tests_labels::painted_text(&mut app, |app, ui| axis_sliders(app, ui, &face));
assert!(text.contains("Weight\n") && text.contains("Width\n") && text.contains("400") && text.contains("100"), "{text}");
// The labels are the font's names in every interface language, not catalog words (ja
// translates the interface's Weight as the stroke weight).
let ja = crate::i18n::Lang::from_code("ja").unwrap();
crate::i18n::set_current(ja);
let text = crate::tests_labels::painted_text(&mut app, |app, ui| axis_sliders(app, ui, &face));
crate::i18n::set_current(crate::i18n::Lang::EN);
let weight = crate::i18n::tr(ja, "Weight");
assert!(text.contains("Weight\n") && text.contains("Width\n") && (weight == "Weight" || !text.contains(weight)), "{text}");
// Screen readers name each slider by its axis.
let tree = crate::widgets::accessibility_tests::tree(|ui| axis_sliders(&mut app, ui, &face));
for name in ["Weight", "Width"] {
assert!(tree.iter().any(|n| n.role() == egui::accesskit::Role::Slider && n.label() == Some(name)), "{name}");
}
// ...with the number its field shows, on the axis's range.
use crate::widgets::accessibility_tests::slider_value;
let wght = face.axes().into_iter().find(|a| &a.tag == b"wght").unwrap();
let range = (Some(f64::from(wght.min)), Some(f64::from(wght.max)));
assert_eq!(slider_value(&tree, "Weight"), (Some(400.0), range.0, range.1, Some("400".into())));
// Drag the Weight slider from its start to past its end.
let ctx = egui::Context::default();
let frame = |app: &mut VectorcraftApp, events: Vec<egui::Event>| {
let mut slider = None;
let input = egui::RawInput { events, ..Default::default() };
ctx.run_ui(input, |ui| {
axis_sliders(app, ui, &face);
slider = ctx.read_response(ui.scope_id().with(("ch-axis", *b"wght"))).map(|r| r.rect);
})
.textures_delta
.clear();
slider
};
frame(&mut app, vec![]);
let r = frame(&mut app, vec![]).expect("the Weight slider is drawn");
let none = egui::Modifiers::NONE;
let (from, to) = (egui::pos2(r.left() + 4.0, r.center().y), egui::pos2(r.right() + 20.0, r.center().y));
let button = |pos, pressed| egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: none };
frame(&mut app, vec![egui::Event::PointerMoved(from), button(from, true)]);
for i in 1..=4 {
frame(&mut app, vec![egui::Event::PointerMoved(from + (to - from) * i as f32 / 4.0)]);
}
frame(&mut app, vec![button(to, false)]);
frame(&mut app, vec![]);
let axes = |app: &VectorcraftApp| shared(app, |s| s.font_axes.clone()).unwrap();
assert_eq!(axes(&app).into_iter().collect::<Vec<_>>(), [("wght".to_string(), 700.0)]);
app.run("edit.undo", json!({})).unwrap();
assert!(axes(&app).is_empty(), "one undo step");
// Regular and Bold selected: the weights differ and the field is blank; the widths agree.
let bold = app.run("text.create", json!({"x": 20, "y": 160, "text": "lIn", "font": TWO_AXIS_FAMILY, "style": "Bold"})).unwrap()["id"].clone();
app.run("select.set", json!({"ids": [id, bold]})).unwrap();
let text = crate::tests_labels::painted_text(&mut app, |app, ui| axis_sliders(app, ui, &face));
assert!(!text.contains("400") && !text.contains("700") && text.contains("100"), "{text}");
let tree = crate::widgets::accessibility_tests::tree(|ui| axis_sliders(&mut app, ui, &face));
assert_eq!(slider_value(&tree, "Weight"), (None, None, None, None), "a blank field: no value");
assert_eq!(slider_value(&tree, "Width").0, Some(100.0));
// Reset Panel leaves the settings, as it leaves the font and style.
app.run("select.set", json!({"ids": [id]})).unwrap();
app.run("text.setFormat", json!({"fontAxes": {"wght": 650}})).unwrap();
reset_panel(&mut app);
assert_eq!(axes(&app).into_iter().collect::<Vec<_>>(), [("wght".to_string(), 650.0)]);
}
/// Bold and Italic pick the family's own faces by name, and toggle back (#724).
#[test]
fn bold_and_italic_pick_the_familys_own_faces() {
let styles: Vec<String> =
["Light", "Narrow", "Regular", "Italic", "Narrow Italic", "SemiBold", "Bold", "Bold Condensed", "Narrow Bold", "Bold Italic", "Black"]
.iter()
.map(|s| s.to_string())
.collect();
assert_eq!(pick_face(&styles, true, false, "Regular"), Some("Bold"), "not Bold Condensed, Narrow Bold or SemiBold");
assert_eq!(pick_face(&styles, true, true, "Italic"), Some("Bold Italic"));
assert_eq!(pick_face(&styles, false, true, "Regular"), Some("Italic"));
assert_eq!(pick_face(&styles, false, false, "Italic"), Some("Regular"), "upright again, not Narrow");
assert_eq!(pick_face(&styles, true, false, "Narrow"), Some("Narrow Bold"), "a Narrow face stays Narrow");
assert_eq!(pick_face(&styles, false, true, "Narrow"), Some("Narrow Italic"));
let oblique: Vec<String> = ["Book", "Oblique", "Bold-Oblique"].iter().map(|s| s.to_string()).collect();
assert_eq!(pick_face(&oblique, true, true, "Oblique"), Some("Bold-Oblique"));
assert_eq!(pick_face(&oblique, false, false, "Oblique"), None, "Book has a weight word: no regular face");
assert_eq!(face_of("Bold Italic"), (true, false, true, vec![]));
assert_eq!(face_of("SemiBold"), (false, false, false, vec![]));
assert_eq!(face_of("Narrow Italic"), (false, true, true, vec!["narrow".to_string()]));
// No type selected: a message, nothing changed.
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 300, "height": 200})).unwrap();
assert!(toggle_face(&mut app, Face::Bold).is_err());
assert!(!crate::menus::enabled(&app, "type.bold"));
}
/// 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.
fn script_buttons(ctx: &egui::Context, app: &mut VectorcraftApp) -> ([egui::Pos2; 3], [bool; 3]) {
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>) {
match s {
egui::Shape::Vec(v) => v.iter().for_each(|s| walk(s, on, ones, lit)),
egui::Shape::Text(t) if t.galley.text() == "1" => ones.push(t.visual_bounding_rect().center()),
egui::Shape::Rect(r) if r.fill == on && r.rect.size() == vec2(30.0, 26.0) => lit.push(r.rect),
_ => {}
}
}
let (mut ones, mut lit) = (vec![], vec![]);
for c in &out.shapes {
walk(&c.shape, Tokens::get(ctx).tool_active, &mut ones, &mut lit);
}
let [sup, sub] = ones[..] else { panic!("two script buttons: {ones:?}") };
assert!(sup.x < sub.x && sup.y < sub.y, "Superscript, then Subscript: {ones:?}");
// 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];
(points, points.map(|p| lit.iter().any(|r| r.contains(p))))
}
/// Each run's (small caps, position).
fn scripts(app: &VectorcraftApp, id: &Value) -> Vec<(Option<f64>, &'static str)> {
match &app.session.active().unwrap().doc.node(NodeId(id.as_u64().unwrap())).unwrap().kind {
NodeKind::Text(t) => t.runs.iter().map(|r| (r.style.small_caps, r.style.position.id())).collect(),
_ => panic!("text"),
}
}
/// The Small Caps, Superscript and Subscript buttons set the selected type's small caps and
/// position, with one undo step for each click, and show them; Superscript and Subscript turn
/// each other off. While the Type tool edits, they set and show the selected characters.
#[test]
fn small_caps_superscript_and_subscript_buttons_set_and_show_the_text() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 300, "height": 200})).unwrap();
app.session.select_tool("type", Default::default()).unwrap();
let id = app.run("text.create", json!({"x": 20, "y": 50, "text": "E=mc2", "size": 12})).unwrap()["id"].clone();
let ctx = egui::Context::default();
let (at, lit) = script_buttons(&ctx, &mut app);
assert_eq!(lit, [false; 3]);
let small = Some(app.session.active().unwrap().doc.setup.small_caps_size);
for (button, runs, shown) in [
(0, (small, "normal"), [true, false, false]),
(1, (small, "superscript"), [true, true, false]),
(2, (small, "subscript"), [true, false, true]),
(2, (small, "normal"), [true, false, false]),
(1, (small, "superscript"), [true, true, false]),
(1, (small, "normal"), [true, false, false]),
(0, (None, "normal"), [false; 3]),
] {
let undo = app.session.active().unwrap().history.undo.len();
super::super::tests_appearance::click(&ctx, &mut app, at[button], show);
assert_eq!(scripts(&app, &id), [runs], "button {button}");
assert_eq!(app.session.active().unwrap().history.undo.len(), undo + 1, "button {button}");
assert_eq!(script_buttons(&ctx, &mut app).1, shown, "button {button}");
}
// The Type tool's selection: "2" alone.
app.session.apply_actions(vec![vectorcraft_tools::Action::Notify("text.editNew".into())]).unwrap();
app.session.set_tool_option("select", &json!({"start": 4, "end": 5}));
assert!(text_editing(&app).is_some());
super::super::tests_appearance::click(&ctx, &mut app, at[1], show);
assert_eq!(scripts(&app, &id), [(None, "normal"), (None, "superscript")]);
assert_eq!(script_buttons(&ctx, &mut app).1, [false, true, false]);
app.session.set_tool_option("select", &json!({"start": 0, "end": 4}));
assert_eq!(script_buttons(&ctx, &mut app).1, [false; 3]);
}
/// The panel menu's Small Caps, Superscript and Subscript set the selected type's small caps
/// and position and show a check mark when on; Superscript and Subscript turn each other off.
#[test]
fn small_caps_superscript_and_subscript_menu_items_set_and_check_the_text() {
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": "E=mc2", "size": 12})).unwrap()["id"].clone();
let ctx = egui::Context::default();
let checked = |app: &mut VectorcraftApp| {
let texts = super::super::tests_appearance::frame_events(&ctx, app, vec![], menu);
["Small Caps", "Superscript", "Subscript"].map(|l| texts.iter().any(|(t, _)| *t == format!("✓ {l}")))
};
assert_eq!(checked(&mut app), [false; 3]);
let small = Some(app.session.active().unwrap().doc.setup.small_caps_size);
for (item, runs, shown) in [
("Small Caps", (small, "normal"), [true, false, false]),
("Superscript", (small, "superscript"), [true, true, false]),
("Subscript", (small, "subscript"), [true, false, true]),
("Subscript", (small, "normal"), [true, false, false]),
("Small Caps", (None, "normal"), [false; 3]),
] {
let texts = super::super::tests_appearance::frame_events(&ctx, &mut app, vec![], menu);
let at = texts.iter().find(|(t, _)| t.trim_start_matches(['✓', ' ']) == item).unwrap_or_else(|| panic!("{item}: {texts:?}")).1;
super::super::tests_appearance::click(&ctx, &mut app, at.center(), menu);
assert_eq!(scripts(&app, &id), [runs], "{item}");
assert_eq!(checked(&mut app), shown, "{item}");
}
}
/// Character panel menu › System Layout is off for a new document, and a click turns whole-point
/// spacing on and back off for every text object.
#[test]
fn system_layout_menu_item_toggles_the_document() {
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": "Hi", "size": 13})).unwrap()["id"].as_u64().unwrap();
let ctx = egui::Context::default();
let on = |app: &VectorcraftApp| {
let st = app.session.active().unwrap();
let object = match &st.doc.node(NodeId(id)).unwrap().kind {
NodeKind::Text(t) => t.system_layout,
_ => panic!("text"),
};
(st.doc.setup.system_layout, object)
};
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(['✓', ' ']) == "System Layout")
};
assert_eq!(on(&app), (false, false));
assert!(row(&mut app).is_some_and(|(t, _)| !t.starts_with('✓')));
let at = row(&mut app).unwrap().1;
super::super::tests_appearance::click(&ctx, &mut app, at.center(), menu);
assert_eq!(on(&app), (true, true));
assert!(row(&mut app).is_some_and(|(t, _)| t.starts_with('✓')));
let at = row(&mut app).unwrap().1;
super::super::tests_appearance::click(&ctx, &mut app, at.center(), menu);
assert_eq!(on(&app), (false, false));
assert!(row(&mut app).is_some_and(|(t, _)| !t.starts_with('✓')));
}
/// Character panel menu › Fractional Widths is on for new type, and a click turns it off and
/// back on for the whole type object.
#[test]
fn fractional_widths_menu_item_toggles_the_type_object() {
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": "Hi", "size": 13})).unwrap()["id"].as_u64().unwrap();
let ctx = egui::Context::default();
let flag = |app: &VectorcraftApp| match &app.session.active().unwrap().doc.node(NodeId(id)).unwrap().kind {
NodeKind::Text(t) => t.fractional_widths,
_ => panic!("text"),
};
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(['✓', ' ']) == "Fractional Widths")
};
assert!(flag(&app));
assert!(row(&mut app).is_some_and(|(t, _)| t.starts_with('✓')));
let at = row(&mut app).unwrap().1;
super::super::tests_appearance::click(&ctx, &mut app, at.center(), menu);
assert!(!flag(&app));
assert!(row(&mut app).is_some_and(|(t, _)| !t.starts_with('✓')));
let at = row(&mut app).unwrap().1;
super::super::tests_appearance::click(&ctx, &mut app, at.center(), menu);
assert!(flag(&app));
assert!(row(&mut app).is_some_and(|(t, _)| t.starts_with('✓')));
}
/// Character Alignment and Proportional Metrics are in the panel menu with the East Asian
/// options only (as Mojikumi Set and Top-to-Top Leading are in the Paragraph panel).
#[test]
fn character_alignment_shows_with_the_east_asian_options() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 300, "height": 200})).unwrap();
let id = app.session.execute("text.create", &json!({"x": 20, "y": 50, "text": "雅楽"})).unwrap()["id"].clone();
app.session.execute("select.set", &json!({"ids": [id]})).unwrap();
let shown = |app: &mut VectorcraftApp| {
let text = crate::tests_labels::painted_text(app, menu);
(text.contains("Character Alignment"), text.contains("Proportional Metrics"))
};
assert_eq!(shown(&mut app), (false, false));
app.session.prefs.show_east_asian_options = true;
assert_eq!(shown(&mut app), (true, true));
}
/// Tate-chu-yoko is in the panel menu with the East Asian options, and choosing it marks the
/// selected characters.
#[test]
fn tate_chu_yoko_menu_item_shows_with_east_asian_options_and_toggles() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 300, "height": 200})).unwrap();
let id = app.session.execute("text.create", &json!({"x": 20, "y": 50, "text": "2026"})).unwrap()["id"].as_u64().unwrap();
app.session.execute("select.set", &json!({"ids": [id]})).unwrap();
let ctx = egui::Context::default();
let painted = |app: &mut VectorcraftApp| crate::tests_labels::painted_text(app, menu);
assert!(!painted(&mut app).contains("Tate-chu-yoko"));
app.session.prefs.show_east_asian_options = true;
let on = |app: &VectorcraftApp| match &app.session.active().unwrap().doc.node(NodeId(id)).unwrap().kind {
NodeKind::Text(t) => t.runs.iter().all(|r| r.style.tate_chu_yoko),
_ => panic!("text"),
};
assert!(!on(&app));
let texts = super::super::tests_appearance::frame_events(&ctx, &mut app, vec![], menu);
let at = texts.iter().find(|(t, _)| t.trim_start_matches(['✓', ' ']) == "Tate-chu-yoko").unwrap_or_else(|| panic!("{texts:?}")).1;
super::super::tests_appearance::click(&ctx, &mut app, at.center(), menu);
assert!(on(&app));
assert!(painted(&mut app).contains("✓ Tate-chu-yoko"));
let texts = super::super::tests_appearance::frame_events(&ctx, &mut app, vec![], menu);
let at = texts.iter().find(|(t, _)| t.trim_start_matches(['✓', ' ']) == "Tate-chu-yoko").unwrap().1;
super::super::tests_appearance::click(&ctx, &mut app, at.center(), menu);
assert!(!on(&app));
}
/// Digits is in the panel menu with the Middle Eastern (Indic) options only, as Paragraph
/// Direction is in the Paragraph panel.
#[test]
fn digits_show_with_the_indic_options() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 300, "height": 200})).unwrap();
let id = app.session.execute("text.create", &json!({"x": 20, "y": 50, "text": "صفحة 3"})).unwrap()["id"].clone();
app.session.execute("select.set", &json!({"ids": [id]})).unwrap();
let shown = |app: &mut VectorcraftApp| crate::tests_labels::painted_text(app, menu).contains("Digits");
assert!(!shown(&mut app));
app.session.prefs.show_indic_options = true;
assert!(shown(&mut app));
}
/// Character Direction is in the panel menu with the Middle Eastern (Indic) options, beside
/// Digits. Its submenu checks the direction the characters the panel acts on share: the
/// selected type's, or the range the Type tool has selected. It checks no item when they differ.
#[test]
fn character_direction_checks_the_direction_of_the_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": "שלום 21 10 2026"})).unwrap()["id"].as_u64().unwrap();
app.run("select.set", json!({"ids": [id]})).unwrap();
let shown = |app: &mut VectorcraftApp| crate::tests_labels::painted_text(app, menu).contains("Character Direction");
assert!(!shown(&mut app));
app.session.prefs.show_indic_options = true;
assert!(shown(&mut app));
let checked = |app: &mut VectorcraftApp| {
let text = crate::tests_labels::painted_text(app, char_direction_items);
text.lines().filter_map(|l| l.strip_prefix("✓ ")).map(str::to_string).collect::<Vec<_>>()
};
assert_eq!(checked(&mut app), ["Default"]);
app.run("text.setRangeStyle", json!({"id": id, "start": 9, "end": 19, "charDirection": "leftToRight"})).unwrap();
// The object's characters differ: nothing is checked.
assert!(checked(&mut app).is_empty());
// The Type tool's selection: the date, then the Hebrew word.
app.select_tool("type");
app.session.apply_actions(vec![vectorcraft_tools::Action::Notify("text.editNew".into())]).unwrap();
app.session.set_tool_option("select", &json!({"start": 9, "end": 19}));
assert_eq!(checked(&mut app), ["Left to Right"]);
app.session.set_tool_option("select", &json!({"start": 0, "end": 8}));
assert_eq!(checked(&mut app), ["Default"]);
}
/// 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() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 300, "height": 200})).unwrap();
let a = app.run("text.create", json!({"x": 20, "y": 50, "text": "A"})).unwrap()["id"].clone();
let b = app.run("text.create", json!({"x": 20, "y": 90, "text": "B"})).unwrap()["id"].clone();
app.run("text.setRangeStyle", json!({"id": a, "start": 0, "end": 1, "smallCaps": true, "position": "superscript"})).unwrap();
app.run("text.setRangeStyle", json!({"id": b, "start": 0, "end": 1, "smallCaps": false, "position": "subscript"})).unwrap();
app.run("select.set", json!({"ids": [a, b]})).unwrap();
assert_eq!(shared(&app, |s| s.small_caps.is_some()), None);
assert_eq!(shared(&app, |s| s.position), None);
let ctx = egui::Context::default();
assert_eq!(script_buttons(&ctx, &mut app).1, [false, false, false]);
let checked = |app: &mut VectorcraftApp| {
let texts = super::super::tests_appearance::frame_events(&ctx, app, vec![], menu);
["Small Caps", "Superscript", "Subscript"].map(|l| texts.iter().any(|(t, _)| *t == format!("✓ {l}")))
};
assert_eq!(checked(&mut app), [false, false, false]);
}
/// 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]
fn character_direction_does_nothing_at_an_empty_caret() {
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": "שלום 21 10 2026"})).unwrap()["id"].as_u64().unwrap();
app.run("select.set", json!({"ids": [id]})).unwrap();
app.select_tool("type");
app.session.apply_actions(vec![vectorcraft_tools::Action::Notify("text.editNew".into())]).unwrap();
app.session.set_tool_option("select", &json!({"start": 3, "end": 3}));
let directions = |app: &VectorcraftApp| match &app.session.active().unwrap().doc.node(NodeId(id)).unwrap().kind {
NodeKind::Text(t) => t.runs.iter().map(|r| (r.text.clone(), r.style.char_direction)).collect::<Vec<_>>(),
_ => panic!("text"),
};
let before = directions(&app);
let ctx = egui::Context::default();
let click_rtl = |app: &mut VectorcraftApp| {
let texts = super::super::tests_appearance::frame_events(&ctx, app, vec![], char_direction_items);
let at = texts.iter().find(|(t, _)| t.trim_start_matches(['✓', ' ']) == "Right to Left").unwrap_or_else(|| panic!("{texts:?}")).1;
super::super::tests_appearance::click(&ctx, app, at.center(), char_direction_items);
};
click_rtl(&mut app);
assert_eq!(directions(&app), before);
// With characters selected, the same click sets their direction.
app.session.set_tool_option("select", &json!({"start": 0, "end": 4}));
click_rtl(&mut app);
assert_ne!(directions(&app), before);
}
/// No Break is in the panel menu, and choosing it marks the selected characters.
#[test]
fn no_break_menu_item_toggles_the_selected_characters() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 300, "height": 200})).unwrap();
let id = app.session.execute("text.create", &json!({"x": 20, "y": 50, "text": "Hello"})).unwrap()["id"].as_u64().unwrap();
app.session.execute("select.set", &json!({"ids": [id]})).unwrap();
let ctx = egui::Context::default();
let painted = |app: &mut VectorcraftApp| crate::tests_labels::painted_text(app, menu);
assert!(painted(&mut app).contains("No Break"));
assert!(!painted(&mut app).contains("✓ No Break"));
let on = |app: &VectorcraftApp| match &app.session.active().unwrap().doc.node(NodeId(id)).unwrap().kind {
NodeKind::Text(t) => t.runs.iter().all(|r| r.style.no_break),
_ => panic!("text"),
};
assert!(!on(&app));
let texts = super::super::tests_appearance::frame_events(&ctx, &mut app, vec![], menu);
let at = texts.iter().find(|(t, _)| t.trim_start_matches(['✓', ' ']) == "No Break").unwrap_or_else(|| panic!("{texts:?}")).1;
super::super::tests_appearance::click(&ctx, &mut app, at.center(), menu);
assert!(on(&app));
assert!(painted(&mut app).contains("✓ No Break"));
let texts = super::super::tests_appearance::frame_events(&ctx, &mut app, vec![], menu);
let at = texts.iter().find(|(t, _)| t.trim_start_matches(['✓', ' ']) == "No Break").unwrap().1;
super::super::tests_appearance::click(&ctx, &mut app, at.center(), menu);
assert!(!on(&app));
}
/// Reset Panel sets every character attribute the panel shows, except the font family, style
/// and size, back to its default (Small Caps, the position, Character Alignment, Proportional
/// Metrics, Tate-chu-yoko, No Break, Character Direction and Digits included), in the selected
/// type and in the text the Type tool has selected while it edits.
#[test]
fn reset_panel_resets_every_attribute_but_the_font() {
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": "One 12"})).unwrap()["id"].as_u64().unwrap();
let runs = |app: &VectorcraftApp| match &app.session.active().unwrap().doc.node(NodeId(id)).unwrap().kind {
NodeKind::Text(t) => t.runs.iter().map(|r| (r.text.clone(), r.style.clone())).collect::<Vec<_>>(),
_ => panic!("text"),
};
let differs = |s: &CharStyle, d: &CharStyle| {
[
s.tracking != d.tracking,
s.leading != d.leading,
s.kerning != d.kerning,
s.baseline_shift != d.baseline_shift,
s.h_scale != d.h_scale,
s.v_scale != d.v_scale,
s.rotation != d.rotation,
s.underline != d.underline,
s.strikethrough != d.strikethrough,
s.all_caps != d.all_caps,
s.small_caps != d.small_caps,
s.position != d.position,
s.char_align != d.char_align,
s.proportional_metrics != d.proportional_metrics,
s.tate_chu_yoko != d.tate_chu_yoko,
s.no_break != d.no_break,
s.char_direction != d.char_direction,
s.digits != d.digits,
]
};
let custom = |app: &mut VectorcraftApp| {
app.run(
"text.setRangeStyle",
json!({
"id": id, "tracking": 50, "leading": 30, "kerning": 20, "baselineShift": 3, "hScale": 120, "vScale": 90, "rotation": 15,
"underline": true, "strikethrough": true, "allCaps": true, "smallCaps": true, "position": "superscript",
"charAlign": "emBoxCenter", "proportionalMetrics": true, "tateChuYoko": true, "noBreak": true,
"charDirection": "rightToLeft", "digits": "hindi"
}),
)
.unwrap();
};
let plain = runs(&app)[0].1.clone();
assert_eq!(differs(&plain, &CharStyle::default()), [false; 18]);
custom(&mut app);
let styled = runs(&app)[0].1.clone();
assert_eq!(differs(&styled, &CharStyle::default()), [true; 18]);
app.run("select.set", json!({"ids": [id]})).unwrap();
reset_panel(&mut app);
assert_eq!(runs(&app), [("One 12".to_string(), plain.clone())]);
// While the Type tool edits text, Reset Panel resets only its selection.
custom(&mut app);
app.select_tool("type");
app.session.apply_actions(vec![vectorcraft_tools::Action::Notify("text.editNew".into())]).unwrap();
app.session.set_tool_option("select", &json!({"start": 4, "end": 6}));
assert!(text_editing(&app).is_some());
reset_panel(&mut app);
assert_eq!(runs(&app), [("One ".to_string(), styled), ("12".to_string(), plain)]);
}
}
mod tests;
@@ -0,0 +1,632 @@
use super::*;
use vectorcraft_engine::Session;
#[test]
fn scaled_dimensions_do_not_show_false_mixed_values_from_roundoff() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 300, "height": 200})).unwrap();
let a = app.run("text.create", json!({"x": 20, "y": 50, "text": "Axis", "size": 12})).unwrap()["id"].clone();
app.run("object.scale", json!({"sx": 120, "sy": 110})).unwrap();
let b = app.run("text.create", json!({"x": 20, "y": 90, "text": "Label", "size": 24})).unwrap()["id"].clone();
app.run("select.set", json!({"ids": [a, b]})).unwrap();
app.run("text.setStyle", json!({"size": 7, "leading": 9})).unwrap();
app.run("text.setFormat", json!({"hScale": 130, "baselineShift": 3})).unwrap();
for (actual, expected) in [
(shared_effective(&app, |s| s.size), 7.0),
(shared_effective(&app, CharStyle::effective_leading), 9.0),
(shared_effective(&app, |s| s.h_scale), 130.0),
(shared_effective(&app, |s| s.baseline_shift), 3.0),
] {
assert!((actual.expect("equal dimensions must not be mixed") - expected).abs() < 1e-8);
}
}
/// A final empty paragraph counts: with the object selected, its Auto Leading differing from
/// the paragraph before it shows Leading as mixed.
#[test]
fn leading_field_counts_the_final_empty_paragraph() {
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": "One\n", "size": 10})).unwrap()["id"].clone();
app.run("select.set", json!({"ids": [id]})).unwrap();
assert!((shared_leading(&app).unwrap() - 12.0).abs() < 1e-9);
app.run("text.setFormat", json!({"autoLeading": 200, "start": 4, "end": 4})).unwrap();
assert_eq!(shared_leading(&app), None, "the empty last paragraph differs");
}
/// Auto leading shows the leading the paragraph's Justification › Auto Leading sets.
#[test]
fn leading_field_shows_the_paragraph_auto_leading() {
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": "One\nTwo", "size": 10})).unwrap()["id"].clone();
app.run("select.set", json!({"ids": [id]})).unwrap();
assert!((shared_leading(&app).unwrap() - 12.0).abs() < 1e-9);
app.run("text.setFormat", json!({"autoLeading": 200, "start": 5, "end": 5})).unwrap();
assert_eq!(shared_leading(&app), None, "the paragraphs differ");
assert_eq!(shared(&app, |s| s.leading.is_none()), Some(true), "still Auto");
app.run("text.setFormat", json!({"autoLeading": 200})).unwrap();
assert!((shared_leading(&app).unwrap() - 20.0).abs() < 1e-9);
app.run("text.setStyle", json!({"leading": 15})).unwrap();
assert!((shared_leading(&app).unwrap() - 15.0).abs() < 1e-9, "a set leading is its own");
}
#[test]
fn scaled_collapsed_text_keeps_nondimensional_controls_available() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 300, "height": 200})).unwrap();
app.run("text.create", json!({"x": 20, "y": 50, "text": "Axis", "size": 12})).unwrap();
app.run("object.transform", json!({"matrix": [0, 0, 0, 0, 0, 0]})).unwrap();
assert!(text_style(&app).is_some());
assert_eq!(shared(&app, |s| s.tracking), Some(0.0));
assert_eq!(shared_effective(&app, |s| s.size), None);
app.run("text.setStyle", json!({"tracking": 40})).unwrap();
app.run("text.setFormat", json!({"leftIndent": 4})).unwrap();
assert_eq!(shared(&app, |s| s.tracking), Some(40.0));
assert_eq!(text_style(&app).unwrap().1.left_indent, 4.0);
let ctx = egui::Context::default();
let texts = super::super::tests_appearance::frame_events(&ctx, &mut app, vec![], show);
assert!(!texts.iter().any(|(s, _)| s == "No text selected"));
}
#[test]
fn scaled_character_fields_show_document_sizes_and_mixed_group_values() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 300, "height": 200})).unwrap();
let a = app.run("text.create", json!({"x": 20, "y": 50, "text": "Axis", "size": 12})).unwrap()["id"].clone();
app.run("object.scale", json!({"sx": 200})).unwrap();
assert_eq!(shared_effective(&app, |s| s.size), Some(24.0));
assert_eq!(text_style(&app).unwrap().0.size, 12.0, "style definitions keep stored attributes");
let b = app.run("text.create", json!({"x": 20, "y": 90, "text": "Label", "size": 24})).unwrap()["id"].clone();
app.run("select.set", json!({"ids": [a, b]})).unwrap();
app.run("object.group", json!({})).unwrap();
assert_eq!(shared_effective(&app, |s| s.size), Some(24.0), "different local sizes, same displayed size");
app.run("object.scale", json!({"sx": 200, "sy": 100})).unwrap();
assert_eq!(shared_effective(&app, |s| s.h_scale), Some(200.0));
app.run("text.setStyle", json!({"size": 10})).unwrap();
assert_eq!(shared_effective(&app, |s| s.size), Some(10.0));
app.run("text.setFormat", json!({"hScale": 100})).unwrap();
assert_eq!(shared_effective(&app, |s| s.h_scale), Some(100.0));
}
#[test]
fn scaled_range_fields_follow_the_type_tool_selection() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 300, "height": 200})).unwrap();
app.session.select_tool("type", Default::default()).unwrap();
let id = app.run("text.create", json!({"x": 20, "y": 50, "text": "Axis label", "size": 12})).unwrap()["id"].clone();
app.session.apply_actions(vec![vectorcraft_tools::Action::Notify("text.editNew".into())]).unwrap();
assert!(text_editing(&app).is_some());
app.run("object.scale", json!({"sx": 200})).unwrap();
app.run("text.setRangeStyle", json!({"id": id, "start": 0, "end": 4, "size": 10})).unwrap();
app.session.set_tool_option("selectAll", &json!(true));
assert_eq!(shared_effective(&app, |s| s.size), None);
app.session.set_tool_option("select", &json!({"start": 0, "end": 4}));
assert_eq!(shared_effective(&app, |s| s.size), Some(10.0));
assert_eq!(text_style(&app).unwrap().0.size, 5.0);
}
/// The Kerning field at a caret kerns that pair, not the whole text (#2217).
#[test]
fn kerning_at_a_caret_sets_the_pair_not_the_whole_text() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 300, "height": 200})).unwrap();
app.session.select_tool("type", Default::default()).unwrap();
let id = app.run("text.create", json!({"x": 20, "y": 50, "text": "AVAX", "size": 12})).unwrap()["id"].clone();
app.session.apply_actions(vec![vectorcraft_tools::Action::Notify("text.editNew".into())]).unwrap();
app.session.set_tool_option("select", &json!({"start": 2, "end": 2}));
set_kerning(&mut app, json!(-50));
let kerning = |app: &VectorcraftApp, at: usize| {
let st = app.session.active().unwrap();
let Some(NodeKind::Text(t)) = st.doc.node(NodeId(id.as_u64().unwrap())).map(|n| &n.kind) else { panic!("text") };
vectorcraft_text::edit::style_at(&t.runs, at + 1).kerning // the character at `at`
};
assert_eq!(kerning(&app, 1), Some(-50.0), "the character before the caret");
assert_eq!(kerning(&app, 3), None, "the rest is untouched");
assert_eq!(kerning(&app, 0), None);
// A selected range still takes it whole.
app.session.set_tool_option("select", &json!({"start": 2, "end": 4}));
set_kerning(&mut app, json!(30));
assert_eq!((kerning(&app, 1), kerning(&app, 2), kerning(&app, 3)), (Some(-50.0), Some(30.0), Some(30.0)));
}
/// A variable font gets the Variable Font button, which opens one slider per axis named as the
/// font names it; a drag along one sets that axis of the selected type in one undo step.
#[test]
fn variable_fonts_get_a_slider_per_axis() {
use vectorcraft_text::test_fonts::{TWO_AXIS_FAMILY, two_axis_font};
let db = vectorcraft_text::FontDb::global();
db.add_font(two_axis_font().unwrap());
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 300, "height": 200})).unwrap();
let plain = app.run("text.create", json!({"x": 20, "y": 50, "text": "Static"})).unwrap()["id"].clone();
let id = app.run("text.create", json!({"x": 20, "y": 120, "text": "lIn", "font": TWO_AXIS_FAMILY})).unwrap()["id"].clone();
app.run("select.set", json!({"ids": [plain]})).unwrap();
assert!(!crate::tests_labels::painted_text(&mut app, show).contains("Variable Font"), "a static font");
app.run("select.set", json!({"ids": [id]})).unwrap();
assert!(crate::tests_labels::painted_text(&mut app, show).contains("Variable Font"));
let face = db.face(TWO_AXIS_FAMILY, "Regular").unwrap();
let text = crate::tests_labels::painted_text(&mut app, |app, ui| axis_sliders(app, ui, &face));
assert!(text.contains("Weight\n") && text.contains("Width\n") && text.contains("400") && text.contains("100"), "{text}");
// The labels are the font's names in every interface language, not catalog words (ja
// translates the interface's Weight as the stroke weight).
let ja = crate::i18n::Lang::from_code("ja").unwrap();
crate::i18n::set_current(ja);
let text = crate::tests_labels::painted_text(&mut app, |app, ui| axis_sliders(app, ui, &face));
crate::i18n::set_current(crate::i18n::Lang::EN);
let weight = crate::i18n::tr(ja, "Weight");
assert!(text.contains("Weight\n") && text.contains("Width\n") && (weight == "Weight" || !text.contains(weight)), "{text}");
// Screen readers name each slider by its axis.
let tree = crate::widgets::accessibility_tests::tree(|ui| axis_sliders(&mut app, ui, &face));
for name in ["Weight", "Width"] {
assert!(tree.iter().any(|n| n.role() == egui::accesskit::Role::Slider && n.label() == Some(name)), "{name}");
}
// ...with the number its field shows, on the axis's range.
use crate::widgets::accessibility_tests::slider_value;
let wght = face.axes().into_iter().find(|a| &a.tag == b"wght").unwrap();
let range = (Some(f64::from(wght.min)), Some(f64::from(wght.max)));
assert_eq!(slider_value(&tree, "Weight"), (Some(400.0), range.0, range.1, Some("400".into())));
// Drag the Weight slider from its start to past its end.
let ctx = egui::Context::default();
let frame = |app: &mut VectorcraftApp, events: Vec<egui::Event>| {
let mut slider = None;
let input = egui::RawInput { events, ..Default::default() };
ctx.run_ui(input, |ui| {
axis_sliders(app, ui, &face);
slider = ctx.read_response(ui.scope_id().with(("ch-axis", *b"wght"))).map(|r| r.rect);
})
.textures_delta
.clear();
slider
};
frame(&mut app, vec![]);
let r = frame(&mut app, vec![]).expect("the Weight slider is drawn");
let none = egui::Modifiers::NONE;
let (from, to) = (egui::pos2(r.left() + 4.0, r.center().y), egui::pos2(r.right() + 20.0, r.center().y));
let button = |pos, pressed| egui::Event::PointerButton { pos, button: egui::PointerButton::Primary, pressed, modifiers: none };
frame(&mut app, vec![egui::Event::PointerMoved(from), button(from, true)]);
for i in 1..=4 {
frame(&mut app, vec![egui::Event::PointerMoved(from + (to - from) * i as f32 / 4.0)]);
}
frame(&mut app, vec![button(to, false)]);
frame(&mut app, vec![]);
let axes = |app: &VectorcraftApp| shared(app, |s| s.font_axes.clone()).unwrap();
assert_eq!(axes(&app).into_iter().collect::<Vec<_>>(), [("wght".to_string(), 700.0)]);
app.run("edit.undo", json!({})).unwrap();
assert!(axes(&app).is_empty(), "one undo step");
// Regular and Bold selected: the weights differ and the field is blank; the widths agree.
let bold = app.run("text.create", json!({"x": 20, "y": 160, "text": "lIn", "font": TWO_AXIS_FAMILY, "style": "Bold"})).unwrap()["id"].clone();
app.run("select.set", json!({"ids": [id, bold]})).unwrap();
let text = crate::tests_labels::painted_text(&mut app, |app, ui| axis_sliders(app, ui, &face));
assert!(!text.contains("400") && !text.contains("700") && text.contains("100"), "{text}");
let tree = crate::widgets::accessibility_tests::tree(|ui| axis_sliders(&mut app, ui, &face));
assert_eq!(slider_value(&tree, "Weight"), (None, None, None, None), "a blank field: no value");
assert_eq!(slider_value(&tree, "Width").0, Some(100.0));
// Reset Panel leaves the settings, as it leaves the font and style.
app.run("select.set", json!({"ids": [id]})).unwrap();
app.run("text.setFormat", json!({"fontAxes": {"wght": 650}})).unwrap();
reset_panel(&mut app);
assert_eq!(axes(&app).into_iter().collect::<Vec<_>>(), [("wght".to_string(), 650.0)]);
}
/// Bold and Italic pick the family's own faces by name, and toggle back (#724).
#[test]
fn bold_and_italic_pick_the_familys_own_faces() {
let styles: Vec<String> =
["Light", "Narrow", "Regular", "Italic", "Narrow Italic", "SemiBold", "Bold", "Bold Condensed", "Narrow Bold", "Bold Italic", "Black"]
.iter()
.map(|s| s.to_string())
.collect();
assert_eq!(pick_face(&styles, true, false, "Regular"), Some("Bold"), "not Bold Condensed, Narrow Bold or SemiBold");
assert_eq!(pick_face(&styles, true, true, "Italic"), Some("Bold Italic"));
assert_eq!(pick_face(&styles, false, true, "Regular"), Some("Italic"));
assert_eq!(pick_face(&styles, false, false, "Italic"), Some("Regular"), "upright again, not Narrow");
assert_eq!(pick_face(&styles, true, false, "Narrow"), Some("Narrow Bold"), "a Narrow face stays Narrow");
assert_eq!(pick_face(&styles, false, true, "Narrow"), Some("Narrow Italic"));
let oblique: Vec<String> = ["Book", "Oblique", "Bold-Oblique"].iter().map(|s| s.to_string()).collect();
assert_eq!(pick_face(&oblique, true, true, "Oblique"), Some("Bold-Oblique"));
assert_eq!(pick_face(&oblique, false, false, "Oblique"), None, "Book has a weight word: no regular face");
assert_eq!(face_of("Bold Italic"), (true, false, true, vec![]));
assert_eq!(face_of("SemiBold"), (false, false, false, vec![]));
assert_eq!(face_of("Narrow Italic"), (false, true, true, vec!["narrow".to_string()]));
// No type selected: a message, nothing changed.
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 300, "height": 200})).unwrap();
assert!(toggle_face(&mut app, Face::Bold).is_err());
assert!(!crate::menus::enabled(&app, "type.bold"));
}
/// 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.
fn script_buttons(ctx: &egui::Context, app: &mut VectorcraftApp) -> ([egui::Pos2; 3], [bool; 3]) {
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>) {
match s {
egui::Shape::Vec(v) => v.iter().for_each(|s| walk(s, on, ones, lit)),
egui::Shape::Text(t) if t.galley.text() == "1" => ones.push(t.visual_bounding_rect().center()),
egui::Shape::Rect(r) if r.fill == on && r.rect.size() == vec2(30.0, 26.0) => lit.push(r.rect),
_ => {}
}
}
let (mut ones, mut lit) = (vec![], vec![]);
for c in &out.shapes {
walk(&c.shape, Tokens::get(ctx).tool_active, &mut ones, &mut lit);
}
let [sup, sub] = ones[..] else { panic!("two script buttons: {ones:?}") };
assert!(sup.x < sub.x && sup.y < sub.y, "Superscript, then Subscript: {ones:?}");
// 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];
(points, points.map(|p| lit.iter().any(|r| r.contains(p))))
}
/// Each run's (small caps, position).
fn scripts(app: &VectorcraftApp, id: &Value) -> Vec<(Option<f64>, &'static str)> {
match &app.session.active().unwrap().doc.node(NodeId(id.as_u64().unwrap())).unwrap().kind {
NodeKind::Text(t) => t.runs.iter().map(|r| (r.style.small_caps, r.style.position.id())).collect(),
_ => panic!("text"),
}
}
/// The Small Caps, Superscript and Subscript buttons set the selected type's small caps and
/// position, with one undo step for each click, and show them; Superscript and Subscript turn
/// each other off. While the Type tool edits, they set and show the selected characters.
#[test]
fn small_caps_superscript_and_subscript_buttons_set_and_show_the_text() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 300, "height": 200})).unwrap();
app.session.select_tool("type", Default::default()).unwrap();
let id = app.run("text.create", json!({"x": 20, "y": 50, "text": "E=mc2", "size": 12})).unwrap()["id"].clone();
let ctx = egui::Context::default();
let (at, lit) = script_buttons(&ctx, &mut app);
assert_eq!(lit, [false; 3]);
let small = Some(app.session.active().unwrap().doc.setup.small_caps_size);
for (button, runs, shown) in [
(0, (small, "normal"), [true, false, false]),
(1, (small, "superscript"), [true, true, false]),
(2, (small, "subscript"), [true, false, true]),
(2, (small, "normal"), [true, false, false]),
(1, (small, "superscript"), [true, true, false]),
(1, (small, "normal"), [true, false, false]),
(0, (None, "normal"), [false; 3]),
] {
let undo = app.session.active().unwrap().history.undo.len();
super::super::tests_appearance::click(&ctx, &mut app, at[button], show);
assert_eq!(scripts(&app, &id), [runs], "button {button}");
assert_eq!(app.session.active().unwrap().history.undo.len(), undo + 1, "button {button}");
assert_eq!(script_buttons(&ctx, &mut app).1, shown, "button {button}");
}
// The Type tool's selection: "2" alone.
app.session.apply_actions(vec![vectorcraft_tools::Action::Notify("text.editNew".into())]).unwrap();
app.session.set_tool_option("select", &json!({"start": 4, "end": 5}));
assert!(text_editing(&app).is_some());
super::super::tests_appearance::click(&ctx, &mut app, at[1], show);
assert_eq!(scripts(&app, &id), [(None, "normal"), (None, "superscript")]);
assert_eq!(script_buttons(&ctx, &mut app).1, [false, true, false]);
app.session.set_tool_option("select", &json!({"start": 0, "end": 4}));
assert_eq!(script_buttons(&ctx, &mut app).1, [false; 3]);
}
/// The panel menu's Small Caps, Superscript and Subscript set the selected type's small caps
/// and position and show a check mark when on; Superscript and Subscript turn each other off.
#[test]
fn small_caps_superscript_and_subscript_menu_items_set_and_check_the_text() {
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": "E=mc2", "size": 12})).unwrap()["id"].clone();
let ctx = egui::Context::default();
let checked = |app: &mut VectorcraftApp| {
let texts = super::super::tests_appearance::frame_events(&ctx, app, vec![], menu);
["Small Caps", "Superscript", "Subscript"].map(|l| texts.iter().any(|(t, _)| *t == format!("✓ {l}")))
};
assert_eq!(checked(&mut app), [false; 3]);
let small = Some(app.session.active().unwrap().doc.setup.small_caps_size);
for (item, runs, shown) in [
("Small Caps", (small, "normal"), [true, false, false]),
("Superscript", (small, "superscript"), [true, true, false]),
("Subscript", (small, "subscript"), [true, false, true]),
("Subscript", (small, "normal"), [true, false, false]),
("Small Caps", (None, "normal"), [false; 3]),
] {
let texts = super::super::tests_appearance::frame_events(&ctx, &mut app, vec![], menu);
let at = texts.iter().find(|(t, _)| t.trim_start_matches(['✓', ' ']) == item).unwrap_or_else(|| panic!("{item}: {texts:?}")).1;
super::super::tests_appearance::click(&ctx, &mut app, at.center(), menu);
assert_eq!(scripts(&app, &id), [runs], "{item}");
assert_eq!(checked(&mut app), shown, "{item}");
}
}
/// Character panel menu › System Layout is off for a new document, and a click turns whole-point
/// spacing on and back off for every text object.
#[test]
fn system_layout_menu_item_toggles_the_document() {
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": "Hi", "size": 13})).unwrap()["id"].as_u64().unwrap();
let ctx = egui::Context::default();
let on = |app: &VectorcraftApp| {
let st = app.session.active().unwrap();
let object = match &st.doc.node(NodeId(id)).unwrap().kind {
NodeKind::Text(t) => t.system_layout,
_ => panic!("text"),
};
(st.doc.setup.system_layout, object)
};
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(['✓', ' ']) == "System Layout")
};
assert_eq!(on(&app), (false, false));
assert!(row(&mut app).is_some_and(|(t, _)| !t.starts_with('✓')));
let at = row(&mut app).unwrap().1;
super::super::tests_appearance::click(&ctx, &mut app, at.center(), menu);
assert_eq!(on(&app), (true, true));
assert!(row(&mut app).is_some_and(|(t, _)| t.starts_with('✓')));
let at = row(&mut app).unwrap().1;
super::super::tests_appearance::click(&ctx, &mut app, at.center(), menu);
assert_eq!(on(&app), (false, false));
assert!(row(&mut app).is_some_and(|(t, _)| !t.starts_with('✓')));
}
/// Character panel menu › Fractional Widths is on for new type, and a click turns it off and
/// back on for the whole type object.
#[test]
fn fractional_widths_menu_item_toggles_the_type_object() {
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": "Hi", "size": 13})).unwrap()["id"].as_u64().unwrap();
let ctx = egui::Context::default();
let flag = |app: &VectorcraftApp| match &app.session.active().unwrap().doc.node(NodeId(id)).unwrap().kind {
NodeKind::Text(t) => t.fractional_widths,
_ => panic!("text"),
};
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(['✓', ' ']) == "Fractional Widths")
};
assert!(flag(&app));
assert!(row(&mut app).is_some_and(|(t, _)| t.starts_with('✓')));
let at = row(&mut app).unwrap().1;
super::super::tests_appearance::click(&ctx, &mut app, at.center(), menu);
assert!(!flag(&app));
assert!(row(&mut app).is_some_and(|(t, _)| !t.starts_with('✓')));
let at = row(&mut app).unwrap().1;
super::super::tests_appearance::click(&ctx, &mut app, at.center(), menu);
assert!(flag(&app));
assert!(row(&mut app).is_some_and(|(t, _)| t.starts_with('✓')));
}
/// Character Alignment and Proportional Metrics are in the panel menu with the East Asian
/// options only (as Mojikumi Set and Top-to-Top Leading are in the Paragraph panel).
#[test]
fn character_alignment_shows_with_the_east_asian_options() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 300, "height": 200})).unwrap();
let id = app.session.execute("text.create", &json!({"x": 20, "y": 50, "text": "雅楽"})).unwrap()["id"].clone();
app.session.execute("select.set", &json!({"ids": [id]})).unwrap();
let shown = |app: &mut VectorcraftApp| {
let text = crate::tests_labels::painted_text(app, menu);
(text.contains("Character Alignment"), text.contains("Proportional Metrics"))
};
assert_eq!(shown(&mut app), (false, false));
app.session.prefs.show_east_asian_options = true;
assert_eq!(shown(&mut app), (true, true));
}
/// Tate-chu-yoko is in the panel menu with the East Asian options, and choosing it marks the
/// selected characters.
#[test]
fn tate_chu_yoko_menu_item_shows_with_east_asian_options_and_toggles() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 300, "height": 200})).unwrap();
let id = app.session.execute("text.create", &json!({"x": 20, "y": 50, "text": "2026"})).unwrap()["id"].as_u64().unwrap();
app.session.execute("select.set", &json!({"ids": [id]})).unwrap();
let ctx = egui::Context::default();
let painted = |app: &mut VectorcraftApp| crate::tests_labels::painted_text(app, menu);
assert!(!painted(&mut app).contains("Tate-chu-yoko"));
app.session.prefs.show_east_asian_options = true;
let on = |app: &VectorcraftApp| match &app.session.active().unwrap().doc.node(NodeId(id)).unwrap().kind {
NodeKind::Text(t) => t.runs.iter().all(|r| r.style.tate_chu_yoko),
_ => panic!("text"),
};
assert!(!on(&app));
let texts = super::super::tests_appearance::frame_events(&ctx, &mut app, vec![], menu);
let at = texts.iter().find(|(t, _)| t.trim_start_matches(['✓', ' ']) == "Tate-chu-yoko").unwrap_or_else(|| panic!("{texts:?}")).1;
super::super::tests_appearance::click(&ctx, &mut app, at.center(), menu);
assert!(on(&app));
assert!(painted(&mut app).contains("✓ Tate-chu-yoko"));
let texts = super::super::tests_appearance::frame_events(&ctx, &mut app, vec![], menu);
let at = texts.iter().find(|(t, _)| t.trim_start_matches(['✓', ' ']) == "Tate-chu-yoko").unwrap().1;
super::super::tests_appearance::click(&ctx, &mut app, at.center(), menu);
assert!(!on(&app));
}
/// Digits is in the panel menu with the Middle Eastern (Indic) options only, as Paragraph
/// Direction is in the Paragraph panel.
#[test]
fn digits_show_with_the_indic_options() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 300, "height": 200})).unwrap();
let id = app.session.execute("text.create", &json!({"x": 20, "y": 50, "text": "صفحة 3"})).unwrap()["id"].clone();
app.session.execute("select.set", &json!({"ids": [id]})).unwrap();
let shown = |app: &mut VectorcraftApp| crate::tests_labels::painted_text(app, menu).contains("Digits");
assert!(!shown(&mut app));
app.session.prefs.show_indic_options = true;
assert!(shown(&mut app));
}
/// Character Direction is in the panel menu with the Middle Eastern (Indic) options, beside
/// Digits. Its submenu checks the direction the characters the panel acts on share: the
/// selected type's, or the range the Type tool has selected. It checks no item when they differ.
#[test]
fn character_direction_checks_the_direction_of_the_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": "שלום 21 10 2026"})).unwrap()["id"].as_u64().unwrap();
app.run("select.set", json!({"ids": [id]})).unwrap();
let shown = |app: &mut VectorcraftApp| crate::tests_labels::painted_text(app, menu).contains("Character Direction");
assert!(!shown(&mut app));
app.session.prefs.show_indic_options = true;
assert!(shown(&mut app));
let checked = |app: &mut VectorcraftApp| {
let text = crate::tests_labels::painted_text(app, char_direction_items);
text.lines().filter_map(|l| l.strip_prefix("✓ ")).map(str::to_string).collect::<Vec<_>>()
};
assert_eq!(checked(&mut app), ["Default"]);
app.run("text.setRangeStyle", json!({"id": id, "start": 9, "end": 19, "charDirection": "leftToRight"})).unwrap();
// The object's characters differ: nothing is checked.
assert!(checked(&mut app).is_empty());
// The Type tool's selection: the date, then the Hebrew word.
app.select_tool("type");
app.session.apply_actions(vec![vectorcraft_tools::Action::Notify("text.editNew".into())]).unwrap();
app.session.set_tool_option("select", &json!({"start": 9, "end": 19}));
assert_eq!(checked(&mut app), ["Left to Right"]);
app.session.set_tool_option("select", &json!({"start": 0, "end": 8}));
assert_eq!(checked(&mut app), ["Default"]);
}
/// 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() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 300, "height": 200})).unwrap();
let a = app.run("text.create", json!({"x": 20, "y": 50, "text": "A"})).unwrap()["id"].clone();
let b = app.run("text.create", json!({"x": 20, "y": 90, "text": "B"})).unwrap()["id"].clone();
app.run("text.setRangeStyle", json!({"id": a, "start": 0, "end": 1, "smallCaps": true, "position": "superscript"})).unwrap();
app.run("text.setRangeStyle", json!({"id": b, "start": 0, "end": 1, "smallCaps": false, "position": "subscript"})).unwrap();
app.run("select.set", json!({"ids": [a, b]})).unwrap();
assert_eq!(shared(&app, |s| s.small_caps.is_some()), None);
assert_eq!(shared(&app, |s| s.position), None);
let ctx = egui::Context::default();
assert_eq!(script_buttons(&ctx, &mut app).1, [false, false, false]);
let checked = |app: &mut VectorcraftApp| {
let texts = super::super::tests_appearance::frame_events(&ctx, app, vec![], menu);
["Small Caps", "Superscript", "Subscript"].map(|l| texts.iter().any(|(t, _)| *t == format!("✓ {l}")))
};
assert_eq!(checked(&mut app), [false, false, false]);
}
/// 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]
fn character_direction_does_nothing_at_an_empty_caret() {
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": "שלום 21 10 2026"})).unwrap()["id"].as_u64().unwrap();
app.run("select.set", json!({"ids": [id]})).unwrap();
app.select_tool("type");
app.session.apply_actions(vec![vectorcraft_tools::Action::Notify("text.editNew".into())]).unwrap();
app.session.set_tool_option("select", &json!({"start": 3, "end": 3}));
let directions = |app: &VectorcraftApp| match &app.session.active().unwrap().doc.node(NodeId(id)).unwrap().kind {
NodeKind::Text(t) => t.runs.iter().map(|r| (r.text.clone(), r.style.char_direction)).collect::<Vec<_>>(),
_ => panic!("text"),
};
let before = directions(&app);
let ctx = egui::Context::default();
let click_rtl = |app: &mut VectorcraftApp| {
let texts = super::super::tests_appearance::frame_events(&ctx, app, vec![], char_direction_items);
let at = texts.iter().find(|(t, _)| t.trim_start_matches(['✓', ' ']) == "Right to Left").unwrap_or_else(|| panic!("{texts:?}")).1;
super::super::tests_appearance::click(&ctx, app, at.center(), char_direction_items);
};
click_rtl(&mut app);
assert_eq!(directions(&app), before);
// With characters selected, the same click sets their direction.
app.session.set_tool_option("select", &json!({"start": 0, "end": 4}));
click_rtl(&mut app);
assert_ne!(directions(&app), before);
}
/// No Break is in the panel menu, and choosing it marks the selected characters.
#[test]
fn no_break_menu_item_toggles_the_selected_characters() {
let mut app = VectorcraftApp::new(Session::new(), Default::default());
app.run("file.new", json!({"width": 300, "height": 200})).unwrap();
let id = app.session.execute("text.create", &json!({"x": 20, "y": 50, "text": "Hello"})).unwrap()["id"].as_u64().unwrap();
app.session.execute("select.set", &json!({"ids": [id]})).unwrap();
let ctx = egui::Context::default();
let painted = |app: &mut VectorcraftApp| crate::tests_labels::painted_text(app, menu);
assert!(painted(&mut app).contains("No Break"));
assert!(!painted(&mut app).contains("✓ No Break"));
let on = |app: &VectorcraftApp| match &app.session.active().unwrap().doc.node(NodeId(id)).unwrap().kind {
NodeKind::Text(t) => t.runs.iter().all(|r| r.style.no_break),
_ => panic!("text"),
};
assert!(!on(&app));
let texts = super::super::tests_appearance::frame_events(&ctx, &mut app, vec![], menu);
let at = texts.iter().find(|(t, _)| t.trim_start_matches(['✓', ' ']) == "No Break").unwrap_or_else(|| panic!("{texts:?}")).1;
super::super::tests_appearance::click(&ctx, &mut app, at.center(), menu);
assert!(on(&app));
assert!(painted(&mut app).contains("✓ No Break"));
let texts = super::super::tests_appearance::frame_events(&ctx, &mut app, vec![], menu);
let at = texts.iter().find(|(t, _)| t.trim_start_matches(['✓', ' ']) == "No Break").unwrap().1;
super::super::tests_appearance::click(&ctx, &mut app, at.center(), menu);
assert!(!on(&app));
}
/// Reset Panel sets every character attribute the panel shows, except the font family, style
/// and size, back to its default (Small Caps, the position, Character Alignment, Proportional
/// Metrics, Tate-chu-yoko, No Break, Character Direction and Digits included), in the selected
/// type and in the text the Type tool has selected while it edits.
#[test]
fn reset_panel_resets_every_attribute_but_the_font() {
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": "One 12"})).unwrap()["id"].as_u64().unwrap();
let runs = |app: &VectorcraftApp| match &app.session.active().unwrap().doc.node(NodeId(id)).unwrap().kind {
NodeKind::Text(t) => t.runs.iter().map(|r| (r.text.clone(), r.style.clone())).collect::<Vec<_>>(),
_ => panic!("text"),
};
let differs = |s: &CharStyle, d: &CharStyle| {
[
s.tracking != d.tracking,
s.leading != d.leading,
s.kerning != d.kerning,
s.baseline_shift != d.baseline_shift,
s.h_scale != d.h_scale,
s.v_scale != d.v_scale,
s.rotation != d.rotation,
s.underline != d.underline,
s.strikethrough != d.strikethrough,
s.all_caps != d.all_caps,
s.small_caps != d.small_caps,
s.position != d.position,
s.char_align != d.char_align,
s.proportional_metrics != d.proportional_metrics,
s.tate_chu_yoko != d.tate_chu_yoko,
s.no_break != d.no_break,
s.char_direction != d.char_direction,
s.digits != d.digits,
]
};
let custom = |app: &mut VectorcraftApp| {
app.run(
"text.setRangeStyle",
json!({
"id": id, "tracking": 50, "leading": 30, "kerning": 20, "baselineShift": 3, "hScale": 120, "vScale": 90, "rotation": 15,
"underline": true, "strikethrough": true, "allCaps": true, "smallCaps": true, "position": "superscript",
"charAlign": "emBoxCenter", "proportionalMetrics": true, "tateChuYoko": true, "noBreak": true,
"charDirection": "rightToLeft", "digits": "hindi"
}),
)
.unwrap();
};
let plain = runs(&app)[0].1.clone();
assert_eq!(differs(&plain, &CharStyle::default()), [false; 18]);
custom(&mut app);
let styled = runs(&app)[0].1.clone();
assert_eq!(differs(&styled, &CharStyle::default()), [true; 18]);
app.run("select.set", json!({"ids": [id]})).unwrap();
reset_panel(&mut app);
assert_eq!(runs(&app), [("One 12".to_string(), plain.clone())]);
// While the Type tool edits text, Reset Panel resets only its selection.
custom(&mut app);
app.select_tool("type");
app.session.apply_actions(vec![vectorcraft_tools::Action::Notify("text.editNew".into())]).unwrap();
app.session.set_tool_option("select", &json!({"start": 4, "end": 6}));
assert!(text_editing(&app).is_some());
reset_panel(&mut app);
assert_eq!(runs(&app), [("One ".to_string(), styled), ("12".to_string(), plain)]);
}