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}