Started implementing zoom around point.

This commit is contained in:
Samuel Guerra 2023-09-11 00:29:53 -03:00
parent 2d0d465790
commit 98f7235f4f
4 changed files with 103 additions and 20 deletions

View File

@ -32,8 +32,8 @@
* Implement touch scroll inertia.
* Implement `ScrollMode::ZOOM`.
- Touch gesture.
- Scroll wheel zoom.
- Center on cursor?
- Scroll wheel zoom center point.
- Scroll commands center point parameter.
- Scroll-to-fill/fit.
# Touch Events

View File

@ -181,6 +181,14 @@ command! {
pub static SCROLL_TO_CMD;
/// Represents the **zoom in** action.
///
/// # Parameter
///
/// This commands accepts an optional [`Point`] parameter that defines the origin of the
/// scale transform, relative values are resolved in the viewport space. The default value
/// is *top-start*.
///
/// [`Point`]: crate::core::units::Point
pub static ZOOM_IN_CMD = {
name: "Zoom In",
shortcut: shortcut!(CTRL+'+'),
@ -188,6 +196,14 @@ command! {
};
/// Represents the **zoom out** action.
///
/// # Parameter
///
/// This commands accepts an optional [`Point`] parameter that defines the origin of the
/// scale transform, relative values are resolved in the viewport space. The default value
/// is *top-start*.
///
/// [`Point`]: crate::core::units::Point
pub static ZOOM_OUT_CMD = {
name: "Zoom Out",
shortcut: shortcut!(CTRL+'-'),

View File

@ -516,6 +516,9 @@ pub fn zoom_commands_node(child: impl UiNode) -> impl UiNode {
let mut zoom_out = CommandHandle::dummy();
let mut zoom_reset = CommandHandle::dummy();
let mut scale_delta = 0.fct();
let mut origin = Point::default();
match_node(child, move |child, op| match op {
UiNodeOp::Init => {
let scope = WIDGET.id();
@ -531,32 +534,57 @@ pub fn zoom_commands_node(child: impl UiNode) -> impl UiNode {
zoom_out = CommandHandle::dummy();
zoom_reset = CommandHandle::dummy();
}
UiNodeOp::Layout { .. } => {
zoom_in.set_enabled(SCROLL.can_zoom_in());
zoom_out.set_enabled(SCROLL.can_zoom_out());
zoom_reset.set_enabled(SCROLL.zoom_scale().get() != 1.fct());
}
UiNodeOp::Event { update } => {
child.event(update);
let scope = WIDGET.id();
if let Some(args) = ZOOM_IN_CMD.scoped(scope).on(update) {
args.handle_enabled(&zoom_in, |_| {
let delta = ZOOM_WHEEL_UNIT_VAR.get();
SCROLL.chase_zoom(|f| f + delta);
args.handle_enabled(&zoom_in, |args| {
origin = args.param::<Point>().cloned().unwrap_or_default();
scale_delta += ZOOM_WHEEL_UNIT_VAR.get();
WIDGET.layout();
});
} else if let Some(args) = ZOOM_OUT_CMD.scoped(scope).on(update) {
args.handle_enabled(&zoom_out, |_| {
let delta = ZOOM_WHEEL_UNIT_VAR.get();
SCROLL.chase_zoom(|f| f - delta);
origin = args.param::<Point>().cloned().unwrap_or_default();
scale_delta -= ZOOM_WHEEL_UNIT_VAR.get();
WIDGET.layout();
});
} else if let Some(args) = ZOOM_RESET_CMD.scoped(scope).on(update) {
args.handle_enabled(&zoom_reset, |_| {
SCROLL.chase_zoom(|_| 1.fct());
scale_delta = 0.fct();
});
}
}
UiNodeOp::Layout { wl, final_size } => {
*final_size = child.layout(wl);
zoom_in.set_enabled(SCROLL.can_zoom_in());
zoom_out.set_enabled(SCROLL.can_zoom_out());
zoom_reset.set_enabled(SCROLL.zoom_scale().get() != 1.fct());
if scale_delta != 0.fct() {
let scroll_info = WIDGET.info().scroll_info().unwrap();
let viewport_size = scroll_info.viewport_size();
let default = PxPoint::new(
Px(0),
match LAYOUT.direction() {
LayoutDirection::LTR => Px(0),
LayoutDirection::RTL => viewport_size.width,
},
);
let center_in_viewport =
LAYOUT.with_constraints(PxConstraints2d::new_fill_size(viewport_size), || origin.layout_dft(default));
SCROLL.zoom(|f| f + scale_delta, center_in_viewport);
scale_delta = 0.fct();
}
}
_ => {}
})
}
@ -656,7 +684,7 @@ pub fn scroll_to_node(child: impl UiNode) -> impl UiNode {
let target_bounds_in_content = target_bounds;
// remove offset
let rendered_offset = SCROLL.rendered_offset_px();
let rendered_offset = SCROLL.rendered_content().origin.to_vector();
target_bounds.origin += rendered_offset;
// replace scale
@ -940,13 +968,12 @@ pub fn scroll_wheel_node(child: impl UiNode) -> impl UiNode {
.and_then(|t| t.transform_point(default))
.unwrap_or(default);
let center = LAYOUT.with_constraints(PxConstraints2d::new_fill_size(scroll_info.viewport_size()), || {
let viewport_size = scroll_info.viewport_size();
let center_in_viewport = LAYOUT.with_constraints(PxConstraints2d::new_fill_size(viewport_size), || {
ZOOM_WHEEL_ORIGIN_VAR.layout_dft(default)
});
println!("!!: TODO, {center:?}");
SCROLL.chase_zoom(|f| f + scale_delta);
SCROLL.zoom(|f| f + scale_delta, center_in_viewport);
scale_delta = 0.fct();
}
}

View File

@ -245,12 +245,15 @@ impl SCROLL {
SCROLL_CONFIG.get().rendered.load(Ordering::Relaxed).z
}
/// Latest rendered offset in pixels.
pub fn rendered_offset_px(&self) -> PxVector {
/// Latest rendered content offset and size.
///
/// This is the content bounds, scaled and in the viewport space.
pub fn rendered_content(&self) -> PxRect {
let viewport = SCROLL_VIEWPORT_SIZE_VAR.get();
let content = SCROLL_CONTENT_SIZE_VAR.get();
let max_scroll = content - viewport;
max_scroll.to_vector() * self.rendered_offset()
let offset = max_scroll.to_vector() * self.rendered_offset();
PxRect::new(offset.to_point(), content)
}
/// Extra vertical offset, requested by touch gesture, that could not be fulfilled because [`vertical_offset`]
@ -616,6 +619,43 @@ impl SCROLL {
}
}
/// Zoom in or out keeping the `origin` point in the viewport aligned with the same point
/// in the content.
pub fn zoom(&self, modify_scale: impl FnOnce(Factor) -> Factor, origin: PxPoint) {
#[cfg(dyn_closure)]
let modify_scale: Box<dyn FnOnce(Factor) -> Factor> = Box::new(modify_scale);
self.zoom_impl(modify_scale, origin);
}
fn zoom_impl(&self, modify_scale: impl FnOnce(Factor) -> Factor, center_in_viewport: PxPoint) {
if !SCROLL_MODE_VAR.get().contains(ScrollMode::ZOOM) {
return;
}
let content = SCROLL.rendered_content();
let mut center_in_content = center_in_viewport + content.origin.to_vector();
SCROLL.chase_zoom(|f| {
center_in_content /= f;
let s = modify_scale(f);
center_in_content *= s;
s
});
let viewport_size = SCROLL_VIEWPORT_SIZE_VAR.get();
// scroll so that new center_in_content is at the same center_in_viewport
let max_scroll = content.size - viewport_size;
let offset = center_in_content - center_in_viewport;
if offset.y != Px(0) && max_scroll.height > Px(0) {
let offset_y = offset.y.0 as f32 / max_scroll.height.0 as f32;
SCROLL.chase_vertical(|_| offset_y.fct());
}
if offset.x != Px(0) && max_scroll.width > Px(0) {
let offset_x = offset.x.0 as f32 / max_scroll.width.0 as f32;
SCROLL.chase_horizontal(|_| offset_x.fct());
}
}
/// Returns `true` if the content height is greater then the viewport height.
pub fn can_scroll_vertical(&self) -> bool {
let viewport = SCROLL_VIEWPORT_SIZE_VAR.get().height;