fix: a11y: magnifier: correct Centered/OnEdge focal-point math and refresh focal point at zoom-in
- I used AI to assist in research of the focal point computation formulae for OnEdge and Centered modes. - I used the formulas found in [KDE's zoom magnifier](https://invent.kde.org/plasma/kwin/-/blob/master/src/plugins/zoom/zoom.cpp) and converted them to Rust and the COSMIC focal point system. - Remove unnecessary focal-point precomputation in `OutputZoomState::new`, because it is now updated on first zoom in anyway. - Do focal point computation in Local coordinates in update_focal_point to eliminate local-global conversion confusion/ issues. - ZoomMovement::OnEdge - single code path rewrite - old two-branch logic caused jumping and unpredictability at high zoom. Fixes [#2683](https://github.com/pop-os/cosmic-epoch/issues/2683), partially addresses [#2688](https://github.com/pop-os/cosmic-epoch/issues/2688). - ZoomMovement::Centered - translation/clamp/invert - prevents "blow up" of calculation at zoom level > 2.0 that caused cursor to disappear off bottom right of screen. Fixes [#1503](https://github.com/pop-os/cosmic-comp/issues/1503). - `is_level_change` - sets `previous_point` so that `animating_focal_point` interpolates instead of jumping. Still room for improvement (needs focal point recomputation per-frame during animation to prevent "boomerang" feel), but this is still an improvement over jumping. - In `trigger_zoom`, call `update_focal_point` when `level > 1.0` - overlaps proposal in [#1859](https://github.com/pop-os/cosmic-comp/pull/1859) + the level gate to prevent stale focal point on zoom in. Fixes [#2511](https://github.com/pop-os/cosmic-comp/issues/2511) and [#1883](https://github.com/pop-os/cosmic-epoch/issues/1883). Partially addresses [#1420](https://github.com/pop-os/cosmic-comp/issues/1420) (item 1 only).
This commit is contained in:
parent
693d1ad775
commit
01c0cb020e
2 changed files with 83 additions and 90 deletions
|
|
@ -2490,6 +2490,14 @@ impl Shell {
|
||||||
increment: zoom_config.increment,
|
increment: zoom_config.increment,
|
||||||
movement: zoom_config.view_moves,
|
movement: zoom_config.view_moves,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
if level > 1.0 {
|
||||||
|
self.update_focal_point(
|
||||||
|
seat,
|
||||||
|
seat.get_pointer().unwrap().current_location().as_global(),
|
||||||
|
zoom_config.view_moves,
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
pub fn update_focal_point(
|
pub fn update_focal_point(
|
||||||
|
|
|
||||||
|
|
@ -27,7 +27,7 @@ use smithay::{
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
output::Output,
|
output::Output,
|
||||||
utils::{IsAlive, Point, Rectangle, Serial, Size},
|
utils::{FrameExtents, IsAlive, Point, Rectangle, Serial, Size},
|
||||||
};
|
};
|
||||||
use tracing::error;
|
use tracing::error;
|
||||||
|
|
||||||
|
|
@ -76,36 +76,8 @@ impl OutputZoomState {
|
||||||
) -> OutputZoomState {
|
) -> OutputZoomState {
|
||||||
theme.transparent = theme.cosmic().frosted_system_interface;
|
theme.transparent = theme.cosmic().frosted_system_interface;
|
||||||
let cursor_position = seat.get_pointer().unwrap().current_location().as_global();
|
let cursor_position = seat.get_pointer().unwrap().current_location().as_global();
|
||||||
|
let focal_point = cursor_position.to_local(output);
|
||||||
let output_geometry = output.geometry().to_f64();
|
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().to_f64().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,
|
|
||||||
(output_geometry.loc.x + output_geometry.size.w).next_down(),
|
|
||||||
);
|
|
||||||
focal_point.y = focal_point.y.clamp(
|
|
||||||
output_geometry.loc.y,
|
|
||||||
(output_geometry.loc.y + output_geometry.size.h).next_down(),
|
|
||||||
);
|
|
||||||
focal_point.to_local(output)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
(output_geometry.size.w / 2., output_geometry.size.h / 2.).into()
|
|
||||||
};
|
|
||||||
|
|
||||||
let program = ZoomProgram::new(level, movement, increment);
|
let program = ZoomProgram::new(level, movement, increment);
|
||||||
let element = IcedElement::new(program, Size::default(), loop_handle, theme);
|
let element = IcedElement::new(program, Size::default(), loop_handle, theme);
|
||||||
|
|
@ -269,86 +241,99 @@ impl ZoomState {
|
||||||
&mut self,
|
&mut self,
|
||||||
output: &Output,
|
output: &Output,
|
||||||
cursor_position: Point<f64, Global>,
|
cursor_position: Point<f64, Global>,
|
||||||
original_position: Point<f64, Global>,
|
_original_position: Point<f64, Global>,
|
||||||
movement: ZoomMovement,
|
movement: ZoomMovement,
|
||||||
) {
|
) {
|
||||||
let output_geometry = output.geometry().to_f64();
|
let output_geometry = output.geometry().to_f64().to_local(output);
|
||||||
let mut zoomed_output_geometry = output.zoomed_geometry().unwrap().to_f64();
|
let zoomed_output_geometry = output.zoomed_geometry().unwrap().to_f64().to_local(output);
|
||||||
|
|
||||||
let output_state = output.user_data().get::<Mutex<OutputZoomState>>().unwrap();
|
let output_state = output.user_data().get::<Mutex<OutputZoomState>>().unwrap();
|
||||||
let mut output_state_ref = output_state.lock().unwrap();
|
let mut output_state_ref = output_state.lock().unwrap();
|
||||||
|
|
||||||
// animate movement type changes
|
let level = output_state_ref.current_level();
|
||||||
if self.movement != movement {
|
|
||||||
|
let is_level_change = output_state_ref
|
||||||
|
.previous_level
|
||||||
|
.is_some_and(|prev| prev.0 != level);
|
||||||
|
|
||||||
|
// animate level and movement type changes
|
||||||
|
if is_level_change || self.movement != movement {
|
||||||
output_state_ref.previous_point = Some((output_state_ref.focal_point, Instant::now()));
|
output_state_ref.previous_point = Some((output_state_ref.focal_point, Instant::now()));
|
||||||
self.movement = movement;
|
self.movement = movement;
|
||||||
}
|
}
|
||||||
|
|
||||||
let cursor_position = cursor_position.to_local(output);
|
let cursor_position = cursor_position.to_local(output);
|
||||||
match movement {
|
match movement {
|
||||||
ZoomMovement::Continuously => output_state_ref.focal_point = cursor_position,
|
ZoomMovement::Continuously => {
|
||||||
|
// Focal point is the cursor position
|
||||||
|
output_state_ref.focal_point = cursor_position;
|
||||||
|
}
|
||||||
ZoomMovement::OnEdge => {
|
ZoomMovement::OnEdge => {
|
||||||
if !zoomed_output_geometry
|
if is_level_change {
|
||||||
.overlaps_or_touches(Rectangle::new(original_position, Size::from((16., 16.))))
|
output_state_ref.focal_point = cursor_position;
|
||||||
{
|
return;
|
||||||
zoomed_output_geometry.loc = cursor_position.to_global(output)
|
}
|
||||||
- zoomed_output_geometry.size.downscale(2.).to_point();
|
|
||||||
let mut focal_point = zoomed_output_geometry
|
// Compute small margin relative to zoomed output to keep cursor within
|
||||||
.loc
|
let margin_size = zoomed_output_geometry.size.h * 0.02;
|
||||||
.to_local(output)
|
let margins = FrameExtents::new(margin_size, margin_size, margin_size, margin_size);
|
||||||
.upscale(output_state_ref.level)
|
let inner_rect = zoomed_output_geometry - margins;
|
||||||
.to_global(output);
|
|
||||||
|
if inner_rect.contains(cursor_position) {
|
||||||
|
// Do not move if cursor within margins
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Compute dx and dy to move the zoomed output based on cursor distance outside margin(s)
|
||||||
|
let dx = if cursor_position.x < inner_rect.loc.x {
|
||||||
|
cursor_position.x - inner_rect.loc.x
|
||||||
|
} else if cursor_position.x > inner_rect.loc.x + inner_rect.size.w {
|
||||||
|
cursor_position.x - (inner_rect.loc.x + inner_rect.size.w)
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
let dy = if cursor_position.y < inner_rect.loc.y {
|
||||||
|
cursor_position.y - inner_rect.loc.y
|
||||||
|
} else if cursor_position.y > inner_rect.loc.y + inner_rect.size.h {
|
||||||
|
cursor_position.y - (inner_rect.loc.y + inner_rect.size.h)
|
||||||
|
} else {
|
||||||
|
0.0
|
||||||
|
};
|
||||||
|
|
||||||
|
let mut focal_point = output_state_ref.focal_point + Point::new(dx, dy);
|
||||||
|
|
||||||
|
// Clamp the final focal point to output geometry
|
||||||
focal_point.x = focal_point.x.clamp(
|
focal_point.x = focal_point.x.clamp(
|
||||||
output_geometry.loc.x,
|
output_geometry.loc.x,
|
||||||
output_geometry.loc.x + output_geometry.size.w - 1.,
|
output_geometry.loc.x + output_geometry.size.w - 1.0,
|
||||||
);
|
);
|
||||||
focal_point.y = focal_point.y.clamp(
|
focal_point.y = focal_point.y.clamp(
|
||||||
output_geometry.loc.y,
|
output_geometry.loc.y,
|
||||||
output_geometry.loc.y + output_geometry.size.h - 1.,
|
output_geometry.loc.y + output_geometry.size.h - 1.0,
|
||||||
);
|
);
|
||||||
output_state_ref.previous_point =
|
|
||||||
Some((output_state_ref.focal_point, Instant::now()));
|
|
||||||
output_state_ref.focal_point = focal_point.to_local(output);
|
|
||||||
} else 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(output_state_ref.level);
|
|
||||||
diff.x = diff.x.clamp(
|
|
||||||
output_geometry.loc.x,
|
|
||||||
(output_geometry.loc.x + output_geometry.size.w).next_down(),
|
|
||||||
);
|
|
||||||
diff.y = diff.y.clamp(
|
|
||||||
output_geometry.loc.y,
|
|
||||||
(output_geometry.loc.y + output_geometry.size.h).next_down(),
|
|
||||||
);
|
|
||||||
diff -= output_state_ref.focal_point.to_global(output);
|
|
||||||
|
|
||||||
output_state_ref.focal_point += diff.as_logical().as_local();
|
output_state_ref.focal_point = focal_point;
|
||||||
}
|
|
||||||
}
|
}
|
||||||
ZoomMovement::Centered => {
|
ZoomMovement::Centered => {
|
||||||
zoomed_output_geometry.loc = cursor_position.to_global(output)
|
let center = (output_geometry.size / 2.).to_point();
|
||||||
- zoomed_output_geometry.size.downscale(2.).to_point();
|
|
||||||
|
|
||||||
let mut focal_point = zoomed_output_geometry
|
if level <= 1.0 + f64::EPSILON {
|
||||||
.loc
|
// Without this break, focal point will jump to (0, 0) on zoom out
|
||||||
.to_local(output)
|
output_state_ref.focal_point = center;
|
||||||
.upscale(
|
return;
|
||||||
(output_geometry.size.w
|
}
|
||||||
/ (output_geometry.size.w - zoomed_output_geometry.size.w)
|
|
||||||
.max(f64::EPSILON))
|
// Compute translation to keep cursor at center of screen
|
||||||
.max(1.),
|
let mut tx = center.x - cursor_position.x * level;
|
||||||
)
|
let mut ty = center.y - cursor_position.y * level;
|
||||||
.to_global(output);
|
|
||||||
focal_point.x = focal_point.x.clamp(
|
// Clamp translation to keep viewport within screen bounds
|
||||||
output_geometry.loc.x,
|
tx = tx.clamp(output_geometry.size.w * (1.0 - level), 0.0);
|
||||||
output_geometry.loc.x + output_geometry.size.w - 1.,
|
ty = ty.clamp(output_geometry.size.h * (1.0 - level), 0.0);
|
||||||
);
|
|
||||||
focal_point.y = focal_point.y.clamp(
|
// Convert translation back to focal point: T = F * (1 - level)
|
||||||
output_geometry.loc.y,
|
output_state_ref.focal_point =
|
||||||
output_geometry.loc.y + output_geometry.size.h - 1.,
|
Point::from((tx / (1.0 - level), ty / (1.0 - level)));
|
||||||
);
|
|
||||||
output_state_ref.focal_point = focal_point.to_local(output);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue