Merge PR #520 from NikitkaRa: Insert: Summary, Section and Slide Zoom with Return to Zoom

Fix-ups in the merge (review): zoom targets, sections and titles are looked up with get()
instead of indexing; Hyperlink literals carry both main's sound fields and zoom_return;
duplicated test statements left by the test-file union are removed.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Brandon Thomas
2026-10-14 15:58:31 -04:00
co-authored by Claude Opus 5.5
20 changed files with 458 additions and 18 deletions
+27 -1
View File
@@ -4,7 +4,7 @@
use std::collections::BTreeMap;
use deckcraft_model::Presentation;
use deckcraft_model::text::Action;
use deckcraft_model::text::{Action, Hyperlink};
use crate::Timeline;
use crate::interact::SlideRun;
@@ -78,6 +78,9 @@ pub struct ShowState {
order: Option<Vec<usize>>,
/// Name of the custom show being played, when there is one.
custom: Option<String>,
/// Return to Zoom: (zoom slide, last zoomed slide). Advancing past the last zoomed slide goes
/// back to the zoom slide, fully built.
pub zoom: Option<(usize, usize)>,
}
/// Slide indices the show visits in order: the custom show or range from Set Up Slide Show, minus
@@ -252,6 +255,16 @@ impl ShowState {
self.playing = true;
return ShowAction::PlayStep(self.step);
}
if let Some((back, until)) = self.zoom
&& until == self.slide
&& back < pres.slides.len()
{
self.zoom = None;
self.enter(pres, back);
self.step = ShowState::steps(pres, back);
self.playing = false;
return ShowAction::Slide(back);
}
match self.next_index(pres) {
Some(i) => {
self.enter(pres, i);
@@ -331,6 +344,7 @@ impl ShowState {
if i >= pres.slides.len() {
return ShowAction::None;
}
self.zoom = None;
self.enter(pres, i);
ShowAction::Slide(i)
}
@@ -363,6 +377,18 @@ impl ShowState {
}
}
/// Follow a clicked link, honouring a zoom's Return to Zoom (see [`Hyperlink::zoom_return`]).
pub fn follow_link(&mut self, pres: &Presentation, link: &Hyperlink, last_viewed: Option<usize>) -> LinkJump {
let from = self.slide;
let jump = self.follow(pres, &link.action, last_viewed);
if let Some(until) = link.zoom_return.and_then(|id| pres.slides.iter().position(|s| s.id == id))
&& matches!(jump, LinkJump::Show(ShowAction::Slide(_)))
{
self.zoom = Some((from, until));
}
jump
}
/// Seconds after which the current slide advances by itself (Advance Slide ▸ After), when the
/// show uses timings.
pub fn advance_after(&self, pres: &Presentation) -> Option<f64> {
+32
View File
@@ -996,3 +996,35 @@ fn media_in_the_click_sequence_cues_and_does_not_hold_the_click() {
let tl = Timeline::new(&slide_with(vec![play, anim(2, AnimClass::Entrance, "fade", AnimStart::AfterPrevious)]), W, H);
assert!((tl.step_duration(0) - 6.0).abs() < 1e-9, "{}", tl.step_duration(0));
}
/// Zoom with Return to Zoom: after the zoomed slides the show goes back to the zoom slide, fully
/// built; without it, or after jumping elsewhere, the show carries on in order.
#[test]
fn show_returns_to_the_zoom_slide() {
use deckcraft_model::text::{Action, Hyperlink};
let p = pres(6);
let link = |to: u32, last: Option<u32>| Hyperlink {
action: Action::Slide { slide: SlideId(1000 + to) },
tooltip: String::new(),
highlight_click: false,
zoom_return: last.map(|l| SlideId(1000 + l)),
sound: None,
stop_sound: false,
};
let mut s = ShowState::new(&p);
// A section zoom on slide 0 for slides 2–3.
assert_eq!(s.follow_link(&p, &link(2, Some(3)), None), LinkJump::Show(ShowAction::Slide(2)));
assert_eq!(s.next(&p), ShowAction::Slide(3));
assert_eq!(s.next(&p), ShowAction::Slide(0), "back to the zoom");
assert_eq!(s.step, ShowState::steps(&p, 0));
assert_eq!(s.next(&p), ShowAction::Slide(1), "then on in order");
// Without Return to Zoom the show carries on.
let mut s = ShowState::new(&p);
s.follow_link(&p, &link(4, None), None);
assert_eq!(s.next(&p), ShowAction::Slide(5));
// Jumping elsewhere (See All Slides, a number) forgets the zoom.
let mut s = ShowState::new(&p);
s.follow_link(&p, &link(2, Some(2)), None);
s.goto(&p, 4);
assert_eq!(s.next(&p), ShowAction::Slide(5));
}