diff --git a/crates/ui-egui/src/show.rs b/crates/ui-egui/src/show.rs index 1c8e6e1e..56f82b79 100644 --- a/crates/ui-egui/src/show.rs +++ b/crates/ui-egui/src/show.rs @@ -33,6 +33,8 @@ pub struct Show { goto: String, started: bool, show_start: f64, + /// When the presenter paused the timer. + timer_paused: Option, timings: Vec<(usize, f64)>, last_move: f64, pub ended: bool, @@ -89,6 +91,7 @@ impl Show { goto: String::new(), started: false, show_start: 0.0, + timer_paused: None, timings: vec![], last_move: 0.0, ended: false, @@ -717,6 +720,23 @@ fn gif_hit(ctx: &egui::Context, ui: &Ui, show: &Show, doc: &Presentation, app: & } impl Show { + /// The presenter timer: time since the show started, frozen while paused. + fn elapsed(&self, now: f64) -> f64 { + self.timer_paused.unwrap_or(now) - self.show_start + } + + fn toggle_timer(&mut self, now: f64) { + match self.timer_paused.take() { + Some(at) => self.show_start += now - at, + None => self.timer_paused = Some(now), + } + } + + fn reset_timer(&mut self, now: f64) { + self.show_start = now; + self.timer_paused = self.timer_paused.map(|_| now); + } + fn reading_bar_hit(&self, ctx: &egui::Context) -> bool { // Clicks on the on-screen controls must not also advance. ctx.input(|i| i.pointer.interact_pos()).is_some_and(|p| { @@ -746,8 +766,12 @@ fn presenter_window(ctx: &egui::Context, show: &mut Show, doc: &Presentation, no } let next_tex = show.next_tex.as_ref().filter(|(k, _)| Some(*k) == next).map(|(_, t)| t.clone()); let notes = doc.slides.get(cur).map(|s| s.notes_text()).unwrap_or_default(); - let elapsed = now - show.show_start; + let elapsed = show.elapsed(now); let mut actions = vec![]; + // The presenter window is dark; default buttons draw dark text on it (#221). + let button = |ui: &mut Ui, s: &str| { + ui.add(egui::Button::new(egui::RichText::new(s).size(15.0).color(Color32::WHITE)).fill(Color32::from_gray(60)).min_size(vec2(0.0, 30.0))) + }; ctx.show_viewport_immediate( egui::ViewportId::from_hash_of("deckcraft_presenter"), egui::ViewportBuilder::default().with_title("Presenter View — DeckCraft").with_inner_size([1100.0, 700.0]), @@ -764,16 +788,22 @@ fn presenter_window(ctx: &egui::Context, show: &mut Show, doc: &Presentation, no .size(22.0) .color(Color32::WHITE), ); + if button(ui, if show.timer_paused.is_some() { "Resume" } else { "Pause" }).clicked() { + show.toggle_timer(now); + } + if button(ui, "Reset").clicked() { + show.reset_timer(now); + } ui.add_space(20.0); ui.label(egui::RichText::new(format!("Slide {} of {}", cur + 1, doc.slides.len())).size(16.0).color(Color32::from_gray(200))); ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| { - if ui.button("End Show").clicked() { + if button(ui, "End Show").clicked() { *keep = false; } - if ui.button("Next ▶").clicked() { + if button(ui, "Next ▶").clicked() { actions.push(show.state.next(doc)); } - if ui.button("◀ Previous").clicked() { + if button(ui, "◀ Previous").clicked() { actions.push(show.state.prev(doc)); } }); @@ -825,6 +855,24 @@ mod tests { use super::*; + /// #221: the presenter could not pause or restart the timer. + #[test] + fn presenter_timer_pauses_resumes_and_resets() { + let doc = deckcraft_engine::Session::with_new().active().unwrap().doc.clone(); + let mut show = Show::new(&doc, 0, false, false); + show.show_start = 10.0; + assert_eq!(show.elapsed(15.0), 5.0); + show.toggle_timer(15.0); + assert_eq!(show.elapsed(40.0), 5.0, "paused"); + show.toggle_timer(40.0); + assert_eq!(show.elapsed(42.0), 7.0, "resumed where it stopped"); + show.reset_timer(50.0); + assert_eq!(show.elapsed(53.0), 3.0, "running from zero"); + show.toggle_timer(53.0); + show.reset_timer(60.0); + assert_eq!(show.elapsed(70.0), 0.0, "a paused reset stays paused at zero"); + } + #[test] fn a_transition_keeps_the_old_slide_in_its_own_texture() { // #33: the old slide's handle was cloned, so the new slide was rendered into the same diff --git a/crates/ui-egui/src/status.rs b/crates/ui-egui/src/status.rs index a6f4eb11..e2fa9d60 100644 --- a/crates/ui-egui/src/status.rs +++ b/crates/ui-egui/src/status.rs @@ -138,15 +138,22 @@ pub fn notes_pane(app: &mut SlideApp, ui: &mut Ui) { app.notes_buf.2 = current.clone(); } let mut buf = app.notes_buf.2.clone(); - let r = ui.add( - egui::TextEdit::multiline(&mut buf) - .id(id) - .hint_text("Click to add notes") - .frame(egui::Frame::NONE) - .desired_width(f32::INFINITY) - .desired_rows(2) - .font(theme::font(13.0)), - ); + // Long notes scroll inside the pane instead of spilling over the slide (#221). + let r = egui::ScrollArea::vertical() + .max_height(ui.available_height()) + .auto_shrink([false, false]) + .show(ui, |ui| { + ui.add( + egui::TextEdit::multiline(&mut buf) + .id(id) + .hint_text("Click to add notes") + .frame(egui::Frame::NONE) + .desired_width(f32::INFINITY) + .desired_rows(2) + .font(theme::font(13.0)), + ) + }) + .inner; app.notes_buf.2 = buf.clone(); if r.lost_focus() && buf != current { let _ = app.run("slide.notes", json!({"text": buf, "index": idx}));