diff --git a/crates/ui-egui/src/panels/timeline.rs b/crates/ui-egui/src/panels/timeline.rs index 0fc8eec1..29fd5b19 100644 --- a/crates/ui-egui/src/panels/timeline.rs +++ b/crates/ui-egui/src/panels/timeline.rs @@ -2086,8 +2086,12 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { continue; } let gresp = ui - .interact(gr, egui::Id::new(("ghost", side, layer.id.0)), Sense::drag()) + .interact(gr, egui::Id::new(("ghost", side, layer.id.0)), Sense::click_and_drag()) .on_hover_text(crate::i18n::tr("Drag to slip the source")); + // Outside the bar it is empty track area: a plain click deselects. + if gresp.clicked() && !ui.input(|i| i.modifiers.shift || i.modifiers.command) { + actions.push(("layer.select".into(), json!({"layers": []}))); + } app.auto.add(&format!("timeline.layer.{}.source{}", layer.id.0, side), gr, "Slip"); if gresp.hovered() || gresp.dragged() { ctx.set_cursor_icon(egui::CursorIcon::ResizeHorizontal); @@ -2825,14 +2829,11 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { if graph_on { super::graph::show(app, ui, &gp, &comp, &ectx, tm, Rect::from_min_max(pos2(graph_x0, rows_rect.min.y), rows_rect.max), &mut actions); } - // Empty-area click in the graph: deselect keys (below the last row, the layers too, as in - // After Effects); drag: box-select keys. - if empty.clicked() { - actions.push(("keys.select".into(), json!({"keys": []}))); - let below = empty.interact_pointer_pos().is_some_and(|p| hit_rows.iter().all(|(r, _)| p.y > r.max.y)); - if below && !ui.input(|i| i.modifiers.shift || i.modifiers.command) { - actions.push(("layer.select".into(), json!({"layers": []}))); - } + // A plain click on empty track area (beside a layer's bar, on a property row's empty span or + // below the last row) deselects the layers and their keys and properties, as in After + // Effects; Shift / Cmd keep the selection. Drag: box-select keys. + if empty.clicked() && !ui.input(|i| i.modifiers.shift || i.modifiers.command) { + actions.push(("layer.select".into(), json!({"layers": []}))); } if let (true, Some(origin), Some(cur)) = (empty.dragged(), empty.interact_pointer_pos().and_then(|_| ctx.input(|i| i.pointer.press_origin())), empty.interact_pointer_pos()) @@ -2938,7 +2939,13 @@ pub fn show(app: &mut EffectcraftApp, ui: &mut egui::Ui, rect: Rect) { } } } - actions.push(("keys.select".into(), json!({"keys": keys, "add": ui.input(|i| i.modifiers.shift), "selectProperties": true}))); + let mods = ui.input(|i| i.modifiers); + if keys.is_empty() && !(mods.shift || mods.command) { + // A marquee that catches nothing leaves nothing selected (layers included). + actions.push(("layer.select".into(), json!({"layers": []}))); + } else { + actions.push(("keys.select".into(), json!({"keys": keys, "add": mods.shift, "selectProperties": true}))); + } } // CTI. diff --git a/crates/ui-egui/tests/ui_workflow_selection.rs b/crates/ui-egui/tests/ui_workflow_selection.rs index 697ff2a2..0510fdae 100644 --- a/crates/ui-egui/tests/ui_workflow_selection.rs +++ b/crates/ui-egui/tests/ui_workflow_selection.rs @@ -88,6 +88,50 @@ fn timeline_empty_outline_marquee_selects_layers() { drag(&mut h, pos2(area.min.x + 210.0, last.max.y + 25.0), pos2(area.min.x + 215.0, top.min.y + 2.0), Modifiers::NONE); assert_eq!(h.state().session.state.selected_layers, ids); } +/// A plain click on empty track area (beside a layer's bar or below the last layer) deselects +/// the layers, as in After Effects; Shift / Cmd keep the selection, a click on a bar selects +/// its layer and a key marquee that catches nothing leaves nothing selected. +#[test] +fn timeline_click_on_empty_track_area_deselects_layers() { + let mut h = harness(); + let ids: Vec = h.state().session.active_comp().unwrap().layers.iter().map(|l| l.id).collect(); + let select = |h: &mut Harness<'_, EffectcraftApp>, layers: &[LayerId]| { + let layers: Vec = layers.iter().map(|l| l.0).collect(); + h.state_mut().session.execute("layer.select", json!({"layers": layers})).unwrap(); + h.run_steps(2); + }; + h.state_mut().session.execute("layer.timing", json!({"layers": [ids[1].0], "in": 1.5, "out": 2.5})).unwrap(); + select(&mut h, &ids[..2]); + let bar = rect(&h, &format!("timeline.layer.{}.bar", ids[1].0)); + let before = pos2(bar.min.x - 30.0, bar.center().y); + let after = pos2(bar.max.x + 30.0, bar.center().y); + click(&mut h, before, Modifiers::NONE); + assert!(h.state().session.state.selected_layers.is_empty(), "a click beside the bar deselects"); + click(&mut h, bar.center(), Modifiers::NONE); + assert_eq!(h.state().session.state.selected_layers, vec![ids[1]], "a click on the bar selects its layer"); + click(&mut h, after, Modifiers::SHIFT); + assert_eq!(h.state().session.state.selected_layers, vec![ids[1]], "Shift-click on empty keeps the selection"); + click(&mut h, after, Modifiers::COMMAND); + assert_eq!(h.state().session.state.selected_layers, vec![ids[1]], "Cmd-click on empty keeps the selection"); + click(&mut h, after, Modifiers::NONE); + assert!(h.state().session.state.selected_layers.is_empty()); + // Below the last layer, in the track area and in the outline. + let last = rect(&h, &format!("timeline.layer.{}.bar", ids[3].0)); + select(&mut h, &ids); + click(&mut h, pos2(last.center().x, last.max.y + 30.0), Modifiers::NONE); + assert!(h.state().session.state.selected_layers.is_empty(), "a click below the last layer deselects"); + let outline = rect(&h, "timeline.layerMarquee"); + select(&mut h, &ids); + click(&mut h, pos2(outline.min.x + 210.0, last.max.y + 30.0), Modifiers::NONE); + assert!(h.state().session.state.selected_layers.is_empty(), "so does one below the layers in the outline"); + // A key marquee over empty track area that catches nothing. + select(&mut h, &ids[1..2]); + drag(&mut h, after, pos2(after.x + 60.0, after.y + 4.0), Modifiers::NONE); + assert!(h.state().session.state.selected_layers.is_empty(), "an empty marquee leaves nothing selected"); + select(&mut h, &ids[1..2]); + drag(&mut h, after, pos2(after.x + 60.0, after.y + 4.0), Modifiers::SHIFT); + assert_eq!(h.state().session.state.selected_layers, vec![ids[1]], "a Shift marquee keeps the selection"); +} #[test] fn project_clear_deletes_items_and_undo_restores_them() { let mut h = harness();