zoom: Handle multiple outputs
This commit is contained in:
parent
61d44b3a9d
commit
531a1c951f
4 changed files with 259 additions and 32 deletions
|
|
@ -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,
|
||||||
|
|
|
||||||
|
|
@ -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
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -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, ¤t_output) {
|
for session in cursor_sessions_for_output(&*shell, ¤t_output) {
|
||||||
|
|
|
||||||
264
src/shell/mod.rs
264
src/shell/mod.rs
|
|
@ -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);
|
||||||
|
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue