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 }
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#[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#[derive(Clone)]
102pub struct Env {
103 pub theme: Theme,
104 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#[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 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 pub drag_session: Option<DragSessionState>,
210}
211
212#[derive(Clone, Debug, PartialEq, Eq)]
214pub enum DragSessionPayload {
215 Internal(Vec<u8>),
217 ExternalFiles(Vec<String>),
219}
220
221impl DragSessionPayload {
222 pub fn kind(&self) -> &'static str {
224 match self {
225 Self::Internal(_) => "internal",
226 Self::ExternalFiles(_) => "files",
227 }
228 }
229}
230
231#[derive(Clone, Debug, PartialEq)]
233pub struct DragSessionState {
234 pub source_node: Option<WidgetId>,
236 pub source_identifier: Option<String>,
238 pub payload: DragSessionPayload,
240 pub point: LayoutPoint,
242 pub target_node: Option<WidgetId>,
244 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, pub anchor: usize, pub history: TextEditHistory,
335 pub preedit: Option<TextPreeditState>,
336 pub pending_model_sync: bool, pub last_model_text: String,
343 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, }
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
808impl 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}