mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-01 05:58:19 -06:00
Merge pull request #1287 from vbasky/fix/1218-wheel-scrub
Viewer: mouse-wheel scrubbing over the Composition monitor (#1218)
This commit is contained in:
@@ -87,6 +87,7 @@ pub fn pages() -> Vec<Page> {
|
||||
s("general.recentItems", "Recent Projects Shown", Kind::Int(1, 30, ""), true),
|
||||
Section("Options"),
|
||||
s("general.showToolTips", "Show Tool Tips", B, true),
|
||||
s("general.viewerWheelScrubbing", "Scrub Composition Frames with Mouse Wheel", B, true),
|
||||
s("general.createLayersAtCompStart", "Create Layers at Composition Start Time", B, true),
|
||||
s("general.switchesAffectNestedComps", "Switches Affect Nested Comps", B, true),
|
||||
s("general.defaultSpatialLinear", "Default Spatial Interpolation to Linear", B, true),
|
||||
|
||||
@@ -31,6 +31,9 @@ page!(General {
|
||||
/// Path Point and Handle Size (px).
|
||||
path_point_size: u32 = 5,
|
||||
show_tool_tips: bool = true,
|
||||
/// Step frames with an unmodified mouse wheel over the Composition viewer.
|
||||
/// Off: the wheel zooms, which is what current After Effects does.
|
||||
viewer_wheel_scrubbing: bool = false,
|
||||
/// Create New Layers at Composition Start Time (off: at the current time).
|
||||
create_layers_at_comp_start: bool = true,
|
||||
switches_affect_nested_comps: bool = true,
|
||||
|
||||
@@ -190,6 +190,13 @@ pub(crate) struct ViewerScroll {
|
||||
/// Factor to zoom by around the pointer: a pinch, Ctrl/Cmd+scroll, a mouse wheel, or
|
||||
/// Alt + a trackpad scroll.
|
||||
pub zoom: f32,
|
||||
/// The gesture came from a trackpad (or is still the smoothed tail of one).
|
||||
pub trackpad: bool,
|
||||
/// Unmodified mouse-wheel lines this frame. Zero for a trackpad, a point-unit wheel, or
|
||||
/// Ctrl/Cmd/Alt+wheel.
|
||||
pub wheel_lines: f32,
|
||||
/// This frame has a Ctrl/Cmd/Alt mouse-wheel event, which keeps zooming.
|
||||
pub wheel_modified: bool,
|
||||
}
|
||||
|
||||
/// Splits the frame's scroll input into a viewer pan and a zoom: a mouse wheel zooms and a
|
||||
@@ -224,13 +231,32 @@ pub(crate) fn viewer_scroll(ctx: &egui::Context) -> ViewerScroll {
|
||||
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 }
|
||||
let (wheel_lines, wheel_modified) = ctx.input(|i| {
|
||||
let mut lines = 0.0;
|
||||
let mut modified = false;
|
||||
if !trackpad {
|
||||
for event in &i.events {
|
||||
if let egui::Event::MouseWheel { unit: egui::MouseWheelUnit::Line, delta, modifiers, source, .. } = event
|
||||
&& !matches!(source, egui::MouseWheelSource::Trackpad | egui::MouseWheelSource::Momentum)
|
||||
{
|
||||
if modifiers.command || modifiers.ctrl || modifiers.alt {
|
||||
modified = true;
|
||||
} else {
|
||||
lines += delta.y;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
(lines, modified)
|
||||
});
|
||||
let (pan, zoom) = if trackpad && !alt {
|
||||
(scroll, pinch)
|
||||
} else if alt {
|
||||
ViewerScroll { pan: egui::Vec2::ZERO, zoom: pinch * (scroll.y / 300.0).exp() }
|
||||
(egui::Vec2::ZERO, pinch * (scroll.y / 300.0).exp())
|
||||
} else {
|
||||
ViewerScroll { pan: egui::vec2(scroll.x, 0.0), zoom: pinch * (scroll.y / 300.0).exp() }
|
||||
}
|
||||
(egui::vec2(scroll.x, 0.0), pinch * (scroll.y / 300.0).exp())
|
||||
};
|
||||
ViewerScroll { pan, zoom, trackpad, wheel_lines, wheel_modified }
|
||||
}
|
||||
|
||||
pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, p: PanelKind, rect: Rect) {
|
||||
|
||||
@@ -870,6 +870,24 @@ fn phase_menu(app: &mut EffectcraftApp, ui: &mut egui::Ui, ectx: &EvalCtx, v: &m
|
||||
resp
|
||||
}
|
||||
|
||||
/// Frames to step when wheel scrubbing is on, or `None` when this gesture should zoom or pan.
|
||||
/// A scrub notch's smoothed tail stays a scrub after the wheel event itself is gone, so that
|
||||
/// tail does not zoom or pan (#1218).
|
||||
fn wheel_scrub_frames(ctx: &egui::Context, pref: bool, scroll: &super::ViewerScroll) -> Option<i64> {
|
||||
let tail_id = egui::Id::unique("viewer-wheel-scrub-tail");
|
||||
let was_tail: bool = ctx.data(|d| d.get_temp(tail_id)).unwrap_or(false);
|
||||
let moving = scroll.pan != egui::Vec2::ZERO || (scroll.zoom - 1.0).abs() > 3e-4;
|
||||
let scrub = pref && !scroll.trackpad && !scroll.wheel_modified && (scroll.wheel_lines.abs() >= 0.01 || (was_tail && moving));
|
||||
ctx.data_mut(|d| {
|
||||
if scrub {
|
||||
d.insert_temp(tail_id, true);
|
||||
} else {
|
||||
d.remove::<bool>(tail_id);
|
||||
}
|
||||
});
|
||||
scrub.then_some((scroll.wheel_lines.round() as i64).clamp(-30, 30))
|
||||
}
|
||||
|
||||
fn phase_hover(app: &mut EffectcraftApp, ui: &mut egui::Ui, ectx: &EvalCtx, resp: &egui::Response, v: &mut Vw<'_>) {
|
||||
let area = v.area;
|
||||
let zoom = v.zoom;
|
||||
@@ -951,12 +969,21 @@ fn phase_hover(app: &mut EffectcraftApp, ui: &mut egui::Ui, ectx: &EvalCtx, resp
|
||||
};
|
||||
ctx.set_cursor_icon(cursor);
|
||||
// The mouse wheel and a trackpad pinch zoom around the pointer; a two-finger trackpad
|
||||
// scroll pans.
|
||||
// scroll pans. With general.viewerWheelScrubbing, an unmodified wheel steps frames
|
||||
// instead, including the smoothed tail of that notch so it does not zoom or pan (#1218).
|
||||
let scroll = super::viewer_scroll(&ctx);
|
||||
if scroll.pan != egui::Vec2::ZERO && area.contains(hp) {
|
||||
let scrub = wheel_scrub_frames(&ctx, app.session.prefs.general.viewer_wheel_scrubbing, &scroll);
|
||||
if let Some(frames) = scrub {
|
||||
if frames != 0 && area.contains(hp) {
|
||||
if app.playback.playing {
|
||||
let _ = crate::menus::invoke(app, &ctx, "playback.stop", json!({}));
|
||||
}
|
||||
let _ = app.session.execute("time.step", json!({"frames": frames}));
|
||||
}
|
||||
} else 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) {
|
||||
if scrub.is_none() && (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);
|
||||
|
||||
@@ -54,6 +54,65 @@ fn drag(h: &mut Harness<'_, EffectcraftApp>, from: Pos2, to: Pos2) {
|
||||
h.run_steps(2);
|
||||
}
|
||||
|
||||
/// #1218: frame stepping is optional, and consumes the whole wheel gesture including its
|
||||
/// smoothed tail. Ctrl/Cmd+wheel still zooms.
|
||||
#[test]
|
||||
fn mouse_wheel_over_the_monitor_scrubs_frames() {
|
||||
let mut h = harness();
|
||||
h.state_mut().session.execute("prefs.set", json!({"key": "general.viewerWheelScrubbing", "value": true})).unwrap();
|
||||
let p = rect(&h, "viewer.area").center();
|
||||
h.input_mut().events.push(Event::PointerMoved(p));
|
||||
h.step();
|
||||
let t0 = h.state().session.time();
|
||||
let line = |dy: f32, modifiers: egui::Modifiers| Event::MouseWheel {
|
||||
unit: egui::MouseWheelUnit::Line,
|
||||
delta: vec2(0.0, dy),
|
||||
modifiers,
|
||||
phase: egui::TouchPhase::Move,
|
||||
source: egui::MouseWheelSource::Wheel,
|
||||
};
|
||||
let z0 = h.state().ui.viewer.zoom;
|
||||
h.input_mut().events.push(line(1.0, egui::Modifiers::NONE));
|
||||
h.step();
|
||||
let t1 = h.state().session.time();
|
||||
for _ in 0..20 {
|
||||
assert_eq!(h.state().ui.viewer.zoom, z0, "frame wheel must never zoom");
|
||||
h.step();
|
||||
}
|
||||
assert!(t1 > t0, "wheel up advanced the playhead ({t0:?} -> {t1:?})");
|
||||
h.input_mut().events.push(line(-2.0, egui::Modifiers::NONE));
|
||||
h.step();
|
||||
assert!(h.state().session.time() < t1, "two lines back moves before the single line forward");
|
||||
let z0 = h.state().ui.viewer.zoom;
|
||||
let t2 = h.state().session.time();
|
||||
let mods = egui::Modifiers { ctrl: true, command: true, ..Default::default() };
|
||||
h.input_mut().events.push(line(3.0, mods));
|
||||
h.step();
|
||||
assert_eq!(h.state().session.time(), t2, "Ctrl+wheel must not scrub");
|
||||
assert_ne!(h.state().ui.viewer.zoom, z0, "Ctrl+wheel still zooms");
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn mouse_wheel_zooms_by_default_without_stepping_frames() {
|
||||
let mut h = harness();
|
||||
assert!(!h.state().session.prefs.general.viewer_wheel_scrubbing);
|
||||
let p = rect(&h, "viewer.area").center();
|
||||
h.input_mut().events.push(Event::PointerMoved(p));
|
||||
h.step();
|
||||
let t0 = h.state().session.time();
|
||||
let z0 = h.state().ui.viewer.zoom;
|
||||
h.input_mut().events.push(Event::MouseWheel {
|
||||
unit: egui::MouseWheelUnit::Line,
|
||||
delta: vec2(0.0, 1.0),
|
||||
modifiers: egui::Modifiers::NONE,
|
||||
phase: egui::TouchPhase::Move,
|
||||
source: egui::MouseWheelSource::Wheel,
|
||||
});
|
||||
h.run_steps(20);
|
||||
assert_eq!(h.state().session.time(), t0);
|
||||
assert_ne!(h.state().ui.viewer.zoom, z0);
|
||||
}
|
||||
|
||||
/// Comp pixel → screen point.
|
||||
fn screen(h: &Harness<'_, EffectcraftApp>, p: [f32; 2]) -> Pos2 {
|
||||
let c = rect(h, "viewer.comp");
|
||||
|
||||
@@ -35,6 +35,7 @@ Commands (CLI, MCP, control channel):
|
||||
- `general.pathPointSize`: Path Point and Handle Size
|
||||
- `general.recentItems`: Recent Projects Shown
|
||||
- `general.showToolTips`: Show Tool Tips
|
||||
- `general.viewerWheelScrubbing`: Scrub Composition Frames with Mouse Wheel (off by default). On: an unmodified mouse wheel over the Composition viewer steps frames, and the smoothed tail of that notch does not zoom or pan. Ctrl/Cmd+wheel and a trackpad still zoom and pan.
|
||||
- `general.createLayersAtCompStart`: Create Layers at Composition Start Time
|
||||
- `general.defaultSpatialLinear`: Default Spatial Interpolation to Linear
|
||||
- `startup.showHomeOnLaunch`: Show Home Screen When Launching
|
||||
|
||||
Reference in New Issue
Block a user