Merge PR #679 from NikitkaRa: Show: Show Media Controls hides the playback bar

# Conflicts:
#	crates/engine/src/cmd/review.rs
#	crates/ui-egui/src/show.rs
This commit is contained in:
Brandon Thomas
2026-10-14 13:46:55 -04:00
5 changed files with 108 additions and 12 deletions
+18 -1
View File
@@ -45,7 +45,7 @@ pub fn specs() -> Vec<CommandSpec> {
"Set Up Slide Show…",
["Slide Show", "Set Up"],
None,
"{type?: speaker|browsed|kiosk, loop?: bool, noNarration?, noAnimation?, useTimings?, from?, to?}",
"{type?: speaker|browsed|kiosk, loop?: bool, noNarration?, noAnimation?, useTimings?, showMediaControls?, from?, to?}",
has_doc,
setup
),
@@ -312,6 +312,9 @@ fn setup(s: &mut Session, p: &Value) -> Result<Value> {
if let Some(v) = bool_param(p, "useTimings") {
sh.use_timings = v;
}
if let Some(v) = bool_param(p, "showMediaControls") {
sh.show_media_controls = v;
}
match (p.get("from").and_then(Value::as_u64), p.get("to").and_then(Value::as_u64)) {
(Some(a), Some(b)) => sh.range = Some((a.min(u32::MAX as u64) as u32, b.min(u32::MAX as u64) as u32)),
(None, None) if p.get("all").is_some() => sh.range = None,
@@ -398,4 +401,18 @@ mod tests {
// On wasm this used to return an empty string, so browser comments were saved undated.
assert!(now().starts_with("20"), "{}", now());
}
#[test]
fn show_media_controls_follow_setup() {
let mut s = Session::with_new();
assert!(s.doc().expect("doc").doc.show.show_media_controls);
let off = s.execute("show.setup", &json!({"showMediaControls": false})).expect("off");
assert_eq!(off["showMediaControls"], false);
assert_eq!(off["useTimings"], true, "other setup flags stay");
let timings = s.execute("show.setup", &json!({"useTimings": false})).expect("timings");
assert_eq!(timings["showMediaControls"], false);
assert_eq!(timings["useTimings"], false);
s.execute("show.setup", &json!({"showMediaControls": true})).expect("on");
assert!(s.doc().expect("doc").doc.show.show_media_controls);
}
}
+3 -1
View File
@@ -1865,7 +1865,9 @@ fn slide_show(app: &mut SlideApp, ui: &mut Ui) {
run(app, "show.setup", json!({"noNarration": !narr}));
}
let mut mc = app.session.active().is_none_or(|d| d.doc.show.show_media_controls);
let _ = ui.checkbox(&mut mc, "Show Media Controls");
if ui.checkbox(&mut mc, "Show Media Controls").changed() {
run(app, "show.setup", json!({"showMediaControls": mc}));
}
});
});
}
+80 -10
View File
@@ -474,8 +474,8 @@ pub fn ui(app: &mut SlideApp, ui: &mut Ui) {
let mut actions: Vec<ShowAction> = vec![];
// The clock of the triggered animations runs before anything reacts to the pointer.
show.tick_triggers(&doc, now);
let on_slide = !show.pen && !show.laser && !show.ended && show.blank.is_none() && !show.reading_bar_hit(&ctx);
let gif_click = if clicked && on_slide { gif_hit(&ctx, ui, &show, doc_uid, &doc, app) } else { None };
let on_slide = !show.pen && !show.laser && !show.ended && show.blank.is_none() && !show.reading_bar_hit(&ctx) && !pointer_on_media_bar(&ctx, ui, &doc, &show);
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 };
let pointing = under.as_ref().is_some_and(|u| u.pick.is_some());
@@ -491,7 +491,7 @@ pub fn ui(app: &mut SlideApp, ui: &mut Ui) {
LinkJump::Exit => keep = false,
LinkJump::None => {}
}
} else if clicked && !show.pen && !show.reading_bar_hit(&ctx) {
} else if clicked && !show.pen && !show.reading_bar_hit(&ctx) && !pointer_on_media_bar(&ctx, ui, &doc, &show) {
if show.ended {
keep = false;
} else if show.blank.is_some() {
@@ -725,13 +725,29 @@ pub fn ui(app: &mut SlideApp, ui: &mut Ui) {
};
crate::media::paint_frame(&painter, &app.media, *id, r);
}
// Play/pause on the animated GIF under the pointer.
if let Some(p) = ctx.input(|i| i.pointer.hover_pos()).filter(|_| !show.pen && !show.laser && now - show.last_move < 2.5) {
for g in &gifs {
let r = on_screen(srect, k, &g.rect);
if r.contains(p) {
let b = crate::media::gif_button_rect(r);
crate::media::paint_gif_button(&painter, b, g.paused, b.contains(p));
// Play/pause on the animated GIF under the pointer, and the audio/video bar.
// Slide Show ▸ Show Media Controls hides both (the click still plays a clip).
if doc.show.show_media_controls {
if let Some(p) = ctx.input(|i| i.pointer.hover_pos()).filter(|_| !show.pen && !show.laser && now - show.last_move < 2.5) {
for g in &gifs {
let r = on_screen(srect, k, &g.rect);
if r.contains(p) {
let b = crate::media::gif_button_rect(r);
crate::media::paint_gif_button(&painter, b, g.paused, b.contains(p));
}
}
}
if let Some((id, clip, bar)) = media_bar_target(&ctx, &doc, &show, srect) {
let action = {
let status = app.media.status(id);
crate::media::control_bar(ui, bar, status.as_ref(), &clip)
};
let pos = app.media.status(id).map(|s| s.position).unwrap_or(0.0);
match action {
Some(crate::media::BarAction::Toggle) => app.media.toggle(doc_uid, &doc, id, &clip, crate::media::Owner::Show),
Some(crate::media::BarAction::Seek(t)) => app.media.seek(doc_uid, &doc, id, &clip, t, crate::media::Owner::Show),
Some(crate::media::BarAction::Nudge(dt)) => app.media.seek(doc_uid, &doc, id, &clip, (pos + dt).max(0.0), crate::media::Owner::Show),
None => {}
}
}
}
@@ -936,6 +952,60 @@ fn run_media_cues(app: &mut SlideApp, show: &mut Show, doc_uid: u64, doc: &Prese
}
}
/// Slide rectangle the show draws into (the reading bar keeps its strip).
fn shown_slide_rect(ui: &Ui, show: &Show, doc: &Presentation) -> Rect {
let full = ui.max_rect();
let avail = if show.reading { Rect::from_min_max(full.min, pos2(full.max.x, full.max.y - 34.0)) } else { full };
fit(avail, doc)
}
/// Playback bar for one clip: along the bottom inside a large shape, just under a small icon.
fn media_bar_rect(shape: Rect) -> Rect {
let h = 26.0f32;
let w = shape.width().clamp(160.0, 420.0);
if shape.height() >= h + 16.0 && shape.width() >= 160.0 {
Rect::from_min_size(pos2(shape.min.x + 4.0, shape.max.y - h - 4.0), vec2((shape.width() - 8.0).min(w), h))
} else {
Rect::from_min_size(pos2(shape.center().x - w / 2.0, shape.max.y + 4.0), vec2(w, h))
}
}
fn slide_media_bars(srect: Rect, doc: &Presentation, slide: usize) -> Vec<(ShapeId, deckcraft_model::MediaClip, Rect, Rect)> {
let k = srect.width() / doc.slide_size.width.max(1.0) as f32;
crate::media::slide_media(doc, slide)
.into_iter()
.map(|(id, x, clip)| {
let r = Rect::from_min_size(pos2(srect.min.x + x.x as f32 * k, srect.min.y + x.y as f32 * k), vec2(x.w as f32 * k, x.h as f32 * k));
let bar = media_bar_rect(r);
(id, clip, r, bar)
})
.collect()
}
/// The clip whose bar or shape is under the pointer, when Show Media Controls is on.
fn media_bar_target(ctx: &egui::Context, doc: &Presentation, show: &Show, srect: Rect) -> Option<(ShapeId, deckcraft_model::MediaClip, Rect)> {
if !doc.show.show_media_controls || show.pen || show.ended || show.blank.is_some() {
return None;
}
let p = ctx.input(|i| i.pointer.hover_pos())?;
let items = slide_media_bars(srect, doc, show.state.slide);
if let Some((id, clip, _, bar)) = items.iter().rev().find(|(_, _, _, bar)| bar.contains(p)) {
return Some((*id, clip.clone(), *bar));
}
let (id, clip, _, bar) = items.iter().rev().find(|(_, _, r, _)| r.contains(p))?;
Some((*id, clip.clone(), *bar))
}
/// 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() {
return false;
}
let Some(p) = ctx.input(|i| i.pointer.interact_pos().or(i.pointer.hover_pos())) else { return false };
let srect = shown_slide_rect(ui, show, doc);
slide_media_bars(srect, doc, show.state.slide).iter().any(|(_, _, _, bar)| bar.contains(p))
}
/// The media shape under the pointer on the shown slide.
fn media_hit(ctx: &egui::Context, ui: &Ui, show: &Show, doc: &Presentation) -> Option<(ShapeId, deckcraft_model::MediaClip)> {
let p = ctx.input(|i| i.pointer.interact_pos())?;