mirror of
https://github.com/storytold/deckcraft.git
synced 2026-10-24 10:37:03 -06:00
Conflicts: crates/ui-egui/src/show.rs (test module: kept both sides' tests)
This commit is contained in:
+121
-8
@@ -316,6 +316,11 @@ impl Show {
|
||||
}
|
||||
}
|
||||
|
||||
/// True while the pointer moved in the last 2.5 s; the media controls and GIF button hide after that.
|
||||
fn pointer_awake(&self, now: f64) -> bool {
|
||||
now - self.last_move < 2.5
|
||||
}
|
||||
|
||||
/// Pen and laser are different pointers: turning one on puts the other away.
|
||||
fn set_pen(&mut self, on: bool) {
|
||||
self.pen = on;
|
||||
@@ -707,7 +712,7 @@ pub fn ui(app: &mut SlideApp, ui: &mut Ui) {
|
||||
&& show.zoom.is_none()
|
||||
&& !show.zoom_arm
|
||||
&& !show.reading_bar_hit(&ctx)
|
||||
&& !pointer_on_media_bar(&ctx, ui, &doc, &show);
|
||||
&& !pointer_on_media_bar(&ctx, ui, &doc, &show, now);
|
||||
let gif_click = if clicked && on_slide && doc.show.show_media_controls { gif_hit(&ctx, ui, &show, doc_uid, &doc, app) } else { None };
|
||||
let media_click = if clicked && on_slide && gif_click.is_none() { media_hit(&ctx, ui, &show, &doc) } else { None };
|
||||
let under = if on_slide && gif_click.is_none() && media_click.is_none() { shapes_under_pointer(&ctx, ui, &mut show, &doc, now) } else { None };
|
||||
@@ -745,7 +750,7 @@ pub fn ui(app: &mut SlideApp, ui: &mut Ui) {
|
||||
}
|
||||
} else if clicked && show.zoom.is_some() && !show.pen && !show.reading_bar_hit(&ctx) {
|
||||
show.dismiss_zoom();
|
||||
} else if clicked && !show.pen && !show.reading_bar_hit(&ctx) && !pointer_on_media_bar(&ctx, ui, &doc, &show) {
|
||||
} else if clicked && !show.pen && !show.reading_bar_hit(&ctx) && !pointer_on_media_bar(&ctx, ui, &doc, &show, now) {
|
||||
if show.ended {
|
||||
keep = false;
|
||||
} else if show.blank.is_some() {
|
||||
@@ -1041,7 +1046,7 @@ pub fn ui(app: &mut SlideApp, ui: &mut Ui) {
|
||||
// Not while zoomed: the controls sit where the whole slide would be.
|
||||
if doc.show.show_media_controls && !show.zoom_arm && show.zoom.is_none() {
|
||||
let k = srect.width() / sw.max(1.0);
|
||||
if let Some(p) = ctx.input(|i| i.pointer.hover_pos()).filter(|_| !show.pen && !show.laser && now - show.last_move < 2.5) {
|
||||
if let Some(p) = ctx.input(|i| i.pointer.hover_pos()).filter(|_| !show.pen && !show.laser && show.pointer_awake(now)) {
|
||||
for g in &gifs {
|
||||
let r = on_screen(srect, k, &g.rect);
|
||||
if r.contains(p) {
|
||||
@@ -1050,7 +1055,8 @@ pub fn ui(app: &mut SlideApp, ui: &mut Ui) {
|
||||
}
|
||||
}
|
||||
}
|
||||
if let Some((id, clip, bar)) = media_bar_target(&ctx, &doc, &show, srect) {
|
||||
if let Some((id, clip, bar)) = media_bar_target(&ctx, &doc, &show, srect).filter(|_| show.pointer_awake(now)) {
|
||||
ctx.request_repaint_after(std::time::Duration::from_millis(500));
|
||||
let action = {
|
||||
let status = app.media.status(id);
|
||||
crate::media::control_bar(ui, bar, status.as_ref(), &clip)
|
||||
@@ -1101,7 +1107,7 @@ pub fn ui(app: &mut SlideApp, ui: &mut Ui) {
|
||||
ctx.set_cursor_icon(egui::CursorIcon::None);
|
||||
} else if show.zoom_arm || show.grid {
|
||||
ctx.set_cursor_icon(egui::CursorIcon::Crosshair);
|
||||
} else if now - show.last_move > 2.5 && !show.reading {
|
||||
} else if !show.pointer_awake(now) && !show.reading {
|
||||
// Idle first: a still pointer hides also over a clickable shape, such as a playing clip.
|
||||
ctx.set_cursor_icon(egui::CursorIcon::None);
|
||||
ctx.request_repaint_after(std::time::Duration::from_millis(500));
|
||||
@@ -1161,7 +1167,7 @@ pub fn ui(app: &mut SlideApp, ui: &mut Ui) {
|
||||
let a = show.state.prev(&doc);
|
||||
show.apply(a, &doc, now);
|
||||
}
|
||||
} else if !show.grid && (now - show.last_move < 2.5 || show.zoom_arm) {
|
||||
} else if !show.grid && (show.pointer_awake(now) || show.zoom_arm) {
|
||||
let bar = tool_bar_rect(full);
|
||||
painter.rect_filled(bar, CornerRadius::same(17), Color32::from_black_alpha(140));
|
||||
let mut child =
|
||||
@@ -1349,8 +1355,8 @@ fn media_bar_target(ctx: &egui::Context, doc: &Presentation, show: &Show, srect:
|
||||
}
|
||||
|
||||
/// True when the pointer is on a visible playback bar, so that click must not advance the show.
|
||||
fn pointer_on_media_bar(ctx: &egui::Context, ui: &Ui, doc: &Presentation, show: &Show) -> bool {
|
||||
if !doc.show.show_media_controls || show.pen || show.ended || show.blank.is_some() {
|
||||
fn pointer_on_media_bar(ctx: &egui::Context, ui: &Ui, doc: &Presentation, show: &Show, now: f64) -> bool {
|
||||
if !doc.show.show_media_controls || !show.pointer_awake(now) || show.pen || show.ended || show.blank.is_some() {
|
||||
return false;
|
||||
}
|
||||
let Some(p) = ctx.input(|i| i.pointer.interact_pos().or(i.pointer.hover_pos())) else { return false };
|
||||
@@ -2190,6 +2196,113 @@ mod tests {
|
||||
assert!(!harness.state().1.presenter_grid, "G again closes it");
|
||||
}
|
||||
|
||||
/// A silent mono 8 kHz WAV, so the clip has a real duration to seek in.
|
||||
fn silent_wav(secs: u32) -> Vec<u8> {
|
||||
let rate = 8_000u32;
|
||||
let data = vec![0u8; (rate * secs * 2) as usize];
|
||||
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 ");
|
||||
b.extend_from_slice(&16u32.to_le_bytes());
|
||||
b.extend_from_slice(&1u16.to_le_bytes());
|
||||
b.extend_from_slice(&1u16.to_le_bytes());
|
||||
b.extend_from_slice(&rate.to_le_bytes());
|
||||
b.extend_from_slice(&(rate * 2).to_le_bytes());
|
||||
b.extend_from_slice(&2u16.to_le_bytes());
|
||||
b.extend_from_slice(&16u16.to_le_bytes());
|
||||
b.extend_from_slice(b"data");
|
||||
b.extend_from_slice(&(data.len() as u32).to_le_bytes());
|
||||
b.extend_from_slice(&data);
|
||||
b
|
||||
}
|
||||
|
||||
/// A running show on a slide with one 10 s clip. Returns the harness, the clip, a point on the
|
||||
/// bar's seek track, and what `pointer_on_media_bar` said on the last frame.
|
||||
fn show_with_clip() -> (egui_kittest::Harness<'static, SlideApp>, ShapeId, Pos2, std::rc::Rc<std::cell::Cell<bool>>) {
|
||||
let size = vec2(1000.0, 700.0);
|
||||
let mut session = deckcraft_engine::Session::with_new();
|
||||
let id = ShapeId(4000);
|
||||
session
|
||||
.edit(|doc, _| {
|
||||
let media = doc.add_media("clip.wav", "audio/wav", silent_wav(10)).unwrap();
|
||||
let (w, h) = (doc.slide_size.width, doc.slide_size.height);
|
||||
let clip = deckcraft_model::MediaClip { media, video: true, volume: 1.0, duration_ms: 10_000, ..Default::default() };
|
||||
let xfrm = deckcraft_geom::Xfrm::new(w * 0.2, h * 0.2, w * 0.6, h * 0.6);
|
||||
let slide = std::sync::Arc::make_mut(&mut doc.slides[0]);
|
||||
slide.shapes.push(deckcraft_model::Shape {
|
||||
id,
|
||||
xfrm: Some(xfrm),
|
||||
kind: deckcraft_model::ShapeKind::Media(clip),
|
||||
..Default::default()
|
||||
});
|
||||
Ok(())
|
||||
})
|
||||
.unwrap();
|
||||
let on_bar = std::rc::Rc::new(std::cell::Cell::new(false));
|
||||
let seen = on_bar.clone();
|
||||
let slide = std::rc::Rc::new(std::cell::Cell::new(Rect::NOTHING));
|
||||
let slide_seen = slide.clone();
|
||||
let mut harness = egui_kittest::Harness::builder().with_size(size).with_step_dt(0.25).build_ui_state(
|
||||
move |ui, app: &mut SlideApp| {
|
||||
app.logic(ui.ctx());
|
||||
if let (Some(show), Some(d)) = (app.show.as_ref(), app.session.active()) {
|
||||
let now = ui.input(|i| i.time);
|
||||
seen.set(pointer_on_media_bar(ui.ctx(), ui, &d.doc, show, now));
|
||||
slide_seen.set(shown_slide_rect(ui, show, &d.doc));
|
||||
}
|
||||
app.ui(ui);
|
||||
},
|
||||
SlideApp::new(session, crate::Services::default()),
|
||||
);
|
||||
harness.run_steps(3);
|
||||
harness.state_mut().start_show(0, false);
|
||||
harness.run_steps(2);
|
||||
let doc = harness.state().session.active().unwrap().doc.clone();
|
||||
let bar = slide_media_bars(slide.get(), &doc, 0)[0].3;
|
||||
let track = pos2(bar.min.x + bar.width() * 0.8, bar.center().y);
|
||||
(harness, id, track, on_bar)
|
||||
}
|
||||
|
||||
/// Press and release the primary button at `p`.
|
||||
fn click_at(harness: &mut egui_kittest::Harness<'static, SlideApp>, p: Pos2) {
|
||||
for pressed in [true, false] {
|
||||
harness.event(egui::Event::PointerButton { pos: p, button: egui::PointerButton::Primary, pressed, modifiers: egui::Modifiers::NONE });
|
||||
harness.step();
|
||||
}
|
||||
}
|
||||
|
||||
/// #967: hovering a clip shows its bar and a click on the bar seeks. After 3 s without moving
|
||||
/// the bar is gone, and a click where it was acts on the clip itself (play), not on the bar.
|
||||
#[test]
|
||||
fn media_controls_hide_when_pointer_is_idle() {
|
||||
// Awake: the bar is there and a click on its timeline seeks, leaving the clip paused.
|
||||
let (mut harness, id, track, on_bar) = show_with_clip();
|
||||
harness.hover_at(track);
|
||||
harness.run_steps(2);
|
||||
assert!(harness.state().show.as_ref().unwrap().pointer_awake(harness.ctx.input(|i| i.time)));
|
||||
assert!(on_bar.get(), "the bar is under a pointer that just moved");
|
||||
click_at(&mut harness, track);
|
||||
harness.run_steps(2);
|
||||
let status = harness.state().media.status(id).expect("the click on the bar cued the clip");
|
||||
assert!(!harness.state().media.is_playing(id), "a bar click seeks, it does not play");
|
||||
assert!(status.position > 1.0, "seeked along the timeline, at {}", status.position);
|
||||
|
||||
// Idle: same spot, no movement for 3 s.
|
||||
let (mut harness, id, track, on_bar) = show_with_clip();
|
||||
harness.hover_at(track);
|
||||
harness.run_steps(2);
|
||||
assert!(on_bar.get());
|
||||
harness.run_steps(12);
|
||||
assert!(!harness.state().show.as_ref().unwrap().pointer_awake(harness.ctx.input(|i| i.time)));
|
||||
assert!(!on_bar.get(), "the bar is hidden once the pointer is idle");
|
||||
click_at(&mut harness, track);
|
||||
harness.run_steps(2);
|
||||
assert!(harness.state().media.is_playing(id), "the click reached the clip");
|
||||
let at = harness.state().media.status(id).map_or(0.0, |s| s.position);
|
||||
assert!(at < 5.0, "the click did not seek along the timeline, position {at}");
|
||||
}
|
||||
|
||||
/// Send real egui key events to the presenter UI, without the audience window's handler.
|
||||
#[test]
|
||||
fn presenter_navigation_uses_its_own_keyboard_events() {
|
||||
|
||||
Reference in New Issue
Block a user