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