Merge pull request #7640 from dotCooCoo/feat/canvas-character-alternates

Type: a selected character's alternate glyphs show beside it on the canvas
This commit is contained in:
BFlat
2026-11-15 03:22:11 +07:00
committed by GitHub
38 changed files with 682 additions and 0 deletions
+1
View File
@@ -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),
+4
View File
@@ -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,
+20
View File
@@ -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.
/// <https://helpx.adobe.com/illustrator/desktop/design-with-text/fonts-and-scripts/edit-text-without-replacing-missing-fonts.html>
+3
View File
@@ -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);
+163
View File
@@ -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",
//! <https://web.archive.org/web/20210601162814/https://helpx.adobe.com/pdf/illustrator_reference.pdf#page=479>;
//! [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<egui::Rect> {
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<egui::Rect>) -> 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<Pos2> = 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;
@@ -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<egui::Event>) -> Option<Rect> {
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<u32>, 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<u32> {
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<Pos2> = 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::<usize>(&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");
}
+3
View File
@@ -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}).
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -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}).
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -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}).
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -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}).
Can't render this file because it is too large.
+3
View File
@@ -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}).
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -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}).
Can't render this file because it is too large.
+3
View File
@@ -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}).
Can't render this file because it is too large.
+3
View File
@@ -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}).
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -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}).
Can't render this file because it contains an unexpected character in line 2 and column 73.
+3
View File
@@ -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}).
Can't render this file because it is too large.
+3
View File
@@ -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 ખેંચો
Can't render this file because it contains an unexpected character in line 72 and column 5.
+3
View File
@@ -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}) है।
Can't render this file because it is too large.
+3
View File
@@ -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}).
Can't render this file because it is too large.
+3
View File
@@ -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}).
1 # Indonesian (id) catalog. Same format as zh-hant.tsv: context <TAB> source <TAB> translation
1636
1637
1638
1639
1640
1641
1642
1643
1644
+3
View File
@@ -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}).
Can't render this file because it is too large.
+3
View File
@@ -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}) です。
Can't render this file because it is too large.
+3
View File
@@ -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}).
Can't render this file because it is too large.
+3
View File
@@ -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})입니다.
Can't render this file because it is too large.
+3
View File
@@ -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}).
Can't render this file because it is too large.
+3
View File
@@ -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}).
Can't render this file because it is too large.
+3
View File
@@ -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}).
Can't render this file because it is too large.
+3
View File
@@ -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}).
Can't render this file because it is too large.
+3
View File
@@ -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}).
Can't render this file because it is too large.
+3
View File
@@ -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}).
Can't render this file because it contains an unexpected character in line 5 and column 25.
+3
View File
@@ -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}).
Can't render this file because it is too large.
+3
View File
@@ -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}).
Can't render this file because it contains an unexpected character in line 204 and column 5.
+3
View File
@@ -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})。
Can't render this file because it is too large.
+3
View File
@@ -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})。
Can't render this file because it is too large.
+123
View File
@@ -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",
/// <https://web.archive.org/web/20210601162814/https://helpx.adobe.com/pdf/illustrator_reference.pdf#page=479>).
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<vectorcraft_text::FontFace>,
/// Up to [`ON_CANVAS_ALTERNATES`] glyphs, none of them the glyph the character is drawn with.
cells: Vec<Variant>,
/// Whether the character has alternates that [`Self::cells`] leaves out.
pub more: bool,
features: Vec<String>,
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<u32> {
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<egui::TextureHandle> {
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<OnCanvas> {
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<Variant> = 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 {
+23
View File
@@ -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() {
+13
View File
@@ -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}}}
+1
View File
@@ -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.