Merge main into #2191

This commit is contained in:
bflatastic
2026-11-12 12:42:24 +07:00
9 changed files with 115 additions and 4 deletions
+21
View File
@@ -103,3 +103,24 @@ fn a_settings_file_without_or_with_a_wrong_interface_size_reads_as_the_systems_s
let p: crate::Prefs = serde_json::from_value(json!({"ui_scale": 99, "theme": "dark"})).unwrap();
assert_eq!((p.ui_scale, p.theme.as_str()), (crate::UI_SCALE_MAX, "dark"));
}
#[test]
fn the_invert_zoom_preference_toggles_and_is_saved() {
let mut s = Session::new();
assert!(!s.prefs.invert_zoom, "off by default");
assert_eq!(s.execute("pref.invertZoom", &json!({})).unwrap()["on"], true);
assert!(s.prefs.invert_zoom);
let saved = serde_json::to_value(&s.prefs).unwrap();
assert_eq!(saved["invert_zoom"], true);
let loaded: crate::Prefs = serde_json::from_value(saved).unwrap();
assert!(loaded.invert_zoom);
assert_eq!(s.execute("pref.invertZoom", &json!({"on": false})).unwrap()["on"], false);
assert!(!s.prefs.invert_zoom);
let saved = serde_json::to_value(&s.prefs).unwrap();
assert_eq!(saved["invert_zoom"], false);
let loaded: crate::Prefs = serde_json::from_value(saved).unwrap();
assert!(!loaded.invert_zoom, "off persists after being on");
// Preferences saved before the setting existed read it as off.
let old: crate::Prefs = serde_json::from_value(json!({"theme": "dark"})).unwrap();
assert!(!old.invert_zoom);
}
+7
View File
@@ -434,6 +434,13 @@ pub fn specs() -> Vec<CommandSpec> {
false,
|s, p| pref(s, p, |x| &mut x.scroll_wheel_zoom)
),
spec!(
"pref.invertZoom",
"Invert zoom direction",
"{on?} reverses wheel zoom: Cmd/Ctrl+wheel, and a plain mouse wheel with Scroll wheel zoom on, zoom out where they would zoom in (a trackpad pinch and panning are unchanged) → {on}",
false,
|s, p| pref(s, p, |x| &mut x.invert_zoom)
),
spec!("pref.reduceMotion", "Reduce motion", "{on?}", false, |s, p| pref(s, p, |x| &mut x.reduce_motion)),
spec!(
"pref.uiScale",
+4
View File
@@ -36,6 +36,9 @@ pub struct Prefs {
/// A mouse wheel zooms the canvas about the pointer, and Cmd/Ctrl+wheel scrolls it (off: the
/// other way round). Trackpads are unchanged.
pub scroll_wheel_zoom: bool,
/// Wheel zoom goes the other way: Cmd/Ctrl+wheel (and a plain mouse wheel with Scroll wheel
/// zoom on) zooms out where it would zoom in. A trackpad pinch is unchanged.
pub invert_zoom: bool,
/// "system" by default; an explicit "light" or "dark" choice stays saved.
pub theme: String,
/// The widths of the main window's left and right panels (the interface keeps them within its
@@ -136,6 +139,7 @@ impl Default for Prefs {
number_keys_opacity: true,
show_dimensions: true,
scroll_wheel_zoom: false,
invert_zoom: false,
theme: "system".into(),
panel_widths: (240.0, 240.0),
pages_height: None,
+1
View File
@@ -886,6 +886,7 @@ name = "English"
"Pasted design layers; the clipboard didn't carry the images of: {layers}" = "Pasted design layers; the clipboard didn't carry the images of: {layers}"
"Remove these results from the file" = "Remove these results from the file"
"Scroll wheel zoom" = "Scroll wheel zoom"
"Invert zoom direction" = "Invert zoom direction"
"Shared by your team on {server}" = "Shared by your team on {server}"
"Slot name" = "Slot name"
"Mar" = "Mar"
+2
View File
@@ -36,6 +36,7 @@ status = "draft — needs native review"
"Couldn't download the variables" = "No se pudieron descargar las variables"
"Couldn't download {name}" = "No se pudo descargar {name}"
"Create from color" = "Crear a partir del color"
"Invert zoom direction" = "Invertir la dirección del zoom"
"New collection" = "Nueva colección"
"Delete results" = "Eliminar resultados"
"Pasted design layers" = "Capas de diseño pegadas"
@@ -2216,6 +2217,7 @@ status = "draft — needs native review"
"plugins.runLast" = "Ejecutar el último plugin"
"pref.autoSave" = "Guardar archivos automáticamente"
"pref.highlightHover" = "Resaltar capas al pasar el cursor"
"pref.invertZoom" = "Invertir la dirección del zoom"
"pref.keepTool" = "Mantener la herramienta tras usarla"
"pref.language" = "Idioma de la interfaz"
"pref.nudge" = "Cantidad de desplazamiento"
+10 -2
View File
@@ -81,10 +81,18 @@ fn wheel_zoom(app: &mut FigApp, ctx: &egui::Context, zoom: f32, shift: bool) ->
if let Some(mouse) = latest {
app.canvas.wheel_is_mouse = mouse;
}
let invert = app.session.prefs.invert_zoom && !pinch;
if !app.session.prefs.scroll_wheel_zoom || !app.canvas.wheel_is_mouse || pinch {
return (wheel, zoom);
return (wheel, if invert { invert_zoom_factor(zoom) } else { zoom });
}
swap_wheel(wheel, zoom, shift, speed)
let (scroll, zoom) = swap_wheel(wheel, zoom, shift, speed);
(scroll, if invert { invert_zoom_factor(zoom) } else { zoom })
}
/// The zoom factor for the opposite direction (Preferences ▸ Invert zoom direction): its
/// reciprocal. A factor that is zero, negative or not finite is left as it is.
pub(crate) fn invert_zoom_factor(zoom: f32) -> f32 {
if zoom.is_finite() && zoom > 0.0 { 1.0 / zoom } else { zoom }
}
/// Whether a scroll event came from a mouse wheel. Only macOS says which device scrolled; elsewhere
+66 -1
View File
@@ -6,7 +6,7 @@ use egui::{Event, Modifiers, MouseWheelSource, MouseWheelUnit, Pos2, RawInput, R
use figcraft_engine::Session;
use serde_json::json;
use super::input::{is_mouse_wheel, swap_wheel};
use super::input::{invert_zoom_factor, is_mouse_wheel, swap_wheel};
use crate::{FigApp, NoServices};
/// The view's zoom, cx and cy.
@@ -25,8 +25,13 @@ fn wheel(unit: MouseWheelUnit, dy: f32, modifiers: Modifiers) -> Event {
impl Canvas {
fn new(scroll_wheel_zoom: bool) -> Self {
Self::with_invert(scroll_wheel_zoom, false)
}
fn with_invert(scroll_wheel_zoom: bool, invert_zoom: bool) -> Self {
let mut c = Self { ctx: egui::Context::default(), app: FigApp::new(Session::new(), Box::new(NoServices)), time: 0.0 };
c.app.run("pref.scrollWheelZoom", json!({"on": scroll_wheel_zoom}));
c.app.run("pref.invertZoom", json!({"on": invert_zoom}));
for _ in 0..3 {
c.frame(vec![], Modifiers::NONE);
}
@@ -126,3 +131,63 @@ fn the_swap_keeps_egui_rates() {
assert_eq!(swap_wheel(vec2(40.0, 0.0), 1.0, true, speed), (vec2(40.0, 0.0), 1.0));
assert_eq!(swap_wheel(Vec2::ZERO, 1.0, false, speed), (Vec2::ZERO, 1.0));
}
#[test]
fn invert_zoom_reverses_cmd_or_ctrl_wheel_by_the_same_factor() {
for mods in [Modifiers::COMMAND, Modifiers::CTRL] {
let (mut off, mut on) = (Canvas::with_invert(false, false), Canvas::with_invert(false, true));
let (a, b, _) = off.gesture(wheel(MouseWheelUnit::Line, 3.0, mods), mods);
let (c, d, _) = on.gesture(wheel(MouseWheelUnit::Line, 3.0, mods), mods);
assert!(b.0 > a.0 * 1.05, "{mods:?} off zooms in: {a:?} -> {b:?}");
assert!(d.0 < c.0 / 1.05, "{mods:?} on zooms out: {c:?} -> {d:?}");
assert!(((b.0 / a.0) * (d.0 / c.0) - 1.0).abs() < 1e-3, "{mods:?} by the same factor");
}
}
#[test]
fn invert_zoom_reverses_a_plain_mouse_wheel_with_scroll_wheel_zoom_on() {
let (mut off, mut on) = (Canvas::with_invert(true, false), Canvas::with_invert(true, true));
let (a, b, _) = off.gesture(wheel(MouseWheelUnit::Line, 3.0, Modifiers::NONE), Modifiers::NONE);
let (c, d, _) = on.gesture(wheel(MouseWheelUnit::Line, 3.0, Modifiers::NONE), Modifiers::NONE);
assert!(b.0 > a.0 * 1.05, "off zooms in: {a:?} -> {b:?}");
assert!(d.0 < c.0 / 1.05, "on zooms out: {c:?} -> {d:?}");
assert!(((b.0 / a.0) * (d.0 / c.0) - 1.0).abs() < 1e-3, "by the same factor");
}
/// Panning is never inverted: the scroll vector is the same with the preference on and off.
#[test]
fn invert_zoom_leaves_panning_unchanged() {
// (Scroll wheel zoom, modifiers): a plain wheel without it, Cmd/Ctrl+wheel with it, Shift+wheel.
for (swz, mods) in [(false, Modifiers::NONE), (true, Modifiers::COMMAND), (true, Modifiers::CTRL), (true, Modifiers::SHIFT)] {
let (mut off, mut on) = (Canvas::with_invert(swz, false), Canvas::with_invert(swz, true));
let (a, b, _) = off.gesture(wheel(MouseWheelUnit::Line, -3.0, mods), mods);
let (c, d, _) = on.gesture(wheel(MouseWheelUnit::Line, -3.0, mods), mods);
assert!((b.0 - a.0).abs() < 1e-9 && (d.0 - c.0).abs() < 1e-9, "{mods:?} no zoom");
assert!(b.1 != a.1 || b.2 != a.2, "{mods:?} off pans: {a:?} -> {b:?}");
assert!(
((b.1 - a.1) - (d.1 - c.1)).abs() < 1e-9 && ((b.2 - a.2) - (d.2 - c.2)).abs() < 1e-9,
"{mods:?} same pan: {a:?} -> {b:?} vs {c:?} -> {d:?}"
);
}
}
#[test]
fn a_pinch_zooms_the_same_with_invert_zoom_on_and_off() {
for (swz, invert, factor) in
[(false, false, 1.5), (false, true, 1.5), (true, false, 1.5), (true, true, 1.5), (true, true, 0.5), (false, true, 0.5)]
{
let mut c = Canvas::with_invert(swz, invert);
let (a, b, _) = c.gesture(Event::Zoom(factor), Modifiers::NONE);
assert!((b.0 / a.0 - f64::from(factor)).abs() < 1e-3, "scroll wheel zoom {swz}, invert {invert}, x{factor}: {a:?} -> {b:?}");
}
}
#[test]
fn the_inverted_factor_is_the_reciprocal_and_guards_bad_values() {
assert!((invert_zoom_factor(2.0) - 0.5).abs() < 1e-6);
assert_eq!(invert_zoom_factor(1.0), 1.0);
for bad in [0.0, -1.0, f32::NAN, f32::INFINITY] {
let r = invert_zoom_factor(bad);
assert!(r == bad || (r.is_nan() && bad.is_nan()), "{bad} left alone");
}
}
+1
View File
@@ -246,6 +246,7 @@ pub fn show(app: &mut FigApp, ctx: &Context) {
(tr("Show dimensions on objects"), prefs.show_dimensions, "pref.showDimensions"),
(tr("Use number keys for opacity"), prefs.number_keys_opacity, "pref.numberKeysOpacity"),
(tr("Scroll wheel zoom"), prefs.scroll_wheel_zoom, "pref.scrollWheelZoom"),
(tr("Invert zoom direction"), prefs.invert_zoom, "pref.invertZoom"),
(tr("Reduce motion"), prefs.reduce_motion, "pref.reduceMotion"),
] {
if crate::widgets::checkbox(ui, val, label) {
+3 -1
View File
File diff suppressed because one or more lines are too long