diff --git a/crates/ui-egui/src/canvas/tests.rs b/crates/ui-egui/src/canvas/tests.rs index df8e53de1..74693a1ca 100644 --- a/crates/ui-egui/src/canvas/tests.rs +++ b/crates/ui-egui/src/canvas/tests.rs @@ -4645,7 +4645,7 @@ fn wheel_over_a_numeric_panel_field_does_not_start_a_canvas_zoom_glide() { /// `ui.wheel`: with Zoom with Mouse Wheel off, Cmd/Ctrl-wheel zooms about the pointer while the /// option is off and scrolls sideways while it is on, and a trackpad pinch, two fingers spread on a touch /// screen, Alt + Cmd/Ctrl-wheel and Alt-wheel still zoom. A Cmd/Ctrl-wheel notch with a plain -/// one right after it scrolls two notches in all, and a trackpad gesture during which Cmd/Ctrl +/// one right after it scrolls two notches in all, and a wheel gesture during which Cmd/Ctrl /// is pressed and released scrolls sideways until the gesture ends, while an Alt gesture that /// pauses past egui's scroll timeout no longer holds Alt for the next move. With Zoom with /// Mouse Wheel also on, Cmd/Ctrl-wheel scrolls sideways and the wheel zooms. @@ -4747,16 +4747,16 @@ fn cmd_wheel_scrolls_sideways_preference() { (zoom - 1.25).abs() < 1e-6 && moved.x > notch - 0.5 && moved.x < 1.25 * notch + 0.5 && moved.y.abs() < 1e-3, "on: a pinch during a Cmd-wheel notch zooms by the pinch, and the notch scrolls sideways ({zoom}, {moved:?})" ); - // A trackpad gesture starts with no modifier, and Cmd is pressed and released during it: - // egui keeps Cmd in its wheel state until the gesture ends, and the canvas keeps scrolling - // sideways until then. + // A gesture egui names as a wheel starts with no modifier, and Cmd is pressed and released + // during it: egui keeps Cmd in its wheel state until the gesture ends, and the canvas keeps + // scrolling sideways until then. let before = *app.view().unwrap(); let swipe = |phase, modifiers| egui::Event::MouseWheel { unit: egui::MouseWheelUnit::Point, delta: vec2(0.0, 6.0), phase, modifiers, - source: egui::MouseWheelSource::Unknown, + source: egui::MouseWheelSource::Wheel, }; app.synthetic.extend([ egui::Event::PointerMoved(at), @@ -4772,7 +4772,7 @@ fn cmd_wheel_scrolls_sideways_preference() { let scroll = (after.center - before.center) * after.zoom; assert!( (after.zoom / before.zoom - 1.0).abs() < 1e-9 && (scroll.x + 12.0).abs() < 0.01 && scroll.y == 0.0, - "on: a trackpad gesture scrolls sideways to its end after Cmd is let go ({}, {scroll:?})", + "on: a wheel gesture scrolls sideways to its end after Cmd is let go ({}, {scroll:?})", after.zoom / before.zoom ); // An Alt-wheel gesture pauses past egui's scroll timeout with no frame in between: egui @@ -4820,6 +4820,79 @@ fn cmd_wheel_scrolls_sideways_preference() { assert!(zooms_in(turn(&mut app, "")), "with Zoom with Mouse Wheel: the wheel zooms in about the pointer"); } +/// With Scroll Sideways with Cmd/Ctrl + Wheel on, Cmd/Ctrl with a touchpad zooms about the pointer +/// while whole line notches scroll sideways: a trackpad or point source, line deltas smaller than +/// a notch, and a whole notch later in the same turn. With Zoom with Mouse Wheel on, Cmd/Ctrl with +/// a touchpad zooms too. +#[test] +fn cmd_with_a_touchpad_zooms_and_a_whole_notch_scrolls_sideways() { + let mut app = VectorcraftApp::new(Session::new(), Default::default()); + app.session.execute("file.new", &json!({"width": 400, "height": 300})).unwrap(); + let ctx = egui::Context::default(); + let mut t = 0.0; + let mut run = |app: &mut VectorcraftApp, events: Vec| { + t += 1.0 / 60.0; + let raw = egui::RawInput { + screen_rect: Some(egui::Rect::from_min_size(Pos2::ZERO, vec2(800.0, 600.0))), + time: Some(t), + events, + ..Default::default() + }; + let mut out = ctx.run_ui(raw, |ui| show(app, ui)); + out.textures_delta.clear(); + }; + run(&mut app, vec![]); + let rect = app.canvas_rect.unwrap(); + let at = rect.center() + vec2(60.0, 40.0); + let ev = |unit, dy: f32, source| egui::Event::MouseWheel { + unit, + delta: vec2(0.0, dy), + phase: egui::TouchPhase::Move, + modifiers: egui::Modifiers::COMMAND, + source, + }; + // Sends each event on its own frame, then lets egui finish and end the turn: + // → (zoom ratio, scroll in points, how far the point under `at` moved). + let mut turn = |app: &mut VectorcraftApp, events: Vec| { + let before = *app.view().unwrap(); + run(app, vec![egui::Event::PointerMoved(at)]); + for e in events { + run(app, vec![e]); + } + for _ in 0..40 { + run(app, vec![]); + } + let after = *app.view().unwrap(); + let moved = Xf::new(rect, &after).to_doc(at).distance(Xf::new(rect, &before).to_doc(at)); + (after.zoom / before.zoom, (after.center - before.center) * after.zoom, moved) + }; + use egui::{MouseWheelSource as S, MouseWheelUnit as U}; + let zooms_in = |(zoom, _, moved): (f64, _, f64)| zoom > 1.0005 && moved < 1e-6; + let sideways = |(zoom, scroll, _): (f64, vectorcraft_geom::Vec2, f64)| (zoom - 1.0).abs() < 1e-9 && scroll.x < -1.0 && scroll.y == 0.0; + assert!(app.session.prefs.ctrl_wheel_scrolls_sideways, "the option is on by default"); + // A wheel message of 30 is a quarter of a 120 notch, and arrives as a quarter line. + assert!(zooms_in(turn(&mut app, vec![ev(U::Line, 0.25, S::Unknown)])), "a line delta smaller than a notch zooms"); + assert!(zooms_in(turn(&mut app, vec![ev(U::Line, 0.25, S::Unknown), ev(U::Line, 0.25, S::Unknown)])), "several small deltas zoom"); + assert!(zooms_in(turn(&mut app, vec![ev(U::Line, 0.25, S::Unknown), ev(U::Line, 1.0, S::Unknown)])), "a whole notch in the same turn zooms"); + assert!(zooms_in(turn(&mut app, vec![ev(U::Point, 6.0, S::Unknown)])), "point deltas of an unnamed source zoom"); + assert!(zooms_in(turn(&mut app, vec![ev(U::Point, 6.0, S::Trackpad)])), "a trackpad scroll zooms"); + assert!(zooms_in(turn(&mut app, vec![ev(U::Point, 6.0, S::Momentum)])), "its momentum zooms"); + assert!(sideways(turn(&mut app, vec![ev(U::Line, 1.0, S::Unknown)])), "a whole notch scrolls sideways"); + assert!(sideways(turn(&mut app, vec![ev(U::Line, 2.0, S::Unknown)])), "a turn of two notches scrolls sideways"); + assert!(sideways(turn(&mut app, vec![ev(U::Line, 0.25, S::Wheel)])), "a part of a line from a named wheel scrolls sideways"); + // A turn ends when the wheel goes idle, so the next whole notch scrolls sideways again. + assert!(zooms_in(turn(&mut app, vec![ev(U::Line, 0.25, S::Unknown)])), "a touchpad turn"); + assert!(sideways(turn(&mut app, vec![ev(U::Line, 1.0, S::Unknown)])), "the notch after it scrolls sideways"); + app.session.execute("prefs.set", &json!({"key": "zoomWithMouseWheel", "value": true})).unwrap(); + assert!(zooms_in(turn(&mut app, vec![ev(U::Line, 0.25, S::Unknown)])), "with Zoom with Mouse Wheel: a small line delta zooms"); + assert!( + zooms_in(turn(&mut app, vec![ev(U::Line, 0.25, S::Unknown), ev(U::Line, 1.0, S::Unknown)])), + "with Zoom with Mouse Wheel: a whole notch in the same turn zooms" + ); + assert!(zooms_in(turn(&mut app, vec![ev(U::Point, 6.0, S::Trackpad)])), "with Zoom with Mouse Wheel: a trackpad scroll zooms"); + assert!(sideways(turn(&mut app, vec![ev(U::Line, 1.0, S::Unknown)])), "with Zoom with Mouse Wheel: a whole notch scrolls sideways"); +} + /// With Scroll Sideways with Cmd/Ctrl + Wheel on and Animated Zoom off, Alt + Cmd-wheel zooms /// about the pointer by egui's Cmd-wheel zoom, as Cmd-wheel does with the option off, and /// Cmd-wheel alone scrolls sideways. diff --git a/crates/ui-egui/src/canvas/view.rs b/crates/ui-egui/src/canvas/view.rs index 39bc80545..3ed6583ae 100644 --- a/crates/ui-egui/src/canvas/view.rs +++ b/crates/ui-egui/src/canvas/view.rs @@ -65,6 +65,9 @@ pub(super) struct WheelTurn { /// When the last wheel event came (`InputState::time`), and whether a trackpad sent it. last: f64, trackpad: bool, + /// A wheel event of this turn came from a touchpad ([`fine_steps`]), so Cmd/Ctrl zooms for the + /// rest of the turn, also through a whole notch. + fine: bool, /// The zoom (natural log) wheel notches still have to glide in. pub(super) glide: f64, } @@ -351,11 +354,26 @@ struct ThumbDrag { max: f32, } +/// Whether a wheel event comes from a touchpad and not from a mouse wheel's notches. egui names +/// the source on the Mac and reports `Unknown` elsewhere. There, point deltas (Wayland) and line +/// deltas that are not whole notches (Windows and X11) count as a touchpad. A browser reports a +/// mouse wheel in points, so the web build counts only line deltas that are not whole notches. +fn fine_steps(unit: egui::MouseWheelUnit, delta: egui::Vec2, source: egui::MouseWheelSource) -> bool { + let notch = |d: f32| (d - d.round()).abs() < 1e-3; + match source { + egui::MouseWheelSource::Trackpad | egui::MouseWheelSource::Momentum => true, + egui::MouseWheelSource::Wheel => false, + egui::MouseWheelSource::Unknown if unit == egui::MouseWheelUnit::Point => !cfg!(target_arch = "wasm32"), + egui::MouseWheelSource::Unknown => !notch(delta.x) || !notch(delta.y), + } +} + /// What the wheel, a pinch and a two-finger drag on a touch screen did over the canvas this frame: /// a zoom factor (about the pointer) and a scroll (screen points the content moves). The wheel scrolls, Cmd- and Alt-wheel (Option /// on the Mac) zoom; with General › Scroll Sideways with Cmd/Ctrl + Wheel, Cmd/Ctrl-wheel scrolls /// sideways instead, and Alt + Cmd/Ctrl-wheel still zooms. With General › Zoom with Mouse Wheel the -/// wheel and Alt-wheel zoom, Shift-wheel scrolls up and down and Cmd/Ctrl-wheel sideways. `line` and `page`: points +/// wheel and Alt-wheel zoom, Shift-wheel scrolls up and down and Cmd/Ctrl-wheel sideways. Cmd/Ctrl +/// with a touchpad ([`fine_steps`]) zooms under either preference. `line` and `page`: points /// per wheel line and page. `turn`: kept by the caller across frames. With Performance › Animated /// Zoom (`animate`) a mouse wheel's notches zoom in a glide over a few frames, as scrolling does /// (#888); off, each notch zooms on the frame it arrives (#4283). A trackpad's fine steps zoom at @@ -385,7 +403,7 @@ pub(super) fn wheel( // `turn.alt` follows `turn.held` after each Start or Move, so a trackpad gesture that held // Alt keeps zooming until it ends, also after Alt is let go. for e in &i.events { - if let egui::Event::MouseWheel { phase, modifiers, source, .. } = e { + if let egui::Event::MouseWheel { unit, delta, phase, modifiers, source, .. } = e { // A wheel event that comes after egui's scroll timeout ends the turn first, as // `on_wheel_event` does (1 s for fingers resting on a trackpad mid-gesture, else // 0.15 s), also when no frame ran in between. @@ -393,9 +411,11 @@ pub(super) fn wheel( if i.time - turn.last > timeout { turn.gesture = false; turn.held = egui::Modifiers::NONE; + turn.fine = false; } turn.last = i.time; turn.trackpad = *source == egui::MouseWheelSource::Trackpad; + turn.fine |= fine_steps(*unit, *delta, *source); match phase { egui::TouchPhase::Start => { turn.gesture = true; @@ -413,6 +433,7 @@ pub(super) fn wheel( egui::TouchPhase::End | egui::TouchPhase::Cancel => { turn.gesture = false; turn.held = egui::Modifiers::NONE; + turn.fine = false; } } } @@ -420,6 +441,7 @@ pub(super) fn wheel( if !i.is_scrolling() { turn.gesture = false; turn.held = egui::Modifiers::NONE; + turn.fine = false; } let (mut zoom, mut scroll) = (f64::from(i.zoom_delta()), i.smooth_scroll_delta); // Scroll Sideways with Cmd/Ctrl + Wheel: while egui's wheel state holds Cmd/Ctrl without @@ -427,7 +449,7 @@ pub(super) fn wheel( // default speed, which the app keeps. The canvas scrolls sideways by those points instead. // A trackpad or web pinch still zooms. With fingers on a touch screen, egui's zoom is the // fingers' alone, and the canvas leaves it as it is. - if prefs.ctrl_wheel_scrolls_sideways && turn.held.command && !turn.held.alt && i.multi_touch().is_none() { + if prefs.ctrl_wheel_scrolls_sideways && turn.held.command && !turn.held.alt && !turn.fine && i.multi_touch().is_none() { let pinch: f64 = i.events.iter().filter_map(|e| if let egui::Event::Zoom(f) = e { Some(f64::from(*f)) } else { None }).product(); let wheel_zoom = zoom / pinch; if wheel_zoom.is_finite() && wheel_zoom > 0.0 { @@ -461,9 +483,15 @@ pub(super) fn wheel( let mut scroll = pan; for e in &i.events { match e { - egui::Event::MouseWheel { unit, delta, modifiers, .. } => { + egui::Event::MouseWheel { unit, delta, modifiers, source, .. } => { let d = points(unit, *delta); - if modifiers.command { + // The turn ends after egui's scroll timeout, as above. + if i.time - turn.last > 0.150 { + turn.fine = false; + } + turn.last = i.time; + turn.fine |= fine_steps(*unit, *delta, *source); + if modifiers.command && !turn.fine { scroll.x += d.x + d.y; } else if modifiers.shift { scroll.y += d.x + d.y; diff --git a/docs/mcp.md b/docs/mcp.md index 8f605eee0..15aadc5b3 100644 --- a/docs/mcp.md +++ b/docs/mcp.md @@ -3995,9 +3995,11 @@ desktop app): in View › Pixel Preview, on softens placed and embedded bitmaps; `disableAutoAddDelete` (off by default): on, the Pen no longer adds or deletes an anchor when a click lands on a segment or an anchor of a selected path. -`ctrlWheelScrollsSideways` (on by default): on, Cmd/Ctrl-wheel scrolls the canvas sideways instead of zooming while -`zoomWithMouseWheel` is off; off, Cmd/Ctrl-wheel zooms about the pointer. Alt + Cmd/Ctrl-wheel and a pinch on a Mac -trackpad, on a touch screen or in the web build still zoom about the pointer. With `zoomWithMouseWheel` off, once a +`ctrlWheelScrollsSideways` (on by default): on, Cmd/Ctrl with a mouse wheel's whole notches scrolls the canvas sideways +instead of zooming while `zoomWithMouseWheel` is off; off, Cmd/Ctrl-wheel zooms about the pointer. Alt + Cmd/Ctrl-wheel +and a pinch on a Mac trackpad, on a touch screen or in the web build still zoom about the pointer. Under either +wheel preference, Cmd/Ctrl zooms about the pointer for the rest of a wheel turn once the turn has a trackpad event, +point deltas from an unnamed source in the desktop app, or line deltas that are not whole notches. With `zoomWithMouseWheel` off, once a trackpad gesture holds Alt, it zooms until it ends, also after Alt is let go. The control channel's `ui.wheel` with `cmd: true` turns the wheel with Cmd/Ctrl held. diff --git a/docs/target-app-parity.md b/docs/target-app-parity.md index 2effc1142..2a0032e35 100644 --- a/docs/target-app-parity.md +++ b/docs/target-app-parity.md @@ -786,7 +786,7 @@ Moved from the ROADMAP's "Shipped so far" on 2026-10-10. Update it when a featur - Preferences that act (#394, in part): Show Tool Tips, Double Click To Isolate, Zoom with Mouse Wheel (the wheel zooms about the pointer, Shift-wheel scrolls up and down, Cmd/Ctrl-wheel sideways; Alt/Option-wheel and a trackpad pinch zoom about the pointer either way), Use Precise Cursors; Selection & Anchor Display › Tolerance (objects, anchors and handles), Object Selection by Path Only, Command Click to Select Objects Behind (cycling down the stack), Zoom to Selection, anchor Size, Highlight anchors on mouse over; Type › Number of Recent Fonts (up to 15); Guides & Grid › guide and grid Color and Style (lines or dots) and Grids In Back. Then Snap to Point (its distance, for moves, drawn points and reference points), Move Locked and Hidden Artwork with Artboard, Enable Rubber Band for the Pen and Curvature tools, Handles style, Show handles when multiple anchors are selected, Hide Corner Widget for angles greater than; General › Transform Pattern Tiles (the transforms' and dialogs' Transform Patterns, Align and Distribute, art moved or duplicated with an artboard, and objects moved whole with direct-selected anchors) and Select Same Tint %; Type › Size/Leading, Tracking and Baseline Shift increments (`type.step`, Cmd+Shift+. and Cmd+Shift+, for the size, the Type tool's Alt+arrows), Enable Missing Glyph Protection and Fill New Type Objects With Placeholder Text. Then Smart Guides › Color, Alignment Guides, Anchor/Path Labels, Measurement Labels, Transform Tools, Object Highlighting (with Smart Guides on) and Snapping Tolerance (4 px by default, for every smart guide snap), then Construction Guides and their Angles (#506: a drawn point near a line at one of those angles through the point it leaves from lands on it). Then Type › Type Object Selection by Path Only (type is picked on the baseline of any of its lines, area type's frame or the path of type on a path, no longer anywhere in its bounds; the Type tools and the Eyedropper still take a click among the characters). Then General › Anti-aliased Artwork (off, the canvas draws the art with hard edges). Then Guides & Grid › Show Pixel Grid (Above 600% Zoom), a line at every document pixel over the art in Pixel Preview, turning with a rotated view as the document grid now does too. Then General › Show The Home Screen When No Documents Are Open (off, closing the last document leaves an empty window; the Home button still opens the screen). Then Units › Numbers Without Units Are Points (on by default: a number typed with no unit into a field in picas is read in points, a typed unit still wins; dimmed unless a unit is Picas). Then User Interface › Auto-Collapse Iconic Panels (off by default: on, a click away from a panel popped out of the icon column puts it away). Then Performance › Animated Zoom (on, with GPU Performance, which it needs and which dims it when off: the Zoom tool dragged sideways zooms about the press, right in and left out, and held still zooms on, Alt out; off, a drag zooms to the area dragged across, now drawn as a marquee), User Interface › Large Tabs (taller document tabs) and Scale Cursor Proportional to UI (off, the tool cursors keep their size whatever the UI Scaling), and File Handling › Display Bitmaps as Anti-aliased Images in Pixel Preview (off by default: Pixel Preview draws images with each pixel taking its nearest image pixel). Type › Auto Size New Area Type works too (new area type gets Auto Size). They reach agents too (`prefs.set`, then `pointer_gesture` or `press_key`; `ui.wheel` on the control channel). Then Smart Guides › Spacing Guides (moves space the selection evenly with its row or column, see Drawing aids). Then Hyphenation › Exceptions (comma-separated words that never break, or break only at their hyphens; `prefs.set` and restore push them into the hyphenator, #394). Then Type › Show Font Names in English (off, font menus label a family by its native-language name when the font has one; documents still use the English family, #394). Then General › Display Print Size at 100% Zoom (off by default: View › Actual Size is one document point per screen point; on, one document inch fills 96 screen points, an inch at the reference density display scaling is set against; `view.actualSize` / `prefs.set`, #394). Then Guides & Grid › Gridline every and Subdivisions (changing either sets the grid of every open document; new and opened documents take that grid too, except a native file and the native document an SVG, PDF, `.ai` or EPS file carries, which open with the grid they were saved with; `file.new`, `document.open`, `file.newFromTemplate`, `file.revert`, #394). Then Appearance of Black › On Screen (Display All Blacks Accurately by default: pure CMYK black shows through the CMYK profile, a dark grey; Display All Blacks as Rich Black shows it on the canvas as RGB black, also in CMYK documents drawn by ink planes, while rich blacks, black tints and images show as before; `RenderOptions::rich_black`, #394). Then Appearance of Black › Printing / Exporting (Output All Blacks Accurately by default; Output All Blacks as Rich Black draws pure CMYK black as RGB black in RGB raster exports and Print as Bitmap; CMYK ink exports and the document stay unchanged; `prefs.blackOutput` / `richBlack`, #394). Then Units › Identify Objects By (Object Name by default: Layers, Variables and Layer Options show and edit free names; XML ID shows and edits AI-style XML ids, `_` for a space and `_xHH_` escapes, and `layer.setProps` / rename refuse invalid ids; the document still stores the object name, #394). Then Type › Highlight Alternate Glyphs (on by default: pink behind OpenType substitutions on screen — ligatures, contextual forms; Document Setup's Highlight Substituted Glyphs stays separate; `prefs.set`, #394). About 14 stored options still do nothing yet (the rest of #394: Open Documents As Tabs, Real-time Drawing and Editing, GPU Performance beyond Animated Zoom, hyphenation language, scratch disks, the legacy clipboard, Touch Workspace). - The desktop app keeps the Color panel's Show Options or Hide Options between sessions, and `ui.colorPanel` sets its mode and options as its panel menu does. - Preferences that act (#394, in part): Show Tool Tips, Double Click To Isolate, Zoom with Mouse Wheel (the wheel zooms about the pointer, Shift-wheel scrolls up and down, Cmd/Ctrl-wheel sideways; Alt/Option-wheel and a trackpad pinch zoom about the pointer either way), Use Precise Cursors; Selection & Anchor Display › Tolerance (objects, anchors and handles), Object Selection by Path Only, Command Click to Select Objects Behind (cycling down the stack), Zoom to Selection, anchor Size, Highlight anchors on mouse over; Type › Number of Recent Fonts (up to 15); Guides & Grid › guide and grid Color and Style (lines or dots) and Grids In Back. Then Snap to Point (its distance, for moves, drawn points and reference points), Move Locked and Hidden Artwork with Artboard, Enable Rubber Band for the Pen and Curvature tools, Handles style, Show handles when multiple anchors are selected, Hide Corner Widget for angles greater than; General › Transform Pattern Tiles (the transforms' and dialogs' Transform Patterns, Align and Distribute, art moved or duplicated with an artboard, and objects moved whole with direct-selected anchors) and Select Same Tint %; Type › Size/Leading, Tracking and Baseline Shift increments (`type.step`, Cmd+Shift+. and Cmd+Shift+, for the size, the Type tool's Alt+arrows), Enable Missing Glyph Protection and Fill New Type Objects With Placeholder Text. Then Smart Guides › Color, Alignment Guides, Anchor/Path Labels, Measurement Labels, Transform Tools, Object Highlighting (with Smart Guides on) and Snapping Tolerance (4 px by default, for every smart guide snap), then Construction Guides and their Angles (#506: a drawn point near a line at one of those angles through the point it leaves from lands on it). Then Type › Type Object Selection by Path Only (type is picked on the baseline of any of its lines, area type's frame or the path of type on a path, no longer anywhere in its bounds; the Type tools and the Eyedropper still take a click among the characters). Then General › Anti-aliased Artwork (off, the canvas draws the art with hard edges). Then Guides & Grid › Show Pixel Grid (Above 600% Zoom), a line at every document pixel over the art in Pixel Preview, turning with a rotated view as the document grid now does too. Then General › Show The Home Screen When No Documents Are Open (off, closing the last document leaves an empty window; the Home button still opens the screen). Then Units › Numbers Without Units Are Points (on by default: a number typed with no unit into a field in picas is read in points, a typed unit still wins; dimmed unless a unit is Picas). Then User Interface › Auto-Collapse Iconic Panels (off by default: on, a click away from a panel popped out of the icon column puts it away). Then Performance › Animated Zoom (on, with GPU Performance, which it needs and which dims it when off: the Zoom tool dragged sideways zooms about the press, right in and left out, and held still zooms on, Alt out; off, a drag zooms to the area dragged across, now drawn as a marquee), User Interface › Large Tabs (taller document tabs) and Scale Cursor Proportional to UI (off, the tool cursors keep their size whatever the UI Scaling), and File Handling › Display Bitmaps as Anti-aliased Images in Pixel Preview (off by default: Pixel Preview draws images with each pixel taking its nearest image pixel). Type › Auto Size New Area Type works too (new area type gets Auto Size). They reach agents too (`prefs.set`, then `pointer_gesture` or `press_key`; `ui.wheel` on the control channel). Then Smart Guides › Spacing Guides (moves space the selection evenly with its row or column, see Drawing aids). Then Hyphenation › Exceptions (comma-separated words that never break, or break only at their hyphens; `prefs.set` and restore push them into the hyphenator, #394). Then Type › Show Font Names in English (off, font menus label a family by its native-language name when the font has one; documents still use the English family, #394). Then General › Display Print Size at 100% Zoom (off by default: View › Actual Size is one document point per screen point; on, one document inch fills 96 screen points, an inch at the reference density display scaling is set against; `view.actualSize` / `prefs.set`, #394). Then Guides & Grid › Gridline every and Subdivisions (changing either sets the grid of every open document; new and opened documents take that grid too, except a native file and the native document an SVG, PDF, `.ai` or EPS file carries, which open with the grid they were saved with; `file.new`, `document.open`, `file.newFromTemplate`, `file.revert`, #394). Then Appearance of Black › On Screen (Display All Blacks Accurately by default: pure CMYK black shows through the CMYK profile, a dark grey; Display All Blacks as Rich Black shows it on the canvas as RGB black, also in CMYK documents drawn by ink planes, while rich blacks, black tints and images show as before; `RenderOptions::rich_black`, #394). Then Appearance of Black › Printing / Exporting (Output All Blacks Accurately by default; Output All Blacks as Rich Black draws pure CMYK black as RGB black in RGB raster exports and Print as Bitmap; CMYK ink exports and the document stay unchanged; `prefs.blackOutput` / `richBlack`, #394). Then Units › Identify Objects By (Object Name by default: Layers, Variables and Layer Options show and edit free names; XML ID shows and edits AI-style XML ids, `_` for a space and `_xHH_` escapes, and `layer.setProps` / rename refuse invalid ids; the document still stores the object name, #394). Then Units › East Asian Type (the bullet below). Then User Interface › Open Documents As Tabs (see Window › Arrange). Three stored options still do nothing yet (the audit below). - - Preferences › General › Scroll Sideways with Cmd/Ctrl + Wheel (on by default; dimmed while Zoom with Mouse Wheel is on): while Zoom with Mouse Wheel is off, Cmd/Ctrl-wheel scrolls the canvas sideways instead of zooming. Alt + Cmd/Ctrl-wheel and a pinch on a Mac trackpad, on a touch screen or in the web build still zoom about the pointer, and once a trackpad gesture holds Alt, it zooms until it ends, also after Alt is let go. + - Preferences › General › Scroll Sideways with Cmd/Ctrl + Wheel (on by default; dimmed while Zoom with Mouse Wheel is on): while Zoom with Mouse Wheel is off, Cmd/Ctrl with a mouse wheel's whole notches scrolls the canvas sideways instead of zooming. Alt + Cmd/Ctrl-wheel and a pinch on a Mac trackpad, on a touch screen or in the web build still zoom about the pointer, and once a trackpad gesture holds Alt, it zooms until it ends, also after Alt is let go. Under either wheel preference, Cmd/Ctrl with a touchpad zooms about the pointer. - Preferences › Type › Highlight Substituted Fonts (on by default) paints pink behind text drawn in a substitute for a missing font in every document, and turning it off removes that highlight everywhere (`prefs.set highlightSubstitutedFonts`); Document Setup keeps Highlight Substituted Glyphs ([help](https://helpx.adobe.com/illustrator/desktop/design-with-text/fonts-and-scripts/edit-text-without-replacing-missing-fonts.html)). - Preferences audit (2026-10-30, #394): every Preferences dialog option is read by shipped code, by its field or its key, except Plug-ins & Scratch Disks › Primary and Secondary, and Clipboard Handling › Legacy vector clipboard and its mode, which are stored only (Units › East Asian Type and User Interface › Open Documents As Tabs, stored only on the day of the audit, act now: East Asian Type since 2026-10-31, Open Documents As Tabs as the Window › Arrange bullet says); [gaps.md G12](gaps.md#g12-preferences-and-panels-that-dont-act-yet) says what each waits on. - Units › East Asian Type (#394, `unitsAsianType`): Points by default; it offers every unit and Q and Ha, a quarter of a millimetre each (0.709 pt: the [Illustrator Scripting Guide](https://ai-scripting.docsforadobe.dev/scripting/measurementUnits/); `q` and `h` are the field suffixes [InDesign's notation table](https://helpx.adobe.com/in/indesign/desktop/layout-and-grid-tools/rulers-and-measure-tools/change-ruler-measurement-units.html) lists), and only it lists them: rulers, Document Setup and the other Units preferences keep their ten. The public pages do not say which fields follow it, so no field uses it yet and that waits for a source. The model stays in points; any length field reads `13q`/`13h`. Evidence: `q_and_ha_are_a_quarter_millimetre`, `east_asian_type_unit_*` (`crates/engine/src/tests/units.rs`).