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 clipboard: String,
177 pub caret_visible: HashMap<WidgetId, bool>,
178 pub gesture: GestureState,
179 pub hero: HeroState,
180}
181
182#[derive(Clone, Debug, Default)]
183pub struct HeroState {
184 pub positions: HashMap<String, (WidgetId, fission_layout::LayoutRect)>,
186}
187
188#[derive(Clone, Debug, Default)]
189pub struct GestureState {
190 pub start_point: Option<LayoutPoint>,
191 pub last_point: Option<LayoutPoint>,
192 pub is_panning: bool,
193 pub target_node: Option<WidgetId>,
194 pub dragging_payload: Option<Vec<u8>>,
195 pub pressed_button: Option<crate::event::PointerButton>,
196 pub scrollbar_drag: Option<crate::scrollbar::ScrollbarDragState>,
197}
198
199#[derive(Clone, Debug, Default)]
200pub struct ScrollStateMap {
201 pub offsets: HashMap<WidgetId, f32>,
202}
203
204impl ScrollStateMap {
205 pub fn get_offset(&self, id: WidgetId) -> f32 {
206 *self.offsets.get(&id).unwrap_or(&0.0)
207 }
208
209 pub fn set_offset(&mut self, id: WidgetId, offset: f32) {
210 self.offsets.insert(id, offset);
211 }
212}
213
214#[derive(Clone, Debug, Default)]
215pub struct TextEditStateMap {
216 pub states: HashMap<WidgetId, TextEditState>,
217 pub restoration: HashMap<String, TextRestorationSnapshot>,
218}
219
220#[derive(Clone, Debug)]
221pub struct TextEditState {
222 pub buffer: TextBuffer,
223 pub caret: usize, pub anchor: usize, pub history: TextEditHistory,
226 pub preedit: Option<TextPreeditState>,
227 pub pending_model_sync: bool, pub last_dispatched_cursor: Option<(usize, usize)>,
232 pub affordances: TextInputAffordanceState,
233}
234
235impl Default for TextEditState {
236 fn default() -> Self {
237 Self {
238 buffer: TextBuffer::new(),
239 caret: 0,
240 anchor: 0,
241 history: TextEditHistory::default(),
242 preedit: None,
243 pending_model_sync: false,
244 last_dispatched_cursor: None,
245 affordances: TextInputAffordanceState::default(),
246 }
247 }
248}
249
250#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
251pub enum TextSelectionHandleKind {
252 #[default]
253 Caret,
254 Start,
255 End,
256}
257
258#[derive(Clone, Debug, Default)]
259pub struct TextInputAffordanceState {
260 pub toolbar_visible: bool,
261 pub toolbar_anchor: Option<LayoutPoint>,
262 pub caret_handle: Option<LayoutPoint>,
263 pub selection_start_handle: Option<LayoutPoint>,
264 pub selection_end_handle: Option<LayoutPoint>,
265 pub active_handle: Option<TextSelectionHandleKind>,
266 pub magnifier_visible: bool,
267 pub magnifier_anchor: Option<LayoutPoint>,
268}
269
270#[derive(Clone, Debug)]
271pub struct TextPreeditState {
272 pub text: String,
273 pub range: (usize, usize),
274 pub cursor: Option<(usize, usize)>,
275}
276
277#[derive(Clone, Debug)]
278pub struct TextHistoryEntry {
279 pub transaction: EditTransaction,
280 pub before_caret: usize,
281 pub before_anchor: usize,
282 pub after_caret: usize,
283 pub after_anchor: usize,
284}
285
286#[derive(Clone, Debug)]
287pub struct TextRestorationSnapshot {
288 pub value: String,
289 pub caret: usize,
290 pub anchor: usize,
291}
292
293#[derive(Clone, Debug)]
294pub struct TextEditHistory {
295 pub undo_stack: Vec<TextHistoryEntry>,
296 pub redo_stack: Vec<TextHistoryEntry>,
297 pub capacity: usize, }
299
300impl Default for TextEditHistory {
301 fn default() -> Self {
302 Self {
303 undo_stack: Vec::new(),
304 redo_stack: Vec::new(),
305 capacity: 100,
306 }
307 }
308}
309
310impl TextEditHistory {
311 pub fn record(&mut self, entry: TextHistoryEntry) {
312 self.undo_stack.push(entry);
313 if self.undo_stack.len() > self.capacity {
314 let overflow = self.undo_stack.len() - self.capacity;
315 self.undo_stack.drain(0..overflow);
316 }
317 self.redo_stack.clear();
318 }
319
320 pub fn undo(&mut self, buffer: &mut TextBuffer) -> Option<(usize, usize)> {
321 let entry = self.undo_stack.pop()?;
322 apply_transaction(buffer, &entry.transaction.inverse());
323 let caret = entry.before_caret;
324 let anchor = entry.before_anchor;
325 self.redo_stack.push(entry);
326 Some((caret, anchor))
327 }
328
329 pub fn redo(&mut self, buffer: &mut TextBuffer) -> Option<(usize, usize)> {
330 let entry = self.redo_stack.pop()?;
331 apply_transaction(buffer, &entry.transaction);
332 let caret = entry.after_caret;
333 let anchor = entry.after_anchor;
334 self.undo_stack.push(entry);
335 Some((caret, anchor))
336 }
337}
338
339fn apply_transaction(buffer: &mut TextBuffer, transaction: &EditTransaction) {
340 for edit in &transaction.edits {
341 buffer.replace(edit.range.clone(), &edit.new_text);
342 }
343}
344
345impl TextEditStateMap {
346 pub fn get_mut_or_default(&mut self, id: WidgetId) -> &mut TextEditState {
347 self.states.entry(id).or_default()
348 }
349 pub fn get(&self, id: WidgetId) -> Option<&TextEditState> {
350 self.states.get(&id)
351 }
352 pub fn sync_from_runtime(
353 &mut self,
354 id: WidgetId,
355 semantic_value: &str,
356 restoration_id: Option<&str>,
357 undo_capacity: Option<usize>,
358 ) {
359 let restoration_snapshot = restoration_id.and_then(|rid| {
360 if semantic_value.is_empty() {
361 self.restoration.get(rid).cloned()
362 } else {
363 None
364 }
365 });
366 let st = self.states.entry(id).or_default();
367 st.sync_from_model(semantic_value);
368 if semantic_value.is_empty() && st.buffer.len_bytes() == 0 {
369 if let Some(snapshot) = restoration_snapshot.as_ref() {
370 st.restore_snapshot(snapshot);
371 }
372 }
373 if let Some(capacity) = undo_capacity {
374 st.set_history_capacity(capacity);
375 }
376 if let Some(rid) = restoration_id {
377 self.restoration.insert(rid.to_string(), st.snapshot());
378 }
379 }
380 pub fn persist_restoration(&mut self, id: WidgetId, restoration_id: Option<&str>) {
381 let Some(rid) = restoration_id else {
382 return;
383 };
384 if let Some(st) = self.states.get(&id) {
385 self.restoration.insert(rid.to_string(), st.snapshot());
386 }
387 }
388 pub fn set_caret(&mut self, id: WidgetId, caret: usize, anchor: Option<usize>) {
389 let st = self.states.entry(id).or_default();
390 st.caret = caret;
391 st.anchor = anchor.unwrap_or(caret);
392 st.pending_model_sync = false;
393 }
394}
395
396impl TextEditState {
397 pub fn snapshot(&self) -> TextRestorationSnapshot {
398 TextRestorationSnapshot {
399 value: self.buffer.to_string(),
400 caret: self.caret,
401 anchor: self.anchor,
402 }
403 }
404
405 pub fn restore_snapshot(&mut self, snapshot: &TextRestorationSnapshot) {
406 self.buffer = TextBuffer::from_str(&snapshot.value);
407 self.caret = snapshot.caret.min(snapshot.value.len());
408 self.anchor = snapshot.anchor.min(snapshot.value.len());
409 self.preedit = None;
410 self.pending_model_sync = false;
411 self.last_dispatched_cursor = None;
412 self.history = TextEditHistory::default();
413 }
414
415 pub fn set_history_capacity(&mut self, capacity: usize) {
416 let capacity = capacity.max(1);
417 self.history.capacity = capacity;
418 if self.history.undo_stack.len() > capacity {
419 let overflow = self.history.undo_stack.len() - capacity;
420 self.history.undo_stack.drain(0..overflow);
421 }
422 if self.history.redo_stack.len() > capacity {
423 let overflow = self.history.redo_stack.len() - capacity;
424 self.history.redo_stack.drain(0..overflow);
425 }
426 }
427
428 pub fn committed_text(&self) -> String {
429 self.buffer.to_string()
430 }
431
432 pub fn sync_from_model(&mut self, semantic_value: &str) {
433 if self.pending_model_sync && self.buffer.to_string() == semantic_value {
434 self.pending_model_sync = false;
435 }
436
437 if !self.pending_model_sync && self.buffer.to_string() != semantic_value {
438 self.buffer = TextBuffer::from_str(semantic_value);
439 self.caret = self.caret.min(semantic_value.len());
440 self.anchor = self.anchor.min(semantic_value.len());
441 self.preedit = None;
442 self.history = TextEditHistory::default();
443 }
444 }
445
446 pub fn selection_range(&self) -> (usize, usize) {
447 if self.caret <= self.anchor {
448 (self.caret, self.anchor)
449 } else {
450 (self.anchor, self.caret)
451 }
452 }
453
454 pub fn clear_preedit(&mut self) {
455 self.preedit = None;
456 }
457
458 pub fn set_preedit(&mut self, text: String, cursor: Option<(usize, usize)>) {
459 if text.is_empty() {
460 self.preedit = None;
461 return;
462 }
463 let cursor = normalize_preedit_cursor(&text, cursor);
464
465 if let Some(preedit) = &mut self.preedit {
466 preedit.text = text;
467 preedit.cursor = cursor;
468 return;
469 }
470
471 self.preedit = Some(TextPreeditState {
472 text,
473 range: self.selection_range(),
474 cursor,
475 });
476 }
477
478 pub fn display_text(&self) -> (String, Option<(usize, usize)>) {
479 let committed = self.buffer.to_string();
480 let Some(preedit) = &self.preedit else {
481 return (committed, None);
482 };
483
484 let start = preedit.range.0.min(committed.len());
485 let end = preedit.range.1.min(committed.len());
486
487 let mut display = String::with_capacity(
488 committed.len() - (end.saturating_sub(start)) + preedit.text.len(),
489 );
490 display.push_str(&committed[..start]);
491 display.push_str(&preedit.text);
492 display.push_str(&committed[end..]);
493 (display, Some((start, start + preedit.text.len())))
494 }
495
496 pub fn display_preedit_cursor_range(&self) -> Option<(usize, usize)> {
497 let preedit = self.preedit.as_ref()?;
498 let cursor = preedit.cursor?;
499 let start = preedit.range.0.min(self.buffer.len_bytes());
500 Some((start + cursor.0, start + cursor.1))
501 }
502
503 pub fn apply_edit(
504 &mut self,
505 range: std::ops::Range<usize>,
506 new_text: &str,
507 next_caret: usize,
508 next_anchor: usize,
509 ) -> String {
510 let buffer_len = self.buffer.len_bytes();
511 let start = range.start.min(buffer_len);
512 let end = range.end.min(buffer_len).max(start);
513 let range = start..end;
514 let old_text = self.buffer.slice(range.clone()).to_string();
515 let mut txn = EditTransaction::new();
516 txn.push(TextEdit::new(range, new_text, old_text));
517 apply_transaction(&mut self.buffer, &txn);
518 self.history.record(TextHistoryEntry {
519 transaction: txn,
520 before_caret: self.caret,
521 before_anchor: self.anchor,
522 after_caret: next_caret,
523 after_anchor: next_anchor,
524 });
525 self.caret = next_caret;
526 self.anchor = next_anchor;
527 self.preedit = None;
528 self.pending_model_sync = true;
529 self.buffer.to_string()
530 }
531
532 pub fn undo(&mut self) -> Option<(String, usize, usize)> {
533 let (caret, anchor) = self.history.undo(&mut self.buffer)?;
534 self.caret = caret;
535 self.anchor = anchor;
536 self.preedit = None;
537 self.pending_model_sync = true;
538 Some((self.buffer.to_string(), caret, anchor))
539 }
540
541 pub fn redo(&mut self) -> Option<(String, usize, usize)> {
542 let (caret, anchor) = self.history.redo(&mut self.buffer)?;
543 self.caret = caret;
544 self.anchor = anchor;
545 self.preedit = None;
546 self.pending_model_sync = true;
547 Some((self.buffer.to_string(), caret, anchor))
548 }
549}
550
551fn normalize_preedit_cursor(text: &str, cursor: Option<(usize, usize)>) -> Option<(usize, usize)> {
552 let (mut start, mut end) = cursor?;
553 start = start.min(text.len());
554 end = end.min(text.len());
555 if start > end {
556 std::mem::swap(&mut start, &mut end);
557 }
558 start = floor_char_boundary(text, start);
559 end = floor_char_boundary(text, end);
560 Some((start, end))
561}
562
563fn floor_char_boundary(text: &str, mut idx: usize) -> usize {
564 idx = idx.min(text.len());
565 while idx > 0 && !text.is_char_boundary(idx) {
566 idx -= 1;
567 }
568 idx
569}
570
571#[derive(Clone, Debug, Default)]
572pub struct InteractionStateMap {
573 pub hovered: HashMap<WidgetId, bool>,
574 pub hover_path: Vec<WidgetId>,
575 pub hover_rich_text_annotation: Option<HoveredRichTextAnnotation>,
576 pub pressed: HashMap<WidgetId, bool>,
577 pub focused: Option<WidgetId>,
578 pub cursor: MouseCursor,
579 pub last_down_point: Option<LayoutPoint>,
580}
581
582#[derive(Clone, Debug, PartialEq, Eq)]
583pub struct HoveredRichTextAnnotation {
584 pub node_id: WidgetId,
585 pub annotation: RichTextAnnotation,
586}
587
588impl InteractionStateMap {
589 pub fn is_hovered(&self, id: WidgetId) -> bool {
590 self.hovered.get(&id).copied().unwrap_or(false)
591 }
592 pub fn is_pressed(&self, id: WidgetId) -> bool {
593 self.pressed.get(&id).copied().unwrap_or(false)
594 }
595 pub fn is_focused(&self, id: WidgetId) -> bool {
596 self.focused == Some(id)
597 }
598
599 pub fn hovered_path(&self) -> &[WidgetId] {
600 &self.hover_path
601 }
602
603 pub fn hovered_rich_text_annotation(&self) -> Option<&HoveredRichTextAnnotation> {
604 self.hover_rich_text_annotation.as_ref()
605 }
606
607 pub fn cursor(&self) -> MouseCursor {
608 self.cursor
609 }
610
611 pub fn set_hovered(&mut self, id: WidgetId, value: bool) {
612 if value {
613 self.hovered.insert(id, true);
614 } else {
615 self.hovered.remove(&id);
616 }
617 }
618
619 pub fn set_hover_path(&mut self, path: Vec<WidgetId>) {
620 self.hover_path = path;
621 }
622
623 pub fn set_hovered_rich_text_annotation(
624 &mut self,
625 annotation: Option<HoveredRichTextAnnotation>,
626 ) {
627 self.hover_rich_text_annotation = annotation;
628 }
629
630 pub fn set_pressed(&mut self, id: WidgetId, value: bool) {
631 if value {
632 self.pressed.insert(id, true);
633 } else {
634 self.pressed.remove(&id);
635 }
636 }
637
638 pub fn set_focused(&mut self, id: Option<WidgetId>) {
639 self.focused = id;
640 }
641
642 pub fn set_cursor(&mut self, cursor: MouseCursor) {
643 self.cursor = cursor;
644 }
645}
646
647#[derive(Clone, Debug, Default)]
648pub struct VideoStateMap {
649 pub states: HashMap<WidgetId, VideoState>,
650}
651
652#[derive(Clone, Debug, Default)]
653pub struct WebState {
654 pub url: String,
655 pub user_agent: Option<String>,
656 pub loading: bool,
657 pub can_go_back: bool,
658 pub can_go_forward: bool,
659 pub title: Option<String>,
660}
661
662#[derive(Clone, Debug, Default)]
663pub struct WebStateMap {
664 pub states: HashMap<WidgetId, WebState>,
665}
666
667impl GlobalState for VideoStateMap {}
670
671#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
672pub struct VideoState {
673 pub status: VideoStatus,
674 pub position_ms: u64,
675 pub duration_ms: Option<u64>,
676 pub rate: f32,
677 pub volume: f32,
678 pub muted: bool,
679 pub looped: bool,
680 pub asset_source: String,
681 pub audio: VideoAudioOptions,
682 pub surface_id: Option<u64>,
683 pub pending_seek: Option<u64>,
684}
685
686impl Default for VideoState {
687 fn default() -> Self {
688 Self {
689 status: VideoStatus::Stopped,
690 position_ms: 0,
691 duration_ms: None,
692 rate: 1.0,
693 volume: 1.0,
694 muted: false,
695 looped: false,
696 asset_source: String::new(),
697 audio: VideoAudioOptions::default(),
698 surface_id: None,
699 pending_seek: None,
700 }
701 }
702}
703
704#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
705pub enum VideoStatus {
706 Stopped,
707 Playing,
708 Paused,
709 Buffering,
710 Ended,
711 Error,
712}