zoom: Handle multiple outputs

This commit is contained in:
Victoria Brekenfeld 2025-01-24 18:07:33 +01:00 committed by Victoria Brekenfeld
parent 61d44b3a9d
commit 531a1c951f
4 changed files with 259 additions and 32 deletions

View file

@ -595,7 +595,7 @@ where
} else { } else {
ElementFilter::All ElementFilter::All
}; };
let zoom_level = shell.read().unwrap().zoom_level(); let zoom_level = shell.read().unwrap().zoom_level(Some(&output));
#[allow(unused_mut)] #[allow(unused_mut)]
let workspace_elements = workspace_elements( let workspace_elements = workspace_elements(
@ -1013,7 +1013,7 @@ where
} else { } else {
ElementFilter::All ElementFilter::All
}; };
let zoom_level = shell.read().unwrap().zoom_level(); let zoom_level = shell.read().unwrap().zoom_level(Some(&output));
let result = render_workspace( let result = render_workspace(
gpu, gpu,

View file

@ -1028,20 +1028,24 @@ impl State {
x @ Action::ZoomIn | x @ Action::ZoomOut => { x @ Action::ZoomIn | x @ Action::ZoomOut => {
let mut shell = self.common.shell.write().unwrap(); let mut shell = self.common.shell.write().unwrap();
let pointer_loc = seat.get_pointer().unwrap().current_location().as_global(); let (zoom_seat, current_level) = shell
let (zoom_seat, _, current_level) = shell .zoom_level(None)
.zoom_level() .map(|(s, _, l)| (s, l))
.unwrap_or_else(|| (seat.clone(), pointer_loc, 1.0)); .unwrap_or_else(|| (seat.clone(), 1.0));
if &zoom_seat == seat { if &zoom_seat == seat {
let increment =
self.common.config.cosmic_conf.accessibility_zoom.increment as f64 / 100.0;
shell.trigger_zoom( shell.trigger_zoom(
seat, seat,
pointer_loc,
match x { match x {
Action::ZoomIn => current_level + 1.0, Action::ZoomIn => current_level + increment,
Action::ZoomOut => (current_level - 1.0).max(1.0), Action::ZoomOut => (current_level - increment).max(1.0),
_ => unreachable!(), _ => unreachable!(),
}, },
self.common.config.cosmic_conf.accessibility_zoom.view_moves,
); );
// TODO: persist state, if enable_on_startup
} }
} }

View file

@ -518,6 +518,11 @@ impl State {
let mut shell = self.common.shell.write().unwrap(); let mut shell = self.common.shell.write().unwrap();
shell.update_pointer_position(position.to_local(&output), &output); shell.update_pointer_position(position.to_local(&output), &output);
shell.update_focal_point(
&seat,
original_position,
self.common.config.cosmic_conf.accessibility_zoom.view_moves,
);
if output != current_output { if output != current_output {
for session in cursor_sessions_for_output(&*shell, &current_output) { for session in cursor_sessions_for_output(&*shell, &current_output) {

View file

@ -10,10 +10,13 @@ use std::{
}; };
use wayland_backend::server::ClientId; use wayland_backend::server::ClientId;
use crate::wayland::{handlers::data_device, protocols::workspace::WorkspaceCapabilities}; use crate::{
utils::{float::NextDown, tween::EasePoint},
wayland::{handlers::data_device, protocols::workspace::WorkspaceCapabilities},
};
use cosmic_comp_config::{ use cosmic_comp_config::{
workspace::{WorkspaceLayout, WorkspaceMode}, workspace::{WorkspaceLayout, WorkspaceMode},
TileBehavior, TileBehavior, ZoomMovement,
}; };
use cosmic_protocols::workspace::v1::server::zcosmic_workspace_handle_v1::TilingState; use cosmic_protocols::workspace::v1::server::zcosmic_workspace_handle_v1::TilingState;
use cosmic_settings_config::shortcuts::action::{Direction, FocusDirection, ResizeDirection}; use cosmic_settings_config::shortcuts::action::{Direction, FocusDirection, ResizeDirection};
@ -245,25 +248,112 @@ pub struct PendingLayer {
pub output: Output, pub output: Output,
} }
pub struct ZoomState { struct ZoomState {
seat: Seat<State>, seat: Seat<State>,
level: f64, level: f64,
focal_point: Point<f64, Global>, movement: ZoomMovement,
previous: Option<(f64, Instant)>, previous_level: Option<(f64, Instant)>,
}
#[derive(Debug)]
struct OutputZoomState {
focal_point: Point<f64, Local>,
previous_point: Option<(Point<f64, Local>, Instant)>,
}
impl OutputZoomState {
pub fn new(
seat: &Seat<State>,
output: &Output,
movement: ZoomMovement,
level: f64,
) -> OutputZoomState {
let cursor_position = seat.get_pointer().unwrap().current_location().as_global();
let output_geometry = output.geometry().to_f64();
let focal_point = if output_geometry.contains(cursor_position) {
match movement {
ZoomMovement::Continuously | ZoomMovement::OnEdge => {
cursor_position.to_local(&output)
}
ZoomMovement::Centered => {
let mut zoomed_output_geometry = output_geometry;
zoomed_output_geometry = zoomed_output_geometry.downscale(level);
zoomed_output_geometry.loc =
cursor_position - zoomed_output_geometry.size.downscale(2.).to_point();
let mut focal_point = zoomed_output_geometry
.loc
.to_local(&output)
.upscale(level)
.to_global(&output);
focal_point.x = focal_point.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
);
focal_point.y = focal_point.y.clamp(
output_geometry.loc.y as f64,
((output_geometry.loc.y + output_geometry.size.h) as f64).next_lower(), // FIXME: Replace with f64::next_down when stable
);
focal_point.to_local(&output)
}
}
} else {
(output_geometry.size.w / 2., output_geometry.size.h / 2.).into()
};
OutputZoomState {
focal_point,
previous_point: None,
}
}
fn focal_point(&mut self) -> Point<f64, Local> {
if let Some((old_point, start)) = self.previous_point.as_ref() {
let duration_since = Instant::now().duration_since(*start);
if duration_since > ANIMATION_DURATION {
self.previous_point.take();
return self.focal_point;
}
let percentage =
duration_since.as_millis() as f32 / ANIMATION_DURATION.as_millis() as f32;
ease(
EaseInOutCubic,
EasePoint(*old_point),
EasePoint(self.focal_point),
percentage,
)
.0
} else {
self.focal_point
}
}
} }
impl ZoomState { impl ZoomState {
pub fn level(&self) -> (Seat<State>, Point<f64, Global>, f64) { pub fn level(&self, output: Option<&Output>) -> (Seat<State>, Point<f64, Global>, f64) {
if let Some((old, start)) = self.previous.as_ref() { let active_output = self.seat.active_output();
let output = output.unwrap_or(&active_output);
let output_state = output.user_data().get_or_insert_threadsafe(|| {
Mutex::new(OutputZoomState::new(
&self.seat,
output,
self.movement,
self.level,
))
});
let focal_point = output_state.lock().unwrap().focal_point().to_global(output);
if let Some((old_level, start)) = self.previous_level.as_ref() {
let percentage = Instant::now().duration_since(*start).as_millis() as f32 let percentage = Instant::now().duration_since(*start).as_millis() as f32
/ ANIMATION_DURATION.as_millis() as f32; / ANIMATION_DURATION.as_millis() as f32;
( (
self.seat.clone(), self.seat.clone(),
self.focal_point, focal_point,
ease(EaseInOutCubic, *old, self.level, percentage), ease(EaseInOutCubic, *old_level, self.level, percentage),
) )
} else { } else {
(self.seat.clone(), self.focal_point, self.level) (self.seat.clone(), focal_point, self.level)
} }
} }
} }
@ -1833,6 +1923,24 @@ impl Shell {
.workspaces .workspaces
.spaces() .spaces()
.any(|workspace| workspace.animations_going()) .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_or_insert_threadsafe(|| {
Mutex::new(OutputZoomState::new(
&state.seat,
o,
state.movement,
state.level,
))
})
.lock()
.unwrap()
.previous_point
.is_some()
})
})
} }
pub fn update_animations(&mut self) -> HashMap<ClientId, Client> { pub fn update_animations(&mut self) -> HashMap<ClientId, Client> {
@ -1966,26 +2074,124 @@ impl Shell {
} }
} }
pub fn trigger_zoom( pub fn trigger_zoom(&mut self, seat: &Seat<State>, level: f64, movement: ZoomMovement) {
if self.zoom_state.is_none() && level == 1. {
return;
}
let previous_level = if let Some(old_state) = self.zoom_state.take() {
if &old_state.seat != seat {
return;
}
old_state.level
} else {
for output in self.outputs() {
if let Some(output_state) = output.user_data().get::<Mutex<OutputZoomState>>() {
*output_state.lock().unwrap() =
OutputZoomState::new(seat, output, movement, level);
}
}
1.0
};
self.zoom_state = Some(ZoomState {
seat: seat.clone(),
level,
movement,
previous_level: Some((previous_level, Instant::now())),
});
}
pub fn update_focal_point(
&mut self, &mut self,
seat: &Seat<State>, seat: &Seat<State>,
focal_point: Point<f64, Global>, original_position: Point<f64, Global>,
level: f64, movement: ZoomMovement,
) { ) {
if level == 1. { if let Some(state) = self.zoom_state.as_mut() {
self.zoom_state.take(); if &state.seat != seat {
} else { return;
self.zoom_state = Some(ZoomState { }
seat: seat.clone(),
level, let output = seat.active_output();
focal_point, let output_state = output.user_data().get_or_insert_threadsafe(|| {
previous: None, Mutex::new(OutputZoomState::new(seat, &output, movement, state.level))
}); });
let mut output_state_ref = output_state.lock().unwrap();
// animate movement type changes
if state.movement != movement {
output_state_ref.previous_point =
Some((output_state_ref.focal_point, Instant::now()));
state.movement = movement;
}
let cursor_position = seat
.get_pointer()
.unwrap()
.current_location()
.as_global()
.to_local(&output);
match movement {
ZoomMovement::Continuously => output_state_ref.focal_point = cursor_position,
ZoomMovement::OnEdge => {
let output_geometry = output.geometry().to_f64();
let mut zoomed_output_geometry = output_geometry;
zoomed_output_geometry.loc -= output_state_ref.focal_point.to_global(&output);
zoomed_output_geometry = zoomed_output_geometry.downscale(state.level);
zoomed_output_geometry.loc += output_state_ref.focal_point.to_global(&output);
if !zoomed_output_geometry.contains(cursor_position.to_global(&output)) {
let mut diff = output_state_ref.focal_point.to_global(&output)
+ (cursor_position.to_global(&output) - original_position)
.upscale(state.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
);
diff.y = diff.y.clamp(
output_geometry.loc.y as f64,
((output_geometry.loc.y + output_geometry.size.h) as f64).next_lower(), // FIXME: Replace with f64::next_down when stable
);
diff -= output_state_ref.focal_point.to_global(&output);
output_state_ref.focal_point += diff.as_logical().as_local();
}
}
ZoomMovement::Centered => {
let output_geometry = output.geometry().to_f64();
let mut zoomed_output_geometry = output_geometry;
zoomed_output_geometry = zoomed_output_geometry.downscale(state.level);
zoomed_output_geometry.loc = cursor_position.to_global(&output)
- zoomed_output_geometry.size.downscale(2.).to_point();
let mut focal_point = zoomed_output_geometry
.loc
.to_local(&output)
.upscale(state.level)
.to_global(&output);
focal_point.x = focal_point.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
);
focal_point.y = focal_point.y.clamp(
output_geometry.loc.y as f64,
((output_geometry.loc.y + output_geometry.size.h) as f64).next_lower(), // FIXME: Replace with f64::next_down when stable
);
output_state_ref.focal_point = focal_point.to_local(&output);
}
}
} }
} }
pub fn zoom_level(&self) -> Option<(Seat<State>, Point<f64, Global>, f64)> { pub fn zoom_level(
self.zoom_state.as_ref().map(|s| s.level()) &self,
output: Option<&Output>,
) -> Option<(Seat<State>, Point<f64, Global>, f64)> {
self.zoom_state.as_ref().map(|s| s.level(output))
} }
fn refresh( fn refresh(
@ -2023,6 +2229,18 @@ 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();
}
}
self.workspaces self.workspaces
.refresh(workspace_state, xdg_activation_state); .refresh(workspace_state, xdg_activation_state);