Skip to main content

fission_core/
env.rs

1use crate::{
2    action::GlobalState, motion::MotionStateMap, state::LocalStateStore, ui::VideoAudioOptions,
3};
4use fission_i18n::{I18nRegistry, Locale};
5use fission_ir::op::RichTextAnnotation;
6use fission_ir::semantics::MouseCursor;
7use fission_ir::WidgetId;
8use fission_layout::{LayoutPoint, LayoutSize};
9use fission_text_engine::{EditTransaction, TextBuffer, TextEdit};
10use fission_theme::{DesignMode, Theme};
11use serde::{Deserialize, Serialize};
12use std::collections::HashMap;
13use std::sync::Arc;
14
15#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
16pub struct WindowInsets {
17    pub top: f32,
18    pub bottom: f32,
19    pub left: f32,
20    pub right: f32,
21}
22
23#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
24pub enum WindowTitle {
25    Plain(String),
26    // Rich(WindowTitleContent),
27}
28
29impl Default for WindowTitle {
30    fn default() -> Self {
31        Self::Plain("Fission".into())
32    }
33}
34
35impl WindowTitle {
36    pub fn plain(title: impl Into<String>) -> Self {
37        Self::Plain(title.into())
38    }
39
40    pub fn plain_text(&self) -> &str {
41        match self {
42            Self::Plain(title) => title,
43        }
44    }
45}
46
47#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
48pub struct WindowEnv {
49    pub title: WindowTitle,
50}
51
52/// Browser-compatible route location supplied by the host shell.
53///
54/// Only `pathname` is required. The remaining fields mirror `window.location`
55/// so desktop, web, and embedded hosts can pass richer navigation context
56/// without coupling applications to a specific shell implementation.
57#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
58pub struct RouteLocation {
59    pub pathname: String,
60    #[serde(default, skip_serializing_if = "Option::is_none")]
61    pub host: Option<String>,
62    #[serde(default, skip_serializing_if = "Option::is_none")]
63    pub hash: Option<String>,
64    #[serde(default, skip_serializing_if = "Option::is_none")]
65    pub hostname: Option<String>,
66    #[serde(default, skip_serializing_if = "Option::is_none")]
67    pub href: Option<String>,
68    #[serde(default, skip_serializing_if = "Option::is_none")]
69    pub origin: Option<String>,
70    #[serde(default, skip_serializing_if = "Option::is_none")]
71    pub port: Option<String>,
72    #[serde(default, skip_serializing_if = "Option::is_none")]
73    pub protocol: Option<String>,
74    #[serde(default, skip_serializing_if = "Option::is_none")]
75    pub search: Option<String>,
76}
77
78impl Default for RouteLocation {
79    fn default() -> Self {
80        Self::new("/")
81    }
82}
83
84impl RouteLocation {
85    pub fn new(pathname: impl Into<String>) -> Self {
86        Self {
87            pathname: pathname.into(),
88            host: None,
89            hash: None,
90            hostname: None,
91            href: None,
92            origin: None,
93            port: None,
94            protocol: None,
95            search: None,
96        }
97    }
98}
99
100// Static environment data (Theme, I18n)
101#[derive(Clone)]
102pub struct Env {
103    pub theme: Theme,
104    /// Current light/dark appearance reported by the host platform.
105    ///
106    /// Applications that offer a "System" preference can select their generated
107    /// design-system theme from this value during environment synchronization.
108    pub system_theme_mode: DesignMode,
109    pub i18n: I18nRegistry,
110    pub locale: Locale,
111    pub window: WindowEnv,
112    pub current_route: RouteLocation,
113    pub window_insets: WindowInsets,
114    pub viewport_size: LayoutSize,
115    pub measurer: Option<Arc<dyn fission_layout::TextMeasurer>>,
116}
117
118impl Default for Env {
119    fn default() -> Self {
120        Self {
121            theme: Theme::default(),
122            system_theme_mode: DesignMode::Light,
123            i18n: I18nRegistry::new(),
124            locale: Locale::default(),
125            window: WindowEnv::default(),
126            current_route: RouteLocation::default(),
127            window_insets: WindowInsets::default(),
128            viewport_size: LayoutSize::default(),
129            measurer: None,
130        }
131    }
132}
133
134impl std::fmt::Debug for Env {
135    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
136        f.debug_struct("Env")
137            .field("theme", &self.theme)
138            .field("system_theme_mode", &self.system_theme_mode)
139            .field("locale", &self.locale)
140            .field("window", &self.window)
141            .field("current_route", &self.current_route)
142            .field("window_insets", &self.window_insets)
143            .field("viewport_size", &self.viewport_size)
144            .finish()
145    }
146}
147
148impl Env {
149    pub fn new(measurer: Arc<dyn fission_layout::TextMeasurer>) -> Self {
150        Self {
151            theme: Theme::default(),
152            system_theme_mode: DesignMode::Light,
153            i18n: I18nRegistry::new(),
154            locale: Locale::default(),
155            window: WindowEnv::default(),
156            current_route: RouteLocation::default(),
157            window_insets: WindowInsets::default(),
158            viewport_size: LayoutSize::default(),
159            measurer: Some(measurer),
160        }
161    }
162}
163
164pub trait Clipboard: Send + Sync {
165    fn get_text(&self) -> Option<String>;
166    fn set_text(&self, text: &str);
167}
168
169pub trait ImeHandler: Send + Sync {
170    fn set_ime_allowed(&self, allowed: bool);
171    fn set_ime_cursor_area(&self, rect: fission_layout::LayoutRect);
172}
173
174// Runtime state managed by framework (Interaction)
175#[derive(Clone, Debug, Default)]
176pub struct RuntimeState {
177    pub local_widget_state: LocalStateStore,
178    pub scroll: ScrollStateMap,
179    pub video: VideoStateMap,
180    pub web: WebStateMap,
181    pub motion: MotionStateMap,
182    pub interaction: InteractionStateMap,
183    pub text_edit: TextEditStateMap,
184    pub selectable_text: SelectableTextStateMap,
185    pub context_menu: ContextMenuState,
186    pub clipboard: String,
187    pub caret_visible: HashMap<WidgetId, bool>,
188    pub gesture: GestureState,
189    pub hero: HeroState,
190}
191
192#[derive(Clone, Debug, Default)]
193pub struct HeroState {
194    // tag -> (Last Known WidgetId, Last Known Rect)
195    pub positions: HashMap<String, (WidgetId, fission_layout::LayoutRect)>,
196}
197
198#[derive(Clone, Debug, Default)]
199pub struct GestureState {
200    pub start_point: Option<LayoutPoint>,
201    pub last_point: Option<LayoutPoint>,
202    pub is_panning: bool,
203    pub target_node: Option<WidgetId>,
204    pub dragging_payload: Option<Vec<u8>>,
205    pub pressed_button: Option<crate::event::PointerButton>,
206    pub scrollbar_drag: Option<crate::scrollbar::ScrollbarDragState>,
207    /// Runtime drag state used by widgets to render previews and hovered
208    /// drop-target feedback during the current frame.
209    pub drag_session: Option<DragSessionState>,
210}
211
212/// Payload currently carried by a drag session.
213#[derive(Clone, Debug, PartialEq, Eq)]
214pub enum DragSessionPayload {
215    /// Opaque bytes from an in-app drag source.
216    Internal(Vec<u8>),
217    /// Files supplied by the host platform during an external drag.
218    ExternalFiles(Vec<String>),
219}
220
221impl DragSessionPayload {
222    /// Human-readable payload family used by demos and diagnostics.
223    pub fn kind(&self) -> &'static str {
224        match self {
225            Self::Internal(_) => "internal",
226            Self::ExternalFiles(_) => "files",
227        }
228    }
229}
230
231/// Runtime-only state for a drag gesture currently in progress.
232#[derive(Clone, Debug, PartialEq)]
233pub struct DragSessionState {
234    /// Semantics node that started the drag, when this is an internal drag.
235    pub source_node: Option<WidgetId>,
236    /// Stable source identifier, if supplied by the drag source widget.
237    pub source_identifier: Option<String>,
238    /// Payload carried by the drag.
239    pub payload: DragSessionPayload,
240    /// Pointer position in layout coordinates.
241    pub point: LayoutPoint,
242    /// Target currently under the pointer that advertises a drop action.
243    pub target_node: Option<WidgetId>,
244    /// Stable target identifier, if supplied by the drop target widget.
245    pub target_identifier: Option<String>,
246}
247
248#[derive(Clone, Debug, Default)]
249pub struct ScrollStateMap {
250    pub offsets: HashMap<WidgetId, f32>,
251}
252
253impl ScrollStateMap {
254    pub fn get_offset(&self, id: WidgetId) -> f32 {
255        *self.offsets.get(&id).unwrap_or(&0.0)
256    }
257
258    pub fn set_offset(&mut self, id: WidgetId, offset: f32) {
259        self.offsets.insert(id, offset);
260    }
261
262    pub fn retain_active(&mut self, active: &std::collections::HashSet<WidgetId>) {
263        self.offsets.retain(|id, _| active.contains(id));
264    }
265}
266
267#[derive(Clone, Debug, Default)]
268pub struct ContextMenuState {
269    pub owner: Option<WidgetId>,
270    pub anchor: Option<LayoutPoint>,
271}
272
273impl ContextMenuState {
274    pub fn open(&mut self, owner: WidgetId, anchor: LayoutPoint) {
275        self.owner = Some(owner);
276        self.anchor = Some(anchor);
277    }
278
279    pub fn close(&mut self) {
280        self.owner = None;
281        self.anchor = None;
282    }
283}
284
285#[derive(Clone, Debug, Default)]
286pub struct SelectableTextStateMap {
287    pub states: HashMap<WidgetId, SelectableTextState>,
288}
289
290impl SelectableTextStateMap {
291    pub fn get(&self, id: WidgetId) -> Option<&SelectableTextState> {
292        self.states.get(&id)
293    }
294
295    pub fn get_mut_or_default(&mut self, id: WidgetId) -> &mut SelectableTextState {
296        self.states.entry(id).or_default()
297    }
298
299    pub fn selection_range(&self, id: WidgetId) -> Option<(usize, usize)> {
300        self.states
301            .get(&id)
302            .and_then(SelectableTextState::selection_range)
303    }
304}
305
306#[derive(Clone, Debug, Default)]
307pub struct SelectableTextState {
308    pub anchor: usize,
309    pub caret: usize,
310    pub selecting: bool,
311}
312
313impl SelectableTextState {
314    pub fn selection_range(&self) -> Option<(usize, usize)> {
315        if self.anchor == self.caret {
316            None
317        } else {
318            Some((self.anchor, self.caret))
319        }
320    }
321}
322
323#[derive(Clone, Debug, Default)]
324pub struct TextEditStateMap {
325    pub states: HashMap<WidgetId, TextEditState>,
326    pub restoration: HashMap<String, TextRestorationSnapshot>,
327}
328
329#[derive(Clone, Debug)]
330pub struct TextEditState {
331    pub buffer: TextBuffer,
332    pub caret: usize,  // byte index into value
333    pub anchor: usize, // selection anchor; if equal to caret then no selection
334    pub history: TextEditHistory,
335    pub preedit: Option<TextPreeditState>,
336    pub pending_model_sync: bool, // True when edits are newer than the currently lowered semantics value
337    /// Last semantic model value observed for this input.
338    ///
339    /// While a local edit is pending, this lets the input distinguish "the app
340    /// has not observed the edit yet" from "the app observed it and produced a
341    /// transformed value".
342    pub last_model_text: String,
343    /// Last cursor position that was dispatched as a CursorChanged action.
344    /// Used to deduplicate dispatches and prevent unnecessary model updates
345    /// that could cause extra rebuild cycles.
346    pub last_dispatched_cursor: Option<(usize, usize)>,
347    pub affordances: TextInputAffordanceState,
348}
349
350impl Default for TextEditState {
351    fn default() -> Self {
352        Self {
353            buffer: TextBuffer::new(),
354            caret: 0,
355            anchor: 0,
356            history: TextEditHistory::default(),
357            preedit: None,
358            pending_model_sync: false,
359            last_model_text: String::new(),
360            last_dispatched_cursor: None,
361            affordances: TextInputAffordanceState::default(),
362        }
363    }
364}
365
366#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
367pub enum TextSelectionHandleKind {
368    #[default]
369    Caret,
370    Start,
371    End,
372}
373
374#[derive(Clone, Debug, Default)]
375pub struct TextInputAffordanceState {
376    pub toolbar_visible: bool,
377    pub toolbar_anchor: Option<LayoutPoint>,
378    pub caret_handle: Option<LayoutPoint>,
379    pub selection_start_handle: Option<LayoutPoint>,
380    pub selection_end_handle: Option<LayoutPoint>,
381    pub active_handle: Option<TextSelectionHandleKind>,
382    pub magnifier_visible: bool,
383    pub magnifier_anchor: Option<LayoutPoint>,
384}
385
386#[derive(Clone, Debug)]
387pub struct TextPreeditState {
388    pub text: String,
389    pub range: (usize, usize),
390    pub cursor: Option<(usize, usize)>,
391}
392
393#[derive(Clone, Debug)]
394pub struct TextHistoryEntry {
395    pub transaction: EditTransaction,
396    pub before_caret: usize,
397    pub before_anchor: usize,
398    pub after_caret: usize,
399    pub after_anchor: usize,
400}
401
402#[derive(Clone, Debug)]
403pub struct TextRestorationSnapshot {
404    pub value: String,
405    pub caret: usize,
406    pub anchor: usize,
407}
408
409#[derive(Clone, Debug)]
410pub struct TextEditHistory {
411    pub undo_stack: Vec<TextHistoryEntry>,
412    pub redo_stack: Vec<TextHistoryEntry>,
413    pub capacity: usize, // Max undo steps
414}
415
416impl Default for TextEditHistory {
417    fn default() -> Self {
418        Self {
419            undo_stack: Vec::new(),
420            redo_stack: Vec::new(),
421            capacity: 100,
422        }
423    }
424}
425
426impl TextEditHistory {
427    pub fn record(&mut self, entry: TextHistoryEntry) {
428        self.undo_stack.push(entry);
429        if self.undo_stack.len() > self.capacity {
430            let overflow = self.undo_stack.len() - self.capacity;
431            self.undo_stack.drain(0..overflow);
432        }
433        self.redo_stack.clear();
434    }
435
436    pub fn undo(&mut self, buffer: &mut TextBuffer) -> Option<(usize, usize)> {
437        let entry = self.undo_stack.pop()?;
438        apply_transaction(buffer, &entry.transaction.inverse());
439        let caret = entry.before_caret;
440        let anchor = entry.before_anchor;
441        self.redo_stack.push(entry);
442        Some((caret, anchor))
443    }
444
445    pub fn redo(&mut self, buffer: &mut TextBuffer) -> Option<(usize, usize)> {
446        let entry = self.redo_stack.pop()?;
447        apply_transaction(buffer, &entry.transaction);
448        let caret = entry.after_caret;
449        let anchor = entry.after_anchor;
450        self.undo_stack.push(entry);
451        Some((caret, anchor))
452    }
453}
454
455fn apply_transaction(buffer: &mut TextBuffer, transaction: &EditTransaction) {
456    for edit in &transaction.edits {
457        buffer.replace(edit.range.clone(), &edit.new_text);
458    }
459}
460
461impl TextEditStateMap {
462    pub fn get_mut_or_default(&mut self, id: WidgetId) -> &mut TextEditState {
463        self.states.entry(id).or_default()
464    }
465    pub fn get(&self, id: WidgetId) -> Option<&TextEditState> {
466        self.states.get(&id)
467    }
468    pub fn sync_from_runtime(
469        &mut self,
470        id: WidgetId,
471        semantic_value: &str,
472        restoration_id: Option<&str>,
473        undo_capacity: Option<usize>,
474    ) {
475        let restoration_snapshot = restoration_id.and_then(|rid| {
476            if semantic_value.is_empty() {
477                self.restoration.get(rid).cloned()
478            } else {
479                None
480            }
481        });
482        let st = self.states.entry(id).or_default();
483        st.sync_from_model(semantic_value);
484        if semantic_value.is_empty() && st.buffer.len_bytes() == 0 {
485            if let Some(snapshot) = restoration_snapshot.as_ref() {
486                st.restore_snapshot(snapshot);
487            }
488        }
489        if let Some(capacity) = undo_capacity {
490            st.set_history_capacity(capacity);
491        }
492        if let Some(rid) = restoration_id {
493            self.restoration.insert(rid.to_string(), st.snapshot());
494        }
495    }
496    pub fn persist_restoration(&mut self, id: WidgetId, restoration_id: Option<&str>) {
497        let Some(rid) = restoration_id else {
498            return;
499        };
500        if let Some(st) = self.states.get(&id) {
501            self.restoration.insert(rid.to_string(), st.snapshot());
502        }
503    }
504    pub fn set_caret(&mut self, id: WidgetId, caret: usize, anchor: Option<usize>) {
505        let st = self.states.entry(id).or_default();
506        st.caret = caret;
507        st.anchor = anchor.unwrap_or(caret);
508        st.pending_model_sync = false;
509    }
510}
511
512impl TextEditState {
513    pub fn snapshot(&self) -> TextRestorationSnapshot {
514        TextRestorationSnapshot {
515            value: self.buffer.to_string(),
516            caret: self.caret,
517            anchor: self.anchor,
518        }
519    }
520
521    pub fn restore_snapshot(&mut self, snapshot: &TextRestorationSnapshot) {
522        self.buffer = TextBuffer::from_str(&snapshot.value);
523        self.caret = snapshot.caret.min(snapshot.value.len());
524        self.anchor = snapshot.anchor.min(snapshot.value.len());
525        self.preedit = None;
526        self.pending_model_sync = false;
527        self.last_model_text = snapshot.value.clone();
528        self.last_dispatched_cursor = None;
529        self.history = TextEditHistory::default();
530    }
531
532    pub fn set_history_capacity(&mut self, capacity: usize) {
533        let capacity = capacity.max(1);
534        self.history.capacity = capacity;
535        if self.history.undo_stack.len() > capacity {
536            let overflow = self.history.undo_stack.len() - capacity;
537            self.history.undo_stack.drain(0..overflow);
538        }
539        if self.history.redo_stack.len() > capacity {
540            let overflow = self.history.redo_stack.len() - capacity;
541            self.history.redo_stack.drain(0..overflow);
542        }
543    }
544
545    pub fn committed_text(&self) -> String {
546        self.buffer.to_string()
547    }
548
549    pub fn sync_from_model(&mut self, semantic_value: &str) {
550        let buffer_text = self.buffer.to_string();
551        if self.pending_model_sync {
552            if buffer_text == semantic_value {
553                self.pending_model_sync = false;
554                self.last_model_text = semantic_value.to_string();
555                return;
556            }
557            if semantic_value == self.last_model_text {
558                return;
559            }
560
561            let selection_was_collapsed = self.caret == self.anchor;
562            self.buffer = TextBuffer::from_str(semantic_value);
563            if selection_was_collapsed {
564                self.caret = semantic_value.len();
565                self.anchor = semantic_value.len();
566            } else {
567                self.caret = self.caret.min(semantic_value.len());
568                self.anchor = self.anchor.min(semantic_value.len());
569            }
570            self.preedit = None;
571            self.history = TextEditHistory::default();
572            self.pending_model_sync = false;
573            self.last_model_text = semantic_value.to_string();
574            return;
575        }
576
577        if buffer_text != semantic_value {
578            self.buffer = TextBuffer::from_str(semantic_value);
579            self.caret = self.caret.min(semantic_value.len());
580            self.anchor = self.anchor.min(semantic_value.len());
581            self.preedit = None;
582            self.history = TextEditHistory::default();
583        }
584        self.last_model_text = semantic_value.to_string();
585    }
586
587    pub fn selection_range(&self) -> (usize, usize) {
588        if self.caret <= self.anchor {
589            (self.caret, self.anchor)
590        } else {
591            (self.anchor, self.caret)
592        }
593    }
594
595    pub fn clear_preedit(&mut self) {
596        self.preedit = None;
597    }
598
599    pub fn set_preedit(&mut self, text: String, cursor: Option<(usize, usize)>) {
600        if text.is_empty() {
601            self.preedit = None;
602            return;
603        }
604        let cursor = normalize_preedit_cursor(&text, cursor);
605
606        if let Some(preedit) = &mut self.preedit {
607            preedit.text = text;
608            preedit.cursor = cursor;
609            return;
610        }
611
612        self.preedit = Some(TextPreeditState {
613            text,
614            range: self.selection_range(),
615            cursor,
616        });
617    }
618
619    pub fn display_text(&self) -> (String, Option<(usize, usize)>) {
620        let committed = self.buffer.to_string();
621        let Some(preedit) = &self.preedit else {
622            return (committed, None);
623        };
624
625        let start = preedit.range.0.min(committed.len());
626        let end = preedit.range.1.min(committed.len());
627
628        let mut display = String::with_capacity(
629            committed.len() - (end.saturating_sub(start)) + preedit.text.len(),
630        );
631        display.push_str(&committed[..start]);
632        display.push_str(&preedit.text);
633        display.push_str(&committed[end..]);
634        (display, Some((start, start + preedit.text.len())))
635    }
636
637    pub fn display_preedit_cursor_range(&self) -> Option<(usize, usize)> {
638        let preedit = self.preedit.as_ref()?;
639        let cursor = preedit.cursor?;
640        let start = preedit.range.0.min(self.buffer.len_bytes());
641        Some((start + cursor.0, start + cursor.1))
642    }
643
644    pub fn apply_edit(
645        &mut self,
646        range: std::ops::Range<usize>,
647        new_text: &str,
648        next_caret: usize,
649        next_anchor: usize,
650    ) -> String {
651        let buffer_len = self.buffer.len_bytes();
652        let start = range.start.min(buffer_len);
653        let end = range.end.min(buffer_len).max(start);
654        let range = start..end;
655        let old_text = self.buffer.slice(range.clone()).to_string();
656        let mut txn = EditTransaction::new();
657        txn.push(TextEdit::new(range, new_text, old_text));
658        apply_transaction(&mut self.buffer, &txn);
659        self.history.record(TextHistoryEntry {
660            transaction: txn,
661            before_caret: self.caret,
662            before_anchor: self.anchor,
663            after_caret: next_caret,
664            after_anchor: next_anchor,
665        });
666        self.caret = next_caret;
667        self.anchor = next_anchor;
668        self.preedit = None;
669        self.pending_model_sync = true;
670        self.buffer.to_string()
671    }
672
673    pub fn undo(&mut self) -> Option<(String, usize, usize)> {
674        let (caret, anchor) = self.history.undo(&mut self.buffer)?;
675        self.caret = caret;
676        self.anchor = anchor;
677        self.preedit = None;
678        self.pending_model_sync = true;
679        Some((self.buffer.to_string(), caret, anchor))
680    }
681
682    pub fn redo(&mut self) -> Option<(String, usize, usize)> {
683        let (caret, anchor) = self.history.redo(&mut self.buffer)?;
684        self.caret = caret;
685        self.anchor = anchor;
686        self.preedit = None;
687        self.pending_model_sync = true;
688        Some((self.buffer.to_string(), caret, anchor))
689    }
690}
691
692fn normalize_preedit_cursor(text: &str, cursor: Option<(usize, usize)>) -> Option<(usize, usize)> {
693    let (mut start, mut end) = cursor?;
694    start = start.min(text.len());
695    end = end.min(text.len());
696    if start > end {
697        std::mem::swap(&mut start, &mut end);
698    }
699    start = floor_char_boundary(text, start);
700    end = floor_char_boundary(text, end);
701    Some((start, end))
702}
703
704fn floor_char_boundary(text: &str, mut idx: usize) -> usize {
705    idx = idx.min(text.len());
706    while idx > 0 && !text.is_char_boundary(idx) {
707        idx -= 1;
708    }
709    idx
710}
711
712#[derive(Clone, Debug, Default)]
713pub struct InteractionStateMap {
714    pub hovered: HashMap<WidgetId, bool>,
715    pub hover_path: Vec<WidgetId>,
716    pub hover_rich_text_annotation: Option<HoveredRichTextAnnotation>,
717    pub pressed: HashMap<WidgetId, bool>,
718    pub focused: Option<WidgetId>,
719    pub cursor: MouseCursor,
720    pub last_down_point: Option<LayoutPoint>,
721}
722
723#[derive(Clone, Debug, PartialEq, Eq)]
724pub struct HoveredRichTextAnnotation {
725    pub node_id: WidgetId,
726    pub annotation: RichTextAnnotation,
727}
728
729impl InteractionStateMap {
730    pub fn is_hovered(&self, id: WidgetId) -> bool {
731        self.hovered.get(&id).copied().unwrap_or(false)
732    }
733    pub fn is_pressed(&self, id: WidgetId) -> bool {
734        self.pressed.get(&id).copied().unwrap_or(false)
735    }
736    pub fn is_focused(&self, id: WidgetId) -> bool {
737        self.focused == Some(id)
738    }
739
740    pub fn hovered_path(&self) -> &[WidgetId] {
741        &self.hover_path
742    }
743
744    pub fn hovered_rich_text_annotation(&self) -> Option<&HoveredRichTextAnnotation> {
745        self.hover_rich_text_annotation.as_ref()
746    }
747
748    pub fn cursor(&self) -> MouseCursor {
749        self.cursor
750    }
751
752    pub fn set_hovered(&mut self, id: WidgetId, value: bool) {
753        if value {
754            self.hovered.insert(id, true);
755        } else {
756            self.hovered.remove(&id);
757        }
758    }
759
760    pub fn set_hover_path(&mut self, path: Vec<WidgetId>) {
761        self.hover_path = path;
762    }
763
764    pub fn set_hovered_rich_text_annotation(
765        &mut self,
766        annotation: Option<HoveredRichTextAnnotation>,
767    ) {
768        self.hover_rich_text_annotation = annotation;
769    }
770
771    pub fn set_pressed(&mut self, id: WidgetId, value: bool) {
772        if value {
773            self.pressed.insert(id, true);
774        } else {
775            self.pressed.remove(&id);
776        }
777    }
778
779    pub fn set_focused(&mut self, id: Option<WidgetId>) {
780        self.focused = id;
781    }
782
783    pub fn set_cursor(&mut self, cursor: MouseCursor) {
784        self.cursor = cursor;
785    }
786}
787
788#[derive(Clone, Debug, Default)]
789pub struct VideoStateMap {
790    pub states: HashMap<WidgetId, VideoState>,
791}
792
793#[derive(Clone, Debug, Default)]
794pub struct WebState {
795    pub url: String,
796    pub user_agent: Option<String>,
797    pub loading: bool,
798    pub can_go_back: bool,
799    pub can_go_forward: bool,
800    pub title: Option<String>,
801}
802
803#[derive(Clone, Debug, Default)]
804pub struct WebStateMap {
805    pub states: HashMap<WidgetId, WebState>,
806}
807
808// Static environment data (Theme, I18n)
809
810impl GlobalState for VideoStateMap {}
811
812#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
813pub struct VideoState {
814    pub status: VideoStatus,
815    pub position_ms: u64,
816    pub duration_ms: Option<u64>,
817    pub rate: f32,
818    pub volume: f32,
819    pub muted: bool,
820    pub looped: bool,
821    pub asset_source: String,
822    pub audio: VideoAudioOptions,
823    pub surface_id: Option<u64>,
824    pub pending_seek: Option<u64>,
825}
826
827impl Default for VideoState {
828    fn default() -> Self {
829        Self {
830            status: VideoStatus::Stopped,
831            position_ms: 0,
832            duration_ms: None,
833            rate: 1.0,
834            volume: 1.0,
835            muted: false,
836            looped: false,
837            asset_source: String::new(),
838            audio: VideoAudioOptions::default(),
839            surface_id: None,
840            pending_seek: None,
841        }
842    }
843}
844
845#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
846pub enum VideoStatus {
847    Stopped,
848    Playing,
849    Paused,
850    Buffering,
851    Ended,
852    Error,
853}
854
855#[cfg(test)]
856mod tests {
857    use super::*;
858
859    #[test]
860    fn environment_exposes_a_safe_light_system_theme_default() {
861        assert_eq!(Env::default().system_theme_mode, DesignMode::Light);
862    }
863}