From 2a95625b99cc8f4aaf81ebfb57beaaf6a80867af Mon Sep 17 00:00:00 2001 From: dotCooCoo Date: Mon, 2 Nov 2026 21:04:24 -0800 Subject: [PATCH] Type: step font families with the mouse wheel Apply wheel steps over the closed family fields in Character, Properties and Control, preserving event order and one undo step per effective notch. Document the supported controls and the application choices. Fixes #5253. --- ROADMAP.md | 4 +- crates/ui-egui/src/font_menu.rs | 160 ++++++++++++++++++--- crates/ui-egui/src/font_menu/tests.rs | 37 +++++ crates/ui-egui/src/panels/character.rs | 6 +- crates/ui-egui/src/tests/font_menu.rs | 181 ++++++++++++++++++++++++ crates/ui-egui/src/widgets.rs | 2 +- crates/ui-egui/src/widgets/dropdowns.rs | 15 +- docs/gaps.md | 10 +- docs/roadmap.md | 3 +- docs/target-app-parity.md | 4 +- docs/type-parity.md | 4 +- docs/ui-parity.md | 4 +- 12 files changed, 398 insertions(+), 32 deletions(-) diff --git a/ROADMAP.md b/ROADMAP.md index 8ebfd6522..bce1cab0c 100644 --- a/ROADMAP.md +++ b/ROADMAP.md @@ -9,7 +9,7 @@ **Stage: alpha** · next: beta, ~13 points (ready for real work ~62% → ~75%, and reliable `.ai` exchange) and ~170–270 h away -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** minor (Progress log row for the Color Effects add-on's Find Nearest Swatch; headline numbers unchanged) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-02 · **Change:** minor (font-family fields accept the mouse wheel, #5253; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) VectorCraft is a clean-room, open-source, pure-Rust reimplementation of the Adobe Illustrator workflow. It runs on macOS, Windows, Linux, FreeBSD and the web (WASM), and agents can drive all of it over MCP, the CLI and a JSON @@ -129,6 +129,7 @@ Ranked; detail in [docs/roadmap.md](docs/roadmap.md) and [docs/gaps.md](docs/gap ## Progress log +- 2026-11-02: Mouse wheel over a closed font-family field in Character, Properties or Control changes selected text through the family list using saved font filters (excluding search text), one undo step per notch, stopping at the ends (#5253). Scores and estimates unchanged. - 2026-10-31: Pattern libraries: Window › Swatch Libraries › Patterns and the Swatches panel's library menu open Geometric Patterns, a built-in library of eleven pattern swatches (dots, stripes, checks, a grid and shapes); clicking one adds its pattern and swatch to the document, once, and applies it to the active fill or stroke (`swatch.library.add`). - 2026-11-01: Find Nearest Swatch joins the Color Effects add-on (Object › Plug-ins › Color Effects): for each colour of the selection, the closest swatches of the document, a built-in library or a library or colour book the user loaded, with their CIEDE2000 difference; Apply gives each colour its nearest swatch as one undo step, linking global and spot swatches and adding a library's to the document first, so a process colour becomes the closest spot colour (`addon.color.nearestSwatch`, `addon.color.applyNearestSwatch`, #4923). - 2026-11-01: Expand Dashes into Paths joins the Path Precision Tools add-on (Object › Plug-ins › Path Precision Tools): each selected path's dashed stroke becomes separate open paths, one per dash, laid out exactly as the stroke draws them (pattern, offset, Align Dashes to Corners and Path Ends), each keeping the stroke with the dashes off, so the gaps are real gaps for cutters and plotters, as one undo step (`addon.pathtools.expandDashes`, #4922). @@ -214,6 +215,7 @@ The full earlier record of what landed is kept in [docs/roadmap.md](docs/roadmap | Date | Change | Summary | |---|---|---| +| 2026-11-02 | minor | Closed font-family fields accept wheel input for selected text in Character, Properties and Control (#5253); scores and estimates unchanged. | | 2026-11-01 | minor | Progress log row for the Color Effects add-on's Find Nearest Swatch (#4923); headline numbers unchanged. | | 2026-11-01 | minor | Progress log row for the Path Precision Tools add-on's Expand Dashes into Paths (#4922); headline numbers unchanged. | | 2026-11-01 | minor | Progress log row for the Arrange Tools add-on's Randomize Opacity (#4925); headline numbers unchanged. | diff --git a/crates/ui-egui/src/font_menu.rs b/crates/ui-egui/src/font_menu.rs index 93ae778e5..540951d58 100644 --- a/crates/ui-egui/src/font_menu.rs +++ b/crates/ui-egui/src/font_menu.rs @@ -8,7 +8,9 @@ //! Technology filter's Variable lists only variable font families. Preferences › Type › Enable //! in-menu font previews and Font Preview Size turn the samples off or size the rows. Recently //! used families appear first, in the order they were last applied, within the same search and -//! filters as the remaining families. +//! filters as the remaining families. In the Character and Properties panels and the Control bar, +//! the mouse wheel turned over the closed field applies the next family down the menu (or up it) to +//! the selected text, one undo step per notch, stopping at the menu's first and last family. use std::collections::HashMap; use std::sync::Arc; @@ -33,9 +35,13 @@ pub enum FontPick { EndPreview, /// The star of a family was clicked: add it to the favourites, or take it out. Favorite(String), + /// The mouse wheel turned over the closed field: apply each of these families in turn, one + /// undo step each. + Wheel(Vec), } -/// How the menu looks: from the preferences and the favourites. +/// How the menu looks (from the preferences and the favourites) and whether its closed field +/// takes the mouse wheel. #[derive(Clone, Copy, Debug)] pub struct MenuLook<'a> { /// Show each family's sample (Preferences › Type › Enable in-menu font previews). @@ -48,12 +54,15 @@ pub struct MenuLook<'a> { pub favorites: &'a [String], /// Recently applied families, newest first, limited by Number of Recent Fonts. pub recent: &'a [String], + /// The mouse wheel turned over the closed field steps through the families in the menu's + /// order ([`FontPick::Wheel`]): the Character and Properties panels and the Control bar. + pub wheel: bool, } impl Default for MenuLook<'static> { - /// Samples on, English names, medium rows, no favourites. + /// Samples on, English names, medium rows, no favourites, no wheel. fn default() -> Self { - Self { samples: true, english_names: true, row: ROW, favorites: &[], recent: &[] } + Self { samples: true, english_names: true, row: ROW, favorites: &[], recent: &[], wheel: false } } } @@ -70,6 +79,7 @@ impl<'a> MenuLook<'a> { row, favorites: &app.ui.favorite_fonts, recent: app.recent_fonts(), + wheel: false, } } } @@ -235,6 +245,10 @@ struct MenuState { rows_key: Option, /// The height of the search field and filters last frame. header: f32, + /// The families the wheel steps through, listed at its first notch over the closed field and + /// kept while the pointer stays on it: each step makes its family a recent one, which would + /// otherwise move it in the order. + wheel_order: Option>, } /// The font family menu showing `current`, `width` points wide, rows sampling `sample` (the @@ -254,7 +268,7 @@ pub fn font_menu( // The search field and filters stay in place over the rows, which scroll. The rows are all // one height (a family's styles too), so only those in view are laid out: there are thousands // of families. - let shown = widgets::combo_menu(ui, &id, current, width, |menu| { + let (field, shown) = widgets::combo_menu_field(ui, &id, current, width, |menu| { menu.height(height).show_rows_with_header( |ui| header(ui, state_id, state, current, look), look.row, @@ -268,6 +282,31 @@ pub fn font_menu( // Closed (Escape, a click outside) with a preview showing. out = Some(FontPick::EndPreview); } + if !look.wheel || drawn || !field.contains_pointer() { + st.wheel_order = None; + ui.data_mut(|d| d.remove::(field.id.with("wheel"))); + } else if out.is_none() { + let notches = wheel_notches(ui, &field); + if !notches.is_empty() { + let mut own = vectorcraft_text::FontDb::global().canonical(current, "").0; + if st.wheel_order.is_none() { + st.wheel_order = Some(wheel_order(&st, look).into()); + } + let mut steps = Vec::new(); + if let Some(order) = st.wheel_order.as_deref() { + for notch in notches { + let next = wheel_steps(order, &own, notch); + if let Some(last) = next.last() { + own.clone_from(last); + } + steps.extend(next); + } + } + if !steps.is_empty() { + out = Some(FontPick::Wheel(steps)); + } + } + } if matches!(out, Some(FontPick::Chosen(..))) { st.previewed = None; } @@ -275,6 +314,83 @@ pub fn font_menu( out } +/// Does the menu list family `f` for search `query` (lowercase) under the filters of `st`? +fn listed(db: &vectorcraft_text::FontDb, f: &str, query: &str, st: &MenuState, look: MenuLook) -> bool { + let label = db.family_display_name(f, look.english_names); + if !(query.is_empty() || f.to_lowercase().contains(query) || label.to_lowercase().contains(query)) { + return false; + } + if st.favorites_only && !look.favorites.iter().any(|x| x.eq_ignore_ascii_case(f)) { + return false; + } + if st.script == 0 && st.class.is_none() && !st.variable_only { + return true; + } + let traits = db.family_traits(f); + script_matches(st.script, traits) && st.class.is_none_or(|c| c == traits.class) && (!st.variable_only || traits.variable) +} + +/// The families the closed field's wheel steps through, in the open menu's order with no search: +/// the recent families first, then the others, under the menu's filters. +fn wheel_order(st: &MenuState, look: MenuLook) -> Vec { + let db = vectorcraft_text::FontDb::global(); + let families: Vec<_> = db.menu_family_list().iter().filter(|f| listed(db, f, "", st, look)).cloned().collect(); + grouped_rows(&families, look.recent, &[], |_| vec![]) + .into_iter() + .filter_map(|r| match r { + Row::Family(f) => Some(f), + Row::Heading(_) | Row::Style(..) => None, + }) + .collect() +} + +/// The families `notches` turns of the wheel step to from family `current` in `order`, one per +/// notch: forward (down the menu) when positive, back when negative. The steps stop at either end +/// of the order, and there are none when `current` is not in it. +fn wheel_steps(order: &[String], current: &str, notches: i32) -> Vec { + let Some(at) = order.iter().position(|f| f.eq_ignore_ascii_case(current)) else { return vec![] }; + let last = order.len().saturating_sub(1); + let n = notches.unsigned_abs() as usize; + let to = if notches >= 0 { at.saturating_add(n).min(last) } else { at.saturating_sub(n) }; + if to >= at { order.iter().skip(at + 1).take(to - at).cloned().collect() } else { order.iter().take(at).skip(to).rev().cloned().collect() } +} + +/// Whole notches of the mouse wheel turned over closed field `resp` this frame, positive when it +/// turns down (towards the user). A wheel notch is a line, or `line_scroll_speed` points of a +/// trackpad or fine wheel; what is left of a notch carries over to the next turn. The wheel over +/// the field is the field's: the panel under it does not scroll. A turn with a modifier key is +/// left to the panel and the canvas. +fn wheel_notches(ui: &Ui, resp: &egui::Response) -> Vec { + let (line, id) = (ui.ctx().options(|o| o.input_options.line_scroll_speed), resp.id.with("wheel")); + let mut notches: f32 = ui.data(|d| d.get_temp(id)).unwrap_or(0.0); + let mut completed = Vec::new(); + let mut turned = false; + ui.input_mut(|i| { + i.events.retain(|e| match e { + egui::Event::MouseWheel { unit, delta, modifiers, .. } if modifiers.is_none() => { + notches -= match unit { + egui::MouseWheelUnit::Point => delta.y / line.max(1.0), + egui::MouseWheelUnit::Line | egui::MouseWheelUnit::Page => delta.y, + }; + let whole = notches.trunc(); + if whole != 0.0 { + completed.push(whole as i32); + notches -= whole; + } + turned = true; + false + } + _ => true, + }); + if turned || i.modifiers.is_none() { + // The rest of a turn egui spreads over the next frames scrolls nothing either. + i.smooth_scroll_delta = egui::Vec2::ZERO; + } + }); + ui.data_mut(|d| d.insert_temp(id, notches)); + completed +} + /// What the menu's header hands its rows. struct Listed<'s> { st: &'s mut MenuState, @@ -372,7 +488,6 @@ fn header<'s>(ui: &mut Ui, state_id: egui::Id, st: &'s mut MenuState, current: & // as it was before any preview. let current = db.canonical(&st.current, "").0; let current = current.as_str(); - let favorite = |f: &str| look.favorites.iter().any(|x| x.eq_ignore_ascii_case(f)); // The generation is read first: fonts that load meanwhile make the next frame list them. let key: RowsKey = ( query.clone(), @@ -387,21 +502,7 @@ fn header<'s>(ui: &mut Ui, state_id: egui::Id, st: &'s mut MenuState, current: & db.generation(), ); if st.rows_key.as_ref() != Some(&key) { - let wanted = |f: &str| { - let label = db.family_display_name(f, look.english_names); - if !(query.is_empty() || f.to_lowercase().contains(&query) || label.to_lowercase().contains(&query)) { - return false; - } - if st.favorites_only && !favorite(f) { - return false; - } - if st.script == 0 && st.class.is_none() && !st.variable_only { - return true; - } - let traits = db.family_traits(f); - script_matches(st.script, traits) && st.class.is_none_or(|c| c == traits.class) && (!st.variable_only || traits.variable) - }; - let families: Vec<_> = db.menu_family_list().iter().filter(|f| wanted(f)).cloned().collect(); + let families: Vec<_> = db.menu_family_list().iter().filter(|f| listed(db, f, &query, st, look)).cloned().collect(); let rows = grouped_rows(&families, look.recent, &st.expanded, |f| db.styles(f)); // Filter changes (including un-starring a favourite) may remove rows; expanding // another family or a background font scan can insert rows before the highlight. @@ -823,6 +924,21 @@ pub(crate) fn apply(app: &mut VectorcraftApp, ctx: &egui::Context, pick: FontPic } } FontPick::Favorite(f) => toggle_favorite(app, f), + FontPick::Wheel(families) => { + if previewing { + app.session.cancel_interaction().ok(); + crate::panels::set_pstate(ctx, PREVIEWING, false); + } + crate::panels::character::end_typing(app); + // Each family is its own step, keeping the style when the family has it, else the + // closest it has. + for f in families { + let (cmd, params) = font_command(app, &f, None); + if app.run(cmd, params).is_err() { + break; + } + } + } FontPick::Chosen(f, style) => { if previewing { app.session.cancel_interaction().ok(); @@ -856,7 +972,7 @@ pub(crate) fn picked(app: &mut VectorcraftApp, pick: Option) -> Option toggle_favorite(app, f); None } - FontPick::Preview(..) | FontPick::EndPreview => None, + FontPick::Preview(..) | FontPick::EndPreview | FontPick::Wheel(_) => None, } } diff --git a/crates/ui-egui/src/font_menu/tests.rs b/crates/ui-egui/src/font_menu/tests.rs index 4fdfc5733..3643c54d2 100644 --- a/crates/ui-egui/src/font_menu/tests.rs +++ b/crates/ui-egui/src/font_menu/tests.rs @@ -225,6 +225,43 @@ fn expanded_style_rows_do_not_retarget_the_highlight() { assert_eq!(remap_highlight(&after, Some(1), &before), None); } +/// #5253: each wheel notch over the closed field steps one family, down the menu or up it, and the +/// steps stop at its first and last family without going round. +#[test] +fn the_wheel_steps_one_family_per_notch_and_stops_at_the_ends() { + let order: Vec = ["Alpha", "Bravo", "Charlie", "Delta"].map(str::to_string).into(); + let steps = |current, notches| super::wheel_steps(&order, current, notches); + assert_eq!(steps("Bravo", 1), ["Charlie"]); + assert_eq!(steps("bravo", -1), ["Alpha"], "any case"); + assert_eq!(steps("Alpha", 2), ["Bravo", "Charlie"], "a step for each notch"); + assert_eq!(steps("Bravo", 5), ["Charlie", "Delta"], "stops at the last family"); + assert_eq!(steps("Charlie", -5), ["Bravo", "Alpha"], "stops at the first family"); + assert!(steps("Delta", 1).is_empty() && steps("Alpha", -1).is_empty(), "no wrapping"); + assert!(steps("Missing", 1).is_empty() && steps("Bravo", 0).is_empty()); +} + +/// #5253: the wheel follows the open menu's order and filters: the recent families first, then the +/// others in the database's order, under the favourites (and other) filters. +#[test] +fn the_wheel_follows_the_menus_order_and_filters() { + let names = vectorcraft_text::FontDb::global().menu_family_list(); + if names.len() < 3 { + return; + } + let recent = vec![names[2].clone()]; + let look = super::MenuLook { samples: false, recent: &recent, ..Default::default() }; + let order = super::wheel_order(&super::MenuState::default(), look); + assert_eq!(order.first(), Some(&names[2]), "the recent family first"); + // Other tests add fonts to the process-wide database meanwhile: compare places, not neighbours. + let at = |f: &String| order.iter().position(|x| x == f); + assert!(matches!((at(&names[0]), at(&names[1])), (Some(a), Some(b)) if 0 < a && a < b), "then the others in the menu's order"); + assert_eq!(order.iter().filter(|f| **f == names[2]).count(), 1, "once each"); + let favorites = vec![names[0].clone(), names[1].clone()]; + let st = super::MenuState { favorites_only: true, ..Default::default() }; + let order = super::wheel_order(&st, super::MenuLook { favorites: &favorites, ..look }); + assert_eq!(order, favorites, "the favourites filter"); +} + /// The Font Technology filter's Variable is the font adjective, not the Variables panel's noun. #[test] fn the_variable_filter_is_named_as_fonts_are() { diff --git a/crates/ui-egui/src/panels/character.rs b/crates/ui-egui/src/panels/character.rs index f1f0707f6..b81ed8cd2 100644 --- a/crates/ui-egui/src/panels/character.rs +++ b/crates/ui-egui/src/panels/character.rs @@ -801,10 +801,12 @@ fn font_height_reference(app: &mut VectorcraftApp, ui: &mut Ui, width: f32) { } /// The font family menu and the font style dropdown of style `s` (Character and Properties panels, -/// Control bar), with their `ids` and `widths`. +/// Control bar), with their `ids` and `widths`. The mouse wheel over the closed family field steps +/// the text through the families. pub(crate) fn font_pickers(app: &mut VectorcraftApp, ui: &mut Ui, s: &CharStyle, ids: (&str, &str), widths: (f32, f32)) { let sample = crate::font_menu::sample_text(app); - if let Some(pick) = crate::font_menu::font_menu(ui, ids.0, &s.font_family, widths.0, sample.as_deref(), crate::font_menu::MenuLook::of(app)) { + let look = crate::font_menu::MenuLook { wheel: true, ..crate::font_menu::MenuLook::of(app) }; + if let Some(pick) = crate::font_menu::font_menu(ui, ids.0, &s.font_family, widths.0, sample.as_deref(), look) { crate::font_menu::apply(app, ui.ctx(), pick); } let styles = vectorcraft_text::FontDb::global().styles(&s.font_family); diff --git a/crates/ui-egui/src/tests/font_menu.rs b/crates/ui-egui/src/tests/font_menu.rs index 680afa6d1..42548244d 100644 --- a/crates/ui-egui/src/tests/font_menu.rs +++ b/crates/ui-egui/src/tests/font_menu.rs @@ -203,6 +203,187 @@ fn a_press_outside_the_menu_ends_the_preview_at_once() { assert!(!frame(vec![button(outside, false)]).1); } +/// A document with point type in Inter, selected; its id. +fn selected_inter(app: &mut VectorcraftApp) -> u64 { + app.run("file.new", json!({"width": 300, "height": 200})).unwrap(); + let id = app.session.execute("text.create", &json!({"x": 20, "y": 50, "text": "Gagaku", "font": "Inter"})).unwrap()["id"].as_u64().unwrap(); + app.session.execute("select.set", &json!({"ids": [id]})).unwrap(); + id +} + +fn first_style(app: &VectorcraftApp, id: u64) -> vectorcraft_doc::CharStyle { + match &app.session.active().unwrap().doc.node(vectorcraft_doc::NodeId(id)).unwrap().kind { + NodeKind::Text(t) => t.first_style(), + _ => panic!("not type"), + } +} + +fn wheel(unit: egui::MouseWheelUnit, dy: f32, modifiers: egui::Modifiers) -> egui::Event { + egui::Event::MouseWheel { unit, delta: egui::vec2(0.0, dy), modifiers, phase: egui::TouchPhase::Move, source: egui::MouseWheelSource::Unknown } +} + +/// One frame of `draw` over `app`, as the shell runs it; the middle of the text naming `family` +/// lowest on the screen (the closed family field), if drawn, and whether the menu's search field +/// is drawn (the menu is open). +fn panel_frame( + ctx: &egui::Context, + app: &mut VectorcraftApp, + events: Vec, + family: &str, + draw: &dyn Fn(&mut VectorcraftApp, &mut egui::Ui), +) -> (Option, bool) { + let input = + egui::RawInput { events, screen_rect: Some(egui::Rect::from_min_size(egui::Pos2::ZERO, egui::vec2(600.0, 800.0))), ..Default::default() }; + let mut out = ctx.run_ui(input, |ui| { + crate::font_menu::end_stale_preview(app, ui.ctx()); + draw(app, ui); + }); + out.textures_delta.clear(); + let text = |label: &str| { + out.shapes.iter().find_map(|s| match &s.shape { + egui::Shape::Text(t) if t.galley.text() == label => Some(t.pos + egui::vec2(4.0, t.galley.size().y * 0.5)), + _ => None, + }) + }; + (text(family), text("Search").is_some()) +} + +/// #5253: in the Character panel, each notch of the mouse wheel over the closed font family field +/// applies the next family down the menu (up it when turned up) to the selected type, each its own +/// undo step, with the menu closed. Leaving the field or Escape keeps it; Undo puts it back. +#[test] +fn the_wheel_over_the_family_field_steps_the_selected_type_through_the_families() { + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + let id = selected_inter(&mut app); + let font = |app: &VectorcraftApp| first_style(app, id).font_family; + let steps = |app: &VectorcraftApp| app.session.active().unwrap().history.undo.len(); + let draw = |app: &mut VectorcraftApp, ui: &mut egui::Ui| crate::panels::character::show(app, ui); + let mut field = None; + for _ in 0..3 { + field = panel_frame(&ctx, &mut app, vec![], "Inter", &draw).0; + } + let field = field.expect("the Character panel shows the family field"); + // Other tests add fonts to the process-wide database meanwhile: compare places, not neighbours. + let place = |f: &str| FontDb::global().menu_family_list().iter().position(|x| x == f); + let before = steps(&app); + let line = egui::MouseWheelUnit::Line; + let (_, open) = panel_frame(&ctx, &mut app, vec![egui::Event::PointerMoved(field), wheel(line, -1.0, Default::default())], "Inter", &draw); + let next = font(&app); + assert!(place(&next) > place("Inter") && steps(&app) == before + 1, "a notch down applies a family below, one step: {next}"); + assert!(!open && !app.session.in_interaction(), "the menu stays closed and nothing is left previewed"); + // The family is now a recent one: the order stays as it was while the pointer stays on the field. + panel_frame(&ctx, &mut app, vec![wheel(line, -1.0, Default::default())], &next, &draw); + let after = font(&app); + assert!(place(&after) > place(&next) && steps(&app) == before + 2, "{after}"); + panel_frame(&ctx, &mut app, vec![wheel(line, 1.0, Default::default())], &after, &draw); + assert_eq!((font(&app), steps(&app)), (next.clone(), before + 3), "a notch up, back"); + // A turn of a fine wheel in points: a notch is `line_scroll_speed` points, the rest carries over. + let speed = ctx.options(|o| o.input_options.line_scroll_speed); + panel_frame(&ctx, &mut app, vec![wheel(egui::MouseWheelUnit::Point, speed * 0.5, Default::default())], &next, &draw); + assert_eq!(font(&app), next, "less than a notch"); + panel_frame(&ctx, &mut app, vec![wheel(egui::MouseWheelUnit::Point, speed * 0.5, Default::default())], &next, &draw); + assert_eq!((font(&app), steps(&app)), ("Inter".to_string(), before + 4), "the rest of the notch"); + // A turn with a modifier key is not the field's. + panel_frame(&ctx, &mut app, vec![wheel(line, -1.0, egui::Modifiers::COMMAND)], "Inter", &draw); + assert_eq!((font(&app), steps(&app)), ("Inter".to_string(), before + 4), "Ctrl/Cmd-wheel"); + // Two notches at once: two families, two steps. + panel_frame(&ctx, &mut app, vec![wheel(line, -2.0, Default::default())], "Inter", &draw); + assert_eq!((font(&app), steps(&app)), (after.clone(), before + 6)); + // Escape and leaving the field keep the family. + panel_frame(&ctx, &mut app, key(egui::Key::Escape), &after, &draw); + panel_frame(&ctx, &mut app, vec![egui::Event::PointerMoved(egui::pos2(580.0, 780.0))], &after, &draw); + panel_frame(&ctx, &mut app, vec![], &after, &draw); + assert_eq!((font(&app), steps(&app)), (after, before + 6), "the family stays applied"); + // The wheel away from the field does not change the font. + let shown = font(&app); + panel_frame(&ctx, &mut app, vec![wheel(line, -1.0, Default::default())], &shown, &draw); + assert_eq!(steps(&app), before + 6, "the wheel off the field"); + app.run("edit.undo", json!({})).unwrap(); + assert_eq!(font(&app), next, "Undo takes back one notch"); + app.run("edit.undo", json!({})).unwrap(); + assert_eq!(font(&app), "Inter"); +} + +/// Opposite wheel notches in one frame apply in order, each with its own undo step. +#[test] +fn opposite_wheel_notches_in_one_frame_keep_their_undo_steps() { + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + let id = selected_inter(&mut app); + let draw = |app: &mut VectorcraftApp, ui: &mut egui::Ui| crate::panels::character::show(app, ui); + let mut field = None; + for _ in 0..3 { + field = panel_frame(&ctx, &mut app, vec![], "Inter", &draw).0; + } + let field = field.expect("the Character panel shows the family field"); + let before = app.session.active().unwrap().history.undo.len(); + let line = egui::MouseWheelUnit::Line; + panel_frame( + &ctx, + &mut app, + vec![egui::Event::PointerMoved(field), wheel(line, -1.0, Default::default()), wheel(line, 1.0, Default::default())], + "Inter", + &draw, + ); + assert_eq!(first_style(&app, id).font_family, "Inter"); + assert_eq!(app.session.active().unwrap().history.undo.len(), before + 2); + app.run("edit.undo", json!({})).unwrap(); + assert_ne!(first_style(&app, id).font_family, "Inter"); + app.run("edit.redo", json!({})).unwrap(); + assert_eq!(first_style(&app, id).font_family, "Inter"); +} + +/// #5253: the Properties panel and the Control bar have the same family field, and the wheel steps +/// the families over it too. +#[test] +fn the_wheel_steps_the_families_in_properties_and_the_control_bar() { + type Draw = fn(&mut VectorcraftApp, &mut egui::Ui); + let places: [(&str, Draw); 2] = [ + ("Properties", |app, ui| crate::panels::properties::type_sections(app, ui)), + ("Control bar", |app, ui| crate::panels::character::control_bar(app, ui)), + ]; + let at = |f: &str| FontDb::global().menu_family_list().iter().position(|x| x == f); + for (place, draw) in places { + let ctx = egui::Context::default(); + crate::theme::install_fonts(&ctx); + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + let id = selected_inter(&mut app); + let mut field = None; + for _ in 0..3 { + field = panel_frame(&ctx, &mut app, vec![], "Inter", &draw).0; + } + let field = field.unwrap_or_else(|| panic!("{place} shows the family field")); + let steps = app.session.active().unwrap().history.undo.len(); + let notch = wheel(egui::MouseWheelUnit::Line, -1.0, Default::default()); + panel_frame(&ctx, &mut app, vec![egui::Event::PointerMoved(field), notch], "Inter", &draw); + let next = first_style(&app, id).font_family; + assert!(at(&next) > at("Inter"), "{place}: {next}"); + assert_eq!(app.session.active().unwrap().history.undo.len(), steps + 1, "{place}"); + } +} + +/// #5253: a family the wheel steps to keeps the type's style when it has that style, else takes the +/// closest it has, as a family picked from the menu does. The menus that only pick a font ignore +/// the wheel. +#[test] +fn a_family_stepped_to_keeps_the_style_it_has() { + let ctx = egui::Context::default(); + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + let id = selected_inter(&mut app); + let db = FontDb::global(); + let bold: Vec = db.menu_family_list().iter().filter(|f| db.styles(f).iter().any(|s| s == "Bold")).take(2).cloned().collect(); + let [from, to] = bold.as_slice() else { panic!("two families with a Bold style: {bold:?}") }; + app.run("text.setStyle", json!({"font": from, "style": "Bold"})).unwrap(); + crate::font_menu::apply(&mut app, &ctx, FontPick::Wheel(vec![to.clone()])); + let s = first_style(&app, id); + assert_eq!((&s.font_family, s.font_style.as_str()), (to, "Bold")); + assert_eq!(crate::font_menu::picked(&mut app, Some(FontPick::Wheel(vec![from.clone()]))), None); + assert_eq!(&first_style(&app, id).font_family, to, "picking menus leave the document alone"); +} + #[test] fn bundled_families_are_classified() { use vectorcraft_text::FontClass; diff --git a/crates/ui-egui/src/widgets.rs b/crates/ui-egui/src/widgets.rs index e8da10df3..0e6bb21a7 100644 --- a/crates/ui-egui/src/widgets.rs +++ b/crates/ui-egui/src/widgets.rs @@ -43,7 +43,7 @@ pub use drag::{ }; #[cfg(test)] use dropdowns::combo_button_id; -pub(crate) use dropdowns::{combo, combo_menu, combo_named}; +pub(crate) use dropdowns::{combo, combo_menu, combo_menu_field, combo_named}; pub use dropdowns::{ dropdown, dropdown_named, dropdown_names, dropdown_names_named, dropdown_with, dropdown_with_named, list_scroll_style, menu_room, menu_scroll, panel_scroll, popover, set_menu_floor, strip_scroll, diff --git a/crates/ui-egui/src/widgets/dropdowns.rs b/crates/ui-egui/src/widgets/dropdowns.rs index 22ae68345..781fd7b78 100644 --- a/crates/ui-egui/src/widgets/dropdowns.rs +++ b/crates/ui-egui/src/widgets/dropdowns.rs @@ -135,6 +135,18 @@ pub(crate) fn combo_menu( width: f32, menu: impl FnOnce(craft_ui::combo::Menu<'_>) -> Option, ) -> Option { + combo_menu_field(ui, id, current, width, menu).1 +} + +/// [`combo_menu`], also returning the closed field's response (the font family field reads the +/// mouse wheel turned over it). +pub(crate) fn combo_menu_field( + ui: &mut Ui, + id: impl std::hash::Hash, + current: &str, + width: f32, + menu: impl FnOnce(craft_ui::combo::Menu<'_>) -> Option, +) -> (Response, Option) { let t = Tokens::get(ui.ctx()); let label = take_previous_label(ui); egui::Frame::NONE @@ -148,7 +160,8 @@ pub(crate) fn combo_menu( egui::Popup::open_id(ui.ctx(), egui::Popup::default_response_id(&resp)); ui.memory_mut(|m| m.stop_text_input()); } - menu(craft_ui::combo::Menu::new(ui, &resp).close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside)) + let shown = menu(craft_ui::combo::Menu::new(ui, &resp).close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside)); + (resp, shown) }) .inner } diff --git a/docs/gaps.md b/docs/gaps.md index cfa4881c1..7c00ad0b9 100644 --- a/docs/gaps.md +++ b/docs/gaps.md @@ -1,9 +1,16 @@ # Gaps -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-03 · **Change:** minor (Symbol editing shows draggable 9-slice guides; G5: dynamic symbols, #4938; estimate 15–26 → 14–24 h) · **Target:** Adobe Illustrator 2026 (30.x; G12: Units › East Asian Type acts, with Q and Ha units; estimate unchanged) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-03 · **Change:** minor (font-family fields accept the mouse wheel, #5253; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; G12: Units › East Asian Type acts, with Q and Ha units; estimate unchanged) · **Target:** Adobe Illustrator 2026 (30.x) ## Feature submission evidence +The closed font-family fields in Character, Properties and Control now accept the +mouse wheel for selected text (#5253). Each effective notch applies one family and +adds one undo step; the list follows saved font filters, ignores search text, and +stops at its ends. These interaction details +are VectorCraft's choices. Scores and estimates are unchanged. + +**Feature evidence last updated:** 2026-11-02. **Feature evidence last updated:** 2026-11-03. The shared assessment status line and revision history are current upstream; this dated section records the submission's feature evidence and revisions without rescoring upstream or claiming a new combined build or runtime measurement. @@ -19,6 +26,7 @@ Artboard Options forwards the active Artboard tool's Scale Artwork with Artboard | Date | Change | Summary | |---|---|---| +| 2026-11-02 | minor | Font-family fields step selected text with the mouse wheel (#5253); no scored gap or estimate reassessed. | | 2026-10-24 | minor | G12: General › Corner Radius is the Rounded Rectangle tool's radius, a change while the tool is active reaches it, and the radius ↑/↓ set while drawing (or a Tool Options value) becomes the preference, within 0–1296 pt (#394, `crates/engine/src/tests/constrain_tools.rs`); no entry closed, estimate unchanged. | | 2026-10-22 | minor | Reconcile upstream documentation after review F-3; preserve every dated revision and the original scaleArt runtime evidence without repeating completed jobs. | | 2026-10-22 | minor | Reconcile upstream documentation after review F-2; preserve every dated revision and the original scaleArt runtime evidence without repeating completed jobs. | diff --git a/docs/roadmap.md b/docs/roadmap.md index a879347a6..bf5f67279 100644 --- a/docs/roadmap.md +++ b/docs/roadmap.md @@ -1,6 +1,6 @@ # Roadmap: milestones and what's next -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-03 · **Change:** minor (Symbol editing shows draggable 9-slice guides; M5: supersampled exports avoid redundant opaque knockout layers; File formats: `.ai`/EPS import fidelity from a user's file audit; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-03 · **Change:** minor (font-family fields accept the mouse wheel, #5253; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) ## Feature submission evidence @@ -145,6 +145,7 @@ separate. Scores and hours unchanged. [Behavior and command details](development | Date | Change | Summary | |---|---|---| +| 2026-11-02 | minor | Font-family fields in Character, Properties and Control step selected text with the wheel (#5253), with one undo step per notch; scores and estimates unchanged. | | 2026-11-03 | minor | Symbol editing shows draggable 9-slice guides (#5280). Guides outside the artwork remain unsupported. Scores and hours remain unchanged. | | 2026-10-30 | patch | M5: opaque solid-fill knockout paths avoid redundant erase/add layers in hard-edge frames and supersampled raster export; exact-pixel regression coverage retains analytical-AA rounding and translucent knockout behavior; scores and estimates unchanged | | 2026-10-30 | minor | M4: G9: `.ai`/EPS editing data: a blend takes the transparency (opacity, blend mode, knockout) in its state block after its record, so its steps knock each other out as the page draws them; a user's file comes in 0.6% from its page instead of 1.3%. Scores and estimates unchanged. | diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index ed3f6b6cc..386418339 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -1,6 +1,6 @@ # Parity with Adobe Illustrator -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-03 · **Change:** minor (SVG ellipse import, inline-graphic kerning, Recolor Artwork displays, and canvas 9-slice guides; Color Effects add-on: Find Nearest Swatch, #4923; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026; Units › East Asian Type acts, with Q and Ha units; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-03 · **Change:** minor (font-family fields accept the mouse wheel, #5253; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026; Units › East Asian Type acts, with Q and Ha units; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x; public release notes through 30.7, August 2026) ## Feature submission evidence @@ -885,6 +885,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - **Manual kerning in right-to-left text:** a manual kerning value on a Hebrew or Arabic character changes the space between it and the next character in reading order, the one drawn to its left, under Auto and Optical kerning. The character before it keeps its place, and the caret between the pair stays beside the next character. The side the space goes on is VectorCraft's own rule. Behavior reference: [Line and character spacing](https://web.archive.org/web/20260916050333/https://helpx.adobe.com/illustrator/using/line-character-spacing.html) ("Kerning is the process of adding or subtracting space between specific pairs of characters."). Test: `rtl_manual_kerning_spaces_the_character_from_the_next_in_reading_order`. - **Manual kerning on inline graphics:** a manual kerning value on an inline graphic's character spaces it from the next character in reading order by the same distance as on a letter of its run's size, in left-to-right, right-to-left and vertical type; Auto and Optical kerning still apply no font pairs to it. This is VectorCraft's own rule. Test: `manual_kerning_on_an_inline_graphic_spaces_it_like_a_letter`. - **Recent font pickers (#3515):** The shared Character, Properties, Glyphs and Find Font family picker lists recently applied families first, once each, in most-recent order and limited by Preferences › Type › Number of Recent Fonts. Search and filters apply to both sections; unavailable families are omitted. Section headings cannot be selected or previewed. Confirmed character-range font edits update the same saved history; previews and failed edits do not. Requirement: [issue #3515](https://github.com/storytold/vectorcraft/issues/3515). +- **Mouse wheel over the font family field (#5253):** In the Character and Properties panels and the Control bar, turning the mouse wheel over the closed font family field applies the next family down the font menu to the selected type (the previous one when turned up), keeping the style when the family has it, else its closest. Each notch is one undo step; leaving the field or Escape keeps the family, and Undo takes it back. The steps follow the menu's saved font filters but ignore its search text, put recent families first, and stop at the first and last family. A turn with a modifier key is left to the panel and the canvas. Behavior reference: the [Illustrator CS6 reference](https://help.adobe.com/archive/en/illustrator/cs6/illustrator_reference.pdf#page=10) describes editing hovered dropdown controls with the mouse wheel; [Find and apply fonts](https://helpx.adobe.com/illustrator/desktop/design-with-text/fonts-and-scripts/find-and-apply-fonts.html) names the family field's places. Applying it to the font field, the order, the ends and the undo steps are VectorCraft's own rules. Tests: `crates/ui-egui/src/tests/font_menu.rs`, `crates/ui-egui/src/font_menu/tests.rs`. - **Type:** Character size, leading, baseline shift and horizontal-scale controls account for object/group transforms (#1034): exact document-point sizes, mixed-value display and keyboard increments, range edits, resettable horizontal stretch, undo/redo and native save/reopen (`tests_typescale`, Character panel tests). Point size is the transformed em-height axis, independent of glyph ink bounds; affine rotation, reflection and shear are retained. Group scaling still scales text; preserving font sizes is a separate enhancement. Behaviour reference: [Transform text](https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/transform-text.html) (public documentation). Text Wrap, Type on a Path effects (Rainbow/Skew/3D Ribbon/Stair Step/Gravity) and Options (Flip, Align to Path, Spacing; OK keeps the type where it is), type on a path's start, centre and end brackets (Selection and Direct Selection drag them to set where the type runs, slide it along its path and, across the path, flip it: #429), Character and Paragraph Styles (override-preserving redefine), paragraph attributes per paragraph (alignment, indents, spacing, hyphenation, hanging punctuation, tab stops, paragraph style, direction, mojikumi and leading model; the Paragraph, Tabs and Paragraph Styles panels act on the paragraphs the Type tool's selection or caret touches, Return continues the paragraph's style, threads and SVG keep them), Area Type Options (rows/columns/inset/first baseline, Text Flow by rows or by columns (#4978), vertical Align Top/Center/Bottom/Justify per row/column, reflowing non-rectangular frames and text wrap), threaded text across any closed shapes, Fit Headline, the Every-line Composer for justified and ragged text (paragraph-wide breaks for an even rag, Single-line per object from the Paragraph panel menu or `text.setFormat {composer}`). Area type resizes like Illustrator's type areas: a bounding-box handle drag (Selection tool) or Area Type Options Width/Height size the frame and the text reflows at its size (threads too); Direct Selection drags a frame corner or edge (`text.reshapeArea`), and an anchor of the path of type on a path outside perspective, the type reflowing along it (`text.reshapePath`, #3980). Point type still scales. Auto Size: the frame height follows the text after every edit, in the same undo step (rectangular frames of horizontal type in one row; columns get the least height that holds them; a height set by hand or threading turns it off), and the "Auto Size New Area Type" preference gives it to new area type. Shrink Text to Fit (beyond Illustrator): overflowing area type scales its sizes, leading and baseline shifts down by the largest factor (in 0.1 pt steps of the first run, down to a minimum percentage) that fits, at layout time so every export sees it. `text.areaOptions` and `document.inspect` report overflow and the shrink factor. The Selection tool shows the type widget beside selected type's bounding box (hollow on point type, filled on area type); double-clicking it converts between them (Type › Convert To Area Type / Convert To Point Type), keeping the text and its styles in place. Variable fonts list every named instance as a style of their family (Figtree Light to Black and their italics, Bahnschrift, Segoe UI Variable), and the instance chosen is shaped, drawn, outlined and measured at its axis settings; PDF and SVG embed it as that instance. The Character panel's Variable Font sliders set each axis (see Fonts). Beyond Illustrator: inline graphics (`text.insertInline`, Type ▸ Insert Inline Symbol) set a document symbol in a run of type as one character that flows, wraps and justifies like a glyph (InDesign-style inline objects, e.g. mana symbols in card rules text): sized to a multiple of the font size, centred on the cap height, opening up tall lines, following Redefine Symbol; a character of its run's size for Character Alignment and Top-to-Top leading, placed by the bidi reordering in right-to-left text (a neutral U+FFFC); drawn on the canvas, kept as live text plus a `` in SVG and as vector art in PDF; Backspace removes it whole. Hebrew and Arabic are laid out right to left with the Unicode bidirectional algorithm (embedded Latin and numbers keep their direction, lines put in visual order after wrapping) in point, area and path type, with caret, selection and arrow keys in visual order; each paragraph's direction comes from its first strong character unless Paragraph Direction sets it (Paragraph panel with Show Indic Options, `text.setFormat {direction}`), and new type aligns to the start of its direction. SVG export outlines right-to-left text. Arabic typefaces: builds made with a craft-fonts checkout that holds its Arabic families (Noto Sans Arabic, the Arabic fallback, plus Cairo, Tajawal, Almarai, IBM Plex Sans Arabic, Noto Kufi Arabic, Amiri, Alexandria, Fustat, Reem Kufi, Aref Ruqaa and Gulzar once craft-fonts adds them, storytold/craft-fonts#11; releases get them when their craft-fonts pin moves) list them in the font menus on desktop; the web build fetches them from beside the wasm (Noto Sans Arabic before start, so Arabic never shows as boxes, the rest in the background). Font Size and Leading are combo boxes in the Character and Properties panels (Font Size in the Control bar too, beside the Character link, font family and style): type, step with ↑/↓ or the spinner, scrub the label, or pick a preset (6–72 pt, shown in the type unit; Leading also Auto), each pick one undo step; Kerning and Tracking list -100…200; a size, leading, kerning or tracking the selected text doesn't share shows blank (#458). Type › Bold and Italic (beyond Illustrator, as page layout apps have them; Cmd+Shift+B / Cmd+Shift+I while the Type tool edits text) switch the selected type to its family's own Bold or Italic face and back, keeping its width (a Narrow face stays Narrow); a family without that face says so (#724). The commands that make or restyle type (and the MCP `add_text` tool) say in their result when area type does not fit its frame, and `add_text` takes Auto Size and Shrink Text to Fit (#1500, fixes #1471). Cmd+Shift+C (Align center) works on Windows and Linux too (#1940). A double-click on area type's bottom middle handle turns on Auto Size (#1940). Print and Print Preview warn about the area type and type on a path that prints without the text that doesn't fit, and give their ids. A tab stop in the Tabs panel drags from where the press landed, a stop just added included (#1487, fixes #1458). Area type in a non-rectangular shape moves a word too wide for the top lines down to the first line that holds it instead of breaking it (#1617, fixes #1615). Stylistic sets (ss01–ss20), stylistic and titling alternates, proportional and lining figures, and superscript and subscript glyphs can be turned on as OpenType features of a character style (`text.setStyle {features}`), and the OpenType panel keeps them (#1611, fixes #1607). Escape while typing ends the edit, keeps the type selected and switches to the Selection tool (#1802, #1686, #1656). Select › Same matches type on its characters' fill and stroke (#1746, #1737). `text.create`, `text.createInPath` and the MCP `add_text` tool take `justify`, so point type can be centred on or end at its point (#1752, #1748). The Character panel menu's Show Font Height Options shows a Set Font Height Reference dropdown under Font Size (`text.setFormat` or `text.setRangeStyle` `{fontHeightReference}`). With Em Box, the default, the font size is the size of the em square; with Cap Height, x-Height or ICF Box, the characters are set at the em size at which the font's OS/2 cap height, its x height or the height of its ideographic character face equals the font size. With a font that has no such value, or one outside 0.1 to 2 em, the size measures the em box. SVG, PDF and legacy `.ai` export and the CSS Properties rules write the em size the characters are drawn at, and DXF export writes 0.7 times that em size as each TEXT entity's height (group 40, the height of its capitals). Behavior reference: [Vary font height](https://web.archive.org/web/20251118174722/https://helpx.adobe.com/illustrator/desktop/design-with-text/edit-format-text/vary-font-height.html). The Every-line Composer composes a paragraph whose characters differ in line metrics (type size, font, leading or vertical scale, or an inline graphic taller than the text) in rectangular area type, rows and columns included. In area type of another shape, or with text wrap, such a paragraph is broken with the Single-line Composer. @@ -1543,6 +1544,7 @@ A double-click on a Repeat (Radial, Grid or Mirror) with the Selection tool ente | Date | Change | Summary | |---|---|---| +| 2026-11-02 | minor | Wheel input over closed font-family fields changes selected text (#5253); public documentation and application choices distinguished; scores and estimates unchanged. | | 2026-11-03 | minor | SVG imports preserve exported ellipses as four-anchor paths (#5243). Inline graphics use manual kerning (#5245). Symbol editing shows draggable 9-slice guides (#5280). Recolor Artwork offers smooth and segmented wheels and color bars (#5249). Scores and hours remain unchanged. | | 2026-11-01 | minor | Repeat: a double-click isolates it to edit its source art, text included (#5054); evidence added; scores unchanged. | | 2026-11-01 | minor | Live Corners: Up/Down while dragging a corner widget step the corner kind (#2898); evidence added; scores unchanged. | diff --git a/docs/type-parity.md b/docs/type-parity.md index 9c0b7d400..9ecf0f166 100644 --- a/docs/type-parity.md +++ b/docs/type-parity.md @@ -1,6 +1,6 @@ # Type parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-01 · **Change:** minor (the Type tools make area type or type on a path from a clicked path; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-02 · **Change:** minor (font-family fields accept the mouse wheel, #5253; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x) Type is Illustrator's largest single area by use (weight 13 of 105 across "Type core" and "Type advanced" in [target-app-parity.md](target-app-parity.md)). This checklist lists the features one by one. Status is from the @@ -18,6 +18,7 @@ against Illustrator's public documentation; **estimated** unless noted. | Feature | Status | Notes | |---|---|---| +| Mouse wheel over the closed font-family field | done | #5253: Character, Properties and Control step selected text through the family list using saved font filters (excluding search text), one undo step per notch; the style is matched and the list stops at either end. Escape and leaving the field keep the change. Order, bounds and Undo grouping are VectorCraft's choices; scores unchanged. | | Soft return (forced line break, Shift+Enter) | done | Shift+Enter inserts a soft return (U+2028), breaking the line without starting a new paragraph; paragraph spacing (space before/after) and first-line indents apply only at paragraph ends; continuation lines in lists do not repeat the marker; soft return lines in justified text do not spread (#2754) | | Native font-family labels | partial | With Show Font Names in English off, localized legacy names are used when typographic names contain no native label (#2208); loaded-font and catalog-only regressions. Choosing among multiple native languages is unchanged. | | Point, area and type-on-a-path type; vertical variants | done / partial | vertical point, area and path type have initial support; fullwidth `!` / `?` keep the nominal glyph upright when no vertical alternate exists (Unicode Tu); in a non-rectangular area a word too wide for the top lines moves down to the first line that holds it (#1615); Escape while typing ends the edit, keeps the type selected and switches to the Selection tool (#1802); `text.create` and `text.createInPath` take `justify` (#1752); MCP `add_text` forwards `vertical` to both commands; the Type and Vertical Type tools make area type when a closed path's outline is clicked and type on a path when an open one is, with pointers that say which, as the public help describes ([Create text](https://web.archive.org/web/20220127090642/https://helpx.adobe.com/illustrator/using/creating-text.html), [Create type on a path](https://web.archive.org/web/20220127090635/https://helpx.adobe.com/illustrator/using/creating-type-path.html)) | @@ -75,6 +76,7 @@ Character Styles menu New/Redefine and footer New capture the first selected cha | Date | Change | Summary | |---|---|---| +| 2026-11-02 | minor | The wheel over Character, Properties and Control font-family fields changes selected text, one undo step per notch (#5253); scores and estimates unchanged. | | 2026-11-01 | minor | The Type and Vertical Type tools make area type in a clicked closed path and type on a path along a clicked open one, with their own pointers; each type tool has its own icon; scores unchanged (estimated) | | 2026-10-31 | minor | Units › East Asian Type acts (#394): Q and Ha (a quarter of a millimetre each) are units that preference offers; scores unchanged | | 2026-11-01 | minor | Glyphs panel (#4908): the character selected with the Type tool (a range's first, else the one before the caret) is highlighted in the grid and scrolled into view when the selection moves to another character; Show › Alternates for Current Selection not yet; scores unchanged | diff --git a/docs/ui-parity.md b/docs/ui-parity.md index bb52e6181..ae4eb5626 100644 --- a/docs/ui-parity.md +++ b/docs/ui-parity.md @@ -1,6 +1,6 @@ # UI and interaction parity -> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-03 · **Change:** minor (Symbol editing shows draggable 9-slice guides; Color Effects add-on: Find Nearest Swatch, #4923) · **Target:** Adobe Illustrator 2026 (30.x; Units › East Asian Type acts, with Q and Ha units; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x) +> **Last reviewed:** 2026-10-10 · **Last updated:** 2026-11-03 · **Change:** minor (font-family fields accept the mouse wheel, #5253; scores and estimates unchanged) · **Target:** Adobe Illustrator 2026 (30.x; Units › East Asian Type acts, with Q and Ha units; scores unchanged) · **Target:** Adobe Illustrator 2026 (30.x) ## Feature submission evidence @@ -194,6 +194,7 @@ tools, never checked side by side) isn't captured row by row. | Behaviour | Status | Notes | |---|---|---| +| Wheel over a closed font-family field in Character, Properties or Control | done | #5253: applies the next or previous family to selected text, one undo step per notch; uses saved font filters, ignores search text and stops at the list's ends. Escape and pointer departure keep the change. Other font-picking menus and modifier-wheel input keep their existing behavior. | | Arrow keys nudge by Keyboard Increment (0.001–1296 pt); Shift ×10; Alt copies | done | | | Fields commit what was typed on a click away or a move to another field, as Enter does; Escape discards it | done | #1787 | | Number fields: Up/Down step, Shift ×10, Cmd/Ctrl ÷10, wheel over a focused field; steps, the wheel and label scrubs stop at the field's bounds (a stroke weight at 0, a miter limit at 1) | done | Shift+arrow in the Stroke weight field doesn't step by 10 (#991); bounds #3370 | @@ -400,6 +401,7 @@ separate. Scores and hours unchanged. [Behavior and command details](development | Date | Change | Summary | |---|---|---| +| 2026-11-02 | minor | Font-family fields in Character, Properties and Control accept the wheel without opening the menu (#5253); scores and estimates unchanged. | | 2026-11-03 | minor | Symbol editing draws and drags 9-slice guides on the canvas (#5280). Guides outside the artwork remain unsupported. Scores and hours remain unchanged. | | 2026-11-01 | minor | A double-click on a Repeat with the Selection tool enters isolation mode on it, where its source art (type included: a double-click there edits the text) is edited and every copy follows (#5054); scores and estimates unchanged | | 2026-11-01 | minor | Live Corners: while a corner widget is pressed or dragged with the Selection or Direct Selection tool, the Up and Down arrows step the corners' kind (round, inverted round, chamfer) with the radius, as one undo step (#2898); scores and estimates unchanged |