Skip to main content

dais_core/
state.rs

1use std::collections::HashMap;
2use std::time::Duration;
3
4use crate::slide_group::SlideGroup;
5
6/// A positioned text overlay on a slide.
7#[derive(Debug, Clone)]
8pub struct TextBox {
9    /// Unique identifier for this text box.
10    pub id: u64,
11    /// Normalized position and size: (x, y, w, h) in 0..1 coordinates.
12    pub rect: (f32, f32, f32, f32),
13    /// Typst markup content.
14    pub content: String,
15    /// Font size in points.
16    pub font_size: f32,
17    /// Text color as RGBA.
18    pub color: [u8; 4],
19    /// Optional background fill as RGBA.
20    pub background: Option<[u8; 4]>,
21}
22
23/// The single authoritative state of the presentation.
24///
25/// The engine owns and mutates this. The UI reads it (via watch channel) and renders it.
26/// The UI holds no authoritative state of its own — all mutations go through
27/// the [`CommandBus`](crate::bus::CommandBus).
28#[derive(Debug, Clone)]
29pub struct PresentationState {
30    // -- Document info --
31    /// Total number of raw PDF pages.
32    pub total_pages: usize,
33    /// Logical slide groups (may be 1:1 with pages if no grouping).
34    pub slide_groups: Vec<SlideGroup>,
35    /// Total number of logical slides.
36    pub total_logical_slides: usize,
37
38    // -- Navigation --
39    /// Current raw PDF page index (0-based).
40    pub current_page: usize,
41    /// Current logical slide index (0-based).
42    pub current_logical_slide: usize,
43    /// Current overlay step within the current group (0-based).
44    pub current_overlay_within_group: usize,
45
46    // -- Display modes --
47    /// Whether the audience display is frozen.
48    pub frozen: bool,
49    /// The page shown on the audience display when frozen (None = not frozen).
50    pub frozen_page: Option<usize>,
51    /// Whether the audience display is blacked out.
52    pub blacked_out: bool,
53    /// Whether the whiteboard is active (blank white canvas on audience).
54    pub whiteboard_active: bool,
55    /// Ink strokes drawn on the whiteboard (persist across navigation).
56    pub whiteboard_strokes: Vec<InkStroke>,
57    /// Whether screen-share mode is active.
58    pub screen_share_mode: bool,
59    /// Whether presentation mode is active (single-monitor fullscreen HUD).
60    pub presentation_mode: bool,
61
62    // -- Presentation aids --
63    /// Whether the laser pointer is active.
64    pub laser_active: bool,
65    /// Current pointer position (normalized 0..1), None if pointer is off-slide.
66    pub pointer_position: Option<(f32, f32)>,
67    /// Pointer visual style.
68    pub pointer_style: PointerStyle,
69    /// Pointer appearance settings for each visual style.
70    pub pointer_appearances: PointerAppearances,
71    /// Whether ink drawing mode is active.
72    pub ink_active: bool,
73    /// Active pen settings — used to initialise the next stroke.
74    pub active_pen: ActivePen,
75    /// Per-page slide ink annotations (`page_index` → strokes).
76    pub slide_ink_by_page: HashMap<usize, Vec<InkStroke>>,
77    /// Per-page text box overlays (`page_index` → boxes).
78    pub slide_text_boxes_by_page: HashMap<usize, Vec<TextBox>>,
79    /// Whether text box placement mode is active.
80    pub text_box_mode: bool,
81    /// The currently selected text box id, if any.
82    pub selected_text_box: Option<u64>,
83    /// Whether the selected text box is in inline edit mode.
84    pub text_box_editing: bool,
85    /// Counter for assigning unique text box IDs.
86    pub next_text_box_id: u64,
87    /// Whether the spotlight overlay is active.
88    pub spotlight_active: bool,
89    /// Spotlight center position (normalized 0..1).
90    pub spotlight_position: Option<(f32, f32)>,
91    /// Spotlight radius in logical pixels.
92    pub spotlight_radius: f32,
93    /// Spotlight dim opacity from 0.0 to 1.0.
94    pub spotlight_dim_opacity: f32,
95    /// Whether zoom is active on the audience display.
96    pub zoom_active: bool,
97    /// Current zoom region, if zoom is active.
98    pub zoom_region: Option<ZoomRegion>,
99
100    // -- Timer --
101    /// Timer state.
102    pub timer: TimerState,
103    /// Total time spent on the current logical slide during this session.
104    pub slide_elapsed: Duration,
105    /// Accumulated time spent on each logical slide during this session.
106    pub slide_elapsed_by_logical: Vec<Duration>,
107
108    // -- UI --
109    /// Whether the slide overview grid is visible.
110    pub overview_visible: bool,
111    /// Whether the notes panel is visible.
112    pub notes_visible: bool,
113    /// Whether the notes panel is in markdown edit mode.
114    pub notes_editing: bool,
115    /// Current notes font size in points.
116    pub notes_font_size: f32,
117    /// Step size for font increment/decrement.
118    pub notes_font_size_step: f32,
119    /// Whether a quit confirmation dialog is showing.
120    pub quit_requested: bool,
121
122    // -- Content --
123    /// Markdown notes for the current logical slide, if any.
124    pub current_notes: Option<String>,
125}
126
127impl PresentationState {
128    /// Create a new state for a presentation with the given slide groups.
129    pub fn new(total_pages: usize, slide_groups: Vec<SlideGroup>) -> Self {
130        let total_logical_slides = slide_groups.len();
131        let current_notes = slide_groups.first().and_then(|g| g.notes.clone());
132        Self {
133            total_pages,
134            slide_groups,
135            total_logical_slides,
136            current_page: 0,
137            current_logical_slide: 0,
138            current_overlay_within_group: 0,
139            frozen: false,
140            frozen_page: None,
141            blacked_out: false,
142            whiteboard_active: false,
143            whiteboard_strokes: Vec::new(),
144            screen_share_mode: false,
145            presentation_mode: false,
146            laser_active: true,
147            pointer_position: None,
148            pointer_style: PointerStyle::Dot,
149            pointer_appearances: PointerAppearances::default(),
150            ink_active: false,
151            active_pen: ActivePen::default(),
152            slide_ink_by_page: HashMap::new(),
153            slide_text_boxes_by_page: HashMap::new(),
154            text_box_mode: false,
155            selected_text_box: None,
156            text_box_editing: false,
157            next_text_box_id: 1,
158            spotlight_active: false,
159            spotlight_position: None,
160            spotlight_radius: 80.0,
161            spotlight_dim_opacity: 0.6,
162            zoom_active: false,
163            zoom_region: None,
164            timer: TimerState::default(),
165            slide_elapsed: Duration::ZERO,
166            slide_elapsed_by_logical: vec![Duration::ZERO; total_logical_slides],
167            overview_visible: false,
168            notes_visible: true,
169            notes_editing: false,
170            notes_font_size: 16.0,
171            notes_font_size_step: 2.0,
172            quit_requested: false,
173            current_notes,
174        }
175    }
176
177    /// The page the audience should see (respects freeze).
178    pub fn audience_page(&self) -> usize {
179        self.frozen_page.unwrap_or(self.current_page)
180    }
181
182    /// Ink strokes for the current presenter page (read-only convenience for UI).
183    pub fn current_page_ink(&self) -> &[InkStroke] {
184        self.slide_ink_by_page.get(&self.current_page).map_or(&[], |v| v.as_slice())
185    }
186
187    /// Text boxes for the current presenter page (read-only convenience for UI).
188    pub fn current_page_text_boxes(&self) -> &[TextBox] {
189        self.slide_text_boxes_by_page.get(&self.current_page).map_or(&[], |v| v.as_slice())
190    }
191
192    /// Appearance for the currently selected pointer style.
193    pub fn current_pointer_appearance(&self) -> PointerAppearance {
194        self.pointer_appearances.for_style(self.pointer_style)
195    }
196}
197
198/// Visual style for the audience laser pointer.
199#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
200pub enum PointerStyle {
201    /// Circular laser dot with glow.
202    #[default]
203    Dot,
204    /// Crosshair with a small center dot.
205    Crosshair,
206    /// Arrow-style marker.
207    Arrow,
208    /// Hollow circle pointer.
209    Ring,
210    /// Ring with a center dot.
211    Bullseye,
212    /// Translucent filled highlight circle.
213    Highlight,
214}
215
216/// Color and size for one pointer visual style.
217#[derive(Debug, Clone, Copy, PartialEq)]
218pub struct PointerAppearance {
219    /// Pointer color as RGBA.
220    pub color: [u8; 4],
221    /// Pointer size in logical pixels.
222    pub size: f32,
223}
224
225impl Default for PointerAppearance {
226    fn default() -> Self {
227        Self { color: [255, 0, 0, 255], size: 12.0 }
228    }
229}
230
231/// Per-style pointer appearance settings.
232#[derive(Debug, Clone, Copy, PartialEq)]
233pub struct PointerAppearances {
234    /// Appearance for the dot pointer.
235    pub dot: PointerAppearance,
236    /// Appearance for the crosshair pointer.
237    pub crosshair: PointerAppearance,
238    /// Appearance for the arrow pointer.
239    pub arrow: PointerAppearance,
240    /// Appearance for the ring pointer.
241    pub ring: PointerAppearance,
242    /// Appearance for the bullseye pointer.
243    pub bullseye: PointerAppearance,
244    /// Appearance for the highlight pointer.
245    pub highlight: PointerAppearance,
246}
247
248impl PointerAppearances {
249    /// Return the appearance for a pointer style.
250    pub fn for_style(&self, style: PointerStyle) -> PointerAppearance {
251        match style {
252            PointerStyle::Dot => self.dot,
253            PointerStyle::Crosshair => self.crosshair,
254            PointerStyle::Arrow => self.arrow,
255            PointerStyle::Ring => self.ring,
256            PointerStyle::Bullseye => self.bullseye,
257            PointerStyle::Highlight => self.highlight,
258        }
259    }
260}
261
262impl Default for PointerAppearances {
263    fn default() -> Self {
264        let default = PointerAppearance::default();
265        Self {
266            dot: default,
267            crosshair: default,
268            arrow: default,
269            ring: default,
270            bullseye: default,
271            highlight: default,
272        }
273    }
274}
275
276/// A single ink stroke drawn on a slide.
277#[derive(Debug, Clone)]
278pub struct InkStroke {
279    /// Points along the stroke (normalized 0..1 coordinates).
280    pub points: Vec<(f32, f32)>,
281    /// Stroke color as RGBA — snapshotted from `ActivePen` at stroke creation.
282    pub color: [u8; 4],
283    /// Stroke width in logical pixels — snapshotted from `ActivePen` at stroke creation.
284    pub width: f32,
285    /// Whether this stroke is complete (pen lifted).
286    pub finished: bool,
287}
288
289/// The currently selected pen settings used to initialise the next stroke.
290///
291/// These are runtime-only; they are not persisted. Changing them never mutates
292/// already-existing strokes — each stroke snapshots `ActivePen` at creation time.
293#[derive(Debug, Clone, Copy)]
294pub struct ActivePen {
295    /// Pen color as RGBA (alpha included for highlighter / semitransparent pens).
296    pub color: [u8; 4],
297    /// Stroke width in logical pixels.
298    pub width: f32,
299}
300
301impl Default for ActivePen {
302    fn default() -> Self {
303        Self { color: [255, 0, 0, 255], width: 3.0 }
304    }
305}
306
307/// Defines a zoom region on the slide.
308#[derive(Debug, Clone, Copy)]
309pub struct ZoomRegion {
310    /// Center of the zoom region (normalized 0..1).
311    pub center: (f32, f32),
312    /// Magnification factor (e.g., 2.0 = 2x zoom).
313    pub factor: f32,
314}
315
316/// Timer state for the presentation.
317#[derive(Debug, Clone)]
318pub struct TimerState {
319    /// Timer mode.
320    pub mode: TimerMode,
321    /// Configured total duration, if any.
322    pub duration: Option<Duration>,
323    /// Time elapsed since the timer started.
324    pub elapsed: Duration,
325    /// Whether the timer is currently running.
326    pub running: bool,
327    /// Threshold for the warning phase.
328    pub warning_threshold: Option<Duration>,
329}
330
331/// Timer counting mode.
332#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
333#[serde(rename_all = "lowercase")]
334pub enum TimerMode {
335    /// Count up from zero.
336    Elapsed,
337    /// Count down from the configured duration.
338    Countdown,
339}
340
341/// Visual phase of the timer, derived from state each frame.
342#[derive(Debug, Clone, Copy, PartialEq, Eq)]
343pub enum TimerPhase {
344    /// Normal — plenty of time remaining.
345    Normal,
346    /// Warning — less than the warning threshold remaining.
347    Warning,
348    /// Overrun — past the configured duration.
349    Overrun,
350}
351
352impl TimerState {
353    /// Compute the current timer phase.
354    pub fn phase(&self) -> TimerPhase {
355        let Some(duration) = self.duration else {
356            return TimerPhase::Normal;
357        };
358
359        if self.elapsed >= duration {
360            TimerPhase::Overrun
361        } else if self.warning_threshold.is_some_and(|warning| self.elapsed + warning >= duration) {
362            TimerPhase::Warning
363        } else {
364            TimerPhase::Normal
365        }
366    }
367
368    /// The display time for the timer.
369    /// For countdown mode, returns time remaining (clamped to 0).
370    /// For elapsed mode, returns time elapsed.
371    pub fn display_time(&self) -> Duration {
372        match self.mode {
373            TimerMode::Countdown => {
374                self.duration.map_or(self.elapsed, |duration| duration.saturating_sub(self.elapsed))
375            }
376            TimerMode::Elapsed => self.elapsed,
377        }
378    }
379}
380
381impl Default for TimerState {
382    fn default() -> Self {
383        Self {
384            mode: TimerMode::Elapsed,
385            duration: None,
386            elapsed: Duration::ZERO,
387            running: false,
388            warning_threshold: None,
389        }
390    }
391}