diff --git a/crates/engine/src/cmd/prefscmds.rs b/crates/engine/src/cmd/prefscmds.rs index 7a948bfc87..b6c500749f 100644 --- a/crates/engine/src/cmd/prefscmds.rs +++ b/crates/engine/src/cmd/prefscmds.rs @@ -217,6 +217,7 @@ pub const PREF_SPECS: &[PrefSpec] = &[ p!("recentFontsCount", "Type", "Options", "Number of Recent Fonts", int(1, 15)), p!("missingGlyphProtection", "Type", "Options", "Enable Missing Glyph Protection", bool), p!("highlightAlternateGlyphs", "Type", "Options", "Highlight Alternate Glyphs", bool), + p!("showCharacterAlternates", "Type", "Options", "Show Character Alternates", bool), p!("highlightSubstitutedFonts", "Type", "Options", "Highlight Substituted Fonts", bool), p!("placeholderText", "Type", "Options", "Fill New Type Objects With Placeholder Text", bool), p!("typedLists", "Type", "Options", "Start Lists From Typed Markers", bool), diff --git a/crates/engine/src/prefs.rs b/crates/engine/src/prefs.rs index 1e4629e448..86525e9214 100644 --- a/crates/engine/src/prefs.rs +++ b/crates/engine/src/prefs.rs @@ -111,6 +111,9 @@ pub struct Prefs { pub recent_fonts_count: u32, pub missing_glyph_protection: bool, pub highlight_alternate_glyphs: bool, + /// Type › Show Character Alternates: with a Type tool and one character selected, the canvas + /// shows that character's alternate glyphs beside it (on by default). + pub show_character_alternates: bool, /// Type › Highlight Substituted Fonts: on screen, text drawn in a substitute for a missing /// font is highlighted, in every document (on by default). pub highlight_substituted_fonts: bool, @@ -442,6 +445,7 @@ impl Default for Prefs { recent_fonts_count: 10, missing_glyph_protection: true, highlight_alternate_glyphs: false, + show_character_alternates: true, highlight_substituted_fonts: true, placeholder_text: true, typed_lists: true, diff --git a/crates/engine/src/tests/prefs.rs b/crates/engine/src/tests/prefs.rs index d88b8250a2..521d2eabde 100644 --- a/crates/engine/src/tests/prefs.rs +++ b/crates/engine/src/tests/prefs.rs @@ -1978,6 +1978,26 @@ fn highlight_alternate_glyphs_preference_is_off_by_default() { assert!(!s.prefs.highlight_alternate_glyphs); } +/// Preferences › Type › Show Character Alternates: on by default, listed in the Type category, +/// saved with the other preferences, and a value that is not true or false is refused. +#[test] +fn show_character_alternates_preference_is_on_by_default_and_saved() { + let mut s = Session::new(); + assert!(s.prefs.show_character_alternates, "on by default"); + let spec = crate::cmd::prefscmds::PREF_SPECS.iter().find(|p| p.key == "showCharacterAlternates").expect("listed"); + assert_eq!((spec.category, spec.label), ("Type", "Show Character Alternates")); + assert_eq!(s.execute("prefs.get", &json!({"key": "showCharacterAlternates"})).unwrap(), json!(true)); + set_pref(&mut s, "showCharacterAlternates", json!(false)); + assert!(!s.prefs.show_character_alternates); + assert!(s.execute("prefs.set", &json!({"key": "showCharacterAlternates", "value": "yes"})).is_err(), "not a boolean"); + assert!(!s.prefs.show_character_alternates, "a refused value changes nothing"); + let saved: Prefs = serde_json::from_value(serde_json::to_value(&s.prefs).unwrap()).unwrap(); + assert!(!saved.show_character_alternates); + // Preferences saved before the key existed read it as on. + let old: Prefs = serde_json::from_value(json!({"highlightAlternateGlyphs": true})).unwrap(); + assert!(old.show_character_alternates); +} + /// Preferences › Type › Highlight Substituted Fonts: on by default, listed in the Type category, /// and saved with the other preferences. /// diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs index 486c70de88..5c30f96246 100644 --- a/crates/ui-egui/src/canvas.rs +++ b/crates/ui-egui/src/canvas.rs @@ -15,6 +15,7 @@ use crate::stylus::StylusFeed; use crate::theme::{self, Tokens}; use crate::{CacheKey, VectorcraftApp, now_ms, widgets}; +mod alternates_bar; mod artboards; mod context; mod contour_bar; @@ -441,6 +442,8 @@ pub fn show(app: &mut VectorcraftApp, ui: &mut Ui) { } if app.ui.screen_mode < 3 { contour_bar::contour_bar(app, ui, &xf); + // After the task bar, which the row of character alternates keeps clear of. + alternates_bar::alternates_bar(app, ui, &xf); } if app.ui.screen_mode < 3 { crate::free_transform::show(app, ui, xf.rect); diff --git a/crates/ui-egui/src/canvas/alternates_bar.rs b/crates/ui-egui/src/canvas/alternates_bar.rs new file mode 100644 index 0000000000..83914ef11d --- /dev/null +++ b/crates/ui-egui/src/canvas/alternates_bar.rs @@ -0,0 +1,163 @@ +//! Character alternates on the canvas: with a Type tool and exactly one character selected, a +//! small row of that character's alternate glyphs shows beside it. A click on a glyph draws the +//! character with it, as a double-click on the glyph in the Glyphs panel's Show › Alternates for +//! Current Selection does, through the same command and so as one undo step. An arrow at the end +//! of the row opens the Glyphs panel when the character has more alternates than the row shows. +//! Preferences › Type › Show Character Alternates (`showCharacterAlternates`) turns the row off. +//! +//! Public behavior: selecting a character shows its alternate glyphs next to it, a click replaces +//! the character's glyph, at most five alternates show, and a control to their right opens the +//! Glyphs panel for the rest ("Replace a character with on-canvas alternate glyphs", +//! ; +//! [Replace characters with alternate glyphs](https://helpx.adobe.com/illustrator/desktop/design-with-text/special-characters-glyphs/replace-characters-with-alternate-glyphs.html)). +//! The row's place, its size and the preference are VectorCraft's own. + +use vectorcraft_doc::{NodeId, NodeKind}; +use vectorcraft_geom::Point; +use vectorcraft_text::FontDb; + +use super::{CornerRadius, Pos2, Sense, Shape, Stroke, StrokeKind, Tokens, Ui, Vec2, VectorcraftApp, Xf, pos2, vec2}; +use crate::panels::glyphs; + +/// The side of one glyph cell, in points. +const CELL: f32 = 28.0; +/// The width of the arrow that opens the Glyphs panel. +const MORE: f32 = 16.0; +/// The space between the row's edge and its cells: the frame's 1-point line and 3 points inside it. +const PAD: f32 = 4.0; +/// The space between the selected character and the row. +const GAP: f32 = 6.0; + +fn bar_id() -> egui::Id { + egui::Id::unique("character-alternates-bar") +} + +/// Where the row was last drawn (none while it isn't shown). +#[cfg(test)] +pub(super) fn bar_rect(ctx: &egui::Context) -> Option { + let id = bar_id(); + ctx.memory(|m| m.areas().is_visible(&egui::LayerId::new(egui::Order::Middle, id)).then(|| m.area_rect(id)).flatten()) +} + +/// The size of a row of `count` glyphs, with the arrow when `more`. +fn bar_size(count: usize, more: bool) -> Vec2 { + let cells = count as f32 * CELL + if more { MORE } else { 0.0 }; + vec2(cells + 2.0 * PAD, CELL + 2.0 * PAD) +} + +/// The document-space highlight quads of bytes `a..b` of text `id`, and whether the text is set +/// vertically. Kept until the document or the selection changes, so the text is laid out once for +/// each, not in every frame. Empty for a character the text does not draw (overset area type). +fn character_quads(app: &VectorcraftApp, ui: &Ui, id: NodeId, (a, b): (usize, usize)) -> Option<(Vec<[Point; 4]>, bool)> { + type Quads = (Vec<[Point; 4]>, bool); + let st = app.session.active()?; + let key = (st.uid, st.revision, id, a, b); + let slot = bar_id().with("quads"); + if let Some((cached_key, quads)) = ui.ctx().data(|d| d.get_temp::<((u64, u64, NodeId, usize, usize), Quads)>(slot)) + && cached_key == key + { + return Some(quads); + } + let NodeKind::Text(tx) = &st.doc.node(id)?.kind else { return None }; + let lay = vectorcraft_text::layout(FontDb::global(), tx); + let quads: Vec<[Point; 4]> = vectorcraft_text::selection_quads(&lay, a, b).into_iter().map(|q| q.map(|c| tx.xf * c)).collect(); + let found = (quads, tx.vertical); + ui.ctx().data_mut(|d| d.insert_temp(slot, (key, found.clone()))); + Some(found) +} + +/// The top-left corner of a row of `size` beside the selected character's screen box `sel`, inside +/// `area`. The row goes under a character of horizontal type, or over it when there is no room +/// under it; beside a character of vertical type, on its right, or on its left when there is no +/// room on the right. A side is also given up when the row would cover `avoid` there (the +/// Contextual Task Bar). With no room on either side the row takes the first one, moved into +/// `area`. +fn place(sel: egui::Rect, size: Vec2, area: egui::Rect, vertical: bool, avoid: Option) -> Pos2 { + let sides = if vertical { + [pos2(sel.right() + GAP, sel.center().y - size.y / 2.0), pos2(sel.left() - GAP - size.x, sel.center().y - size.y / 2.0)] + } else { + [pos2(sel.center().x - size.x / 2.0, sel.bottom() + GAP), pos2(sel.center().x - size.x / 2.0, sel.top() - GAP - size.y)] + }; + let last = (area.max - size).max(area.min); + // Along the character's line the row may slide to stay on the canvas; across it, it may not. + let slide = |at: Pos2| if vertical { pos2(at.x, at.y.clamp(area.min.y, last.y)) } else { pos2(at.x.clamp(area.min.x, last.x), at.y) }; + let fits = |at: Pos2| { + let r = egui::Rect::from_min_size(at, size); + area.contains_rect(r) && avoid.is_none_or(|bar| !bar.intersects(r)) + }; + let sides = sides.map(slide); + sides.into_iter().find(|at| fits(*at)).unwrap_or_else(|| sides[0].clamp(area.min, last)) +} + +/// The row of alternate glyphs beside the Type tool's one selected character. +pub(super) fn alternates_bar(app: &mut VectorcraftApp, ui: &mut Ui, xf: &Xf) { + // Not while a press or a drag is choosing the selection: the row would follow the pointer. + if !app.session.prefs.show_character_alternates || !app.session.tool_wants_text() || app.session.tool_busy() { + return; + } + let Some(found) = glyphs::on_canvas(app, ui) else { return }; + let Some((quads, vertical)) = character_quads(app, ui, found.id, found.bytes) else { return }; + let corners: Vec = quads.iter().flatten().map(|p| xf.to_screen(*p)).collect(); + if corners.is_empty() { + return; + } + let sel = egui::Rect::from_points(&corners); + // A character scrolled off the canvas has no row. + if !sel.intersects(xf.rect) { + return; + } + let t = Tokens::get(ui.ctx()); + let count = found.count(); + let at = place(sel, bar_size(count, found.more), xf.rect.shrink(4.0), vertical, super::task_bar_rect(ui.ctx())); + let px = (CELL * ui.ctx().pixels_per_point()).round().clamp(8.0, 256.0) as u32; + let (mut pick, mut more) = (None, false); + egui::Area::new(bar_id()).order(egui::Order::Middle).fixed_pos(at).show(ui.ctx(), |ui| { + egui::Frame::NONE + .fill(t.panel) + .stroke(Stroke::new(1.0, t.border)) + .corner_radius(CornerRadius::same(4)) + .inner_margin(egui::Margin::same(PAD as i8 - 1)) + .shadow(egui::epaint::Shadow { offset: [0, 3], blur: 10, spread: 0, color: egui::Color32::from_black_alpha(70) }) + .show(ui, |ui| { + ui.horizontal(|ui| { + ui.spacing_mut().item_spacing = vec2(0.0, 0.0); + for i in 0..count { + let (rect, resp) = ui.allocate_exact_size(vec2(CELL, CELL), Sense::click()); + if resp.hovered() { + ui.painter().rect_filled(rect, 2, t.hover); + } + ui.painter().rect_stroke(rect.shrink(0.5), 0, Stroke::new(0.5, t.border), StrokeKind::Inside); + if let Some(tex) = found.texture(ui.ctx(), i, px) { + let uv = egui::Rect::from_min_max(pos2(0.0, 0.0), pos2(1.0, 1.0)); + ui.painter().image(tex.id(), rect, uv, t.text_strong); + } + let tip = found.tip(i); + resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, &tip)); + if resp.on_hover_text(&tip).clicked() { + pick = Some(i); + } + } + if found.more { + let (rect, resp) = ui.allocate_exact_size(vec2(MORE, CELL), Sense::click()); + if resp.hovered() { + ui.painter().rect_filled(rect, 2, t.hover); + } + let c = rect.center(); + let arrow = vec![pos2(c.x - 2.5, c.y - 5.0), pos2(c.x + 3.5, c.y), pos2(c.x - 2.5, c.y + 5.0)]; + ui.painter().add(Shape::convex_polygon(arrow, t.text_strong, Stroke::NONE)); + let tip = tl!("Show all alternates in the Glyphs panel"); + resp.widget_info(|| egui::WidgetInfo::labeled(egui::Role::Button, true, tip)); + more = resp.on_hover_text(tip).clicked(); + } + }); + }); + }); + if let Some(i) = pick { + found.pick(app, i); + } else if more { + glyphs::show_alternates(app, ui.ctx()); + } +} + +#[cfg(test)] +mod tests; diff --git a/crates/ui-egui/src/canvas/alternates_bar/tests.rs b/crates/ui-egui/src/canvas/alternates_bar/tests.rs new file mode 100644 index 0000000000..be2eb59f97 --- /dev/null +++ b/crates/ui-egui/src/canvas/alternates_bar/tests.rs @@ -0,0 +1,247 @@ +use egui::{Pos2, Rect}; +use serde_json::{Value, json}; +use vectorcraft_doc::{NodeId, NodeKind}; +use vectorcraft_text::FontDb; + +use super::*; + +/// The Type tool editing `text` in a new document. +fn editing(text: &str, vertical: bool) -> (VectorcraftApp, Value) { + let mut app = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default()); + app.run("file.new", json!({"width": 400, "height": 200})).unwrap(); + app.session.select_tool("type", Default::default()).unwrap(); + let id = app.run("text.create", json!({"x": 120, "y": 80, "text": text, "vertical": vertical})).unwrap()["id"].clone(); + app.session.apply_actions(vec![vectorcraft_tools::Action::Notify("text.editNew".into())]).unwrap(); + (app, id) +} + +fn select(app: &mut VectorcraftApp, start: usize, end: usize) { + app.session.set_tool_option("select", &json!({"start": start, "end": end})); +} + +const SCREEN: Rect = Rect::from_min_max(Pos2::ZERO, Pos2::new(800.0, 600.0)); + +/// One frame of the canvas with `events`; where the row of alternates is afterwards, if it shows. +fn frame(app: &mut VectorcraftApp, ctx: &egui::Context, events: Vec) -> Option { + egui_extras::install_image_loaders(ctx); + let raw = egui::RawInput { screen_rect: Some(SCREEN), events, ..Default::default() }; + let mut out = ctx.run_ui(raw, |ui| super::super::show(app, ui)); + out.textures_delta.clear(); + bar_rect(ctx) +} + +/// The glyphs the row offers and whether it has the arrow, as the canvas finds them. +fn offered(app: &mut VectorcraftApp, ctx: &egui::Context) -> Option<(Vec, bool)> { + let mut found = None; + let raw = egui::RawInput { screen_rect: Some(SCREEN), ..Default::default() }; + let mut out = ctx.run_ui(raw, |ui| { + found = glyphs::on_canvas(app, ui).map(|f| ((0..f.count()).filter_map(|i| f.gid(i)).collect(), f.more)); + }); + out.textures_delta.clear(); + found +} + +/// A click at `at`, with a frame before it that lays the row out and moves the pointer there. +fn click(app: &mut VectorcraftApp, ctx: &egui::Context, at: Pos2) { + let button = |pressed| egui::Event::PointerButton { pos: at, button: egui::PointerButton::Primary, pressed, modifiers: Default::default() }; + frame(app, ctx, vec![egui::Event::PointerMoved(at)]); + frame(app, ctx, vec![button(true), button(false)]); +} + +/// The center of cell `i` of the row at `bar`. +fn cell(bar: Rect, i: usize) -> Pos2 { + bar.min + vec2(PAD + CELL * (i as f32 + 0.5), PAD + CELL / 2.0) +} + +/// The center of the arrow of a row of `count` cells at `bar`. +fn arrow(bar: Rect, count: usize) -> Pos2 { + bar.min + vec2(PAD + CELL * count as f32 + MORE / 2.0, PAD + CELL / 2.0) +} + +fn glyph_ids(app: &VectorcraftApp, id: &Value) -> Vec { + let NodeKind::Text(text) = &app.session.active().unwrap().doc.node(NodeId(id.as_u64().unwrap())).unwrap().kind else { panic!("text") }; + let mut glyphs = vectorcraft_text::layout(FontDb::global(), text).glyphs; + glyphs.sort_by_key(|g| g.byte); + glyphs.iter().map(|g| g.gid).collect() +} + +fn undo_steps(app: &VectorcraftApp) -> usize { + app.session.active().unwrap().history.undo.len() +} + +/// The screen box of bytes `a..b` of the text, as the canvas of [`frame`] draws it. +fn character_box(app: &VectorcraftApp, id: &Value, a: usize, b: usize) -> Rect { + let NodeKind::Text(text) = &app.session.active().unwrap().doc.node(NodeId(id.as_u64().unwrap())).unwrap().kind else { panic!("text") }; + let lay = vectorcraft_text::layout(FontDb::global(), text); + let xf = app.canvas_xf().expect("a frame was drawn"); + let corners: Vec = vectorcraft_text::selection_quads(&lay, a, b).iter().flatten().map(|p| xf.to_screen(text.xf * *p)).collect(); + Rect::from_points(&corners) +} + +/// With the Type tool and exactly one character selected, the canvas shows up to five of that +/// character's alternates beside it, without the glyph it is drawn with, and an arrow when it has +/// more. A click draws the character with the glyph as one undo step and keeps the text. A caret, +/// a longer selection, a character without alternates, another tool and the preference turned off +/// each show no row. +#[test] +fn one_selected_character_shows_its_alternates_and_a_click_replaces_its_glyph() { + use vectorcraft_text::test_fonts::{alternates_family, alternates_font, unencoded_glyphs}; + FontDb::global().add_font(alternates_font(true).expect("the test font builds")); + let [u1, ..] = unencoded_glyphs().expect("unencoded glyphs"); + let (mut app, id) = editing("xay", false); + app.run("text.setRangeStyle", json!({"id": id, "font": alternates_family(true)})).unwrap(); + let ctx = egui::Context::default(); + let drawn = glyph_ids(&app, &id); + + select(&mut app, 1, 1); + assert_eq!(frame(&mut app, &ctx, vec![]), None, "a caret selects no character"); + select(&mut app, 0, 2); + assert_eq!(frame(&mut app, &ctx, vec![]), None, "two characters are selected"); + select(&mut app, 0, 1); + assert_eq!(frame(&mut app, &ctx, vec![]), None, "x has no alternates"); + + select(&mut app, 1, 2); + let bar = frame(&mut app, &ctx, vec![]).expect("the a has alternates"); + let task_bar = super::super::task_bar_rect(&ctx).expect("the task bar shows for type being edited"); + let (gids, more) = offered(&mut app, &ctx).expect("offered"); + assert_eq!(gids.len(), glyphs::ON_CANVAS_ALTERNATES, "five of the six alternates"); + assert!(more, "the sixth is in the Glyphs panel"); + assert_eq!(gids[0], u1); + assert!(!gids.contains(&drawn[1]), "the glyph the a is drawn with is not offered"); + assert_eq!(bar.size(), bar_size(5, true), "five cells and the arrow"); + // The Contextual Task Bar is under the type, so the row goes over the character. + // The row is placed on whole pixels, so it is within one of where the character's box puts it. + let sel = character_box(&app, &id, 1, 2); + assert!(task_bar.intersects(Rect::from_min_size(pos2(bar.left(), sel.bottom() + GAP), bar.size())), "{task_bar:?} {sel:?}"); + assert!((sel.top() - bar.bottom() - GAP).abs() < 1.0, "over the character: {bar:?} {sel:?}"); + assert!((bar.center().x - sel.center().x).abs() < 1.0, "centered on it"); + // Without the task bar the row is under the character. + app.ui.task_bar = false; + frame(&mut app, &ctx, vec![]); + let under = frame(&mut app, &ctx, vec![]).expect("the row"); + assert!((under.top() - sel.bottom() - GAP).abs() < 1.0, "under the character: {under:?} {sel:?}"); + app.ui.task_bar = true; + frame(&mut app, &ctx, vec![]); + assert_eq!(frame(&mut app, &ctx, vec![]), Some(bar)); + + // A click on the first glyph. + let undo = undo_steps(&app); + click(&mut app, &ctx, cell(bar, 0)); + assert_eq!(undo_steps(&app), undo + 1, "one undo step; status: {}", app.ui.status); + assert_eq!(glyph_ids(&app, &id), [drawn[0], u1, drawn[2]], "the a is drawn with the glyph"); + let NodeKind::Text(text) = &app.session.active().unwrap().doc.node(NodeId(id.as_u64().unwrap())).unwrap().kind else { panic!("text") }; + assert_eq!(text.plain_text(), "xay", "the text keeps its characters"); + + // The row stays for the same character: its default glyph first, the drawn glyph left out. + let bar = frame(&mut app, &ctx, vec![]).expect("still one character selected"); + let (gids, _) = offered(&mut app, &ctx).expect("offered"); + assert_eq!(gids[0], drawn[1], "the default glyph comes first"); + assert!(!gids.contains(&u1)); + click(&mut app, &ctx, cell(bar, 0)); + assert_eq!(undo_steps(&app), undo + 2); + assert_eq!(glyph_ids(&app, &id), drawn, "the default glyph again"); + app.run("edit.undo", json!({})).unwrap(); + assert_eq!(glyph_ids(&app, &id), [drawn[0], u1, drawn[2]], "undo takes one pick back"); + app.run("edit.undo", json!({})).unwrap(); + assert_eq!(glyph_ids(&app, &id), drawn); + + // Preferences › Type › Show Character Alternates. + select(&mut app, 1, 2); + assert!(frame(&mut app, &ctx, vec![]).is_some()); + app.run("prefs.set", json!({"key": "showCharacterAlternates", "value": false})).unwrap(); + assert_eq!(frame(&mut app, &ctx, vec![]), None, "the preference is off"); + app.run("prefs.set", json!({"key": "showCharacterAlternates", "value": true})).unwrap(); + assert!(frame(&mut app, &ctx, vec![]).is_some()); + + app.session.select_tool("select", Default::default()).unwrap(); + assert_eq!(frame(&mut app, &ctx, vec![]), None, "the Selection tool edits no text"); +} + +/// The arrow at the end of the row opens the Glyphs panel on Show › Alternates for Current +/// Selection and changes nothing in the document. +#[test] +fn the_arrow_opens_the_glyphs_panel_on_the_alternates() { + use vectorcraft_text::test_fonts::{alternates_family, alternates_font}; + FontDb::global().add_font(alternates_font(true).expect("the test font builds")); + let (mut app, id) = editing("xay", false); + app.run("text.setRangeStyle", json!({"id": id, "font": alternates_family(true)})).unwrap(); + select(&mut app, 1, 2); + let ctx = egui::Context::default(); + app.run("window.panel.close", json!({"panel": "glyphs"})).ok(); + assert!(!crate::panel_docking::visible(&app.ui, "glyphs")); + let bar = frame(&mut app, &ctx, vec![]).expect("the row"); + let (undo, drawn) = (undo_steps(&app), glyph_ids(&app, &id)); + click(&mut app, &ctx, arrow(bar, 5)); + assert!(crate::panel_docking::visible(&app.ui, "glyphs"), "status: {}", app.ui.status); + // The Show menu's entry after its seven character subsets. + assert_eq!(crate::panels::pstate::(&ctx, "gl-subset"), 7); + assert_eq!(undo_steps(&app), undo); + assert_eq!(glyph_ids(&app, &id), drawn); +} + +/// A medial letter of a joined Arabic word gets the alternates of its medial form, and a click +/// applies the feature that draws one to that letter alone: its neighbors keep their joining +/// forms. Three alternates need no arrow. +#[test] +fn a_letter_of_a_joined_word_keeps_the_word_joined() { + use vectorcraft_text::test_fonts::{JOINING_ALTERNATES_FAMILY, joining_alternates_font, source_sans_gid}; + FontDb::global().add_font(joining_alternates_font().expect("the test font builds")); + let g = |c| u32::from(source_sans_gid(c).expect("a glyph")); + let (mut app, id) = editing("\u{628}\u{62A}\u{646}", false); + app.run("text.setRangeStyle", json!({"id": id, "font": JOINING_ALTERNATES_FAMILY})).unwrap(); + assert_eq!(glyph_ids(&app, &id), [g('B'), g('u'), g('M')]); + select(&mut app, 2, 4); + let ctx = egui::Context::default(); + let bar = frame(&mut app, &ctx, vec![]).expect("the medial teh has alternates"); + let (gids, more) = offered(&mut app, &ctx).expect("offered"); + assert_eq!((gids.len(), more), (3, false), "the medial form's three alternates: {gids:?}"); + assert_eq!(bar.size(), bar_size(3, false), "three cells and no arrow"); + let at = gids.iter().position(|gid| *gid == g('x')).expect("the alternate salt=2 draws"); + let undo = undo_steps(&app); + click(&mut app, &ctx, cell(bar, at)); + assert_eq!(undo_steps(&app), undo + 1, "status: {}", app.ui.status); + assert_eq!(glyph_ids(&app, &id), [g('B'), g('x'), g('M')], "the neighbors keep their initial and final forms"); + let NodeKind::Text(text) = &app.session.active().unwrap().doc.node(NodeId(id.as_u64().unwrap())).unwrap().kind else { panic!("text") }; + assert_eq!(text.plain_text(), "\u{628}\u{62A}\u{646}"); + assert!(text.runs.iter().all(|r| r.style.glyph_alternate.is_none()), "a feature draws it, not a glyph choice"); +} + +/// The row stands on the right of a character of vertical type, level with it. +#[test] +fn the_row_stands_beside_a_character_of_vertical_type() { + use vectorcraft_text::test_fonts::{alternates_family, alternates_font}; + FontDb::global().add_font(alternates_font(true).expect("the test font builds")); + let (mut app, id) = editing("xay", true); + app.run("text.setRangeStyle", json!({"id": id, "font": alternates_family(true)})).unwrap(); + select(&mut app, 1, 2); + let ctx = egui::Context::default(); + app.ui.task_bar = false; + // The first frame fits the view to the canvas. + frame(&mut app, &ctx, vec![]); + let bar = frame(&mut app, &ctx, vec![]).expect("the row"); + let sel = character_box(&app, &id, 1, 2); + assert!((bar.left() - sel.right() - GAP).abs() < 1.0, "on the right of the character: {bar:?} {sel:?}"); + assert!((bar.center().y - sel.center().y).abs() < 1.0, "level with it"); +} + +/// [`place`]: under the character, over it when the canvas ends below or the task bar is there, +/// on the right or left of vertical type, and inside the canvas when neither side has room. +#[test] +fn the_row_is_placed_on_the_side_that_has_room() { + let area = Rect::from_min_max(pos2(0.0, 0.0), pos2(400.0, 300.0)); + let size = vec2(100.0, 34.0); + let sel = Rect::from_min_max(pos2(190.0, 100.0), pos2(210.0, 130.0)); + assert_eq!(place(sel, size, area, false, None), pos2(150.0, 136.0), "under"); + let low = sel.translate(vec2(0.0, 150.0)); + assert_eq!(place(low, size, area, false, None), pos2(150.0, 210.0), "over: no room under"); + let task_bar = Rect::from_min_max(pos2(100.0, 140.0), pos2(300.0, 180.0)); + assert_eq!(place(sel, size, area, false, Some(task_bar)), pos2(150.0, 60.0), "over: the task bar is under"); + let edge = sel.translate(vec2(-185.0, 0.0)); + assert_eq!(place(edge, size, area, false, None), pos2(0.0, 136.0), "moved along the line to stay on the canvas"); + assert_eq!(place(sel, size, area, true, None), pos2(216.0, 98.0), "vertical type: on the right"); + let right = sel.translate(vec2(150.0, 0.0)); + assert_eq!(place(right, size, area, true, None), pos2(234.0, 98.0), "vertical type: on the left"); + let small = Rect::from_min_max(pos2(0.0, 0.0), pos2(400.0, 60.0)); + let inside = Rect::from_min_max(pos2(190.0, 20.0), pos2(210.0, 40.0)); + assert_eq!(place(inside, size, small, false, None), pos2(150.0, 26.0), "no room on either side: kept inside the canvas"); +} diff --git a/crates/ui-egui/src/i18n/bg.tsv b/crates/ui-egui/src/i18n/bg.tsv index 28944173dd..28e1f4b523 100644 --- a/crates/ui-egui/src/i18n/bg.tsv +++ b/crates/ui-egui/src/i18n/bg.tsv @@ -889,6 +889,9 @@ font-technology Variable Променливи The selected character's font is not available. Шрифтът на избрания знак не е наличен. No alternates for the selected character. Няма алтернативи за избрания знак. Double-click to replace Двойно щракване за замяна + Click to replace Щракване за замяна + Show all alternates in the Glyphs panel Показване на всички алтернативи в панела „Глифи“ + Show Character Alternates Показване на алтернативите на знака Select one character with the Type tool first. Първо изберете един знак с инструмента „Текст“. {glyph} is a glyph of {font}, not of the font the selected text is set in ({text_font}). {glyph} е глиф на {font}, а не на шрифта, с който е зададен избраният текст ({text_font}). {glyph} belongs to {base} (U+{base_code}) {place}; your selection is {sel} (U+{code}). {glyph} принадлежи на {base} (U+{base_code}) {place}; избраният от вас знак е {sel} (U+{code}). diff --git a/crates/ui-egui/src/i18n/bs.tsv b/crates/ui-egui/src/i18n/bs.tsv index cdaebd3bf1..7cf9916fd0 100644 --- a/crates/ui-egui/src/i18n/bs.tsv +++ b/crates/ui-egui/src/i18n/bs.tsv @@ -1701,6 +1701,9 @@ digits Malayalam Malajalam The selected character's font is not available. Font odabranog znaka nije dostupan. No alternates for the selected character. Nema alternativa za odabrani znak. Double-click to replace Dvostruki klik za zamjenu + Click to replace Klik za zamjenu + Show all alternates in the Glyphs panel Prikaži sve alternative u panelu Glifovi + Show Character Alternates Prikaži alternative znaka Select one character with the Type tool first. Prvo odaberite jedan znak alatom za tekst. {glyph} is a glyph of {font}, not of the font the selected text is set in ({text_font}). {glyph} je glif fonta {font}, a ne fonta kojim je postavljen odabrani tekst ({text_font}). {glyph} belongs to {base} (U+{base_code}) {place}; your selection is {sel} (U+{code}). {glyph} pripada znaku {base} (U+{base_code}) {place}; vaš odabir je {sel} (U+{code}). diff --git a/crates/ui-egui/src/i18n/ca.tsv b/crates/ui-egui/src/i18n/ca.tsv index 3f631efd9c..c9f18653b4 100644 --- a/crates/ui-egui/src/i18n/ca.tsv +++ b/crates/ui-egui/src/i18n/ca.tsv @@ -1307,6 +1307,9 @@ antiAlias Sharp Nítid The selected character's font is not available. El tipus de lletra del caràcter seleccionat no està disponible. No alternates for the selected character. No hi ha alternatives per al caràcter seleccionat. Double-click to replace Feu doble clic per substituir + Click to replace Feu clic per substituir + Show all alternates in the Glyphs panel Mostra totes les alternatives al tauler Glifs + Show Character Alternates Mostra les alternatives del caràcter Select one character with the Type tool first. Selecciona primer un caràcter amb l’eina de text. {glyph} is a glyph of {font}, not of the font the selected text is set in ({text_font}). {glyph} és un glif de {font}, no del tipus de lletra amb què està escrit el text seleccionat ({text_font}). {glyph} belongs to {base} (U+{base_code}) {place}; your selection is {sel} (U+{code}). {glyph} pertany a {base} (U+{base_code}) {place}; la teva selecció és {sel} (U+{code}). diff --git a/crates/ui-egui/src/i18n/cs.tsv b/crates/ui-egui/src/i18n/cs.tsv index 71e2c0afc0..1ffc7925ff 100644 --- a/crates/ui-egui/src/i18n/cs.tsv +++ b/crates/ui-egui/src/i18n/cs.tsv @@ -1730,6 +1730,9 @@ digits Malayalam Malajálamské The selected character's font is not available. Písmo vybraného znaku není k dispozici. No alternates for the selected character. Vybraný znak nemá žádné alternativy. Double-click to replace Dvojklikem nahradíte + Click to replace Kliknutím nahradíte + Show all alternates in the Glyphs panel Zobrazit všechny alternativy v panelu Glyfy + Show Character Alternates Zobrazovat alternativy znaku Select one character with the Type tool first. Nejprve nástrojem Text vyberte jeden znak. {glyph} is a glyph of {font}, not of the font the selected text is set in ({text_font}). {glyph} je glyf písma {font}, ne písma, kterým je vybraný text nastaven ({text_font}). {glyph} belongs to {base} (U+{base_code}) {place}; your selection is {sel} (U+{code}). {glyph} patří znaku {base} (U+{base_code}) {place}; váš výběr je {sel} (U+{code}). diff --git a/crates/ui-egui/src/i18n/da.tsv b/crates/ui-egui/src/i18n/da.tsv index b3de0ae89a..043fb6d76e 100644 --- a/crates/ui-egui/src/i18n/da.tsv +++ b/crates/ui-egui/src/i18n/da.tsv @@ -1636,6 +1636,9 @@ digits Malayalam Malayalam The selected character's font is not available. Skrifttypen for det markerede tegn er ikke tilgængelig. No alternates for the selected character. Ingen alternativer til det markerede tegn. Double-click to replace Dobbeltklik for at erstatte + Click to replace Klik for at erstatte + Show all alternates in the Glyphs panel Vis alle alternativer i Glyffer-panelet + Show Character Alternates Vis tegnalternativer Select one character with the Type tool first. Markér først ét tegn med tekstværktøjet. {glyph} is a glyph of {font}, not of the font the selected text is set in ({text_font}). {glyph} er et glyf i {font}, ikke i den skrifttype, den markerede tekst er sat i ({text_font}). {glyph} belongs to {base} (U+{base_code}) {place}; your selection is {sel} (U+{code}). {glyph} hører til {base} (U+{base_code}) {place}; din markering er {sel} (U+{code}). diff --git a/crates/ui-egui/src/i18n/de.tsv b/crates/ui-egui/src/i18n/de.tsv index 3a97954267..a9a96c0ce7 100644 --- a/crates/ui-egui/src/i18n/de.tsv +++ b/crates/ui-egui/src/i18n/de.tsv @@ -891,6 +891,9 @@ font-technology Variable Variabel The selected character's font is not available. Die Schrift des ausgewählten Zeichens ist nicht verfügbar. No alternates for the selected character. Keine Alternativen für das ausgewählte Zeichen. Double-click to replace Doppelklicken zum Ersetzen + Click to replace Klicken zum Ersetzen + Show all alternates in the Glyphs panel Alle Alternativen im Bedienfeld „Glyphen“ anzeigen + Show Character Alternates Zeichenalternativen anzeigen Select one character with the Type tool first. Wählen Sie zuerst mit dem Text-Werkzeug ein Zeichen aus. {glyph} is a glyph of {font}, not of the font the selected text is set in ({text_font}). {glyph} ist eine Glyphe von {font}, nicht der Schrift, in der der ausgewählte Text gesetzt ist ({text_font}). {glyph} belongs to {base} (U+{base_code}) {place}; your selection is {sel} (U+{code}). {glyph} gehört zu {base} (U+{base_code}) {place}; Ihre Auswahl ist {sel} (U+{code}). diff --git a/crates/ui-egui/src/i18n/el.tsv b/crates/ui-egui/src/i18n/el.tsv index 78e8285c1b..90b7a0b4e5 100644 --- a/crates/ui-egui/src/i18n/el.tsv +++ b/crates/ui-egui/src/i18n/el.tsv @@ -862,6 +862,9 @@ font-technology Variable Μεταβλητές The selected character's font is not available. Η γραμματοσειρά του επιλεγμένου χαρακτήρα δεν είναι διαθέσιμη. No alternates for the selected character. Δεν υπάρχουν εναλλακτικοί για τον επιλεγμένο χαρακτήρα. Double-click to replace Διπλό κλικ για αντικατάσταση + Click to replace Κλικ για αντικατάσταση + Show all alternates in the Glyphs panel Εμφάνιση όλων των εναλλακτικών στον πίνακα Γλύφοι + Show Character Alternates Εμφάνιση εναλλακτικών χαρακτήρα Select one character with the Type tool first. Επιλέξτε πρώτα έναν χαρακτήρα με το εργαλείο κειμένου. {glyph} is a glyph of {font}, not of the font the selected text is set in ({text_font}). Ο γλύφος {glyph} ανήκει στη γραμματοσειρά {font}, όχι σε αυτήν του επιλεγμένου κειμένου ({text_font}). {glyph} belongs to {base} (U+{base_code}) {place}; your selection is {sel} (U+{code}). Ο γλύφος {glyph} ανήκει στο {base} (U+{base_code}) {place}· η επιλογή σας είναι {sel} (U+{code}). diff --git a/crates/ui-egui/src/i18n/es.tsv b/crates/ui-egui/src/i18n/es.tsv index 9132b8ea30..5fcc1a2f5c 100644 --- a/crates/ui-egui/src/i18n/es.tsv +++ b/crates/ui-egui/src/i18n/es.tsv @@ -904,6 +904,9 @@ font-technology Variable Variables The selected character's font is not available. La fuente del carácter seleccionado no está disponible. No alternates for the selected character. No hay alternativas para el carácter seleccionado. Double-click to replace Doble clic para reemplazar + Click to replace Clic para reemplazar + Show all alternates in the Glyphs panel Mostrar todas las alternativas en el panel Glifos + Show Character Alternates Mostrar alternativas de carácter Select one character with the Type tool first. Primero selecciona un carácter con la herramienta Texto. {glyph} is a glyph of {font}, not of the font the selected text is set in ({text_font}). {glyph} es un glifo de {font}, no de la fuente del texto seleccionado ({text_font}). {glyph} belongs to {base} (U+{base_code}) {place}; your selection is {sel} (U+{code}). {glyph} pertenece a {base} (U+{base_code}) {place}; tu selección es {sel} (U+{code}). diff --git a/crates/ui-egui/src/i18n/fi.tsv b/crates/ui-egui/src/i18n/fi.tsv index f847330a6d..8b2b690c80 100644 --- a/crates/ui-egui/src/i18n/fi.tsv +++ b/crates/ui-egui/src/i18n/fi.tsv @@ -1703,6 +1703,9 @@ digits Malayalam Malajalam The selected character's font is not available. Valitun merkin fontti ei ole saatavilla. No alternates for the selected character. Valitulla merkillä ei ole vaihtoehtoja. Double-click to replace Korvaa kaksoisnapsauttamalla + Click to replace Korvaa napsauttamalla + Show all alternates in the Glyphs panel Näytä kaikki vaihtoehdot Merkit-paneelissa + Show Character Alternates Näytä merkin vaihtoehdot Select one character with the Type tool first. Valitse ensin yksi merkki tekstityökalulla. {glyph} is a glyph of {font}, not of the font the selected text is set in ({text_font}). {glyph} on fontin {font} glyfi, ei valitun tekstin fontin ({text_font}). {glyph} belongs to {base} (U+{base_code}) {place}; your selection is {sel} (U+{code}). {glyph} kuuluu merkille {base} (U+{base_code}) {place}; valintasi on {sel} (U+{code}). diff --git a/crates/ui-egui/src/i18n/fr.tsv b/crates/ui-egui/src/i18n/fr.tsv index a32307544b..fd43bff358 100644 --- a/crates/ui-egui/src/i18n/fr.tsv +++ b/crates/ui-egui/src/i18n/fr.tsv @@ -907,6 +907,9 @@ font-technology Variable Variables The selected character's font is not available. La police du caractère sélectionné n’est pas disponible. No alternates for the selected character. Aucune variante pour le caractère sélectionné. Double-click to replace Double-cliquez pour remplacer + Click to replace Cliquez pour remplacer + Show all alternates in the Glyphs panel Afficher toutes les variantes dans le panneau Glyphes + Show Character Alternates Afficher les variantes de caractère Select one character with the Type tool first. Sélectionnez d’abord un caractère avec l’outil Texte. {glyph} is a glyph of {font}, not of the font the selected text is set in ({text_font}). {glyph} est un glyphe de {font}, et non de la police du texte sélectionné ({text_font}). {glyph} belongs to {base} (U+{base_code}) {place}; your selection is {sel} (U+{code}). {glyph} appartient à {base} (U+{base_code}) {place} ; votre sélection est {sel} (U+{code}). diff --git a/crates/ui-egui/src/i18n/gu.tsv b/crates/ui-egui/src/i18n/gu.tsv index 716d851655..ab1ae3d432 100644 --- a/crates/ui-egui/src/i18n/gu.tsv +++ b/crates/ui-egui/src/i18n/gu.tsv @@ -1046,6 +1046,9 @@ Double-click to insert દાખલ કરવા ડબલ-ક્લિક કરો Double-click to rename નામ બદલવા ડબલ-ક્લિક કરો Double-click to replace બદલવા ડબલ-ક્લિક કરો + Click to replace બદલવા ક્લિક કરો + Show all alternates in the Glyphs panel ગ્લિફ્સ પેનલમાં બધા વૈકલ્પિક બતાવો + Show Character Alternates અક્ષરના વૈકલ્પિક બતાવો Double-click to target the contents સામગ્રી લક્ષ્ય બનાવવા ડબલ-ક્લિક કરો Download: ડાઉનલોડ: Drag ખેંચો diff --git a/crates/ui-egui/src/i18n/hi.tsv b/crates/ui-egui/src/i18n/hi.tsv index a80622e738..bef5689fa5 100644 --- a/crates/ui-egui/src/i18n/hi.tsv +++ b/crates/ui-egui/src/i18n/hi.tsv @@ -1463,6 +1463,9 @@ antiAlias Sharp शार्प Double-click to insert सम्मिलित करने के लिए डबल क्लिक करें Double-click to rename नाम बदलने के लिए डबल क्लिक करें Double-click to replace प्रतिस्थापित करने के लिए डबल क्लिक करें + Click to replace प्रतिस्थापित करने के लिए क्लिक करें + Show all alternates in the Glyphs panel ग्लिफ़्स पैनल में सभी ऑप्शन दिखाएँ + Show Character Alternates कैरेक्टर के ऑप्शन दिखाएँ Select one character with the Type tool first. पहले टाइप टूल से एक कैरेक्टर सिलेक्ट करें। {glyph} is a glyph of {font}, not of the font the selected text is set in ({text_font}). {glyph} {font} का ग्लिफ़ है, उस फ़ॉन्ट का नहीं जिसमें सिलेक्ट किया टेक्स्ट सेट है ({text_font})। {glyph} belongs to {base} (U+{base_code}) {place}; your selection is {sel} (U+{code}). {glyph} {base} (U+{base_code}) का है, {place}; आपका सिलेक्शन {sel} (U+{code}) है। diff --git a/crates/ui-egui/src/i18n/hu.tsv b/crates/ui-egui/src/i18n/hu.tsv index d12f4f0446..8f531b4d8a 100644 --- a/crates/ui-egui/src/i18n/hu.tsv +++ b/crates/ui-egui/src/i18n/hu.tsv @@ -1172,6 +1172,9 @@ font-technology Variable Változó The selected character's font is not available. A kijelölt karakter betűtípusa nem érhető el. No alternates for the selected character. A kijelölt karakternek nincsenek változatai. Double-click to replace Csere dupla kattintással + Click to replace Csere kattintással + Show all alternates in the Glyphs panel Minden változat megjelenítése a Glifák panelen + Show Character Alternates Karakterváltozatok megjelenítése Select one character with the Type tool first. Előbb jelölj ki egy karaktert a Szöveg eszközzel. {glyph} is a glyph of {font}, not of the font the selected text is set in ({text_font}). A(z) {glyph} a(z) {font} glifája, nem a kijelölt szöveg betűtípusáé ({text_font}). {glyph} belongs to {base} (U+{base_code}) {place}; your selection is {sel} (U+{code}). A(z) {glyph} ehhez tartozik: {base} (U+{base_code}) {place}; a te kijelölésed: {sel} (U+{code}). diff --git a/crates/ui-egui/src/i18n/id.tsv b/crates/ui-egui/src/i18n/id.tsv index c7c6b956db..8abbc41b00 100644 --- a/crates/ui-egui/src/i18n/id.tsv +++ b/crates/ui-egui/src/i18n/id.tsv @@ -1636,6 +1636,9 @@ digits Malayalam Malayalam The selected character's font is not available. Font karakter yang dipilih tidak tersedia. No alternates for the selected character. Tidak ada alternatif untuk karakter yang dipilih. Double-click to replace Klik ganda untuk mengganti + Click to replace Klik untuk mengganti + Show all alternates in the Glyphs panel Tampilkan semua alternatif di panel Glif + Show Character Alternates Tampilkan alternatif karakter Select one character with the Type tool first. Pilih satu karakter dengan alat teks terlebih dahulu. {glyph} is a glyph of {font}, not of the font the selected text is set in ({text_font}). {glyph} adalah glif {font}, bukan dari font yang dipakai teks terpilih ({text_font}). {glyph} belongs to {base} (U+{base_code}) {place}; your selection is {sel} (U+{code}). {glyph} milik {base} (U+{base_code}) {place}; pilihan Anda adalah {sel} (U+{code}). diff --git a/crates/ui-egui/src/i18n/it.tsv b/crates/ui-egui/src/i18n/it.tsv index 9f6e83db25..ef6e76cce3 100644 --- a/crates/ui-egui/src/i18n/it.tsv +++ b/crates/ui-egui/src/i18n/it.tsv @@ -912,6 +912,9 @@ font-technology Variable Variabili The selected character's font is not available. Il font del carattere selezionato non è disponibile. No alternates for the selected character. Nessuna alternativa per il carattere selezionato. Double-click to replace Fai doppio clic per sostituire + Click to replace Fai clic per sostituire + Show all alternates in the Glyphs panel Mostra tutte le alternative nel pannello Glifi + Show Character Alternates Mostra alternative carattere Select one character with the Type tool first. Seleziona prima un carattere con lo strumento Testo. {glyph} is a glyph of {font}, not of the font the selected text is set in ({text_font}). {glyph} è un glifo di {font}, non del font del testo selezionato ({text_font}). {glyph} belongs to {base} (U+{base_code}) {place}; your selection is {sel} (U+{code}). {glyph} appartiene a {base} (U+{base_code}) {place}; la tua selezione è {sel} (U+{code}). diff --git a/crates/ui-egui/src/i18n/ja.tsv b/crates/ui-egui/src/i18n/ja.tsv index 442f46e398..4043f024f8 100644 --- a/crates/ui-egui/src/i18n/ja.tsv +++ b/crates/ui-egui/src/i18n/ja.tsv @@ -1557,6 +1557,9 @@ antiAlias Sharp シャープ The selected character's font is not available. 選択した文字のフォントを使用できません。 No alternates for the selected character. 選択した文字には異体字がありません。 Double-click to replace ダブルクリックで置換 + Click to replace クリックで置換 + Show all alternates in the Glyphs panel 字形パネルですべての異体字を表示 + Show Character Alternates 文字の異体字を表示 Select one character with the Type tool first. 先に文字ツールで文字を 1 つ選択してください。 {glyph} is a glyph of {font}, not of the font the selected text is set in ({text_font}). {glyph} は {font} のグリフで、選択したテキストのフォント ({text_font}) のものではありません。 {glyph} belongs to {base} (U+{base_code}) {place}; your selection is {sel} (U+{code}). {glyph} は {base} (U+{base_code}) の{place}のグリフです。選択中の文字は {sel} (U+{code}) です。 diff --git a/crates/ui-egui/src/i18n/ka.tsv b/crates/ui-egui/src/i18n/ka.tsv index e60b5bb753..9b46dcd7ec 100644 --- a/crates/ui-egui/src/i18n/ka.tsv +++ b/crates/ui-egui/src/i18n/ka.tsv @@ -1574,6 +1574,9 @@ digits Malayalam მალაიალამური The selected character's font is not available. მონიშნული სიმბოლოს შრიფტი მიუწვდომელია. No alternates for the selected character. მონიშნულ სიმბოლოს ალტერნატივები არ აქვს. Double-click to replace ორჯერ დაწკაპეთ ჩასანაცვლებლად + Click to replace დაწკაპეთ ჩასანაცვლებლად + Show all alternates in the Glyphs panel ყველა ალტერნატივის ჩვენება გლიფების პანელში + Show Character Alternates სიმბოლოს ალტერნატივების ჩვენება Select one character with the Type tool first. ჯერ ტექსტის ხელსაწყოთი მონიშნეთ ერთი სიმბოლო. {glyph} is a glyph of {font}, not of the font the selected text is set in ({text_font}). {glyph} არის {font}-ის გლიფი და არა იმ შრიფტისა, რომლითაც მონიშნული ტექსტია დაწყობილი ({text_font}). {glyph} belongs to {base} (U+{base_code}) {place}; your selection is {sel} (U+{code}). {glyph} ეკუთვნის სიმბოლოს {base} (U+{base_code}) {place}; თქვენი მონიშნულია {sel} (U+{code}). diff --git a/crates/ui-egui/src/i18n/ko.tsv b/crates/ui-egui/src/i18n/ko.tsv index edba9d6cc1..34a56686fa 100644 --- a/crates/ui-egui/src/i18n/ko.tsv +++ b/crates/ui-egui/src/i18n/ko.tsv @@ -1547,6 +1547,9 @@ antiAlias Sharp 날카롭게 The selected character's font is not available. 선택한 문자의 글꼴을 사용할 수 없습니다. No alternates for the selected character. 선택한 문자의 대체 글리프가 없습니다. Double-click to replace 더블클릭하여 바꾸기 + Click to replace 클릭하여 바꾸기 + Show all alternates in the Glyphs panel 글리프 패널에서 모든 대체 글리프 표시 + Show Character Alternates 문자의 대체 글리프 표시 Select one character with the Type tool first. 먼저 문자 도구로 문자 하나를 선택하세요. {glyph} is a glyph of {font}, not of the font the selected text is set in ({text_font}). {glyph}은(는) {font}의 글리프이며, 선택한 텍스트의 글꼴({text_font})의 것이 아닙니다. {glyph} belongs to {base} (U+{base_code}) {place}; your selection is {sel} (U+{code}). {glyph}은(는) {base}(U+{base_code})의 {place} 글리프입니다. 선택한 문자는 {sel}(U+{code})입니다. diff --git a/crates/ui-egui/src/i18n/nl.tsv b/crates/ui-egui/src/i18n/nl.tsv index af98d0816e..67cfffd967 100644 --- a/crates/ui-egui/src/i18n/nl.tsv +++ b/crates/ui-egui/src/i18n/nl.tsv @@ -2154,6 +2154,9 @@ digits Malayalam Malayalam The selected character's font is not available. Het lettertype van het geselecteerde teken is niet beschikbaar. No alternates for the selected character. Geen alternatieven voor het geselecteerde teken. Double-click to replace Dubbelklik om te vervangen + Click to replace Klik om te vervangen + Show all alternates in the Glyphs panel Alle alternatieven tonen in het paneel Glyphs + Show Character Alternates Tekenalternatieven tonen Select one character with the Type tool first. Selecteer eerst één teken met het Type-gereedschap. {glyph} is a glyph of {font}, not of the font the selected text is set in ({text_font}). {glyph} is een glyph van {font}, niet van het lettertype van de geselecteerde tekst ({text_font}). {glyph} belongs to {base} (U+{base_code}) {place}; your selection is {sel} (U+{code}). {glyph} hoort bij {base} (U+{base_code}) {place}; uw selectie is {sel} (U+{code}). diff --git a/crates/ui-egui/src/i18n/pl.tsv b/crates/ui-egui/src/i18n/pl.tsv index cde8490eeb..191d0146fa 100644 --- a/crates/ui-egui/src/i18n/pl.tsv +++ b/crates/ui-egui/src/i18n/pl.tsv @@ -1642,6 +1642,9 @@ digits Malayalam Malajalam The selected character's font is not available. Czcionka zaznaczonego znaku jest niedostępna. No alternates for the selected character. Brak alternatyw dla zaznaczonego znaku. Double-click to replace Kliknij dwukrotnie, aby zastąpić + Click to replace Kliknij, aby zastąpić + Show all alternates in the Glyphs panel Pokaż wszystkie alternatywy w panelu Glify + Show Character Alternates Pokaż alternatywy znaku Select one character with the Type tool first. Najpierw zaznacz jeden znak narzędziem Tekst. {glyph} is a glyph of {font}, not of the font the selected text is set in ({text_font}). {glyph} to glif czcionki {font}, a nie czcionki zaznaczonego tekstu ({text_font}). {glyph} belongs to {base} (U+{base_code}) {place}; your selection is {sel} (U+{code}). {glyph} należy do znaku {base} (U+{base_code}) {place}; Twoje zaznaczenie to {sel} (U+{code}). diff --git a/crates/ui-egui/src/i18n/pt-br.tsv b/crates/ui-egui/src/i18n/pt-br.tsv index a89d1389f9..f52d729edd 100644 --- a/crates/ui-egui/src/i18n/pt-br.tsv +++ b/crates/ui-egui/src/i18n/pt-br.tsv @@ -814,6 +814,9 @@ antiAlias Sharp Nítido The selected character's font is not available. A fonte do caractere selecionado não está disponível. No alternates for the selected character. Nenhuma alternativa para o caractere selecionado. Double-click to replace Clique duplo para substituir + Click to replace Clique para substituir + Show all alternates in the Glyphs panel Mostrar todas as alternativas no painel Glifos + Show Character Alternates Mostrar Alternativas de Caractere Select one character with the Type tool first. Primeiro selecione um caractere com a ferramenta Texto. {glyph} is a glyph of {font}, not of the font the selected text is set in ({text_font}). {glyph} é um glifo de {font}, não da fonte do texto selecionado ({text_font}). {glyph} belongs to {base} (U+{base_code}) {place}; your selection is {sel} (U+{code}). {glyph} pertence a {base} (U+{base_code}) {place}; sua seleção é {sel} (U+{code}). diff --git a/crates/ui-egui/src/i18n/ru.tsv b/crates/ui-egui/src/i18n/ru.tsv index 9e0aa4f518..11a550881e 100644 --- a/crates/ui-egui/src/i18n/ru.tsv +++ b/crates/ui-egui/src/i18n/ru.tsv @@ -902,6 +902,9 @@ font-technology Variable Вариативные The selected character's font is not available. Шрифт выделенного символа недоступен. No alternates for the selected character. У выделенного символа нет альтернатив. Double-click to replace Дважды щёлкните для замены + Click to replace Щёлкните для замены + Show all alternates in the Glyphs panel Показать все альтернативы в панели «Глифы» + Show Character Alternates Показывать альтернативы символа Select one character with the Type tool first. Сначала выделите один символ инструментом «Текст». {glyph} is a glyph of {font}, not of the font the selected text is set in ({text_font}). {glyph} — глиф шрифта {font}, а не шрифта выделенного текста ({text_font}). {glyph} belongs to {base} (U+{base_code}) {place}; your selection is {sel} (U+{code}). {glyph} относится к символу {base} (U+{base_code}) {place}; выделен символ {sel} (U+{code}). diff --git a/crates/ui-egui/src/i18n/sv.tsv b/crates/ui-egui/src/i18n/sv.tsv index 7e23fcd0e1..357609dc89 100644 --- a/crates/ui-egui/src/i18n/sv.tsv +++ b/crates/ui-egui/src/i18n/sv.tsv @@ -891,6 +891,9 @@ font-technology Variable Variabla The selected character's font is not available. Typsnittet för det markerade tecknet är inte tillgängligt. No alternates for the selected character. Inga alternativ för det markerade tecknet. Double-click to replace Dubbelklicka för att ersätta + Click to replace Klicka för att ersätta + Show all alternates in the Glyphs panel Visa alla alternativ i panelen Glyfer + Show Character Alternates Visa teckenalternativ Select one character with the Type tool first. Markera först ett tecken med textverktyget. {glyph} is a glyph of {font}, not of the font the selected text is set in ({text_font}). {glyph} är en glyf i {font}, inte i det typsnitt den markerade texten är satt i ({text_font}). {glyph} belongs to {base} (U+{base_code}) {place}; your selection is {sel} (U+{code}). {glyph} hör till {base} (U+{base_code}) {place}; din markering är {sel} (U+{code}). diff --git a/crates/ui-egui/src/i18n/tr.tsv b/crates/ui-egui/src/i18n/tr.tsv index 4ab7cbcd6a..0dac2b58fd 100644 --- a/crates/ui-egui/src/i18n/tr.tsv +++ b/crates/ui-egui/src/i18n/tr.tsv @@ -883,6 +883,9 @@ font-technology Variable Değişken The selected character's font is not available. Seçili karakterin yazı tipi kullanılamıyor. No alternates for the selected character. Seçili karakter için alternatif yok. Double-click to replace Değiştirmek için çift tıklayın + Click to replace Değiştirmek için tıklayın + Show all alternates in the Glyphs panel Tüm alternatifleri Glifler panelinde göster + Show Character Alternates Karakter Alternatiflerini Göster Select one character with the Type tool first. Önce Yazım aracıyla bir karakter seçin. {glyph} is a glyph of {font}, not of the font the selected text is set in ({text_font}). {glyph}, seçili metnin yazı tipine ({text_font}) değil, {font} yazı tipine ait bir glif. {glyph} belongs to {base} (U+{base_code}) {place}; your selection is {sel} (U+{code}). {glyph}, {base} (U+{base_code}) karakterine aittir ({place}); seçiminiz {sel} (U+{code}). diff --git a/crates/ui-egui/src/i18n/uk.tsv b/crates/ui-egui/src/i18n/uk.tsv index 02706d2aa3..459e726af5 100644 --- a/crates/ui-egui/src/i18n/uk.tsv +++ b/crates/ui-egui/src/i18n/uk.tsv @@ -906,6 +906,9 @@ font-technology Variable Варіативні The selected character's font is not available. Шрифт виділеного символу недоступний. No alternates for the selected character. Виділений символ не має альтернатив. Double-click to replace Двічі клацніть для заміни + Click to replace Клацніть для заміни + Show all alternates in the Glyphs panel Показати всі альтернативи на панелі «Гліфи» + Show Character Alternates Показувати альтернативи символу Select one character with the Type tool first. Спершу виділіть один символ інструментом «Текст». {glyph} is a glyph of {font}, not of the font the selected text is set in ({text_font}). {glyph} — гліф шрифту {font}, а не шрифту виділеного тексту ({text_font}). {glyph} belongs to {base} (U+{base_code}) {place}; your selection is {sel} (U+{code}). {glyph} належить символу {base} (U+{base_code}) {place}; виділено символ {sel} (U+{code}). diff --git a/crates/ui-egui/src/i18n/vi.tsv b/crates/ui-egui/src/i18n/vi.tsv index e37628d3cd..101174ad05 100644 --- a/crates/ui-egui/src/i18n/vi.tsv +++ b/crates/ui-egui/src/i18n/vi.tsv @@ -1065,6 +1065,9 @@ font-technology Variable Biến thiên The selected character's font is not available. Phông chữ của ký tự đã chọn không khả dụng. No alternates for the selected character. Ký tự đã chọn không có glyph thay thế. Double-click to replace Bấm đúp để thay thế + Click to replace Bấm để thay thế + Show all alternates in the Glyphs panel Hiện tất cả glyph thay thế trong bảng Glyph + Show Character Alternates Hiện glyph thay thế của ký tự Select one character with the Type tool first. Trước hết hãy chọn một ký tự bằng công cụ Chữ. {glyph} is a glyph of {font}, not of the font the selected text is set in ({text_font}). {glyph} là glyph của {font}, không phải của phông chữ mà văn bản đã chọn đang dùng ({text_font}). {glyph} belongs to {base} (U+{base_code}) {place}; your selection is {sel} (U+{code}). {glyph} thuộc về {base} (U+{base_code}) {place}; ký tự bạn chọn là {sel} (U+{code}). diff --git a/crates/ui-egui/src/i18n/zh-hans.tsv b/crates/ui-egui/src/i18n/zh-hans.tsv index 1fa8a17251..b3c6a4ddcb 100644 --- a/crates/ui-egui/src/i18n/zh-hans.tsv +++ b/crates/ui-egui/src/i18n/zh-hans.tsv @@ -846,6 +846,9 @@ antiAlias Sharp 锐利 The selected character's font is not available. 所选字符的字体不可用。 No alternates for the selected character. 所选字符没有替代字。 Double-click to replace 双击以替换 + Click to replace 单击以替换 + Show all alternates in the Glyphs panel 在“字形”面板中显示所有替代字 + Show Character Alternates 显示字符的替代字 Select one character with the Type tool first. 请先用文字工具选择一个字符。 {glyph} is a glyph of {font}, not of the font the selected text is set in ({text_font}). {glyph} 是 {font} 的字形,不是所选文本所用字体({text_font})的字形。 {glyph} belongs to {base} (U+{base_code}) {place}; your selection is {sel} (U+{code}). {glyph} 属于 {base}(U+{base_code}){place};您选择的是 {sel}(U+{code})。 diff --git a/crates/ui-egui/src/i18n/zh-hant.tsv b/crates/ui-egui/src/i18n/zh-hant.tsv index 6ec3e2b043..7f0f53575e 100644 --- a/crates/ui-egui/src/i18n/zh-hant.tsv +++ b/crates/ui-egui/src/i18n/zh-hant.tsv @@ -875,6 +875,9 @@ font-technology Variable 可變 The selected character's font is not available. 選取字元的字體無法使用。 No alternates for the selected character. 選取的字元沒有替代字。 Double-click to replace 按兩下以取代 + Click to replace 按一下以取代 + Show all alternates in the Glyphs panel 在「字符」面板中顯示所有替代字 + Show Character Alternates 顯示字元的替代字 Select one character with the Type tool first. 請先用文字工具選取一個字元。 {glyph} is a glyph of {font}, not of the font the selected text is set in ({text_font}). {glyph} 是 {font} 的字形,不是所選文字所用字體({text_font})的字形。 {glyph} belongs to {base} (U+{base_code}) {place}; your selection is {sel} (U+{code}). {glyph} 屬於 {base}(U+{base_code}){place};您選取的是 {sel}(U+{code})。 diff --git a/crates/ui-egui/src/panels/glyphs.rs b/crates/ui-egui/src/panels/glyphs.rs index 7c0a18d14f..2a3251b012 100644 --- a/crates/ui-egui/src/panels/glyphs.rs +++ b/crates/ui-egui/src/panels/glyphs.rs @@ -564,6 +564,129 @@ fn apply_alternate(app: &mut VectorcraftApp, id: NodeId, bytes: (usize, usize), } } +/// The most alternate glyphs the canvas shows beside a selected character. Public behavior: at +/// most five alternates show on the canvas, and an icon to their right opens the Glyphs panel when +/// the font has more ("Replace a character with on-canvas alternate glyphs", +/// ). +pub(crate) const ON_CANVAS_ALTERNATES: usize = 5; + +/// The alternate glyphs the canvas offers beside the Type tool's one selected character +/// ([`on_canvas`]). +pub(crate) struct OnCanvas { + /// The text the character is in. + pub id: NodeId, + /// The character's bytes in the text, start and end. + pub bytes: (usize, usize), + /// The face that draws the character's glyph. + face: std::sync::Arc, + /// Up to [`ON_CANVAS_ALTERNATES`] glyphs, none of them the glyph the character is drawn with. + cells: Vec, + /// Whether the character has alternates that [`Self::cells`] leaves out. + pub more: bool, + features: Vec, + has_choice: bool, +} + +impl OnCanvas { + /// How many glyphs are offered: 1 to [`ON_CANVAS_ALTERNATES`]. + pub fn count(&self) -> usize { + self.cells.len() + } + + /// The id of glyph `i`. + #[cfg(test)] + pub fn gid(&self, i: usize) -> Option { + self.cells.get(i).map(|c| c.gid) + } + + /// The preview of glyph `i` in a square of `px` pixels. + pub fn texture(&self, ctx: &egui::Context, i: usize, px: u32) -> Option { + self.cells.get(i).map(|c| glyph_texture(ctx, &self.face, c.gid, px)) + } + + /// The tooltip of glyph `i`: its id, the feature that draws it, and what a click does. + pub fn tip(&self, i: usize) -> String { + let Some(c) = self.cells.get(i) else { return String::new() }; + let glyph = crate::i18n::fmt(tl!("glyph {id}"), &[("id", &c.gid.to_string())]); + let glyph = match (&c.apply, c.default) { + (_, true) => glyph, + (Some(tag), _) => format!("{glyph} ({tag})"), + (None, _) => format!("{glyph} ({})", String::from_utf8_lossy(&c.feature)), + }; + let mut tip = format!("{glyph}\n{}", tl!("Click to replace")); + if !c.default && c.apply.is_none() { + tip.push('\n'); + tip.push_str(tl!("No OpenType feature draws this glyph here, so it replaces the character's glyph and may not join its neighbors.")); + } + tip + } + + /// Draw the character with glyph `i`, as a double-click on that glyph in Show › Alternates for + /// Current Selection does: one command, so one undo step. + pub fn pick(&self, app: &mut VectorcraftApp, i: usize) { + let Some(c) = self.cells.get(i) else { return }; + let pick = match (&c.apply, c.default) { + (_, true) => Pick::Default, + (Some(tag), _) => Pick::Feature(tag.clone()), + (None, _) => Pick::Glyph(c.gid), + }; + super::character::end_typing(app); + apply_alternate(app, self.id, self.bytes, &self.features, self.has_choice, pick); + } +} + +/// What the canvas offers beside the Type tool's selection: the alternates of the selected +/// character as it stands in its word ([`alternates_found`], found once for each document +/// revision), in the font's order and without the glyph the character is drawn with. A character +/// drawn with an alternate gets its default glyph first. `None` unless the Type tool's selection +/// is exactly one character of one type object whose font is available and gives it another glyph. +pub(crate) fn on_canvas(app: &VectorcraftApp, ui: &Ui) -> Option { + let (_, a, b) = super::character::text_editing(app)?; + if a == b || super::character::text_story(app).is_some() { + return None; + } + let target = selected_target(app)?; + let (start, end) = (a.min(b), a.max(b)); + if target.byte != start || start + target.ch.len_utf8() != end { + return None; + } + let db = FontDb::global(); + let face = db.style_face(target.style).filter(|_| db.has_family(&target.style.font_family))?; + let found = alternates_found(app, ui, target.id, start, face.id())?; + let face = db.face_by_id(found.font_id).unwrap_or(face); + let default = (found.default != found.current).then_some(Variant { gid: found.default, feature: [0; 4], apply: None, default: true }); + let others = found.alternates.iter().filter(|a| a.gid != found.default && a.gid != found.current).map(|a| Variant { + gid: a.gid, + feature: a.feature, + apply: a.apply.clone(), + default: false, + }); + let mut cells: Vec = default.into_iter().chain(others).collect(); + if cells.is_empty() { + return None; + } + let more = cells.len() > ON_CANVAS_ALTERNATES; + cells.truncate(ON_CANVAS_ALTERNATES); + Some(OnCanvas { + id: target.id, + bytes: (start, end), + face, + cells, + more, + features: target.style.features.clone(), + has_choice: target.style.glyph_alternate.is_some(), + }) +} + +/// Show the Glyphs panel on Show › Alternates for Current Selection, for the canvas's control that +/// stands for the alternates it leaves out. A panel that is already shown stays where it is. +pub(crate) fn show_alternates(app: &mut VectorcraftApp, ctx: &egui::Context) { + set_pstate(ctx, "gl-subset", SUBSETS.len()); + if let Err(e) = crate::panel_docking::reveal_panel(app, "glyphs") { + app.status(e); + } +} + /// The Type tool's selected character, and what its word offers, for a double-click on a glyph no /// character maps in Show › Entire Font ([`Fit::pick`]). struct Fit { diff --git a/crates/ui-egui/src/prefs_dialog/tests.rs b/crates/ui-egui/src/prefs_dialog/tests.rs index 6c7382200f..f45ba93410 100644 --- a/crates/ui-egui/src/prefs_dialog/tests.rs +++ b/crates/ui-egui/src/prefs_dialog/tests.rs @@ -55,6 +55,29 @@ fn start_lists_from_typed_markers_is_a_saved_type_preference() { } } +/// Type › Show Character Alternates is a checkbox in the Type panel, on by default; turning it +/// off is saved, and every bundled language translates its label and the row's two tooltips. +#[test] +fn show_character_alternates_is_a_saved_type_preference() { + const LABEL: &str = "Show Character Alternates"; + let mut a = VectorcraftApp::new(vectorcraft_engine::Session::new(), Default::default()); + assert!(a.session.prefs.show_character_alternates); + open(&mut a, Some("Type")); + let text = crate::tests_labels::painted_text(&mut a, |a, ui| show(a, ui.ctx())); + assert!(text.contains(LABEL), "{text}"); + assert_eq!(a.ui.dialog.as_ref().unwrap().fields.get("showCharacterAlternates"), Some(&json!(true))); + a.ui.dialog.as_mut().unwrap().fields.insert("showCharacterAlternates".into(), json!(false)); + confirm(&mut a).unwrap(); + assert!(!a.session.prefs.show_character_alternates); + let saved: Prefs = serde_json::from_value(a.ui.engine_prefs.clone()).unwrap(); + assert!(!saved.show_character_alternates); + for lang in crate::i18n::Lang::all().filter(|l| l.code() != crate::i18n::Lang::EN.code()) { + for source in [LABEL, "Click to replace", "Show all alternates in the Glyphs panel"] { + assert!(crate::i18n::has(lang, source), "{}: {source}", lang.code()); + } + } +} + /// A resting checkbox must have a visible outline in both states and every brightness. #[test] fn preference_checkboxes_draw_outlines_without_hover() { diff --git a/docs/mcp.md b/docs/mcp.md index a98c36051c..5a5acfd66d 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -6371,6 +6371,10 @@ Pen's second anchor at (150, 431), in line with the square's centre. - `highlightAlternateGlyphs` (off by default): Preferences › Type › Highlight Alternate Glyphs. On, the canvas paints pink behind OpenType substitutions (ligatures, contextual forms). Document Setup's Highlight Substituted Glyphs stays separate. Agents set it with `prefs.set`; the desktop canvas reads it on the next paint. +- `showCharacterAlternates` (on by default): Preferences › Type › Show Character Alternates. On, the desktop canvas + shows the alternate glyphs of the one character selected with a Type tool beside it (see + [Alternate glyphs](#alternate-glyphs)). Off, it shows none. It is VectorCraft's own option. `prefs.set` refuses a + value that is not a boolean. - `highlightSubstitutedFonts` (on by default): Preferences › Type › Highlight Substituted Fonts. On, the canvas paints pink behind text drawn in a substitute for a missing font, in every open document. Off, no document shows that highlight. `document.setup` ignores this key and adds a `warnings` entry that names `prefs.set` to its reply. A file @@ -9488,6 +9492,15 @@ and a glyph that the character's style does not draw (All Caps, Small Caps or an system). Choosing the glyph a character already draws is not an edit. Character styles do not take `glyphAlternate`: `charStyle.new`, `charStyle.edit` and `charStyle.setAttrs` refuse it. +On the desktop canvas, a Type tool with exactly one character selected shows up to five of that character's alternates +in a row beside it, in the same order and without the glyph the character is drawn with; a character drawn with an +alternate gets its default glyph first. A click on a glyph runs the same command as the panel's double-click +(`text.setRangeStyle {featureChanges}`, or `text.setGlyphAlternate` for a glyph no feature draws there), as one undo +step. When the character has more alternates than the row shows, an arrow at the end of the row opens the Glyphs panel +on Alternates for Current Selection. The row has no command of its own: an agent reads the alternates with +`text.glyphAlternates` and applies one with the commands above. The `showCharacterAlternates` preference turns the row +off. + ```json {"name":"run_command","arguments":{"command":"text.glyphAlternates","params":{"id":7,"start":2}}} {"name":"run_command","arguments":{"command":"text.setGlyphAlternate","params":{"id":7,"start":2,"gid":1043}}} diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 569ab0acc5..3e29897763 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1366,6 +1366,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur The OpenType panel's Position menu lists Default Position, Superscript, Subscript, Numerator and Denominator, as [Format text with OpenType features](https://helpx.adobe.com/illustrator/desktop/design-with-text/special-characters-glyphs/format-text-with-opentype-features.html) describes; Superscript, Subscript, Numerator and Denominator set `sups`, `subs`, `numr` and `dnom` for the selected type and turn the other three off, and Default Position turns all four off. The menu shows Mixed when the characters it acts on differ, and an entry chosen then applies to all of them. These two rules are VectorCraft's own, as is setting the feature on type whose font lacks the glyphs, which then draws unchanged. The panel's menus and checkboxes change only their own features on each selected character, so characters keep their other OpenType features (`text.setStyle` and `text.setRangeStyle` `{featureChanges}`). Tests: `panels::opentype::tests`, `features::tests::numerators_and_denominators_are_kept_and_shaped`. The Glyphs panel's Show menu and panel menu have Alternates for Current Selection, as [Replace characters with alternate glyphs](https://helpx.adobe.com/illustrator/desktop/design-with-text/special-characters-glyphs/replace-characters-with-alternate-glyphs.html) describes. The grid lists the selected character's alternate glyphs from `GSUB` single and alternate substitutions of `aalt`, then `salt`, `ss01` to `ss20`, `swsh`, `cv01` to `cv99` and a few other alternate features, each glyph once. A double-click applies the feature that draws the variant, or a glyph choice if no supported feature draws it, as detailed below. The list uses the font of the character's text even when the panel browses another font, and the panel shows a note when the character has no alternates, its font is missing or no character is selected; these rules are VectorCraft's own. Native files keep a glyph choice with the face it was made in; another font or version draws the default glyph. Tests: `panels::glyphs::tests`, `tests::alternates`, `tests::textedit::a_glyph_alternate_replaces_one_character_through_undo_and_reopen`. The Glyphs panel's Alternates for Current Selection (#5514) applies the OpenType feature that draws the glyph to the selected character alone, as [Replace characters with alternate glyphs](https://helpx.adobe.com/illustrator/desktop/design-with-text/special-characters-glyphs/replace-characters-with-alternate-glyphs.html) describes (Alternates For Current Selection in the Show menu; double-click the glyph to replace the selected character). One deliberate difference from a literal reading: the text keeps the Unicode character and gains a character-style feature with a value (`salt=2`, `ss03`; `text.setRangeStyle {featureChanges}`) instead of taking an unencoded glyph in its place. The grid lists the alternates of the glyph the shaper makes for the character in its word (a medial letter shows its medial variants; the default glyph comes first and the current glyph is highlighted); a variant no supported feature draws there stays enabled as a replacement glyph (`text.setGlyphAlternate`, including positional alternates), and its tooltip says it may not join its neighbors; the default glyph takes the character's alternate features off again. Runs that differ only in their OpenType features are shaped as one piece of text, each run's features over its own characters, so the letters of an Arabic word keep their joining forms. Show › Entire Font also lists the glyphs no character maps, with the font's name for them (or `gid N`) and the feature that gives them. It applies the selected letter's contextual alternates the same way, and applies a glyph no feature reaches whose name does not belong to another letter as a glyph choice; other glyphs are dimmed with the reason. An Arabic presentation form picked there inserts its base letter. These rules are VectorCraft's own. Tests: `tests::glyph_alternates` in the text crate (a fixture font; Kufi Murabba PIIIX when it is installed), `panels::glyphs::tests` (including positional glyph choices, unlisted glyph name ownership, apply and Undo), `tests::textedit::feature_values_are_set_replaced_and_saved`. + With a Type tool and exactly one character selected, the canvas shows that character's alternate glyphs in a row beside it, and a click on one replaces the character's glyph, as [Replace characters with alternate glyphs](https://helpx.adobe.com/illustrator/desktop/design-with-text/special-characters-glyphs/replace-characters-with-alternate-glyphs.html) describes ("Replace a character with on-canvas alternate glyphs"). The row shows at most five alternates, and an arrow at its end opens the Glyphs panel on Alternates for Current Selection when the character has more, as the [reference manual](https://web.archive.org/web/20210601162814/https://helpx.adobe.com/pdf/illustrator_reference.pdf#page=479) describes. A click runs the command the panel's double-click runs, as one undo step, so a letter of an Arabic word keeps the word joined. These rules are VectorCraft's own: the row leaves out the glyph the character is drawn with and puts the default glyph first for a character drawn with an alternate; it stands under a character of horizontal type and on the right of a character of vertical type, or on the other side when the canvas ends there or the Contextual Task Bar is there; Preferences › Type › Show Character Alternates (`showCharacterAlternates`, on by default) turns it off. Tests: `canvas::alternates_bar::tests`, `tests::prefs::show_character_alternates_preference_is_on_by_default_and_saved`. The OpenType panel's Stylistic Sets menu offers `ss01`–`ss20` sets present in the selected text's font, using the font's custom labels when available and numbered labels otherwise. Sets can be combined and deselected individually. With mixed fonts, the menu shows the first selected character's font's sets and applies the feature tag to all selected text; a font without that set draws unchanged. The mixed-font rule is a VectorCraft design choice. Behavior reference: [Add stylistic sets to selected text](https://helpx.adobe.com/illustrator/desktop/design-with-text/special-characters-glyphs/add-stylistic-sets-to-selected-text.html) and the [OpenType specification](https://learn.microsoft.com/en-us/typography/opentype/spec/features_pt). `text.setStyle {stroke, strokeWidth}` sets the character stroke's color and weight of the selected type or of a range of its characters (the weight in document points, as `stroke.set` sets it), in the same undo step as the command's other changes. Tests: `tests::textedit::set_style_sets_the_character_stroke`. - **Paragraph alignment:** the shared Paragraph/Properties alignment row leaves every button unselected when the paragraphs it acts on differ. Auto resolves per paragraph; choosing an alignment uses the existing command. Headless paragraph_mixed_alignment_* regressions exercise mixed → Center → Undo, caret/uniform states, Unicode range boundaries, Auto/direction, split runs and grouped text. These regressions and full integration CI pass. Original captures confirm mixed → Center → Undo in the existing alignment-button layout. The [public Paragraph panel guide](https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/paragraph-panel-overview.html) lists the seven controls, but does not specify mixed-state rendering. Other paragraph fields remain outside this change; scores and hours unchanged.