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 }
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 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#[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 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 pub drag_session: Option<DragSessionState>,
202}
203
204#[derive(Clone, Debug, PartialEq, Eq)]
206pub enum DragSessionPayload {
207 Internal(Vec<u8>),
209 ExternalFiles(Vec<String>),
211}
212
213impl DragSessionPayload {
214 pub fn kind(&self) -> &'static str {
216 match self {
217 Self::Internal(_) => "internal",
218 Self::ExternalFiles(_) => "files",
219 }
220 }
221}
222
223#[derive(Clone, Debug, PartialEq)]
225pub struct DragSessionState {
226 pub source_node: Option<WidgetId>,
228 pub source_identifier: Option<String>,
230 pub payload: DragSessionPayload,
232 pub point: LayoutPoint,
234 pub target_node: Option<WidgetId>,
236 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, pub anchor: usize, pub history: TextEditHistory,
327 pub preedit: Option<TextPreeditState>,
328 pub pending_model_sync: bool, pub last_model_text: String,
335 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, }
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
800impl 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}