collective

Browse / github.com/storytold/deckcraft / patch

Fix the editor's media bar ignoring clicks

Fix the editor's media bar ignoring clicks In the editor, the play/pause bar under a selected audio or video did not respond: media only played in the slide show. A press on the bar reached the editor canvas behind it (the bar's buttons only sensed clicks, so the canvas got the drag). The canvas started a selection marquee, and the bar hides while the tool is dragging, so its buttons were gone before the click landed. The bar's buttons and background now sense drags too, as the GIF play button already does, so the press stays on the bar. An earlier revision of this PR also made a click on a video play it in the editor; following revi

Status
open
Score
0.00
Adopters
0 other operators
Author
kinower (imported from pull request #1226, open)
Base
aaf6c1c38aa6c59beda8110bd014cc6c96c6ec74
Size
+80 −1 in 3 files
Review
pending
Shared
2026-10-28
Id
f01fb1b2b13ac6c2b7de0166

How it went

No other operator has reported using it yet.

Diff

raw · crates/ui-egui/src/canvas.rs crates/ui-egui/src/media.rs crates/ui-egui/src/media_bar_tests.rs

diff --git a/crates/ui-egui/src/canvas.rs b/crates/ui-egui/src/canvas.rs--- a/crates/ui-egui/src/canvas.rs+++ b/crates/ui-egui/src/canvas.rs@@ -2196,3 +2196,7 @@ mod ruler_tests; #[cfg(test)] #[path = "guides_tests.rs"] mod guides_tests;++#[cfg(test)]+#[path = "media_bar_tests.rs"]+mod media_bar_tests;diff --git a/crates/ui-egui/src/media.rs b/crates/ui-egui/src/media.rs--- a/crates/ui-egui/src/media.rs+++ b/crates/ui-egui/src/media.rs@@ -472,6 +472,9 @@ pub enum BarAction { pub fn control_bar(ui: &mut Ui, rect: Rect, status: Option<&VoiceStatus>, clip: &MediaClip) -> Option<BarAction> {     let painter = ui.painter_at(rect.expand(2.0));     painter.rect_filled(rect, CornerRadius::same(4), Color32::from_rgba_unmultiplied(28, 28, 32, 230));+    // Sensing drags too keeps a press on the bar from reaching the editor canvas: there it would+    // start a selection marquee, which hides the bar before the click lands.+    ui.interact(rect, ui.scope_id().with("media_bar_back"), Sense::click_and_drag());     let playing = status.is_some_and(|s| matches!(s.state, PlayState::Playing | PlayState::Loading));     let duration = status.map(|s| s.duration).filter(|d| *d > 0.0).unwrap_or(clip.duration_ms as f64 / 1000.0);     let pos = status.map(|s| s.position).unwrap_or(clip.trim_start_ms as f64 / 1000.0);@@ -481,7 +484,7 @@ pub fn control_bar(ui: &mut Ui, rect: Rect, status: Option<&VoiceStatus>, clip:     let button = |ui: &mut Ui, x: &mut f32, id: &str, paint: &dyn Fn(&egui::Painter, Rect)| -> bool {         let r = Rect::from_min_size(pos2(*x, rect.min.y + 2.0), vec2(h - 4.0, h - 4.0));         *x += h - 2.0;-        let resp = ui.interact(r, ui.scope_id().with(("media_bar", id)), Sense::click());+        let resp = ui.interact(r, ui.scope_id().with(("media_bar", id)), Sense::click_and_drag());         let name = match id {             "play" => "Play or pause media",             "back" => "Back 0.25 seconds",diff --git a/crates/ui-egui/src/media_bar_tests.rs b/crates/ui-egui/src/media_bar_tests.rsnew file mode 100644--- /dev/null+++ b/crates/ui-egui/src/media_bar_tests.rs@@ -0,0 +1,72 @@+//! The play/pause and timeline bar under a selected audio or video in the editor, clicked with the+//! mouse: a press on it used to reach the canvas, start a selection marquee there and hide the bar+//! before the click landed, so Play only worked in the slide show.++use deckcraft_engine::Session;+use deckcraft_model::ShapeId;+use egui::{Modifiers, PointerButton, Pos2, pos2, vec2};+use egui_kittest::Harness;+use serde_json::json;++use crate::{Services, SlideApp};++/// 16-bit mono WAV of silence, `secs` long at 8 kHz.+fn wav(secs: f64) -> Vec<u8> {+    let data = vec![0u8; (8_000.0 * secs) as usize * 2];+    let mut b = Vec::new();+    b.extend_from_slice(b"RIFF");+    b.extend_from_slice(&(36 + data.len() as u32).to_le_bytes());+    b.extend_from_slice(b"WAVEfmt ");+    for v in [16u32.to_le_bytes(), [1, 0, 1, 0], 8_000u32.to_le_bytes(), 16_000u32.to_le_bytes(), [2, 0, 16, 0]] {+        b.extend_from_slice(&v);+    }+    b.extend_from_slice(b"data");+    b.extend_from_slice(&(data.len() as u32).to_le_bytes());+    b.extend_from_slice(&data);+    b+}++fn click(h: &mut Harness<'_, SlideApp>, p: Pos2) {+    h.hover_at(p);+    h.run_steps(1);+    h.event(egui::Event::PointerButton { pos: p, button: PointerButton::Primary, pressed: true, modifiers: Modifiers::NONE });+    h.run_steps(1);+    h.event(egui::Event::PointerButton { pos: p, button: PointerButton::Primary, pressed: false, modifiers: Modifiers::NONE });+    h.run_steps(2);+}++#[test]+fn play_on_the_editor_media_bar_plays_the_selected_clip() {+    let mut s = Session::with_new();+    s.execute("slide.new", &json!({"layout": "blank"})).unwrap();+    let r = s.execute("insert.audio", &json!({"name": "tone.wav", "data": deckcraft_engine::cmd::base64_encode(&wav(2.0))})).unwrap();+    let id = ShapeId(r["id"].as_u64().unwrap() as u32);+    let app = SlideApp::new(s, Services::default());+    let mut h = Harness::builder().with_size(vec2(1400.0, 900.0)).with_step_dt(1.0 / 60.0).build_ui_state(+        |ui, app: &mut SlideApp| {+            app.logic(ui.ctx());+            app.ui(ui);+        },+        app,+    );+    h.run_steps(3);+    let d = h.state().session.active().unwrap();+    assert_eq!(d.selection.shapes, vec![id], "the inserted clip is selected");+    let x = d.doc.slides[d.selection.slide].shape(id).unwrap().xfrm.unwrap();+    let (slide, k) = (h.state().slide_rect.unwrap(), h.state().canvas_scale);+    // The bar sits 6 points under the clip, centred, at least 230 wide; Play is its first button.+    let (left, right, bottom) = (slide.min.x + x.x as f32 * k, slide.min.x + (x.x + x.w) as f32 * k, slide.min.y + (x.y + x.h) as f32 * k);+    let w = (right - left).max(230.0);+    let bar = pos2((left + right) / 2.0 - w / 2.0, bottom + 6.0);+    assert!(h.state().media.status(id).is_none());++    click(&mut h, bar + vec2(15.0, 13.0));+    assert!(h.state().media.status(id).is_some(), "Play starts the clip");+    let d = h.state().session.active().unwrap();+    assert_eq!(d.selection.shapes, vec![id], "the click did not reach the slide behind the bar");+    assert!(!h.state().session.tool.dragging());++    // Between the buttons and the timeline the bar still keeps the press from the slide.+    click(&mut h, bar + vec2(w - 30.0, 13.0));+    assert_eq!(h.state().session.active().unwrap().selection.shapes, vec![id]);+}