Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,20 @@ This section tracks work toward v0.2.0 (editor completeness). See

## [v0.4.0 — in progress]

### Added — Wave 20 (custom layouts + rehearse timings)

- **Layout picker**: a dropdown in the slide context menu showing the
current template's available layouts. Selecting one sets the slide's
layout and re-renders with placeholder guides.
- **Rehearse timings**: a "Rehearse" button in the presenter records
per-slide durations. When done, timings are committed to the deck. A
"Use timings" toggle enables auto-advance — the presenter advances
automatically after each slide's rehearsed duration, with a countdown
indicator.
- `Slide.rehearsed_duration_ms: Option<u32>` (additive). New
`SetSlideRehearsedDuration` command (reversible).
- `docs/sprint-records/wave-20.md` documenting the wave scope.

### Added — Wave 19 (animation enhancements)

- **Trigger model**: build steps now fire on click, with the previous step,
Expand Down
26 changes: 26 additions & 0 deletions apps/desktop/src-tauri/src/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -428,6 +428,10 @@ pub struct SlideSnapshot {
/// build-ins instantly. `None` defers to the system preference.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reduce_motion: Option<bool>,
/// Per-slide rehearsed duration in milliseconds, for auto-advance. `None`
/// means no timing has been recorded yet.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub rehearsed_duration_ms: Option<u32>,
}

/// A named placeholder frame, mirroring [`slides_core::PlaceholderDef`].
Expand Down Expand Up @@ -1403,6 +1407,27 @@ pub fn set_slide_layout(
Ok(snapshot)
}

/// Sets or clears a slide's rehearsed duration (for auto-advance) and returns
/// the updated deck snapshot. Pass `None` (or omit `duration_ms`) to clear the
/// timing. The change is applied via the verified
/// [`slides_core::SetSlideRehearsedDuration`] command so it is undoable.
#[tauri::command]
pub fn set_slide_rehearsed_duration(
slide_id: String,
duration_ms: Option<u32>,
app: AppHandle,
state: State<'_, AppState>,
) -> Result<DeckSnapshot, String> {
let mut guard = state.session.lock().map_err(|e| e.to_string())?;
let session = guard.as_mut().ok_or("no deck is open")?;
let command = Box::new(slides_core::SetSlideRehearsedDuration::new(slide_id, duration_ms));
session.execute(command).map_err(|e| e.to_string())?;
let snapshot = state.snapshot(session.deck());
drop(guard);
schedule_recovery(&app, &state);
Ok(snapshot)
}

/// Opens a PPTX file from the given path and returns its deck snapshot.
#[tauri::command]
pub fn open_deck(path: String, state: State<'_, AppState>) -> Result<DeckSnapshot, String> {
Expand Down Expand Up @@ -3255,6 +3280,7 @@ fn slide_to_dto(slide: &slides_core::Slide) -> SlideSnapshot {
.map(|paragraphs| paragraphs.iter().map(paragraph_to_dto).collect()),
layout_ref: slide.layout_ref.clone(),
reduce_motion: slide.reduce_motion,
rehearsed_duration_ms: slide.rehearsed_duration_ms,
}
}

Expand Down
1 change: 1 addition & 0 deletions apps/desktop/src-tauri/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ fn main() {
commands::set_presenter_settings,
commands::set_template,
commands::set_slide_layout,
commands::set_slide_rehearsed_duration,
commands::list_templates,
commands::render_slide_svg,
commands::export_svg,
Expand Down
17 changes: 17 additions & 0 deletions apps/desktop/src/App.svelte
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@
HeadingLevelDto,
ParagraphDto,
ParagraphStyleDto,
PlaceholderDefDto,
RecoverySnapshot,
RunDto,
SlideSectionDto,
Expand Down Expand Up @@ -141,6 +142,21 @@
/** Name of the layout the active slide uses, or '' when none. */
const activeLayoutRef = $derived<string>(activeSlide?.layoutRef ?? '')

/** Effective placeholder frames for the active slide's layout: the deck
* master's placeholders overridden by the selected layout's overrides
* (matched by name). Empty when no layout is selected, so no guides render. */
const activeLayoutPlaceholders = $derived.by<PlaceholderDefDto[]>(() => {
const layoutName = activeSlide?.layoutRef
if (!layoutName) return []
const masterPlaceholders = deck?.master.placeholders ?? []
const layout = (deck?.layouts ?? []).find((l) => l.name === layoutName)
if (!layout) return masterPlaceholders
const byName = new Map<string, PlaceholderDefDto>()
for (const p of masterPlaceholders) byName.set(p.name, p)
for (const p of layout.placeholders) byName.set(p.name, p)
return Array.from(byName.values())
})

/** Preset aspect-ratio slide sizes, in EMU, matching the Rust constructors. */
const ASPECT_PRESETS: Record<'16:9' | '4:3' | '16:10', SlideSizeDto> = {
'16:9': { widthEmu: 12_192_000, heightEmu: 6_858_000 },
Expand Down Expand Up @@ -1311,6 +1327,7 @@
slideSize={slideSize}
highContrast={highContrast}
selectedShapeIndex={a11ySelectedShapeIndex}
placeholderGuides={activeLayoutPlaceholders}
onEditTextBox={handleTextEdit}
onSetCellText={handleSetCellText}
onCellFocus={handleCellFocus}
Expand Down
Loading
Loading