mirror of
https://github.com/storytold/effectcraft.git
synced 2026-10-30 04:57:54 -06:00
Viewer trackpad gestures: two-finger scroll pans, pinch zooms
The Composition and Layer viewers took every scroll as a zoom, so a trackpad could not pan. A trackpad's two-finger scroll now pans; a pinch, Ctrl/Cmd+scroll, a mouse wheel and Alt + a trackpad scroll zoom around the pointer. Where the platform names the device (egui's MouseWheelSource, macOS), that decides. Elsewhere browsers and some high-resolution wheels report a mouse wheel in points, as a trackpad does, so the unit alone doesn't decide. A scroll in points pans only once it has shown it is a trackpad: a touch phase, movement on both axes at once, or a pinch. That holds until a wheel scrolls in lines. Until then the vertical part zooms and the horizontal part pans. ui.scroll gains unit (point|line), source (wheel|trackpad) and pinch so agents can drive both gestures. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
5ed248bece
commit
caedb0727a
@@ -9,7 +9,7 @@
|
||||
//! - `ui.inspect`: UI state + summary; `ui.elements {prefix?}`: registered widgets (id, label, rect)
|
||||
//! - `ui.set {tool?, workspace?, theme?, focused?, viewer?: {zoom, res, pan, grid, …}, timeline?: {pps, start, graphEditor, showModes}}`
|
||||
//! - `ui.panel.show {panel}` / `ui.panel.close {panel}`
|
||||
//! - `ui.click {id | x,y, button?, count?, modifiers?}` / `ui.move {x,y}` / `ui.scroll {x,y,dx,dy}`
|
||||
//! - `ui.click {id | x,y, button?, count?, modifiers?}` / `ui.move {x,y}` / `ui.scroll {x,y,dx,dy,unit?,source?,pinch?}`
|
||||
//! - `ui.drag {from:{id|x,y}, to:{id|x,y}, steps?, modifiers?}`: synthetic press-move-release
|
||||
//! - `ui.key {key, command?, shift?, alt?, ctrl?}` / `ui.type {text}`
|
||||
//! - `ui.viewer.locate {x, y}`: screen point of a comp pixel; `ui.viewer.hit {x, y}`: comp pixel at a screen point
|
||||
@@ -500,8 +500,9 @@ fn drag(app: &mut EffectcraftApp, p: &Value) -> Outcome {
|
||||
Outcome::AfterInput
|
||||
}
|
||||
|
||||
/// Queues a move to the target point, a wheel event of `dx`, `dy` points with the request's
|
||||
/// modifiers, and a released Extra1 button. Replies [`Outcome::AfterInput`].
|
||||
/// Queues a move to the target point, a wheel event of `dx`, `dy` (`unit`: points or lines) with
|
||||
/// the request's modifiers, a pinch's zoom factor, and a released Extra1 button. Replies
|
||||
/// [`Outcome::AfterInput`].
|
||||
fn scroll(app: &mut EffectcraftApp, p: &Value) -> Outcome {
|
||||
let pos = match point(app, p) {
|
||||
Ok(p) => p,
|
||||
@@ -509,14 +510,23 @@ fn scroll(app: &mut EffectcraftApp, p: &Value) -> Outcome {
|
||||
};
|
||||
let dx = p.get("dx").and_then(Value::as_f64).unwrap_or(0.0) as f32;
|
||||
let dy = p.get("dy").and_then(Value::as_f64).unwrap_or(0.0) as f32;
|
||||
let line = p.get("unit").and_then(Value::as_str) == Some("line");
|
||||
let unit = if line { egui::MouseWheelUnit::Line } else { egui::MouseWheelUnit::Point };
|
||||
// Without a `source`, a scroll in lines is a mouse wheel and one in points comes from an
|
||||
// unnamed device, as on Windows, Linux and the web.
|
||||
let source = match p.get("source").and_then(Value::as_str) {
|
||||
Some("trackpad") => egui::MouseWheelSource::Trackpad,
|
||||
Some("wheel") => egui::MouseWheelSource::Wheel,
|
||||
_ if line => egui::MouseWheelSource::Wheel,
|
||||
_ => egui::MouseWheelSource::Unknown,
|
||||
};
|
||||
app.synthetic.push(egui::Event::PointerMoved(pos));
|
||||
app.synthetic.push(egui::Event::MouseWheel {
|
||||
unit: egui::MouseWheelUnit::Point,
|
||||
delta: egui::vec2(dx, dy),
|
||||
modifiers: modifiers(p),
|
||||
phase: egui::TouchPhase::Move,
|
||||
source: egui::MouseWheelSource::Wheel,
|
||||
});
|
||||
if dx != 0.0 || dy != 0.0 {
|
||||
app.synthetic.push(egui::Event::MouseWheel { unit, delta: egui::vec2(dx, dy), modifiers: modifiers(p), phase: egui::TouchPhase::Move, source });
|
||||
}
|
||||
if let Some(pinch) = p.get("pinch").and_then(Value::as_f64).filter(|z| z.is_finite() && *z > 0.0) {
|
||||
app.synthetic.push(egui::Event::Zoom(pinch as f32));
|
||||
}
|
||||
app.synthetic.push(egui::Event::PointerButton {
|
||||
pos: egui::pos2(-100.0, -100.0),
|
||||
button: egui::PointerButton::Extra1,
|
||||
|
||||
@@ -175,6 +175,56 @@ pub(crate) fn space_hand(ctx: &egui::Context) -> bool {
|
||||
ctx.input(|i| i.key_down(egui::Key::Space)) && !ctx.egui_wants_keyboard_input()
|
||||
}
|
||||
|
||||
/// What a scroll gesture over a viewer (Composition, Layer) does this frame.
|
||||
pub(crate) struct ViewerScroll {
|
||||
/// Points to pan by: a two-finger trackpad scroll.
|
||||
pub pan: egui::Vec2,
|
||||
/// Factor to zoom by around the pointer: a pinch, Ctrl/Cmd+scroll, a mouse wheel, or
|
||||
/// Alt + a trackpad scroll.
|
||||
pub zoom: f32,
|
||||
}
|
||||
|
||||
/// Splits the frame's scroll input into a viewer pan and a zoom: a mouse wheel zooms and a
|
||||
/// trackpad's two-finger scroll pans.
|
||||
///
|
||||
/// Where the platform names the device (`MouseWheelSource`, macOS), that decides. Elsewhere a
|
||||
/// wheel scrolls in lines, but in a browser and with some high-resolution wheels it scrolls in
|
||||
/// points like a trackpad does, so the unit alone doesn't tell them apart. A scroll in points is
|
||||
/// a trackpad's once it has shown what a wheel can't do: a touch phase, movement on both axes at
|
||||
/// once, or a pinch. That holds until a wheel scrolls in lines. Until then the vertical part
|
||||
/// zooms; the horizontal part always pans.
|
||||
pub(crate) fn viewer_scroll(ctx: &egui::Context) -> ViewerScroll {
|
||||
let id = egui::Id::unique("viewer-scroll-trackpad");
|
||||
// The scroll is smoothed over the frames after its event, so the device is remembered.
|
||||
let was: bool = ctx.data(|d| d.get_temp(id)).unwrap_or(false);
|
||||
let trackpad = ctx.input(|i| {
|
||||
i.events.iter().fold(was, |t, e| match e {
|
||||
egui::Event::MouseWheel { source: egui::MouseWheelSource::Trackpad | egui::MouseWheelSource::Momentum, .. } => true,
|
||||
egui::Event::MouseWheel { source: egui::MouseWheelSource::Wheel, .. } => false,
|
||||
egui::Event::MouseWheel { unit: egui::MouseWheelUnit::Point, delta, phase, modifiers, .. } => {
|
||||
// Shift turns a wheel's scroll sideways, which is still one axis.
|
||||
t || *phase == egui::TouchPhase::Start || (delta.x != 0.0 && delta.y != 0.0 && !modifiers.shift)
|
||||
}
|
||||
egui::Event::MouseWheel { .. } => false,
|
||||
egui::Event::Zoom(_) => true,
|
||||
_ => t,
|
||||
})
|
||||
});
|
||||
if trackpad != was {
|
||||
ctx.data_mut(|d| d.insert_temp(id, trackpad));
|
||||
}
|
||||
let (scroll, pinch, alt) = ctx.input(|i| (i.smooth_scroll_delta, i.zoom_delta(), i.modifiers.alt));
|
||||
let pinch = if pinch.is_finite() && pinch > 0.0 { pinch } else { 1.0 };
|
||||
let scroll = if scroll.x.is_finite() && scroll.y.is_finite() { scroll } else { egui::Vec2::ZERO };
|
||||
if trackpad && !alt {
|
||||
ViewerScroll { pan: scroll, zoom: pinch }
|
||||
} else if alt {
|
||||
ViewerScroll { pan: egui::Vec2::ZERO, zoom: pinch * (scroll.y / 300.0).exp() }
|
||||
} else {
|
||||
ViewerScroll { pan: egui::vec2(scroll.x, 0.0), zoom: pinch * (scroll.y / 300.0).exp() }
|
||||
}
|
||||
}
|
||||
|
||||
pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: PanelKind, rect: Rect) {
|
||||
match p {
|
||||
PanelKind::Composition => viewers::show(app, ui, 0, rect),
|
||||
|
||||
@@ -310,13 +310,17 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
app.auto.add("layerPanel.canvas", frame, &layer.name);
|
||||
app.auto.add("layerPanel.area", area, "Layer panel");
|
||||
|
||||
// Navigation (as the Composition panel): the wheel / pinch zooms around the pointer with any
|
||||
// tool; the Hand tool or Space-drag pans and the Zoom tool clicks zoom in (Alt: out).
|
||||
// Navigation (as the Composition panel): the wheel / pinch zooms around the pointer and a
|
||||
// two-finger trackpad scroll pans with any tool; the Hand tool or Space-drag pans and the
|
||||
// Zoom tool clicks zoom in (Alt: out).
|
||||
let hovered = ui.rect_contains_pointer(area);
|
||||
if let Some(hp) = ui.input(|i| i.pointer.hover_pos()).filter(|_| hovered) {
|
||||
let scroll = ui.input(|i| i.smooth_scroll_delta.y + i.zoom_delta().ln() * 300.0);
|
||||
if scroll.is_finite() && scroll.abs() > 0.1 {
|
||||
zoom_at(app, area, [lw, lh], hp, to_layer(hp), zoom as f32 * (scroll / 300.0).exp());
|
||||
let scroll = super::viewer_scroll(&ctx);
|
||||
if (scroll.zoom - 1.0).abs() > 3e-4 {
|
||||
zoom_at(app, area, [lw, lh], hp, to_layer(hp), zoom as f32 * scroll.zoom);
|
||||
} else if scroll.pan != egui::Vec2::ZERO {
|
||||
app.ui.layer_zoom.get_or_insert(zoom as f32);
|
||||
app.ui.layer_pan = [pan.x + scroll.pan.x, pan.y + scroll.pan.y];
|
||||
}
|
||||
}
|
||||
// A middle-drag pans with any tool; painting and Roto Brush strokes don't run meanwhile.
|
||||
|
||||
@@ -935,11 +935,14 @@ fn phase_hover(app: &mut EffectcraftApp, ui: &mut egui::Ui, ectx: &EvalCtx, resp
|
||||
}
|
||||
};
|
||||
ctx.set_cursor_icon(cursor);
|
||||
// Scroll wheel zooms around the pointer.
|
||||
let scroll = ui.input(|i| i.smooth_scroll_delta.y + i.zoom_delta().ln() * 300.0);
|
||||
if scroll.abs() > 0.1 && area.contains(hp) {
|
||||
let k = (scroll / 300.0).exp();
|
||||
let nz = (zoom * k).clamp(0.01, 32.0);
|
||||
// The mouse wheel and a trackpad pinch zoom around the pointer; a two-finger trackpad
|
||||
// scroll pans.
|
||||
let scroll = super::viewer_scroll(&ctx);
|
||||
if scroll.pan != egui::Vec2::ZERO && area.contains(hp) {
|
||||
app.ui.viewer.pan = [app.ui.viewer.pan[0] + scroll.pan.x, app.ui.viewer.pan[1] + scroll.pan.y];
|
||||
}
|
||||
if (scroll.zoom - 1.0).abs() > 3e-4 && area.contains(hp) {
|
||||
let nz = (zoom * scroll.zoom).clamp(0.01, 32.0);
|
||||
let before = map.to_comp(hp);
|
||||
let new_origin = hp - vec2(before[0] as f32 * nz, before[1] as f32 * nz);
|
||||
let new_center = new_origin + vec2(cw * nz / 2.0, ch * nz / 2.0);
|
||||
|
||||
@@ -80,6 +80,7 @@ mod ui_tracker;
|
||||
mod ui_unreadable_footage;
|
||||
mod ui_video_preview_off;
|
||||
mod ui_viewer;
|
||||
mod ui_viewer_trackpad;
|
||||
mod ui_viewers;
|
||||
mod ui_visual_review;
|
||||
mod ui_web_gpu_storage;
|
||||
|
||||
@@ -171,8 +171,8 @@ fn viewer_zooms_and_pans_over_the_track_points() {
|
||||
let zoom = h.state().ui.viewer.zoom;
|
||||
h.input_mut().events.push(egui::Event::PointerMoved(at));
|
||||
h.input_mut().events.push(egui::Event::MouseWheel {
|
||||
unit: egui::MouseWheelUnit::Point,
|
||||
delta: egui::vec2(0.0, 120.0),
|
||||
unit: egui::MouseWheelUnit::Line,
|
||||
delta: egui::vec2(0.0, 3.0),
|
||||
modifiers: Default::default(),
|
||||
phase: egui::TouchPhase::Move,
|
||||
source: egui::MouseWheelSource::Wheel,
|
||||
|
||||
@@ -107,8 +107,8 @@ fn wheel_zooms_around_the_pointer_and_hand_tool_pans() {
|
||||
let at = canvas.min + canvas.size() * 0.25;
|
||||
h.input_mut().events.push(Event::PointerMoved(at));
|
||||
h.input_mut().events.push(Event::MouseWheel {
|
||||
unit: egui::MouseWheelUnit::Point,
|
||||
delta: vec2(0.0, 120.0),
|
||||
unit: egui::MouseWheelUnit::Line,
|
||||
delta: vec2(0.0, 3.0),
|
||||
modifiers: Default::default(),
|
||||
phase: egui::TouchPhase::Move,
|
||||
source: egui::MouseWheelSource::Wheel,
|
||||
@@ -176,3 +176,33 @@ fn roto_brush_in_the_composition_panel_says_where_it_works() {
|
||||
assert_eq!(h.state().ui.layer_panel, Some(box_id(&h)));
|
||||
assert_eq!(h.state().ui.focused, PanelKind::Layer);
|
||||
}
|
||||
|
||||
/// A two-finger trackpad scroll pans the layer with any tool and a pinch zooms around the pointer.
|
||||
#[test]
|
||||
fn trackpad_scroll_pans_and_pinch_zooms_around_the_pointer() {
|
||||
let mut h = harness();
|
||||
let canvas = rect(&h, "layerPanel.canvas");
|
||||
let at = canvas.min + canvas.size() * 0.25;
|
||||
h.input_mut().events.push(Event::PointerMoved(at));
|
||||
h.input_mut().events.push(Event::MouseWheel {
|
||||
unit: egui::MouseWheelUnit::Point,
|
||||
delta: vec2(60.0, -30.0),
|
||||
modifiers: Default::default(),
|
||||
phase: egui::TouchPhase::Move,
|
||||
source: egui::MouseWheelSource::Unknown,
|
||||
});
|
||||
h.run_steps(30);
|
||||
let panned = rect(&h, "layerPanel.canvas");
|
||||
let moved = panned.min - canvas.min;
|
||||
assert!((moved.x - 60.0).abs() < 1.5 && (moved.y + 30.0).abs() < 1.5, "the scroll pans: {moved:?}");
|
||||
assert!((panned.width() - canvas.width()).abs() < 0.5, "a scroll doesn't zoom: {} → {}", canvas.width(), panned.width());
|
||||
// Pinch: zooms, keeping the point under the pointer.
|
||||
let at = panned.min + panned.size() * 0.25;
|
||||
h.input_mut().events.push(Event::PointerMoved(at));
|
||||
h.input_mut().events.push(Event::Zoom(1.5));
|
||||
h.run_steps(3);
|
||||
let zoomed = rect(&h, "layerPanel.canvas");
|
||||
assert!((zoomed.width() / panned.width() - 1.5).abs() < 0.01, "the pinch zooms: {} → {}", panned.width(), zoomed.width());
|
||||
let still = zoomed.min + zoomed.size() * 0.25;
|
||||
assert!(still.distance(at) < 2.0, "the point under the pointer stays put: {at:?} vs {still:?}");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,156 @@
|
||||
//! Trackpad gestures in the Composition viewer: a two-finger scroll pans, a pinch (and the mouse
|
||||
//! wheel, also one that scrolls in pixels) zooms around the pointer (egui_kittest, UI logic only).
|
||||
|
||||
use effectcraft_engine::Session;
|
||||
use effectcraft_ui_egui::EffectcraftApp;
|
||||
use effectcraft_ui_egui::dock::PanelKind;
|
||||
use egui::{Event, Rect, pos2, vec2};
|
||||
use egui_kittest::Harness;
|
||||
use serde_json::json;
|
||||
|
||||
fn harness() -> Harness<'static, EffectcraftApp> {
|
||||
let mut s = Session::default();
|
||||
s.execute("comp.new", json!({"name": "View", "width": 640, "height": 360, "duration": 4})).unwrap();
|
||||
let mut h = Harness::builder().with_size(vec2(1600.0, 1000.0)).build_eframe(|_| EffectcraftApp::new(s));
|
||||
h.state_mut().show_panel(PanelKind::Composition);
|
||||
h.run_steps(4);
|
||||
h
|
||||
}
|
||||
|
||||
fn rect(h: &Harness<'_, EffectcraftApp>, id: &str) -> Rect {
|
||||
let e = h.state().auto.find(id).unwrap_or_else(|| panic!("no {id}"));
|
||||
Rect::from_min_size(pos2(e.rect[0], e.rect[1]), vec2(e.rect[2], e.rect[3]))
|
||||
}
|
||||
|
||||
fn wheel(h: &mut Harness<'_, EffectcraftApp>, unit: egui::MouseWheelUnit, delta: egui::Vec2, modifiers: egui::Modifiers) {
|
||||
h.input_mut().events.push(Event::MouseWheel { unit, delta, modifiers, phase: egui::TouchPhase::Move, source: egui::MouseWheelSource::Unknown });
|
||||
h.run_steps(30);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn trackpad_scroll_pans_the_composition() {
|
||||
let mut h = harness();
|
||||
let comp = rect(&h, "viewer.comp");
|
||||
h.input_mut().events.push(Event::PointerMoved(comp.center()));
|
||||
wheel(&mut h, egui::MouseWheelUnit::Point, vec2(80.0, -40.0), Default::default());
|
||||
let after = rect(&h, "viewer.comp");
|
||||
let moved = after.min - comp.min;
|
||||
assert!((moved.x - 80.0).abs() < 1.5 && (moved.y + 40.0).abs() < 1.5, "the scroll pans: {moved:?}");
|
||||
assert!((after.width() - comp.width()).abs() < 0.5, "a scroll doesn't zoom: {} → {}", comp.width(), after.width());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn pinch_and_mouse_wheel_zoom_around_the_pointer() {
|
||||
let mut h = harness();
|
||||
let comp = rect(&h, "viewer.comp");
|
||||
let at = comp.min + comp.size() * 0.25;
|
||||
h.input_mut().events.push(Event::PointerMoved(at));
|
||||
h.input_mut().events.push(Event::Zoom(1.5));
|
||||
h.run_steps(3);
|
||||
let zoomed = rect(&h, "viewer.comp");
|
||||
assert!((zoomed.width() / comp.width() - 1.5).abs() < 0.01, "the pinch zooms: {} → {}", comp.width(), zoomed.width());
|
||||
let still = zoomed.min + zoomed.size() * 0.25;
|
||||
assert!(still.distance(at) < 2.0, "the point under the pointer stays put: {at:?} vs {still:?}");
|
||||
// A mouse wheel (it scrolls in lines) zooms too, and so does Alt + a trackpad scroll.
|
||||
wheel(&mut h, egui::MouseWheelUnit::Line, vec2(0.0, 3.0), Default::default());
|
||||
let wheeled = rect(&h, "viewer.comp");
|
||||
assert!(wheeled.width() > zoomed.width() * 1.1, "the wheel zooms in: {} → {}", zoomed.width(), wheeled.width());
|
||||
h.input_mut().events.push(Event::ModifiersChanged(egui::Modifiers::ALT));
|
||||
wheel(&mut h, egui::MouseWheelUnit::Point, vec2(0.0, -120.0), egui::Modifiers::ALT);
|
||||
let out = rect(&h, "viewer.comp");
|
||||
assert!(out.width() < wheeled.width() * 0.9, "Alt + scroll zooms out: {} → {}", wheeled.width(), out.width());
|
||||
}
|
||||
|
||||
/// Browsers and some high-resolution wheels report a mouse wheel in pixels, as a trackpad does:
|
||||
/// it still zooms.
|
||||
#[test]
|
||||
fn a_pixel_unit_mouse_wheel_zooms() {
|
||||
let mut h = harness();
|
||||
let comp = rect(&h, "viewer.comp");
|
||||
let at = comp.min + comp.size() * 0.25;
|
||||
h.input_mut().events.push(Event::PointerMoved(at));
|
||||
wheel(&mut h, egui::MouseWheelUnit::Point, vec2(0.0, 120.0), Default::default());
|
||||
let zoomed = rect(&h, "viewer.comp");
|
||||
assert!(zoomed.width() > comp.width() * 1.1, "a pixel wheel zooms in: {} → {}", comp.width(), zoomed.width());
|
||||
let still = zoomed.min + zoomed.size() * 0.25;
|
||||
assert!(still.distance(at) < 2.0, "the point under the pointer stays put: {at:?} vs {still:?}");
|
||||
// Shift turns the wheel's scroll sideways: still a wheel, and the next notch zooms.
|
||||
h.input_mut().events.push(Event::ModifiersChanged(egui::Modifiers::SHIFT));
|
||||
wheel(&mut h, egui::MouseWheelUnit::Point, vec2(0.0, 120.0), egui::Modifiers::SHIFT);
|
||||
h.input_mut().events.push(Event::ModifiersChanged(egui::Modifiers::NONE));
|
||||
let before = rect(&h, "viewer.comp");
|
||||
wheel(&mut h, egui::MouseWheelUnit::Point, vec2(0.0, -120.0), Default::default());
|
||||
let out = rect(&h, "viewer.comp");
|
||||
assert!(out.width() < before.width() * 0.9, "the wheel still zooms: {} → {}", before.width(), out.width());
|
||||
}
|
||||
|
||||
/// A straight vertical trackpad scroll pans when the touch phase says it is a trackpad,
|
||||
/// and after any diagonal scroll; a mouse wheel in lines takes the zoom back.
|
||||
#[test]
|
||||
fn a_vertical_trackpad_scroll_pans_once_the_trackpad_is_known() {
|
||||
let mut h = harness();
|
||||
let comp = rect(&h, "viewer.comp");
|
||||
h.input_mut().events.push(Event::PointerMoved(comp.center()));
|
||||
h.input_mut().events.push(Event::MouseWheel {
|
||||
unit: egui::MouseWheelUnit::Point,
|
||||
delta: egui::Vec2::ZERO,
|
||||
modifiers: Default::default(),
|
||||
source: egui::MouseWheelSource::Unknown,
|
||||
phase: egui::TouchPhase::Start,
|
||||
});
|
||||
h.input_mut().events.push(Event::MouseWheel {
|
||||
unit: egui::MouseWheelUnit::Point,
|
||||
delta: vec2(0.0, 50.0),
|
||||
modifiers: Default::default(),
|
||||
source: egui::MouseWheelSource::Unknown,
|
||||
phase: egui::TouchPhase::Move,
|
||||
});
|
||||
h.step();
|
||||
h.input_mut().events.push(Event::MouseWheel {
|
||||
unit: egui::MouseWheelUnit::Point,
|
||||
delta: egui::Vec2::ZERO,
|
||||
modifiers: Default::default(),
|
||||
source: egui::MouseWheelSource::Unknown,
|
||||
phase: egui::TouchPhase::End,
|
||||
});
|
||||
h.run_steps(30);
|
||||
let panned = rect(&h, "viewer.comp");
|
||||
assert!((panned.min.y - comp.min.y - 50.0).abs() < 1.5, "the scroll pans: {:?}", panned.min - comp.min);
|
||||
assert!((panned.width() - comp.width()).abs() < 0.5, "a trackpad scroll doesn't zoom");
|
||||
// Without phases (Windows, Linux, the web) it stays a trackpad.
|
||||
wheel(&mut h, egui::MouseWheelUnit::Point, vec2(0.0, 30.0), Default::default());
|
||||
let again = rect(&h, "viewer.comp");
|
||||
assert!((again.min.y - panned.min.y - 30.0).abs() < 1.5 && (again.width() - comp.width()).abs() < 0.5, "{again:?}");
|
||||
// A wheel that scrolls in lines is a mouse: it zooms, and so does a pixel wheel after it.
|
||||
wheel(&mut h, egui::MouseWheelUnit::Line, vec2(0.0, 3.0), Default::default());
|
||||
let zoomed = rect(&h, "viewer.comp");
|
||||
assert!(zoomed.width() > again.width() * 1.1, "the wheel zooms: {} → {}", again.width(), zoomed.width());
|
||||
wheel(&mut h, egui::MouseWheelUnit::Point, vec2(0.0, 120.0), Default::default());
|
||||
assert!(rect(&h, "viewer.comp").width() > zoomed.width() * 1.1, "a pixel wheel zooms again");
|
||||
}
|
||||
|
||||
/// Where the platform names the device (macOS), a straight vertical trackpad scroll pans at once
|
||||
/// and a wheel in points zooms.
|
||||
#[test]
|
||||
fn the_wheel_source_decides_where_the_platform_reports_it() {
|
||||
let mut h = harness();
|
||||
let comp = rect(&h, "viewer.comp");
|
||||
let at = comp.min + comp.size() * 0.25;
|
||||
h.input_mut().events.push(Event::PointerMoved(at));
|
||||
let scroll = |h: &mut Harness<'_, EffectcraftApp>, source, dy| {
|
||||
h.input_mut().events.push(Event::MouseWheel {
|
||||
unit: egui::MouseWheelUnit::Point,
|
||||
delta: vec2(0.0, dy),
|
||||
modifiers: Default::default(),
|
||||
phase: egui::TouchPhase::Move,
|
||||
source,
|
||||
});
|
||||
h.run_steps(30);
|
||||
};
|
||||
scroll(&mut h, egui::MouseWheelSource::Trackpad, 40.0);
|
||||
let panned = rect(&h, "viewer.comp");
|
||||
assert!((panned.min.y - comp.min.y - 40.0).abs() < 1.5, "the trackpad pans: {:?}", panned.min - comp.min);
|
||||
assert!((panned.width() - comp.width()).abs() < 0.5, "a trackpad scroll doesn't zoom");
|
||||
scroll(&mut h, egui::MouseWheelSource::Wheel, 120.0);
|
||||
assert!(rect(&h, "viewer.comp").width() > panned.width() * 1.1, "the wheel zooms");
|
||||
}
|
||||
@@ -103,7 +103,7 @@ Points are logical window coordinates. A target is `{id}` (the element's centre;
|
||||
| `ui.click` | `{id \| x,y, button?: left\|right\|middle, count?, modifiers?}` |
|
||||
| `ui.move` | `{id \| x,y}` |
|
||||
| `ui.drag` | `{from: target, to: target, steps? (12), modifiers?}`: press, move, release |
|
||||
| `ui.scroll` | `{id \| x,y, dx, dy}` |
|
||||
| `ui.scroll` | `{id \| x,y, dx?, dy?, unit?: point\|line, source?: wheel\|trackpad, pinch?, modifiers?}`: `line` is a mouse wheel (zooms the viewers); `point` (default) is a pixel-precise scroll; `source: trackpad` pans the viewers (as on macOS); with no `source` the viewers take a scroll in points as a wheel until one moves on both axes (a trackpad: from then on it pans); `pinch` is a trackpad pinch's zoom factor |
|
||||
| `ui.key` | `{key}`, for example `Space`, `F9`, `Cmd+Z`, `Cmd+Shift+Z`, `PageDown`, `Escape` |
|
||||
| `ui.type` | `{text}`: text input into the focused field |
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ monitors are missing, and two GPU start-up failures are open.
|
||||
| HDR display | HDR monitoring on EDR / HDR displays | tone-mapped SDR view; no EDR output | no HDR output | no | no | Colour engine handles HDR maths (Rec. 2100 PQ/HLG output files) |
|
||||
| Pen tablets (pressure, tilt) | yes, for Paint and Roto | Brushes panel has pressure options, but no tablet pressure is read | same | same | same | Paint strokes store pressure per vertex; input never fills it |
|
||||
| Multiple monitors | panels and windows across screens | panels move into their own native windows on any screen (Move Panel to New Window; desktop builds) | same | same (#486) | — | gap 20 |
|
||||
| Touch / trackpad gestures | pinch zoom in viewers | egui pointer and scroll; no pinch-to-zoom audited | — | — | — | — |
|
||||
| Touch / trackpad gestures | pinch zoom in viewers | Composition and Layer viewers: two-finger scroll pans, pinch zooms around the pointer (Alt + scroll and the mouse wheel zoom too); Timeline pinch zooms time | two-finger scroll pans once it moves diagonally (a pixel-precise wheel keeps zooming); pinch arrives as Ctrl + scroll and zooms | same | same | No rotate or swipe gestures; tested in `ui_viewer_trackpad.rs` (UI logic, not on hardware) |
|
||||
| Audio hardware | Core Audio / ASIO / WASAPI, device choice, latency | cpal output; preview sound late after a start offset (#504 open; #554 closed) | cpal | cpal (ALSA) | Web Audio | No input or device latency settings |
|
||||
| Machine learning acceleration | Apple Neural Engine / GPU for Object Matte, Roto Brush 3 | CPU inference (pure Rust, rayon) | CPU | CPU | CPU | No GPU inference for MobileSAM or MediaPipe |
|
||||
| Memory management | RAM reserve, disk cache | RAM Reserved, Reduce Cache When Low on Memory, disk cache | same | same | OPFS disk cache | good |
|
||||
|
||||
Reference in New Issue
Block a user