a11y: magnifier: change zoom stepping to multiplicative, bound max, add presets

1. Change magnifier zoom stepping from additive to multiplicative,
   because zoom is a ratio, not a difference, and human perception is
   logarithmic (browser zoom steps, map zoom levels, microscope powers).

2. Add `pub const ZoomConfig::ZOOM_INCREMENT_PRESETS` with two
   additional presets: 10% (macOS, Cinnamon) and 75% (for users at
   70-90x zoom).

3. Bound the zoom level to a generous maximum, beyond what anyone is expected
   to need, to prevent runaway / unrecoverable levels (#1230).

Full background and rationale at #2615.

I used AI to assist with research and issue writeup. Code is 100% mine.

- Closes #2615.
- Fixes #1239.
- Partially addresses #1230 and #1237.
This commit is contained in:
Tom Grushka 2026-07-22 00:50:14 -06:00 • committed by Victoria Brekenfeld
parent f20de50e41
commit 738e0b3124
3 changed files with 16 additions and 4 deletions

View file

@ -198,6 +198,10 @@ pub struct ZoomConfig {
pub enable_mouse_zoom_shortcuts: bool, pub enable_mouse_zoom_shortcuts: bool,
} }
impl ZoomConfig {
pub const ZOOM_INCREMENT_PRESETS: &[u32] = &[10, 25, 50, 75, 100, 150, 200];
}
impl Default for ZoomConfig { impl Default for ZoomConfig {
fn default() -> Self { fn default() -> Self {
ZoomConfig { ZoomConfig {

View file

@ -29,6 +29,8 @@ use std::{os::unix::process::CommandExt, thread};
use super::gestures; use super::gestures;
const MAX_ZOOM: f64 = 256.0;
fn propagate_by_default(action: &shortcuts::Action) -> bool { fn propagate_by_default(action: &shortcuts::Action) -> bool {
matches!( matches!(
action, action,
@ -1116,7 +1118,13 @@ impl State {
} }
if zoom_seat == *seat { if zoom_seat == *seat {
let new_level = (current_level + change).max(1.0); let factor = 1.0 + change.abs();
let new_level = if change < 0. {
current_level / factor
} else {
current_level * factor
}
.clamp(1.0, MAX_ZOOM);
shell.trigger_zoom( shell.trigger_zoom(
seat, seat,
Some(&output), Some(&output),

View file

@ -7,7 +7,7 @@ use cosmic::{
theme, theme,
widget::{self, icon::Named}, widget::{self, icon::Named},
}; };
use cosmic_comp_config::ZoomMovement; use cosmic_comp_config::{ZoomConfig, ZoomMovement};
use cosmic_config::ConfigSet; use cosmic_config::ConfigSet;
use keyframe::{ease, functions::Linear}; use keyframe::{ease, functions::Linear};
use smithay::{ use smithay::{
@ -447,7 +447,7 @@ pub enum MenuMessage {
impl ZoomProgram { impl ZoomProgram {
pub fn new(level: f64, movement: ZoomMovement, increment: u32) -> Self { pub fn new(level: f64, movement: ZoomMovement, increment: u32) -> Self {
let mut increments = vec![25, 50, 100, 150, 200]; let mut increments = ZoomConfig::ZOOM_INCREMENT_PRESETS.to_vec();
if !increments.contains(&increment) { if !increments.contains(&increment) {
increments.push(increment); increments.push(increment);
} }
@ -828,7 +828,7 @@ impl Program for ZoomProgram {
if let Some(pos) = self.increments.iter().position(|val| *val == increment) { if let Some(pos) = self.increments.iter().position(|val| *val == increment) {
self.increment_idx = pos; self.increment_idx = pos;
} else { } else {
let mut increments = vec![25, 50, 100, 150, 200]; let mut increments = ZoomConfig::ZOOM_INCREMENT_PRESETS.to_vec();
if !increments.contains(&increment) { if !increments.contains(&increment) {
increments.push(increment); increments.push(increment);
} }