From 0ba0a0cdaa0d2714bfb97b8efe7c67e9e8be48f0 Mon Sep 17 00:00:00 2001 From: Victoria Brekenfeld Date: Tue, 25 Mar 2025 17:31:48 +0100 Subject: [PATCH] a11y/zoom: zoom level per output --- src/backend/mod.rs | 9 ++- src/backend/render/mod.rs | 4 +- src/input/actions.rs | 4 +- src/input/mod.rs | 2 +- src/shell/focus/order.rs | 2 +- src/shell/grabs/menu/mod.rs | 8 +-- src/shell/mod.rs | 118 ++++++++++++++++------------------- src/shell/zoom.rs | 98 ++++++++++++++++++----------- src/utils/geometry.rs | 17 ++++- src/utils/prelude.rs | 6 +- src/wayland/handlers/a11y.rs | 3 +- 11 files changed, 154 insertions(+), 117 deletions(-) diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 86816171..66fdf271 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -79,7 +79,14 @@ pub fn init_backend_auto( .accessibility_zoom .start_on_login { - state.update_zoom(&initial_seat, 1.0, true); + state.common.shell.write().unwrap().trigger_zoom( + &initial_seat, + None, + 1.0 + (state.common.config.cosmic_conf.accessibility_zoom.increment as f64 / 100.), + &state.common.config.cosmic_conf.accessibility_zoom, + true, + &state.common.event_loop_handle, + ); } let desired_numlock = state diff --git a/src/backend/render/mod.rs b/src/backend/render/mod.rs index 2e6f0895..348525e7 100644 --- a/src/backend/render/mod.rs +++ b/src/backend/render/mod.rs @@ -433,7 +433,7 @@ where .map(|state| { ( state.animating_focal_point(Some(&output)).to_local(&output), - state.animating_level(), + state.animating_level(&output), ) }) .unwrap_or_else(|| ((0., 0.).into(), 1.)); @@ -741,7 +741,7 @@ where .map(|state| { ( state.animating_focal_point(Some(&output)).to_local(&output), - state.animating_level(), + state.animating_level(&output), ) }) .unwrap_or_else(|| ((0., 0.).into(), 1.)); diff --git a/src/input/actions.rs b/src/input/actions.rs index 0492a985..0eb7b7a4 100644 --- a/src/input/actions.rs +++ b/src/input/actions.rs @@ -1027,10 +1027,11 @@ impl State { } pub fn update_zoom(&mut self, seat: &Seat, change: f64, animate: bool) { + let output = seat.active_output(); let mut shell = self.common.shell.write().unwrap(); let (zoom_seat, current_level) = shell .zoom_state() - .map(|state| (state.current_seat(), state.current_level())) + .map(|state| (state.current_seat(), state.current_level(&output))) .unwrap_or_else(|| (seat.clone(), 1.0)); if current_level == 1. && change <= 0. { @@ -1041,6 +1042,7 @@ impl State { let new_level = (current_level + change).max(1.0); shell.trigger_zoom( &seat, + Some(&output), new_level, &self.common.config.cosmic_conf.accessibility_zoom, animate, diff --git a/src/input/mod.rs b/src/input/mod.rs index 90b62f3d..80456477 100644 --- a/src/input/mod.rs +++ b/src/input/mod.rs @@ -2247,7 +2247,7 @@ where B::Device: 'static, { let geometry = zoom_state - .and_then(|state| output.zoomed_geometry(state.current_level())) + .and_then(|_| output.zoomed_geometry()) .unwrap_or_else(|| output.geometry()); let transform = output.current_transform(); let size = transform diff --git a/src/shell/focus/order.rs b/src/shell/focus/order.rs index f88bb1ca..ed2114de 100644 --- a/src/shell/focus/order.rs +++ b/src/shell/focus/order.rs @@ -73,7 +73,7 @@ fn render_input_order_internal( if shell .zoom_state .as_ref() - .is_some_and(|state| state.show_overlay && state.current_level() != 1.0) + .is_some_and(|state| state.show_overlay && state.current_level(output) != 1.0) { callback(Stage::ZoomUI)?; } diff --git a/src/shell/grabs/menu/mod.rs b/src/shell/grabs/menu/mod.rs index 733031c6..15a90934 100644 --- a/src/shell/grabs/menu/mod.rs +++ b/src/shell/grabs/menu/mod.rs @@ -522,11 +522,11 @@ impl PointerGrab for MenuGrab { let mut guard = self.elements.lock().unwrap(); let elements = &mut *guard; let event_location = if let Some(output) = self.screen_space_relative.as_ref() { - if let Some(zoom_state) = state.common.shell.read().unwrap().zoom_state() { + if state.common.shell.read().unwrap().zoom_state().is_some() { event .location .as_global() - .to_zoomed(output, zoom_state.level) + .to_zoomed(output) .to_global(output) .as_logical() } else { @@ -726,11 +726,11 @@ impl TouchGrab for MenuGrab { let mut guard = self.elements.lock().unwrap(); let elements = &mut *guard; let event_location = if let Some(output) = self.screen_space_relative.as_ref() { - if let Some(zoom_state) = data.common.shell.read().unwrap().zoom_state() { + if data.common.shell.read().unwrap().zoom_state().is_some() { event .location .as_global() - .to_zoomed(output, zoom_state.level) + .to_zoomed(output) .to_global(output) .as_logical() } else { diff --git a/src/shell/mod.rs b/src/shell/mod.rs index 79141b7f..69099c69 100644 --- a/src/shell/mod.rs +++ b/src/shell/mod.rs @@ -1202,7 +1202,7 @@ impl Common { Mutex::new(OutputZoomState::new( &state.seat, output, - state.level, + 1.0, state.increment, state.movement, self.event_loop_handle.clone(), @@ -1253,11 +1253,9 @@ impl Common { for output in shell_ref.workspaces.sets.keys() { let output_state = output.user_data().get::>().unwrap(); - output_state.lock().unwrap().update( - zoom_state.level, - zoom_state.movement, - zoom_state.increment, - ); + let mut output_state_ref = output_state.lock().unwrap(); + let level = output_state_ref.level; + output_state_ref.update(level, false, zoom_state.movement, zoom_state.increment); } } @@ -1861,13 +1859,12 @@ impl Shell { .workspaces .spaces() .any(|workspace| workspace.animations_going()) - || self.zoom_state.as_ref().is_some_and(|state| { - state.previous_level.is_some() - || self.outputs().any(|o| { - o.user_data() - .get::>() - .is_some_and(|state| state.lock().unwrap().is_animating()) - }) + || self.zoom_state.as_ref().is_some_and(|_| { + self.outputs().any(|o| { + o.user_data() + .get::>() + .is_some_and(|state| state.lock().unwrap().is_animating()) + }) }) } @@ -2005,6 +2002,7 @@ impl Shell { pub fn trigger_zoom( &mut self, seat: &Seat, + output: Option<&Output>, level: f64, zoom_config: &ZoomConfig, animate: bool, @@ -2014,51 +2012,53 @@ impl Shell { return; } - let previous_level = if let Some(old_state) = self.zoom_state.as_ref() { - if &old_state.seat != seat { - return; - } - + let outputs = output.map(|o| vec![o]).unwrap_or(self.outputs().collect()); + if self.zoom_state.is_none() { for output in self.outputs() { - let output_state = output.user_data().get::>().unwrap(); - output_state.lock().unwrap().update( - level, - zoom_config.view_moves, - zoom_config.increment, - ); - } - - old_state.animating_level() - } else { - for output in self.outputs() { - let output_state = output.user_data().get_or_insert_threadsafe(|| { + output.user_data().insert_if_missing_threadsafe(|| { Mutex::new(OutputZoomState::new( seat, output, - level, + 1.0, zoom_config.increment, zoom_config.view_moves, loop_handle.clone(), self.theme.clone(), )) }); - *output_state.lock().unwrap() = OutputZoomState::new( - seat, - output, - level, - zoom_config.increment, - zoom_config.view_moves, - loop_handle.clone(), - self.theme.clone(), - ); } + } - 1. - }; + let mut toggled = self.zoom_state.is_none(); + if let Some(old_state) = self.zoom_state.as_ref() { + if &old_state.seat != seat { + return; + } + } + + for output in &outputs { + let output_state = output.user_data().get::>().unwrap(); + output_state.lock().unwrap().update( + level, + animate, + zoom_config.view_moves, + zoom_config.increment, + ); + } + + let all_outputs_off = self.outputs().all(|o| { + o.user_data() + .get::>() + .unwrap() + .lock() + .unwrap() + .current_level() + == 1.0 + }); + toggled = toggled || all_outputs_off; - let toggled = previous_level != level && (previous_level == 1.0 || level == 1.0); if toggled { - let value = previous_level == 1.0; + let value = !all_outputs_off; let _ = loop_handle.insert_idle(move |state| { state.common.a11y_state.set_screen_magnifier(value); }); @@ -2067,10 +2067,8 @@ impl Shell { self.zoom_state = Some(ZoomState { seat: seat.clone(), show_overlay: zoom_config.show_overlay, - level, increment: zoom_config.increment, movement: zoom_config.view_moves, - previous_level: animate.then_some((previous_level, Instant::now())), }); } @@ -2135,31 +2133,21 @@ impl Shell { _ => {} } - if let Some(zoom_state) = self.zoom_state.as_mut() { - if zoom_state - .previous_level - .as_ref() - .is_some_and(|(_, start)| { - Instant::now().duration_since(*start) > ANIMATION_DURATION - }) - { - zoom_state.previous_level.take(); - } - - if zoom_state.level == 1. && zoom_state.previous_level.is_none() { - self.zoom_state.take(); - } - - if self.zoom_state.is_some() { - for output in self.outputs() { - output + if self.zoom_state.is_some() { + let mut all_outputs_off = true; + for output in self.outputs() { + all_outputs_off = all_outputs_off + && output .user_data() .get::>() .unwrap() .lock() .unwrap() .refresh(); - } + } + + if all_outputs_off { + self.zoom_state.take(); } } diff --git a/src/shell/zoom.rs b/src/shell/zoom.rs index cfb74498..01f416a9 100644 --- a/src/shell/zoom.rs +++ b/src/shell/zoom.rs @@ -52,14 +52,14 @@ use super::{ pub struct ZoomState { pub(super) seat: Seat, pub(super) show_overlay: bool, - pub(super) level: f64, pub(super) increment: u32, pub(super) movement: ZoomMovement, - pub(super) previous_level: Option<(f64, Instant)>, } #[derive(Debug)] pub struct OutputZoomState { + pub(super) level: f64, + pub(super) previous_level: Option<(f64, Instant)>, focal_point: Point, previous_point: Option<(Point, Instant)>, element: ZoomElement, @@ -118,6 +118,8 @@ impl OutputZoomState { element.set_additional_scale(level.min(4.)); OutputZoomState { + level, + previous_level: None, focal_point, previous_point: None, element, @@ -150,15 +152,40 @@ impl OutputZoomState { self.focal_point } + pub fn current_level(&self) -> f64 { + self.level + } + + pub fn animating_level(&self) -> f64 { + if let Some((old_level, start)) = self.previous_level.as_ref() { + let percentage = Instant::now().duration_since(*start).as_millis() as f32 + / ANIMATION_DURATION.as_millis() as f32; + + ease(EaseInOutCubic, *old_level, self.level, percentage) + } else { + self.level + } + } + pub fn is_animating(&self) -> bool { - self.previous_point.is_some() + self.previous_point.is_some() || self.previous_level.is_some() } - pub fn refresh(&self) { - self.element.refresh() + pub fn refresh(&mut self) -> bool { + if self + .previous_level + .as_ref() + .is_some_and(|(_, start)| Instant::now().duration_since(*start) > ANIMATION_DURATION) + { + self.previous_level.take(); + } + self.element.refresh(); + self.level == 1. && self.previous_level.is_none() } - pub fn update(&self, level: f64, movement: ZoomMovement, increment: u32) { + pub fn update(&mut self, level: f64, animate: bool, movement: ZoomMovement, increment: u32) { + self.previous_level = animate.then_some((self.animating_level(), Instant::now())); + self.level = level; self.element.set_additional_scale(level.min(4.)); self.element.queue_message(ZoomMessage::Update { level, @@ -189,25 +216,20 @@ impl OutputZoomState { } impl ZoomState { - pub fn current_level(&self) -> f64 { - self.level - } - - pub fn animating_level(&self) -> f64 { - if let Some((old_level, start)) = self.previous_level.as_ref() { - let percentage = Instant::now().duration_since(*start).as_millis() as f32 - / ANIMATION_DURATION.as_millis() as f32; - - ease(EaseInOutCubic, *old_level, self.level, percentage) - } else { - self.level - } - } - pub fn current_seat(&self) -> Seat { self.seat.clone() } + pub fn current_level(&self, output: &Output) -> f64 { + let output_state = output.user_data().get::>().unwrap(); + output_state.lock().unwrap().current_level() + } + + pub fn animating_level(&self, output: &Output) -> f64 { + let output_state = output.user_data().get::>().unwrap(); + output_state.lock().unwrap().animating_level() + } + pub fn animating_focal_point(&self, output: Option<&Output>) -> Point { let active_output = self.seat.active_output(); let output = output.unwrap_or(&active_output); @@ -242,7 +264,7 @@ impl ZoomState { let cursor_position = cursor_position.to_i32_round(); let original_position = original_position.to_i32_round(); let output_geometry = output.geometry(); - let mut zoomed_output_geometry = output.zoomed_geometry(self.level).unwrap(); + let mut zoomed_output_geometry = output.zoomed_geometry().unwrap(); let output_state = output.user_data().get::>().unwrap(); let mut output_state_ref = output_state.lock().unwrap(); @@ -285,7 +307,7 @@ impl ZoomState { let mut diff = output_state_ref.focal_point.to_global(&output) + (cursor_position.to_global(&output) - original_position) .to_f64() - .upscale(self.level); + .upscale(output_state_ref.level); diff.x = diff.x.clamp( output_geometry.loc.x as f64, ((output_geometry.loc.x + output_geometry.size.w) as f64).next_lower(), // FIXME: Replace with f64::next_down when stable @@ -333,9 +355,9 @@ impl ZoomState { pos: Point, ) -> Option<(PointerFocusTarget, Point)> { let output_geometry = output.geometry(); - let zoomed_output_geometry = output.zoomed_geometry(self.level).unwrap().to_f64(); + let zoomed_output_geometry = output.zoomed_geometry().unwrap().to_f64(); + let local_pos = global_pos_to_screen_space(pos, output); - let local_pos = global_pos_to_screen_space(pos, output, self.level); let output_state = output.user_data().get::>().unwrap(); let output_state_ref = output_state.lock().unwrap(); @@ -351,7 +373,7 @@ impl ZoomState { PointerFocusTarget::ZoomUI(output_state_ref.element.clone().into()), { // and vise-versa from screen-space to zoom-space... - let scaled_loc = location.downscale(self.level); + let scaled_loc = location.downscale(output_state_ref.level); let global_loc = Point::::from((scaled_loc.x, scaled_loc.y)) + zoomed_output_geometry.loc; @@ -359,7 +381,7 @@ impl ZoomState { // the relative position for us... Which will be wrong given the cursor movement will // be scaled, while this element isn't, as it exists in screen-space and not workspace-space. // So we shift the location relatively to make up for the scaled movement... - let diff = (pos - global_loc).upscale(self.level - 1.); + let diff = (pos - global_loc).upscale(output_state_ref.level - 1.); global_loc - diff }, @@ -383,10 +405,16 @@ impl ZoomState { fn global_pos_to_screen_space( pos: impl Into>, output: &Output, - level: f64, ) -> Point { let pos = pos.into(); - let zoomed_output_geometry = output.zoomed_geometry(level).unwrap().to_f64(); + let zoomed_output_geometry = output.zoomed_geometry().unwrap().to_f64(); + let level = output + .user_data() + .get::>() + .unwrap() + .lock() + .unwrap() + .current_level(); // lets try to get the global cursor position into screen space let relative_to_zoom_geo = Point::::from(( @@ -551,11 +579,10 @@ impl Program for ZoomProgram { let shell = state.common.shell.read().unwrap(); let output = seat.active_output(); - if let Some(zoom_state) = shell.zoom_state() { + if shell.zoom_state().is_some() { let location = global_pos_to_screen_space( start_data.location().as_global(), &output, - zoom_state.current_level(), ); let output_geometry = output.geometry(); @@ -573,6 +600,7 @@ impl Program for ZoomProgram { location.x, elem_location.y + elem_size.h / 2., )); + let level = output_state_ref.level; std::mem::drop(output_state_ref); let grab = MenuGrab::new( @@ -684,7 +712,7 @@ impl Program for ZoomProgram { (elem_size.h / 2.).round() as u32, false, ), - Some(zoom_state.level.min(4.)), + Some(level.min(4.)), state.common.event_loop_handle.clone(), state.common.theme.clone(), ); @@ -715,11 +743,10 @@ impl Program for ZoomProgram { let shell = state.common.shell.read().unwrap(); let output = seat.active_output(); - if let Some(zoom_state) = shell.zoom_state() { + if shell.zoom_state().is_some() { let location = global_pos_to_screen_space( start_data.location().as_global(), &output, - zoom_state.current_level(), ); let output_geometry = output.geometry(); @@ -737,6 +764,7 @@ impl Program for ZoomProgram { location.x, elem_location.y + (elem_size.h / 2.), )); + let level = output_state_ref.level; std::mem::drop(output_state_ref); let grab = MenuGrab::new( @@ -758,7 +786,7 @@ impl Program for ZoomProgram { }), position.to_global(&output).to_i32_round(), MenuAlignment::PREFER_CENTERED, - Some(zoom_state.level.min(4.)), + Some(level.min(4.)), state.common.event_loop_handle.clone(), state.common.theme.clone(), ); diff --git a/src/utils/geometry.rs b/src/utils/geometry.rs index 8b241514..0ac53d18 100644 --- a/src/utils/geometry.rs +++ b/src/utils/geometry.rs @@ -1,8 +1,12 @@ +use std::sync::Mutex; + use smithay::{ output::Output, utils::{Coordinate, Logical, Point, Rectangle, Size}, }; +use crate::shell::zoom::OutputZoomState; + use super::prelude::OutputExt; /// Marker type for coordinates in global space @@ -20,7 +24,7 @@ pub trait PointExt { pub trait PointGlobalExt { fn to_local(self, output: &Output) -> Point; - fn to_zoomed(self, output: &Output, level: f64) -> Point; + fn to_zoomed(self, output: &Output) -> Point; fn as_logical(self) -> Point; } @@ -66,8 +70,15 @@ impl PointGlobalExt for Point { (C::from_f64(point.x), C::from_f64(point.y)).into() } - fn to_zoomed(self, output: &Output, level: f64) -> Point { - let zoomed_output_geometry = output.zoomed_geometry(level).unwrap(); + fn to_zoomed(self, output: &Output) -> Point { + let zoomed_output_geometry = output.zoomed_geometry().unwrap(); + let level = output + .user_data() + .get::>() + .unwrap() + .lock() + .unwrap() + .current_level(); let point = (self.to_f64() - zoomed_output_geometry.loc.to_f64()) .upscale(level) .as_logical(); diff --git a/src/utils/prelude.rs b/src/utils/prelude.rs index 94dd6fcb..f7952459 100644 --- a/src/utils/prelude.rs +++ b/src/utils/prelude.rs @@ -23,7 +23,7 @@ use std::{ pub trait OutputExt { fn geometry(&self) -> Rectangle; - fn zoomed_geometry(&self, level: f64) -> Option>; + fn zoomed_geometry(&self) -> Option>; fn adaptive_sync(&self) -> AdaptiveSync; fn set_adaptive_sync(&self, vrr: AdaptiveSync); @@ -57,7 +57,7 @@ impl OutputExt for Output { .as_global() } - fn zoomed_geometry(&self, level: f64) -> Option> { + fn zoomed_geometry(&self) -> Option> { let output_geometry = self.geometry(); let output_state = self.user_data().get::>()?; @@ -66,7 +66,7 @@ impl OutputExt for Output { let focal_point = output_state_ref.current_focal_point().to_global(self); let mut zoomed_output_geo = output_geometry.to_f64(); zoomed_output_geo.loc -= focal_point; - zoomed_output_geo = zoomed_output_geo.downscale(level); + zoomed_output_geo = zoomed_output_geo.downscale(output_state_ref.current_level()); zoomed_output_geo.loc += focal_point; Some(zoomed_output_geo.to_i32_round()) diff --git a/src/wayland/handlers/a11y.rs b/src/wayland/handlers/a11y.rs index ba150d66..235d1e12 100644 --- a/src/wayland/handlers/a11y.rs +++ b/src/wayland/handlers/a11y.rs @@ -16,7 +16,7 @@ impl A11yHandler for State { if shell .zoom_state() - .is_some_and(|state| state.current_level() != 1.0) + .is_some_and(|state| shell.outputs().any(|o| state.current_level(o) != 1.0)) != enabled { let seat = shell.seats.last_active().clone(); @@ -29,6 +29,7 @@ impl A11yHandler for State { shell.trigger_zoom( &seat, + None, level, zoom_config, true,