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