use std::collections::HashMap;
use std::time::Duration;
use crate::slide_group::SlideGroup;
#[derive(Debug, Clone)]
pub struct TextBox {
pub id: u64,
pub rect: (f32, f32, f32, f32),
pub content: String,
pub font_size: f32,
pub color: [u8; 4],
pub background: Option<[u8; 4]>,
}
#[derive(Debug, Clone)]
pub struct PresentationState {
pub total_pages: usize,
pub slide_groups: Vec<SlideGroup>,
pub total_logical_slides: usize,
pub current_page: usize,
pub current_logical_slide: usize,
pub current_overlay_within_group: usize,
pub frozen: bool,
pub frozen_page: Option<usize>,
pub blacked_out: bool,
pub whiteboard_active: bool,
pub whiteboard_strokes: Vec<InkStroke>,
pub screen_share_mode: bool,
pub presentation_mode: bool,
pub laser_active: bool,
pub pointer_position: Option<(f32, f32)>,
pub pointer_style: PointerStyle,
pub pointer_appearances: PointerAppearances,
pub ink_active: bool,
pub active_pen: ActivePen,
pub slide_ink_by_page: HashMap<usize, Vec<InkStroke>>,
pub slide_text_boxes_by_page: HashMap<usize, Vec<TextBox>>,
pub text_box_mode: bool,
pub selected_text_box: Option<u64>,
pub text_box_editing: bool,
pub next_text_box_id: u64,
pub spotlight_active: bool,
pub spotlight_position: Option<(f32, f32)>,
pub spotlight_radius: f32,
pub spotlight_dim_opacity: f32,
pub zoom_active: bool,
pub zoom_region: Option<ZoomRegion>,
pub timer: TimerState,
pub slide_elapsed: Duration,
pub slide_elapsed_by_logical: Vec<Duration>,
pub overview_visible: bool,
pub notes_visible: bool,
pub notes_editing: bool,
pub notes_font_size: f32,
pub notes_font_size_step: f32,
pub quit_requested: bool,
pub current_notes: Option<String>,
}
impl PresentationState {
pub fn new(total_pages: usize, slide_groups: Vec<SlideGroup>) -> Self {
let total_logical_slides = slide_groups.len();
let current_notes = slide_groups.first().and_then(|g| g.notes.clone());
Self {
total_pages,
slide_groups,
total_logical_slides,
current_page: 0,
current_logical_slide: 0,
current_overlay_within_group: 0,
frozen: false,
frozen_page: None,
blacked_out: false,
whiteboard_active: false,
whiteboard_strokes: Vec::new(),
screen_share_mode: false,
presentation_mode: false,
laser_active: true,
pointer_position: None,
pointer_style: PointerStyle::Dot,
pointer_appearances: PointerAppearances::default(),
ink_active: false,
active_pen: ActivePen::default(),
slide_ink_by_page: HashMap::new(),
slide_text_boxes_by_page: HashMap::new(),
text_box_mode: false,
selected_text_box: None,
text_box_editing: false,
next_text_box_id: 1,
spotlight_active: false,
spotlight_position: None,
spotlight_radius: 80.0,
spotlight_dim_opacity: 0.6,
zoom_active: false,
zoom_region: None,
timer: TimerState::default(),
slide_elapsed: Duration::ZERO,
slide_elapsed_by_logical: vec![Duration::ZERO; total_logical_slides],
overview_visible: false,
notes_visible: true,
notes_editing: false,
notes_font_size: 16.0,
notes_font_size_step: 2.0,
quit_requested: false,
current_notes,
}
}
pub fn audience_page(&self) -> usize {
self.frozen_page.unwrap_or(self.current_page)
}
pub fn current_page_ink(&self) -> &[InkStroke] {
self.slide_ink_by_page.get(&self.current_page).map_or(&[], |v| v.as_slice())
}
pub fn current_page_text_boxes(&self) -> &[TextBox] {
self.slide_text_boxes_by_page.get(&self.current_page).map_or(&[], |v| v.as_slice())
}
pub fn current_pointer_appearance(&self) -> PointerAppearance {
self.pointer_appearances.for_style(self.pointer_style)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum PointerStyle {
#[default]
Dot,
Crosshair,
Arrow,
Ring,
Bullseye,
Highlight,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PointerAppearance {
pub color: [u8; 4],
pub size: f32,
}
impl Default for PointerAppearance {
fn default() -> Self {
Self { color: [255, 0, 0, 255], size: 12.0 }
}
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct PointerAppearances {
pub dot: PointerAppearance,
pub crosshair: PointerAppearance,
pub arrow: PointerAppearance,
pub ring: PointerAppearance,
pub bullseye: PointerAppearance,
pub highlight: PointerAppearance,
}
impl PointerAppearances {
pub fn for_style(&self, style: PointerStyle) -> PointerAppearance {
match style {
PointerStyle::Dot => self.dot,
PointerStyle::Crosshair => self.crosshair,
PointerStyle::Arrow => self.arrow,
PointerStyle::Ring => self.ring,
PointerStyle::Bullseye => self.bullseye,
PointerStyle::Highlight => self.highlight,
}
}
}
impl Default for PointerAppearances {
fn default() -> Self {
let default = PointerAppearance::default();
Self {
dot: default,
crosshair: default,
arrow: default,
ring: default,
bullseye: default,
highlight: default,
}
}
}
#[derive(Debug, Clone)]
pub struct InkStroke {
pub points: Vec<(f32, f32)>,
pub color: [u8; 4],
pub width: f32,
pub finished: bool,
}
#[derive(Debug, Clone, Copy)]
pub struct ActivePen {
pub color: [u8; 4],
pub width: f32,
}
impl Default for ActivePen {
fn default() -> Self {
Self { color: [255, 0, 0, 255], width: 3.0 }
}
}
#[derive(Debug, Clone, Copy)]
pub struct ZoomRegion {
pub center: (f32, f32),
pub factor: f32,
}
#[derive(Debug, Clone)]
pub struct TimerState {
pub mode: TimerMode,
pub duration: Option<Duration>,
pub elapsed: Duration,
pub running: bool,
pub warning_threshold: Option<Duration>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum TimerMode {
Elapsed,
Countdown,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TimerPhase {
Normal,
Warning,
Overrun,
}
impl TimerState {
pub fn phase(&self) -> TimerPhase {
let Some(duration) = self.duration else {
return TimerPhase::Normal;
};
if self.elapsed >= duration {
TimerPhase::Overrun
} else if self.warning_threshold.is_some_and(|warning| self.elapsed + warning >= duration) {
TimerPhase::Warning
} else {
TimerPhase::Normal
}
}
pub fn display_time(&self) -> Duration {
match self.mode {
TimerMode::Countdown => {
self.duration.map_or(self.elapsed, |duration| duration.saturating_sub(self.elapsed))
}
TimerMode::Elapsed => self.elapsed,
}
}
}
impl Default for TimerState {
fn default() -> Self {
Self {
mode: TimerMode::Elapsed,
duration: None,
elapsed: Duration::ZERO,
running: false,
warning_threshold: None,
}
}
}