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