mirror of
https://github.com/storytold/effectcraft.git
synced 2026-11-05 18:18:49 +00:00
Merge PR #2935
This commit is contained in:
commit
b97a8f796a
5 files changed
+185
-18
No files matched your search
@@ -77,6 +77,17 @@ pub(crate) fn bar_color(label: Color32, selected: bool) -> Color32 {
|
||||
const TRIM_OUTSIDE: f32 = 4.0;
|
||||
const TRIM_INSIDE: f32 = 5.0;
|
||||
|
||||
/// A middle-drag in the timeline pans it (`hand_tool_pan`, #2854): a middle press here starts no
|
||||
/// drag, so it can't move a bar or a key. A widget that takes a drag on press was given it this frame,
|
||||
/// before anything here ran. Handing that drag to an id nothing uses and ending it means no
|
||||
/// widget sees it start or stop.
|
||||
fn ignore_middle_press(ui: &egui::Ui, rect: Rect) {
|
||||
if ui.rect_contains_pointer(rect) && ui.input(|i| i.pointer.button_pressed(egui::PointerButton::Middle) && !i.pointer.primary_down()) {
|
||||
ui.ctx().set_dragged_id(egui::Id::unique("tl-middle-press"));
|
||||
ui.ctx().stop_dragging();
|
||||
}
|
||||
}
|
||||
|
||||
/// A layer bar's body (drag to move) and its In/Out trim zones. Short bars keep their middle
|
||||
/// third as body; a bar too short for that is all trim zones (the Out one on top).
|
||||
fn trim_zones(bar: Rect) -> (Rect, Rect, Rect) {
|
||||
@@ -1850,6 +1861,7 @@ fn begin_rows(app: &mut EffectcraftApp, ui: &mut egui::Ui, tl: Tl) -> Rw {
|
||||
/// open rows and the drags in progress) lives in
|
||||
/// `app.ui.timeline` and egui's temp data.
|
||||
pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
ignore_middle_press(ui, rect);
|
||||
let Some(cid) = app.session.active_comp_id() else {
|
||||
empty_timeline(app, ui, rect);
|
||||
return;
|
||||
@@ -4835,7 +4847,7 @@ fn comp_markers_ui(app: &mut EffectcraftApp, ui: &mut egui::Ui, g: &Rw) {
|
||||
super::markers_ui::marker_bin(app, ui, &comp, tm, bin, mstrip);
|
||||
}
|
||||
|
||||
/// Spacebar held: the Hand tool.
|
||||
/// Spacebar held: the Hand tool. A middle-drag begun in the rows, layer list included, pans too.
|
||||
fn hand_tool_pan(app: &mut EffectcraftApp, ui: &mut egui::Ui, g: &Rw) {
|
||||
let rect = g.tl.rect;
|
||||
let graph_x0 = g.tl.graph_x0;
|
||||
@@ -4847,6 +4859,7 @@ fn hand_tool_pan(app: &mut EffectcraftApp, ui: &mut egui::Ui, g: &Rw) {
|
||||
let max_scroll = g.tl.max_scroll;
|
||||
let ctx = g.tl.ctx.clone();
|
||||
let comp = g.tl.comp.clone();
|
||||
let mut pan = ui.input(|i| i.pointer.middle_down() && !i.pointer.primary_down() && i.pointer.press_origin().is_some_and(|p| rows_rect.contains(p)));
|
||||
if super::space_hand(&ctx) {
|
||||
let area = Rect::from_min_max(pos2(graph_x0, ruler.min.y), pos2(rect.max.x, rows_rect.max.y));
|
||||
let hand = ui.interact(area, egui::Id::unique("tl-hand"), Sense::drag());
|
||||
@@ -4854,14 +4867,15 @@ fn hand_tool_pan(app: &mut EffectcraftApp, ui: &mut egui::Ui, g: &Rw) {
|
||||
if hand.hovered() || hand.dragged() {
|
||||
ctx.set_cursor_icon(if hand.dragged() { egui::CursorIcon::Grabbing } else { egui::CursorIcon::Grab });
|
||||
}
|
||||
if hand.dragged() {
|
||||
let dy = ui.input(|i| i.pointer.delta().y);
|
||||
app.ui.timeline.scroll_y = (app.ui.timeline.scroll_y - dy).clamp(0.0, max_scroll);
|
||||
if app.ui.timeline.pps.is_some() {
|
||||
let start = app.ui.timeline.start;
|
||||
let last = (comp.duration.seconds() - (tm.t(graph_x1) - tm.start)).max(start).max(0.0);
|
||||
app.ui.timeline.start = (start - hand.drag_delta().x as f64 / pps).clamp(0.0, last);
|
||||
}
|
||||
pan |= hand.dragged();
|
||||
}
|
||||
if pan {
|
||||
let delta = ui.input(|i| i.pointer.delta());
|
||||
app.ui.timeline.scroll_y = (app.ui.timeline.scroll_y - delta.y).clamp(0.0, max_scroll);
|
||||
if app.ui.timeline.pps.is_some() {
|
||||
let start = app.ui.timeline.start;
|
||||
let last = (comp.duration.seconds() - (tm.t(graph_x1) - tm.start)).max(start).max(0.0);
|
||||
app.ui.timeline.start = (start - delta.x as f64 / pps).clamp(0.0, last);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -216,11 +216,13 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) {
|
||||
let resp = phase_menu(app, ui, &ectx, &mut v);
|
||||
phase_hover(app, ui, &ectx, &resp, &mut v);
|
||||
phase_press(app, ui, &ectx, &resp, &mut v);
|
||||
phase_dragstart(app, ui, &ectx, &resp, &mut v);
|
||||
phase_drag(app, ui, &ectx, &resp, &mut v);
|
||||
phase_stopped(app, ui, &ectx, &resp, &mut v);
|
||||
phase_clicked(app, &ectx, &resp, &mut v);
|
||||
phase_dblclick(app, &ectx, &resp, &mut v);
|
||||
if !middle_pan(app, ui, &resp, &v) {
|
||||
phase_dragstart(app, ui, &ectx, &resp, &mut v);
|
||||
phase_drag(app, ui, &ectx, &resp, &mut v);
|
||||
phase_stopped(app, ui, &ectx, &resp, &mut v);
|
||||
phase_clicked(app, &ectx, &resp, &mut v);
|
||||
phase_dblclick(app, &ectx, &resp, &mut v);
|
||||
}
|
||||
phase_tail(app, ui, &ectx, &mut v);
|
||||
}
|
||||
|
||||
@@ -1064,6 +1066,24 @@ fn phase_press(app: &mut EffectcraftApp, ui: &mut egui::Ui, ectx: &EvalCtx, resp
|
||||
v.text_owns = text_owns;
|
||||
}
|
||||
|
||||
/// A middle-drag pans with any tool and doesn't enter the gesture phases, so nothing else can
|
||||
/// happen under it (#2854). A left-button gesture already running keeps going until it ends.
|
||||
fn middle_pan(app: &mut EffectcraftApp, ui: &egui::Ui, resp: &egui::Response, v: &Vw<'_>) -> bool {
|
||||
let (middle, primary) = ui.input(|i| (i.pointer.middle_down(), i.pointer.primary_down() || i.pointer.primary_released()));
|
||||
if !middle || !resp.is_pointer_button_down_on() || (primary && ui.data(|d| d.get_temp::<Gesture>(v.gid)).is_some()) {
|
||||
return false;
|
||||
}
|
||||
// No left button: a stored gesture is one whose release the viewer missed.
|
||||
ui.data_mut(|d| d.remove::<Gesture>(v.gid));
|
||||
// Movement from before the press (in its frame) is not part of the drag.
|
||||
let d = ui.input(|i| if i.pointer.button_pressed(egui::PointerButton::Middle) { egui::Vec2::ZERO } else { i.pointer.delta() });
|
||||
if d != egui::Vec2::ZERO && d.x.is_finite() && d.y.is_finite() {
|
||||
app.ui.viewer.pan = [app.ui.viewer.pan[0] + d.x, app.ui.viewer.pan[1] + d.y];
|
||||
}
|
||||
v.ctx.set_cursor_icon(egui::CursorIcon::Grabbing);
|
||||
true
|
||||
}
|
||||
|
||||
fn phase_dragstart(app: &mut EffectcraftApp, ui: &mut egui::Ui, ectx: &EvalCtx, resp: &egui::Response, v: &mut Vw<'_>) {
|
||||
let comp_rect = v.comp_rect;
|
||||
let zoom = v.zoom;
|
||||
@@ -1100,7 +1120,7 @@ fn phase_dragstart(app: &mut EffectcraftApp, ui: &mut egui::Ui, ectx: &EvalCtx,
|
||||
{
|
||||
let cpt = map.to_comp(pos);
|
||||
let press = ui.input(|i| i.pointer.press_origin()).unwrap_or(pos);
|
||||
let tool = if space_pan || resp.dragged_by(egui::PointerButton::Middle) { Tool::Hand } else { app.ui.tool };
|
||||
let tool = if space_pan { Tool::Hand } else { app.ui.tool };
|
||||
let g = match tool {
|
||||
Tool::Hand => Some(Gesture::Pan { start_pan: app.ui.viewer.pan }),
|
||||
// Region of Interest armed: the drag draws it.
|
||||
|
||||
@@ -42,6 +42,47 @@ fn keys(h: &Harness<'_, EffectcraftApp>, uid: u64) -> Vec<Pos2> {
|
||||
ks
|
||||
}
|
||||
|
||||
/// #2854: a middle-drag on a key, a layer bar, its trim edge or the marker bin edits nothing and
|
||||
/// adds no undo step; it only pans.
|
||||
#[test]
|
||||
fn middle_drag_on_a_key_or_layer_bar_changes_nothing() {
|
||||
let (mut h, id, uid) = harness();
|
||||
let center = |h: &Harness<'_, EffectcraftApp>, name: String| {
|
||||
let e = h.state().auto.find(&name).unwrap_or_else(|| panic!("no {name}")).clone();
|
||||
pos2(e.rect[0] + e.rect[2] / 2.0, e.rect[1] + e.rect[3] / 2.0)
|
||||
};
|
||||
let bar = center(&h, format!("timeline.layer.{}.bar", id.0));
|
||||
let trim = center(&h, format!("timeline.layer.{}.bar.in", id.0));
|
||||
let bin = center(&h, "timeline.markerBin".to_string());
|
||||
let key = keys(&h, uid)[1];
|
||||
let state = |h: &Harness<'_, EffectcraftApp>| {
|
||||
let comp = h.state().session.active_comp().unwrap();
|
||||
let l = comp.layer(id).unwrap();
|
||||
(l.in_point, l.out_point, l.start_time, comp.markers.len(), key_times(h, id), h.state().session.history.undo.len())
|
||||
};
|
||||
h.state_mut().session.execute("edit.deselectAll", json!({})).unwrap();
|
||||
h.run_steps(2);
|
||||
let before = state(&h);
|
||||
for from in [key, bar, trim, bin] {
|
||||
let to = from + egui::vec2(90.0, 0.0);
|
||||
h.input_mut().events.push(Event::PointerMoved(from));
|
||||
h.step();
|
||||
h.input_mut().events.push(Event::PointerButton { pos: from, button: egui::PointerButton::Middle, pressed: true, modifiers: Default::default() });
|
||||
h.step();
|
||||
for i in 1..=8 {
|
||||
h.input_mut().events.push(Event::PointerMoved(from + (to - from) * (i as f32 / 8.0)));
|
||||
h.step();
|
||||
}
|
||||
h.input_mut().events.push(Event::PointerButton { pos: to, button: egui::PointerButton::Middle, pressed: false, modifiers: Default::default() });
|
||||
h.run_steps(2);
|
||||
assert_eq!(state(&h), before, "from {from:?}");
|
||||
assert!(h.state().session.state.selected_layers.is_empty(), "from {from:?}: the press selects nothing");
|
||||
}
|
||||
// The left button still drags the key.
|
||||
drag(&mut h, key, key + egui::vec2(90.0, 0.0), egui::Modifiers::NONE);
|
||||
assert_ne!(key_times(&h, id), before.4);
|
||||
}
|
||||
|
||||
/// Press at `from`, move to `to` in small steps (a real drag), release.
|
||||
fn drag(h: &mut Harness<'_, EffectcraftApp>, from: Pos2, to: Pos2, modifiers: egui::Modifiers) {
|
||||
h.input_mut().events.push(Event::PointerMoved(from));
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
//! Spacebar-drag panning in the Timeline: the Hand tool scrolls the layer rows vertically as well
|
||||
//! as the time graph horizontally (#2083).
|
||||
//! as the time graph horizontally (#2083). A middle-drag does the same, from the layer list too
|
||||
//! (#2854).
|
||||
|
||||
use effectcraft_engine::Session;
|
||||
use effectcraft_ui_egui::EffectcraftApp;
|
||||
@@ -28,18 +29,49 @@ fn space(h: &mut Harness<'_, EffectcraftApp>, pressed: bool) {
|
||||
}
|
||||
|
||||
fn drag(h: &mut Harness<'_, EffectcraftApp>, from: Pos2, to: Pos2) {
|
||||
drag_with(h, egui::PointerButton::Primary, from, to);
|
||||
}
|
||||
|
||||
fn drag_with(h: &mut Harness<'_, EffectcraftApp>, button: egui::PointerButton, from: Pos2, to: Pos2) {
|
||||
h.event(Event::PointerMoved(from));
|
||||
h.step();
|
||||
h.event(Event::PointerButton { pos: from, button: egui::PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE });
|
||||
h.event(Event::PointerButton { pos: from, button, pressed: true, modifiers: Modifiers::NONE });
|
||||
h.step();
|
||||
for k in 1..=10 {
|
||||
h.event(Event::PointerMoved(from + (to - from) * (k as f32 / 10.0)));
|
||||
h.step();
|
||||
}
|
||||
h.event(Event::PointerButton { pos: to, button: egui::PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE });
|
||||
h.event(Event::PointerButton { pos: to, button, pressed: false, modifiers: Modifiers::NONE });
|
||||
h.run_steps(3);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn middle_drag_pans_from_the_layer_list_and_the_bars() {
|
||||
let (mut h, layer) = harness();
|
||||
h.state_mut().ui.timeline.pps = Some(400.0);
|
||||
h.state_mut().ui.timeline.start = 1.0;
|
||||
h.run_steps(3);
|
||||
let order = |h: &Harness<'_, EffectcraftApp>| h.state().session.active_comp().unwrap().layers.iter().map(|l| l.id).collect::<Vec<_>>();
|
||||
let before = order(&h);
|
||||
let center = |r: [f32; 4]| pos2(r[0] + r[2] / 2.0, r[1] + r[3] / 2.0);
|
||||
let row = center(h.state().auto.find(&format!("timeline.layer.{layer}.row")).expect("layer row").rect);
|
||||
drag_with(&mut h, egui::PointerButton::Middle, row, row - vec2(0.0, 180.0));
|
||||
let y = h.state().ui.timeline.scroll_y;
|
||||
assert!(y > 100.0, "the list scrolled: {y}");
|
||||
assert_eq!(h.state().ui.timeline.start, 1.0, "a vertical drag leaves time alone");
|
||||
assert_eq!(order(&h), before, "the layers stay in order");
|
||||
assert!(h.state().session.state.selected_layers.is_empty(), "the press selects nothing");
|
||||
// In the time graph, just right of the row, whatever the zoom has left of its bar.
|
||||
h.state_mut().ui.timeline.scroll_y = 0.0;
|
||||
h.run_steps(2);
|
||||
let r = h.state().auto.find(&format!("timeline.layer.{layer}.row")).expect("layer row").rect;
|
||||
let graph = pos2(r[0] + r[2] + 100.0, r[1] + r[3] / 2.0);
|
||||
drag_with(&mut h, egui::PointerButton::Middle, graph, graph - vec2(80.0, 0.0));
|
||||
let start = h.state().ui.timeline.start;
|
||||
assert!(start > 1.1, "time panned: {start}");
|
||||
assert_eq!(order(&h), before);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn spacebar_drag_pans_the_rows_vertically() {
|
||||
let (mut h, layer) = harness();
|
||||
|
||||
@@ -1771,6 +1771,66 @@ fn middle_and_hand_drags_pan_the_viewer_and_the_pan_stays_after_release() {
|
||||
assert!((end[0] - after[0] + 50.0).abs() < 1.0 && (end[1] - after[1] + 30.0).abs() < 1.0, "{after:?} → {end:?}");
|
||||
}
|
||||
|
||||
/// #2854: a middle-drag over a layer pans the view and leaves the layer alone, selected or not,
|
||||
/// with any tool, and adds no undo step.
|
||||
#[test]
|
||||
fn middle_drag_over_a_layer_pans_and_leaves_the_layer_where_it_is() {
|
||||
let mut h = harness();
|
||||
let id = layer_named(&h, "Box");
|
||||
let c = rect(&h, "viewer.comp").center();
|
||||
for (select, tool) in [(false, Tool::Selection), (true, Tool::Selection), (true, Tool::Rotate), (true, Tool::Type)] {
|
||||
let selection = if select { json!({"layers": [id.0]}) } else { json!({"layers": []}) };
|
||||
h.state_mut().session.execute("layer.select", selection).unwrap();
|
||||
h.state_mut().ui.tool = tool;
|
||||
h.state_mut().ui.viewer.pan = [0.0, 0.0];
|
||||
h.run_steps(2);
|
||||
let (p0, steps) = (position_of(&h, id), h.state().session.history.undo.len());
|
||||
drag_with(&mut h, egui::PointerButton::Middle, c, c + vec2(120.0, 60.0));
|
||||
let pan = h.state().ui.viewer.pan;
|
||||
assert!((pan[0] - 120.0).abs() < 1.0 && (pan[1] - 60.0).abs() < 1.0, "{select} {tool:?}: {pan:?}");
|
||||
assert_eq!(position_of(&h, id), p0, "{select} {tool:?}: the layer stays");
|
||||
assert_eq!(h.state().session.history.undo.len(), steps, "{select} {tool:?}: no undo step");
|
||||
}
|
||||
}
|
||||
|
||||
/// #2854: a left drag whose release the viewer never saw (the panel wasn't drawn on that frame)
|
||||
/// leaves its move gesture stored. A middle press must not run it.
|
||||
#[test]
|
||||
fn middle_drag_does_not_run_a_gesture_left_over_from_a_missed_release() {
|
||||
let mut h = harness();
|
||||
let id = layer_named(&h, "Box");
|
||||
h.state_mut().session.execute("layer.select", json!({"layers": [id.0]})).unwrap();
|
||||
h.run_steps(2);
|
||||
let c = rect(&h, "viewer.comp").center();
|
||||
let p = egui::PointerButton::Primary;
|
||||
h.input_mut().events.push(Event::PointerMoved(c));
|
||||
h.input_mut().events.push(Event::PointerButton { pos: c, button: p, pressed: true, modifiers: Default::default() });
|
||||
h.step();
|
||||
for i in 1..=4 {
|
||||
h.input_mut().events.push(Event::PointerMoved(c + vec2(10.0 * i as f32, 0.0)));
|
||||
h.step();
|
||||
}
|
||||
let ctx = h.ctx.clone();
|
||||
effectcraft_ui_egui::menus::invoke(h.state_mut(), &ctx, "layer.openLayer", json!({"layer": id.0})).unwrap();
|
||||
h.step();
|
||||
h.input_mut().events.push(Event::PointerButton { pos: c + vec2(40.0, 0.0), button: p, pressed: false, modifiers: Default::default() });
|
||||
h.run_steps(2);
|
||||
h.state_mut().show_panel(effectcraft_ui_egui::dock::PanelKind::Composition);
|
||||
h.run_steps(3);
|
||||
let c = rect(&h, "viewer.comp").center();
|
||||
let p0 = position_of(&h, id);
|
||||
drag_with(&mut h, egui::PointerButton::Middle, c, c + vec2(120.0, 60.0));
|
||||
assert_eq!(position_of(&h, id), p0, "the stored move didn't run");
|
||||
}
|
||||
|
||||
fn layer_named(h: &Harness<'_, EffectcraftApp>, name: &str) -> LayerId {
|
||||
h.state().session.active_comp().unwrap().layers.iter().find(|l| l.name == name).unwrap().id
|
||||
}
|
||||
|
||||
fn position_of(h: &Harness<'_, EffectcraftApp>, id: LayerId) -> [f64; 3] {
|
||||
h.state().session.active_comp().unwrap().layer(id).unwrap().props.prop("transform/position").unwrap().value.as_vec3()
|
||||
}
|
||||
|
||||
/// A Spacebar press or release (`repeat`: the keyboard's auto-repeat while it is held).
|
||||
fn space(h: &mut Harness<'_, EffectcraftApp>, pressed: bool, repeat: bool) {
|
||||
h.input_mut().events.push(Event::Key { key: egui::Key::Space, physical_key: None, pressed, repeat, modifiers: Default::default() });
|
||||
|
||||
Reference in new issue
Block a user