mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 18:46:57 -06:00
This commit is contained in:
+118
-10
@@ -66,19 +66,31 @@ pub fn show(app: &mut SlideApp, ui: &mut Ui) {
|
||||
}
|
||||
return;
|
||||
}
|
||||
let (content_rect, scale) = layout(app, avail);
|
||||
// Scroll when zoomed beyond the window.
|
||||
let scroll_id = ui.id().with("canvas_scroll");
|
||||
let last_id = scroll_id.with("last");
|
||||
let mut offset: egui::Vec2 = ui.data_mut(|d| d.get_temp(scroll_id).unwrap_or_default());
|
||||
let overflow = vec2((content_rect.width() + 56.0 - avail.width()).max(0.0), (content_rect.height() + 56.0 - avail.height()).max(0.0));
|
||||
let last: Option<(Rect, f32)> = ui.data_mut(|d| d.get_temp(last_id));
|
||||
let resp = ui.allocate_rect(avail, Sense::click_and_drag());
|
||||
let (wheel, zoom_delta, pinch_pos) = ui.input(|i| (i.smooth_scroll_delta, i.zoom_delta(), i.pointer.hover_pos()));
|
||||
if resp.hovered() {
|
||||
if (zoom_delta - 1.0).abs() > 1e-3 {
|
||||
let cur = app.ui.zoom.unwrap_or(scale);
|
||||
app.ui.zoom = Some((cur * zoom_delta).clamp(0.1, 4.0));
|
||||
let _ = pinch_pos;
|
||||
} else if overflow != egui::Vec2::ZERO {
|
||||
let (wheel, zoom_delta, hover) = ui.input(|i| (i.smooth_scroll_delta, i.zoom_delta(), i.pointer.hover_pos()));
|
||||
let zooming = resp.hovered() && (zoom_delta - 1.0).abs() > 1e-3;
|
||||
if zooming {
|
||||
let cur = app.ui.zoom.unwrap_or_else(|| layout(app, avail).1);
|
||||
app.ui.zoom = Some((cur * zoom_delta).clamp(0.1, 4.0));
|
||||
}
|
||||
let (content_rect, scale) = layout(app, avail);
|
||||
// A new zoom keeps the slide point under the pointer (wheel, pinch) or under the middle of the
|
||||
// canvas (Zoom In/Out, the slider) where it was, instead of growing from the corner (#106).
|
||||
if let Some((last_rect, last_scale)) = last
|
||||
&& (scale - last_scale).abs() > 1e-6
|
||||
{
|
||||
let anchor = if zooming { hover.unwrap_or(avail.center()) } else { avail.center() };
|
||||
let at = (anchor - last_rect.min) / last_scale;
|
||||
offset = content_rect.min + at * scale - anchor;
|
||||
}
|
||||
let overflow = vec2((content_rect.width() + 56.0 - avail.width()).max(0.0), (content_rect.height() + 56.0 - avail.height()).max(0.0));
|
||||
if resp.hovered() && !zooming {
|
||||
if overflow != egui::Vec2::ZERO {
|
||||
offset -= wheel;
|
||||
} else if wheel.y.abs() > 18.0 && app.session.tool.marquee.is_none() && !app.session.tool.dragging() {
|
||||
// Wheel over a fitted slide pages through slides (like PowerPoint).
|
||||
@@ -94,8 +106,11 @@ pub fn show(app: &mut SlideApp, ui: &mut Ui) {
|
||||
}
|
||||
offset.x = offset.x.clamp(0.0, overflow.x);
|
||||
offset.y = offset.y.clamp(0.0, overflow.y);
|
||||
ui.data_mut(|d| d.insert_temp(scroll_id, offset));
|
||||
let slide_rect = content_rect.translate(-offset);
|
||||
ui.data_mut(|d| {
|
||||
d.insert_temp(scroll_id, offset);
|
||||
d.insert_temp(last_id, (slide_rect, scale));
|
||||
});
|
||||
app.slide_rect = Some(slide_rect);
|
||||
app.canvas_scale = scale;
|
||||
let xf = Xf { origin: slide_rect.min, scale };
|
||||
@@ -700,3 +715,96 @@ fn context_menu(app: &mut SlideApp, resp: &egui::Response) {
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#![allow(clippy::unwrap_used, clippy::expect_used)]
|
||||
|
||||
use deckcraft_engine::Session;
|
||||
use egui::{Event, Modifiers, MouseWheelUnit, Pos2, TouchPhase, pos2, vec2};
|
||||
use egui_kittest::Harness;
|
||||
|
||||
use crate::{Services, SlideApp};
|
||||
|
||||
fn harness() -> Harness<'static, SlideApp> {
|
||||
let mut h = Harness::builder().with_size(egui::vec2(1200.0, 800.0)).build_ui_state(
|
||||
|ui, app: &mut SlideApp| {
|
||||
app.logic(ui.ctx());
|
||||
app.ui(ui);
|
||||
},
|
||||
SlideApp::new(Session::with_new(), Services::default()),
|
||||
);
|
||||
h.run_steps(3);
|
||||
h
|
||||
}
|
||||
|
||||
/// The slide point (in slide points) under screen position `p`.
|
||||
fn slide_point(h: &Harness<'_, SlideApp>, p: Pos2) -> Pos2 {
|
||||
let (r, s) = (h.state().slide_rect.unwrap(), h.state().canvas_scale);
|
||||
pos2((p.x - r.min.x) / s, (p.y - r.min.y) / s)
|
||||
}
|
||||
|
||||
fn ctrl_wheel(h: &mut Harness<'_, SlideApp>, lines: f32) {
|
||||
let wheel = Event::MouseWheel { unit: MouseWheelUnit::Line, delta: vec2(0.0, lines), phase: TouchPhase::Move, modifiers: Modifiers::COMMAND };
|
||||
h.event(wheel);
|
||||
// egui spreads a wheel notch over several frames.
|
||||
h.run_steps(30);
|
||||
}
|
||||
|
||||
fn assert_near(a: Pos2, b: Pos2, what: &str) {
|
||||
assert!((a - b).length() < 0.5, "{what}: {a:?} moved to {b:?}");
|
||||
}
|
||||
|
||||
/// #106: wheel and pinch zoom zoomed toward the slide's corner instead of the pointer.
|
||||
#[test]
|
||||
fn wheel_and_pinch_zoom_keep_the_point_under_the_pointer() {
|
||||
let mut h = harness();
|
||||
let fit = h.state().canvas_scale;
|
||||
let r = h.state().slide_rect.unwrap();
|
||||
let p = r.min + vec2(r.width() * 0.8, r.height() * 0.7);
|
||||
h.event(Event::PointerMoved(p));
|
||||
h.run_steps(1);
|
||||
let under = slide_point(&h, p);
|
||||
|
||||
h.event(Event::Zoom(2.0));
|
||||
h.run_steps(2);
|
||||
assert!((h.state().canvas_scale - fit * 2.0).abs() < 1e-3);
|
||||
assert_near(under, slide_point(&h, p), "pinch in");
|
||||
|
||||
ctrl_wheel(&mut h, 1.0);
|
||||
assert!(h.state().canvas_scale > fit * 2.1, "Ctrl/Cmd + wheel up zooms in");
|
||||
assert_near(under, slide_point(&h, p), "wheel in");
|
||||
|
||||
ctrl_wheel(&mut h, -1.0);
|
||||
ctrl_wheel(&mut h, -1.0);
|
||||
assert!(h.state().canvas_scale < fit * 2.0, "Ctrl/Cmd + wheel down zooms out");
|
||||
assert_near(under, slide_point(&h, p), "wheel out");
|
||||
|
||||
h.event(Event::Zoom(0.8));
|
||||
h.run_steps(2);
|
||||
assert_near(under, slide_point(&h, p), "pinch out");
|
||||
|
||||
// Zoomed out below the window size, the slide is centred again.
|
||||
h.event(Event::Zoom(0.25));
|
||||
h.run_steps(2);
|
||||
assert!(h.state().canvas_scale < fit);
|
||||
let (canvas, slide) = (h.state().canvas_rect.unwrap(), h.state().slide_rect.unwrap());
|
||||
assert!((canvas.center() - slide.center()).length() < 1.0, "{canvas:?} {slide:?}");
|
||||
}
|
||||
|
||||
/// Zoom In/Out (keys, buttons, slider) keeps the middle of the view where it is.
|
||||
#[test]
|
||||
fn zoom_commands_keep_the_middle_of_the_view() {
|
||||
let mut h = harness();
|
||||
let c = h.state().canvas_rect.unwrap().center();
|
||||
let mid = slide_point(&h, c);
|
||||
for _ in 0..4 {
|
||||
h.state_mut().run("view.zoomIn", serde_json::json!({})).unwrap();
|
||||
h.run_steps(2);
|
||||
assert_near(mid, slide_point(&h, c), "zoom in");
|
||||
}
|
||||
h.state_mut().run("view.zoomOut", serde_json::json!({})).unwrap();
|
||||
h.run_steps(2);
|
||||
assert_near(mid, slide_point(&h, c), "zoom out");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user