From 7a9c459e4206c1dbaf18382af8cc6e96642eb9f7 Mon Sep 17 00:00:00 2001 From: bflatastic Date: Fri, 9 Oct 2026 18:31:29 +0700 Subject: [PATCH] Settings: Appearance > UI Scale sizes the whole interface (#284) Settings > Appearance > UI Scale (appearance.uiScale, 75-200 %, default 100) scales the whole interface on top of the display's own scaling, live as it is chosen; Cancel puts it back, OK saves it and a saved scale applies from the first frame. The Settings dialog's choice rows show a number setting with choices as its text. Automation id: settings.appearance.uiScale. egui's own Ctrl+= / Ctrl+- / Ctrl+0 zoom is off now: Ctrl+0 (Window > Project) also reset the zoom behind the app's back, and the scale is a setting. Test: engine tests_settings ui_scale_is_a_percentage_from_75_to_200; ui_panels ui_scale_setting_sizes_the_interface. Co-Authored-By: Claude Opus 5.5 --- crates/engine/src/prefs.rs | 7 +++++ crates/engine/src/tests_settings.rs | 22 ++++++++++++++ crates/ui-egui/src/lib.rs | 4 +++ crates/ui-egui/src/panels/settings.rs | 3 +- crates/ui-egui/tests/ui_panels.rs | 42 +++++++++++++++++++++++++++ docs/parity.md | 2 +- docs/preferences.md | 2 ++ 7 files changed, 80 insertions(+), 2 deletions(-) diff --git a/crates/engine/src/prefs.rs b/crates/engine/src/prefs.rs index 881724c06..f4c8700b3 100644 --- a/crates/engine/src/prefs.rs +++ b/crates/engine/src/prefs.rs @@ -25,6 +25,9 @@ pub const PREFS_FILE: &str = "prefs.json"; /// Settings ▸ General ▸ Language: (label, `general.language` value). pub const LANGUAGES: &[(&str, &str)] = &[("Match System", "system"), ("English", "en"), ("日本語", "ja"), ("简体中文", "zh-hans"), ("繁體中文", "zh-hant")]; +/// Settings ▸ Appearance ▸ UI Scale: (label, `appearance.uiScale` percent). +pub const UI_SCALES: &[(&str, &str)] = &[("75%", "75"), ("100%", "100"), ("125%", "125"), ("150%", "150"), ("175%", "175"), ("200%", "200")]; + /// Settings ▸ Startup & Repair ▸ Window Graphics: (label, `startup.windowGraphics` value). pub const WINDOW_GRAPHICS: &[(&str, &str)] = &[("Automatic", "auto"), ("OpenGL (compatibility)", "gl")]; @@ -140,6 +143,8 @@ page!(Appearance { theme: String = "dark".into(), /// User interface brightness, -1 (darker) … 1 (lighter). brightness: f64 = 0.0, + /// The size of the whole interface in percent (75–200), on top of the display's own scale. + ui_scale: u32 = 100, use_label_color_for_handles: bool = true, use_label_color_for_tabs: bool = true, cycle_mask_colors: bool = true, @@ -454,6 +459,7 @@ impl Prefs { a.interval_minutes = a.interval_minutes.clamp(1, 240); a.max_versions = a.max_versions.clamp(1, 99); self.appearance.brightness = self.appearance.brightness.clamp(-1.0, 1.0); + self.appearance.ui_scale = self.appearance.ui_scale.clamp(75, 200); let defaults = default_labels(); self.labels.truncate(defaults.len()); for d in defaults.iter().skip(self.labels.len()) { @@ -1009,6 +1015,7 @@ pub fn pages() -> Vec { items: vec![ s("appearance.theme", "Theme", Kind::Choice(&[("Dark", "dark"), ("Darker", "darker"), ("Light", "light")]), true), s("appearance.brightness", "Brightness", Kind::Slider(-1.0, 1.0), true), + s("appearance.uiScale", "UI Scale", Kind::Choice(UI_SCALES), true), Section("Labels and Colors"), s("appearance.useLabelColorForHandles", "Use Label Color for Layer Handles and Paths", B, true), s("appearance.useLabelColorForTabs", "Use Label Color for Related Tabs", B, true), diff --git a/crates/engine/src/tests_settings.rs b/crates/engine/src/tests_settings.rs index 52d3fab78..98a49643c 100644 --- a/crates/engine/src/tests_settings.rs +++ b/crates/engine/src/tests_settings.rs @@ -87,6 +87,28 @@ fn prefs_set_validates_keys_types_and_ranges() { assert!(p.reset(Some("bogus")).is_err()); } +/// Settings ▸ Appearance ▸ UI Scale (#284): a percentage, 100 by default, kept within 75–200; +/// the dialog's choices set it as a number. +#[test] +fn ui_scale_is_a_percentage_from_75_to_200() { + let mut p = Prefs::default(); + assert_eq!(p.appearance.ui_scale, 100); + p.set("appearance.uiScale", json!("150")).unwrap(); + assert_eq!(p.appearance.ui_scale, 150); + assert_eq!(p.get("appearance.uiScale"), Some(json!(150))); + p.set("appearance.uiScale", json!(500)).unwrap(); + assert_eq!(p.appearance.ui_scale, 200); + p.set("appearance.uiScale", json!(10)).unwrap(); + assert_eq!(p.appearance.ui_scale, 75); + let page = crate::prefs::pages().into_iter().find(|pg| pg.id == "appearance").unwrap(); + let listed = page.items.iter().any(|i| { + matches!(i, crate::prefs::Item::Setting { key: "appearance.uiScale", kind: crate::prefs::Kind::Choice(c), live: true, .. } if c.contains(&("200%", "200"))) + }); + assert!(listed, "Settings ▸ Appearance offers it"); + // Older settings files without it get the default. + assert_eq!(Prefs::from_json(r#"{"version": 2, "appearance": {"theme": "light"}}"#).appearance.ui_scale, 100); +} + #[test] fn every_schema_key_exists_and_docs_list_the_todo_settings() { let p = Prefs::default(); diff --git a/crates/ui-egui/src/lib.rs b/crates/ui-egui/src/lib.rs index e18264c37..aba240630 100644 --- a/crates/ui-egui/src/lib.rs +++ b/crates/ui-egui/src/lib.rs @@ -373,6 +373,10 @@ impl EffectcraftApp { theme::apply_visuals(ctx, &self.tokens); let tips = p.general.show_tool_tips; ctx.all_styles_mut(|s| s.interaction.tooltip_delay = if tips { 0.5 } else { f32::INFINITY }); + // Settings ▸ Appearance ▸ UI Scale (#284). egui's own Ctrl+= / Ctrl+- / Ctrl+0 zoom stays + // off: Ctrl+0 is Window ▸ Project, and the scale is a setting. + ctx.options_mut(|o| o.zoom_with_keyboard = false); + ctx.set_zoom_factor(p.appearance.ui_scale as f32 / 100.0); self.ui.cache_when_idle = p.previews.cache_frames_when_idle; self.ui.viewer.fast_preview = p.previews.fast_previews; self.frames.set_budget(p.cache_budgets(self.session.sys_memory).preview); diff --git a/crates/ui-egui/src/panels/settings.rs b/crates/ui-egui/src/panels/settings.rs index d69e14350..bd0af97a4 100644 --- a/crates/ui-egui/src/panels/settings.rs +++ b/crates/ui-egui/src/panels/settings.rs @@ -141,7 +141,8 @@ fn page_ui(app: &mut EffectcraftApp, ui: &mut egui::Ui, page: &Page, cur: &Value r } Kind::Choice(opts) => { - let curv = v.as_str().unwrap_or("").to_string(); + // (A number with choices, UI Scale, shows as its text.) + let curv = v.as_str().map_or_else(|| v.to_string(), str::to_string); let shown = opts.iter().find(|o| o.1 == curv).map(|o| o.0).unwrap_or(curv.as_str()).to_string(); egui::ComboBox::from_id_salt(("settings", key)) .selected_text(shown) diff --git a/crates/ui-egui/tests/ui_panels.rs b/crates/ui-egui/tests/ui_panels.rs index 9fd7d94b7..d58c2146d 100644 --- a/crates/ui-egui/tests/ui_panels.rs +++ b/crates/ui-egui/tests/ui_panels.rs @@ -190,6 +190,48 @@ fn leaving_the_learn_workspace_closes_its_tutorials() { } } +/// #284: Settings ▸ Appearance ▸ UI Scale sizes the whole interface as soon as it is chosen, +/// Cancel puts it back, OK keeps it; Ctrl+0 (Window ▸ Project) doesn't reset it. +#[test] +fn ui_scale_setting_sizes_the_interface() { + let mut h = harness(); + let ctx = h.ctx.clone(); + let choose = |h: &mut Harness<'_, EffectcraftApp>, label: &str| { + let ctx = h.ctx.clone(); + effectcraft_ui_egui::menus::invoke(h.state_mut(), &ctx, "app.settings", json!({"page": "appearance"})).unwrap(); + h.run_steps(3); + click(h, "settings.appearance.uiScale"); + let option = h.query_by_label(label).unwrap_or_else(|| panic!("the {label} choice")).rect().center(); + click_at(h, option); + h.run_steps(2); + }; + assert_eq!(ctx.zoom_factor(), 1.0); + choose(&mut h, "150%"); + assert_eq!(h.state().session.prefs.appearance.ui_scale, 150); + assert_eq!(ctx.zoom_factor(), 1.5, "live"); + click(&mut h, "settings.cancel"); + h.run_steps(2); + assert_eq!((h.state().session.prefs.appearance.ui_scale, ctx.zoom_factor()), (100, 1.0), "Cancel put it back"); + choose(&mut h, "125%"); + click(&mut h, "settings.ok"); + h.run_steps(2); + assert_eq!(h.state().dialog, None); + assert_eq!((h.state().session.prefs.appearance.ui_scale, ctx.zoom_factor()), (125, 1.25), "OK kept it"); + // Ctrl+0 opens the Project panel; egui's own Ctrl+0 zoom reset stays out of it. + for pressed in [true, false] { + h.input_mut().events.push(Event::Key { key: egui::Key::Num0, physical_key: None, pressed, repeat: false, modifiers: egui::Modifiers::COMMAND }); + h.step(); + } + h.run_steps(2); + assert_eq!(ctx.zoom_factor(), 1.25); + // A saved scale applies from the first frame. + let mut s = Session::default(); + s.prefs.set("appearance.uiScale", json!(175)).unwrap(); + let mut h2 = Harness::builder().with_size(egui::vec2(1600.0, 1000.0)).build_eframe(|_| EffectcraftApp::new(s)); + h2.run_steps(3); + assert_eq!(h2.ctx.zoom_factor(), 1.75); +} + /// Headless look at the panels (wgpu offscreen; needs a GPU adapter). Run with /// `PANELS_SNAPSHOT=/abs/dir cargo test -p effectcraft-ui-egui --test ui_panels -- --ignored`. #[test] diff --git a/docs/parity.md b/docs/parity.md index 11c97bf86..53be4d855 100644 --- a/docs/parity.md +++ b/docs/parity.md @@ -171,7 +171,7 @@ the disk cache in the formats wave; the 3D Camera Tracker in M12.6.) | Web | 98% | 0.1 | No shared-memory threads inside one engine instance (the decided design is one engine instance per worker; a threaded build needs nightly `build-std`) (M13.30: Render Queue, analyses and Content-Aware Fill render on each job worker's own WebGPU device in passes, particles and Advanced 3D read back under keys, layer buffers in the browser's disk cache; browser storage, Web Audio, Web Worker renders/analyses, WebGPU viewer and offline install landed in M15.2; viewer frames in frame workers fed by project diffs, Roto Brush propagation in a worker, non-blocking `wait: true` jobs and a browser Media Browser (File System Access folders, browser storage) in M13.10; WEB-1 in M13.24: GPU effects and the GPU compositor in the frame workers on their own WebGPU devices with deferred readbacks (frames render in passes; Backend Auto per comp), the disk cache in the Origin Private File System (written by the workers with sync access handles, LRU under the settings' limit, served after a reload), the storage manager (Settings ▸ Disk ▸ Browser Storage, `storage.info` / `storage.persist` / `storage.clear`), and a WGSL constant Chrome rejected (it disabled every GPU kernel in Chrome) fixed) | | 3D | 88% | 5.5 | reflections of other layers (Advanced 3D surfaces reflect only the Environment light; Material Options ▸ Appears in Reflections is stored but has no visible effect yet, #261), multi-view layouts, the Extended Viewer for Advanced 3D comps (Classic 3D Extended Viewer landed in M13.5 UI completion); collapsed precomps of another size seen through the parent's camera render (fixed in M13.2); stereo rigs, orbit nulls, lights controlled by the camera, cameras/lights from glTF models, environment backgrounds, Advanced 3D motion blur, blend modes and track mattes landed in M7.7; Classic 3D iris-shaped bokeh with highlights, progressive depth of field on tilted layers and the focus-link commands landed in M13.5; Advanced 3D depth of field with the iris and highlight options, collapsed precomps as real Advanced 3D geometry and extruded text/shape strokes landed in M13.8; Advanced 3D (glTF/OBJ models, primitives, extruded text and shapes, PBR, image-based light, shadow maps, GPU rasteriser) in M7.4–M7.6; Advanced 3D end to end on the GPU (motion blur, iris depth of field, compositing) in M13.11 | | Effects | ≈ 85% | 3.0 | GPU versions of the remaining effects (280 run on the GPU since M13.29, EFF-5: M13.28 added the remaining pixel effects and the geometry and audio generators (Lightning, Advanced Lightning, Beam, Lens Flare, Radio Waves, Vegas, Stroke, Scribble, Write-on, Paint Bucket, Eyedropper Fill, CC Glue Gun, CC Threads, Audio Spectrum / Waveform, Basic / Path Text); M13.29 added the particle effects (CC Particle World / Systems II, Particle Playground, CC Ball Action, CC Pixel Polly, CC Scatterize, Curl Noise) and removed the fallbacks of Shatter's wireframe views, Foam's extras, Timewarp's Matte Layer, strong Warp Fisheye / Twist bends and custom `.ocio` configs; M13.23 added the 3D Channel and Immersive Video families, Apply Color LUT, the OCIO effects, Color Profile Converter and the simulations' render passes (Card Wipe included); M13.22 added the CC light family, the CC transitions, Block Dissolve, Radial Shadow, CC Bender / Blobbylize / Cylinder / Sphere / Spotlight / Environment, 3D Glasses, Numbers, Timecode and the time effects joined the 166 of M13.13; Fractal and the analysis / tool effects still render on the CPU) and the missing controls listed as partial in [effects.md](effects.md) (every After Effects effect exists since M9.11, M12.5 and M12.6; parameter names, order, twirl-downs, popups, units and defaults were aligned in M9.12) | -| Interface | 75% | 5.0 | more Learn tutorials and pixel-level fidelity of dialogs (G2 on 9 October: the in-window menu bar works from the keyboard (Alt, arrows, Enter, Escape) and the app's shortcuts wait while a menu is open, #279; the Timeline's composition marker bin and numpad * add comp markers, #275; G2 on 7 October: Project items, files and effects dropped on the Composition viewer, Project items dropped in the Timeline between layers and at a time, #85, #88, #89; popup menus moved to fit the window take clicks, so Render Queue templates apply, #117; View ▸ New Viewer landed in M13.32; an automation id on every control of Composition and Solid Settings, Group / Ungroup Shapes in the Layer menu, Window ▸ Learn, the flowchart shortcuts and Reveal in Explorer landed in M3.10 and M3.13; the Home ▸ Templates gallery (eight original built-in templates, user templates from File ▸ Save as Template…) and View ▸ Simulate Output ▸ My Custom RGB… landed in M13.25; Timeline layer reordering by drag, a non-snapping viewer pan, a working rename field and twirl arrows, and the full set of property reveal shortcuts — double presses, Alt+Shift keyframes, Ctrl+` — landed in M13.17–M13.20; the Home ▸ Learn tab with interactive tutorials and a UI fidelity pass landed in M13.5 UI completion; visual editors for Lumetri RGB / hue-saturation curves, Colorama's output cycle wheel, Glow's colour map and Reshape's correspondence points (viewer handles), a scrolling Preview panel and AE-style Composition / Timeline tabs (close, label swatch, viewer lock) landed in M13.10; Timeline outline and Project panel columns scroll horizontally, the Layer Style dialog, ROI resize handles, Pan Behind snapping and 3D Reference Axes landed in M13.5; native macOS menu bar, Timeline columns/search/reveal-add, Home screen with recent projects and all AE workspaces landed; viewer rulers/snapping/channels/snapshots landed in M0.13) | +| Interface | 75% | 5.0 | more Learn tutorials and pixel-level fidelity of dialogs (G2 on 9 October: the in-window menu bar works from the keyboard (Alt, arrows, Enter, Escape) and the app's shortcuts wait while a menu is open, #279; the Timeline's composition marker bin and numpad * add comp markers, #275; Settings ▸ Appearance ▸ UI Scale (75–200 %, beyond After Effects, which follows the system's scaling), #284; G2 on 7 October: Project items, files and effects dropped on the Composition viewer, Project items dropped in the Timeline between layers and at a time, #85, #88, #89; popup menus moved to fit the window take clicks, so Render Queue templates apply, #117; View ▸ New Viewer landed in M13.32; an automation id on every control of Composition and Solid Settings, Group / Ungroup Shapes in the Layer menu, Window ▸ Learn, the flowchart shortcuts and Reveal in Explorer landed in M3.10 and M3.13; the Home ▸ Templates gallery (eight original built-in templates, user templates from File ▸ Save as Template…) and View ▸ Simulate Output ▸ My Custom RGB… landed in M13.25; Timeline layer reordering by drag, a non-snapping viewer pan, a working rename field and twirl arrows, and the full set of property reveal shortcuts — double presses, Alt+Shift keyframes, Ctrl+` — landed in M13.17–M13.20; the Home ▸ Learn tab with interactive tutorials and a UI fidelity pass landed in M13.5 UI completion; visual editors for Lumetri RGB / hue-saturation curves, Colorama's output cycle wheel, Glow's colour map and Reshape's correspondence points (viewer handles), a scrolling Preview panel and AE-style Composition / Timeline tabs (close, label swatch, viewer lock) landed in M13.10; Timeline outline and Project panel columns scroll horizontally, the Layer Style dialog, ROI resize handles, Pan Behind snapping and 3D Reference Axes landed in M13.5; native macOS menu bar, Timeline columns/search/reveal-add, Home screen with recent projects and all AE workspaces landed; viewer rulers/snapping/channels/snapshots landed in M0.13) | | Project | ≈ 68% | 5.5 | OCIO displays beyond the built-in tone map; relative footage paths and relinking when a project moves (unsaved-changes prompts, a modified mark that follows undo and project files that name their version and always reopen landed in M3.9 and M3.14; auto-save and crash recovery in M3.7; Color Engine with OCIO/ACES working spaces, HDR compand/tone mapping, Rec. 2100 PQ/HLG output, Feet + Frames, display colour management, Simulate Output and the locked viewer landed in M7.7; proxies and Interpret Footage fields / pixel aspect / alpha guess landed: PRJ-8, PRJ-3) | | Masks & roto | 74% | 5.0 | (audit estimates; Roto Brush 2.0 / 3.0's trained model, MobileSAM in the swappable `segment` module, has since landed in M13.35; variable-width mask feather points with the Mask Feather tool landed in M13.5; mask tracking and Mask Interpolation landed in M6.6; Roto Brush & Refine Edge with graph-cut segmentation, flow propagation, edge matting, decontamination and Freeze in M6.7) | | Preview | 77% | 2.8 | GPU effects for the audio-driven effects and the stepped particle systems; content-keyed RAM preview frames (an edit drops every comp's), audio scrubbing, more snapshot slots (G2 on 7 October: Audio, Lock and Shy switches keep the cached frames and playback with audio waits for frames instead of skipping them, #103; running out of video memory renders the frame on the CPU and keeps fewer GPU frames instead of crashing, #106; RAM preview keys with the render options, least-recently-shown eviction, Auto resolution while playing, Cache Before Playback that fits, an achieved-fps readout, Cache Frames When Idle and purges that clear what they name landed in M4.5–M4.8; 3D channel, VR, OCIO, Card Wipe and the simulations' render passes run on the GPU since M13.23; Advanced 3D layers with blend modes / track mattes / Preserve Transparency and environment backgrounds composite on the GPU since M13.22; pooled GPU textures and fused quantisation since M13.13: Lower Third GPU warm 21 → 4 ms/frame, and Auto now picks the CPU or the GPU per comp from measured frame times; Advanced 3D runs — raster, motion blur, iris depth of field, compositing — and wireframes run on the GPU since M13.11: CPU warm 3394 → GPU warm 290 ms/frame at 1920×1080 (11.7×; Half 968 → 85 ms), GPU ≠ CPU on 0.005 % of pixels, on an M4 Pro under load; Classic 3D bokeh depth of field — iris shapes, highlights, fringe, progressive blur on tilted planes — runs in WGSL since M13.6: 3D Showcase GPU warm 205 → 54 ms/frame at full size on an M4 Pro; the Preview panel's five shortcuts with their own Include / Loop / Cache Before Playback / Range / Play From / Frame Rate / Skip / Resolution / Full Screen / stop options and `playback.settings.get/set` landed in M13.8; Classic 3D runs and adjustment layers composite on the GPU since M12.7; persistent disk cache with the blue cache bar landed; region of interest, snapshots, exposure and Fast Previews landed in M0.13) | diff --git a/docs/preferences.md b/docs/preferences.md index a51ac97d7..df948f0da 100644 --- a/docs/preferences.md +++ b/docs/preferences.md @@ -56,6 +56,8 @@ Commands (CLI, MCP, control channel): - `previews.fastPreviews`: Fast Previews (Draft 3D, Faster Effects) - `appearance.theme`: Theme - `appearance.brightness`: Brightness +- `appearance.uiScale`: UI Scale, the size of the whole interface (75–200 %, on top of the + display's own scaling), applied as soon as it is chosen - `appearance.useLabelColorForHandles`: Use Label Color for Layer Handles and Paths - `grids.gridColor`: Color - `grids.gridSpacing`: Gridline Every