1use crate::{action::GlobalState, motion::MotionStateMap, state::LocalStateStore};
2use fission_i18n::{I18nRegistry, Locale};
3use fission_ir::op::RichTextAnnotation;
4use fission_ir::semantics::MouseCursor;
5use fission_ir::WidgetId;
6use fission_layout::{LayoutPoint, LayoutSize};
7use fission_text_engine::{EditTransaction, TextBuffer, TextEdit};
8use fission_theme::Theme;
9use serde::{Deserialize, Serialize};
10use std::collections::HashMap;
11use std::sync::Arc;
12
13#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
14pub struct WindowInsets {
15 pub top: f32,
16 pub bottom: f32,
17 pub left: f32,
18 pub right: f32,
19}
20
21#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
22pub enum WindowTitle {
23 Plain(String),
24 }
26
27impl Default for WindowTitle {
28 fn default() -> Self {
29 Self::Plain("Fission".into())
30 }
31}
32
33impl WindowTitle {
34 pub fn plain(title: impl Into<String>) -> Self {
35 Self::Plain(title.into())
36 }
37
38 pub fn plain_text(&self) -> &str {
39 match self {
40 Self::Plain(title) => title,
41 }
42 }
43}
44
45#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq, Eq)]
46pub struct WindowEnv {
47 pub title: WindowTitle,
48}
49
50#[derive(Clone, Debug, Serialize, Deserialize, PartialEq, Eq)]
56pub struct RouteLocation {
57 pub pathname: String,
58 #[serde(default, skip_serializing_if = "Option::is_none")]
59 pub host: Option<String>,
60 #[serde(default, skip_serializing_if = "Option::is_none")]
61 pub hash: Option<String>,
62 #[serde(default, skip_serializing_if = "Option::is_none")]
63 pub hostname: Option<String>,
64 #[serde(default, skip_serializing_if = "Option::is_none")]
65 pub href: Option<String>,
66 #[serde(default, skip_serializing_if = "Option::is_none")]
67 pub origin: Option<String>,
68 #[serde(default, skip_serializing_if = "Option::is_none")]
69 pub port: Option<String>,
70 #[serde(default, skip_serializing_if = "Option::is_none")]
71 pub protocol: Option<String>,
72 #[serde(default, skip_serializing_if = "Option::is_none")]
73 pub search: Option<String>,
74}
75
76impl Default for RouteLocation {
77 fn default() -> Self {
78 Self::new("/")
79 }
80}
81
82impl RouteLocation {
83 pub fn new(pathname: impl Into<String>) -> Self {
84 Self {
85 pathname: pathname.into(),
86 host: None,
87 hash: None,
88 hostname: None,
89 href: None,
90 origin: None,
91 port: None,
92 protocol: None,
93 search: None,
94 }
95 }
96}
97
98#[derive(Clone)]
100pub struct Env {
101 pub theme: Theme,
102 pub i18n: I18nRegistry,
103 pub locale: Locale,
104 pub window: WindowEnv,
105 pub current_route: RouteLocation,
106 pub window_insets: WindowInsets,
107 pub viewport_size: LayoutSize,
108 pub measurer: Option<Arc<dyn fission_layout::TextMeasurer>>,
109}
110
111impl Default for Env {
112 fn default() -> Self {
113 Self {
114 theme: Theme::default(),
115 i18n: I18nRegistry::new(),
116 locale: Locale::default(),
117 window: WindowEnv::default(),
118 current_route: RouteLocation::default(),
119 window_insets: WindowInsets::default(),
120 viewport_size: LayoutSize::default(),
121 measurer: None,
122 }
123 }
124}
125
126impl std::fmt::Debug for Env {
127 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
128 f.debug_struct("Env")
129 .field("theme", &self.theme)
130 .field("locale", &self.locale)
131 .field("window", &self.window)
132 .field("current_route", &self.current_route)
133 .field("window_insets", &self.window_insets)
134 .field("viewport_size", &self.viewport_size)
135 .finish()
136 }
137}
138
139impl Env {
140 pub fn new(measurer: Arc<dyn fission_layout::TextMeasurer>) -> Self {
141 Self {
142 theme: Theme::default(),
143 i18n: I18nRegistry::new(),
144 locale: Locale::default(),
145 window: WindowEnv::default(),
146 current_route: RouteLocation::default(),
147 window_insets: WindowInsets::default(),
148 viewport_size: LayoutSize::default(),
149 measurer: Some(measurer),
150 }
151 }
152}
153
154pub trait Clipboard: Send + Sync {
155 fn get_text(&self) -> Option<String>;
156 fn set_text(&self, text: &str);
157}
158
159pub trait ImeHandler: Send + Sync {
160 fn set_ime_allowed(&self, allowed: bool);
161 fn set_ime_cursor_area(&self, rect: fission_layout::LayoutRect);
162}
163
164#[derive(Clone, Debug, Default)]
166pub struct RuntimeState {
167 pub local_widget_state: LocalStateStore,
168 pub scroll: ScrollStateMap,
169 pub video: VideoStateMap,
170 pub web: WebStateMap,
171 pub motion: MotionStateMap,
172 pub interaction: InteractionStateMap,
173 pub text_edit: TextEditStateMap,
174 pub clipboard: String,
175 pub caret_visible: HashMap<WidgetId, bool>,
176 pub gesture: GestureState,
177 pub hero: HeroState,
178}
179
180#[derive(Clone, Debug, Default)]
181pub struct HeroState {
182 pub positions: HashMap<String, (WidgetId, fission_layout::LayoutRect)>,
184}
185
186#[derive(Clone, Debug, Default)]
187pub struct GestureState {
188 pub start_point: Option<LayoutPoint>,
189 pub last_point: Option<LayoutPoint>,
190 pub is_panning: bool,
191 pub target_node: Option<WidgetId>,
192 pub dragging_payload: Option<Vec<u8>>,
193 pub pressed_button: Option<crate::event::PointerButton>,
194 pub scrollbar_drag: Option<crate::scrollbar::ScrollbarDragState>,
195}
196
197#[derive(Clone, Debug, Default)]
198pub struct ScrollStateMap {
199 pub offsets: HashMap<WidgetId, f32>,
200}
201
202impl ScrollStateMap {
203 pub fn get_offset(&self, id: WidgetId) -> f32 {
204 *self.offsets.get(&id).unwrap_or(&0.0)
205 }
206
207 pub fn set_offset(&mut self, id: WidgetId, offset: f32) {
208 self.offsets.insert(id, offset);
209 }
210}
211
212#[derive(Clone, Debug, Default)]
213pub struct TextEditStateMap {
214 pub states: HashMap<WidgetId, TextEditState>,
215 pub restoration: HashMap<String, TextRestorationSnapshot>,
216}
217
218#[derive(Clone, Debug)]
219pub struct TextEditState {
220 pub buffer: TextBuffer,
221 pub caret: usize, pub anchor: usize, pub history: TextEditHistory,
224 pub preedit: Option<TextPreeditState>,
225 pub pending_model_sync: bool, pub last_dispatched_cursor: Option<(usize, usize)>,
230 pub affordances: TextInputAffordanceState,
231}
232
233impl Default for TextEditState {
234 fn default() -> Self {
235 Self {
236 buffer: TextBuffer::new(),
237 caret: 0,
238 anchor: 0,
239 history: TextEditHistory::default(),
240 preedit: None,
241 pending_model_sync: false,
242 last_dispatched_cursor: None,
243 affordances: TextInputAffordanceState::default(),
244 }
245 }
246}
247
248#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
249pub enum TextSelectionHandleKind {
250 #[default]
251 Caret,
252 Start,
253 End,
254}
255
256#[derive(Clone, Debug, Default)]
257pub struct TextInputAffordanceState {
258 pub toolbar_visible: bool,
259 pub toolbar_anchor: Option<LayoutPoint>,
260 pub caret_handle: Option<LayoutPoint>,
261 pub selection_start_handle: Option<LayoutPoint>,
262 pub selection_end_handle: Option<LayoutPoint>,
263 pub active_handle: Option<TextSelectionHandleKind>,
264 pub magnifier_visible: bool,
265 pub magnifier_anchor: Option<LayoutPoint>,
266}
267
268#[derive(Clone, Debug)]
269pub struct TextPreeditState {
270 pub text: String,
271 pub range: (usize, usize),
272 pub cursor: Option<(usize, usize)>,
273}
274
275#[derive(Clone, Debug)]
276pub struct TextHistoryEntry {
277 pub transaction: EditTransaction,
278 pub before_caret: usize,
279 pub before_anchor: usize,
280 pub after_caret: usize,
281 pub after_anchor: usize,
282}
283
284#[derive(Clone, Debug)]
285pub struct TextRestorationSnapshot {
286 pub value: String,
287 pub caret: usize,
288 pub anchor: usize,
289}
290
291#[derive(Clone, Debug)]
292pub struct TextEditHistory {
293 pub undo_stack: Vec<TextHistoryEntry>,
294 pub redo_stack: Vec<TextHistoryEntry>,
295 pub capacity: usize, }
297
298impl Default for TextEditHistory {
299 fn default() -> Self {
300 Self {
301 undo_stack: Vec::new(),
302 redo_stack: Vec::new(),
303 capacity: 100,
304 }
305 }
306}
307
308impl TextEditHistory {
309 pub fn record(&mut self, entry: TextHistoryEntry) {
310 self.undo_stack.push(entry);
311 if self.undo_stack.len() > self.capacity {
312 let overflow = self.undo_stack.len() - self.capacity;
313 self.undo_stack.drain(0..overflow);
314 }
315 self.redo_stack.clear();
316 }
317
318 pub fn undo(&mut self, buffer: &mut TextBuffer) -> Option<(usize, usize)> {
319 let entry = self.undo_stack.pop()?;
320 apply_transaction(buffer, &entry.transaction.inverse());
321 let caret = entry.before_caret;
322 let anchor = entry.before_anchor;
323 self.redo_stack.push(entry);
324 Some((caret, anchor))
325 }
326
327 pub fn redo(&mut self, buffer: &mut TextBuffer) -> Option<(usize, usize)> {
328 let entry = self.redo_stack.pop()?;
329 apply_transaction(buffer, &entry.transaction);
330 let caret = entry.after_caret;
331 let anchor = entry.after_anchor;
332 self.undo_stack.push(entry);
333 Some((caret, anchor))
334 }
335}
336
337fn apply_transaction(buffer: &mut TextBuffer, transaction: &EditTransaction) {
338 for edit in &transaction.edits {
339 buffer.replace(edit.range.clone(), &edit.new_text);
340 }
341}
342
343impl TextEditStateMap {
344 pub fn get_mut_or_default(&mut self, id: WidgetId) -> &mut TextEditState {
345 self.states.entry(id).or_default()
346 }
347 pub fn get(&self, id: WidgetId) -> Option<&TextEditState> {
348 self.states.get(&id)
349 }
350 pub fn sync_from_runtime(
351 &mut self,
352 id: WidgetId,
353 semantic_value: &str,
354 restoration_id: Option<&str>,
355 undo_capacity: Option<usize>,
356 ) {
357 let restoration_snapshot = restoration_id.and_then(|rid| {
358 if semantic_value.is_empty() {
359 self.restoration.get(rid).cloned()
360 } else {
361 None
362 }
363 });
364 let st = self.states.entry(id).or_default();
365 st.sync_from_model(semantic_value);
366 if semantic_value.is_empty() && st.buffer.len_bytes() == 0 {
367 if let Some(snapshot) = restoration_snapshot.as_ref() {
368 st.restore_snapshot(snapshot);
369 }
370 }
371 if let Some(capacity) = undo_capacity {
372 st.set_history_capacity(capacity);
373 }
374 if let Some(rid) = restoration_id {
375 self.restoration.insert(rid.to_string(), st.snapshot());
376 }
377 }
378 pub fn persist_restoration(&mut self, id: WidgetId, restoration_id: Option<&str>) {
379 let Some(rid) = restoration_id else {
380 return;
381 };
382 if let Some(st) = self.states.get(&id) {
383 self.restoration.insert(rid.to_string(), st.snapshot());
384 }
385 }
386 pub fn set_caret(&mut self, id: WidgetId, caret: usize, anchor: Option<usize>) {
387 let st = self.states.entry(id).or_default();
388 st.caret = caret;
389 st.anchor = anchor.unwrap_or(caret);
390 st.pending_model_sync = false;
391 }
392}
393
394impl TextEditState {
395 pub fn snapshot(&self) -> TextRestorationSnapshot {
396 TextRestorationSnapshot {
397 value: self.buffer.to_string(),
398 caret: self.caret,
399 anchor: self.anchor,
400 }
401 }
402
403 pub fn restore_snapshot(&mut self, snapshot: &TextRestorationSnapshot) {
404 self.buffer = TextBuffer::from_str(&snapshot.value);
405 self.caret = snapshot.caret.min(snapshot.value.len());
406 self.anchor = snapshot.anchor.min(snapshot.value.len());
407 self.preedit = None;
408 self.pending_model_sync = false;
409 self.last_dispatched_cursor = None;
410 self.history = TextEditHistory::default();
411 }
412
413 pub fn set_history_capacity(&mut self, capacity: usize) {
414 let capacity = capacity.max(1);
415 self.history.capacity = capacity;
416 if self.history.undo_stack.len() > capacity {
417 let overflow = self.history.undo_stack.len() - capacity;
418 self.history.undo_stack.drain(0..overflow);
419 }
420 if self.history.redo_stack.len() > capacity {
421 let overflow = self.history.redo_stack.len() - capacity;
422 self.history.redo_stack.drain(0..overflow);
423 }
424 }
425
426 pub fn committed_text(&self) -> String {
427 self.buffer.to_string()
428 }
429
430 pub fn sync_from_model(&mut self, semantic_value: &str) {
431 if self.pending_model_sync && self.buffer.to_string() == semantic_value {
432 self.pending_model_sync = false;
433 }
434
435 if !self.pending_model_sync && self.buffer.to_string() != semantic_value {
436 self.buffer = TextBuffer::from_str(semantic_value);
437 self.caret = self.caret.min(semantic_value.len());
438 self.anchor = self.anchor.min(semantic_value.len());
439 self.preedit = None;
440 self.history = TextEditHistory::default();
441 }
442 }
443
444 pub fn selection_range(&self) -> (usize, usize) {
445 if self.caret <= self.anchor {
446 (self.caret, self.anchor)
447 } else {
448 (self.anchor, self.caret)
449 }
450 }
451
452 pub fn clear_preedit(&mut self) {
453 self.preedit = None;
454 }
455
456 pub fn set_preedit(&mut self, text: String, cursor: Option<(usize, usize)>) {
457 if text.is_empty() {
458 self.preedit = None;
459 return;
460 }
461 let cursor = normalize_preedit_cursor(&text, cursor);
462
463 if let Some(preedit) = &mut self.preedit {
464 preedit.text = text;
465 preedit.cursor = cursor;
466 return;
467 }
468
469 self.preedit = Some(TextPreeditState {
470 text,
471 range: self.selection_range(),
472 cursor,
473 });
474 }
475
476 pub fn display_text(&self) -> (String, Option<(usize, usize)>) {
477 let committed = self.buffer.to_string();
478 let Some(preedit) = &self.preedit else {
479 return (committed, None);
480 };
481
482 let start = preedit.range.0.min(committed.len());
483 let end = preedit.range.1.min(committed.len());
484
485 let mut display = String::with_capacity(
486 committed.len() - (end.saturating_sub(start)) + preedit.text.len(),
487 );
488 display.push_str(&committed[..start]);
489 display.push_str(&preedit.text);
490 display.push_str(&committed[end..]);
491 (display, Some((start, start + preedit.text.len())))
492 }
493
494 pub fn display_preedit_cursor_range(&self) -> Option<(usize, usize)> {
495 let preedit = self.preedit.as_ref()?;
496 let cursor = preedit.cursor?;
497 let start = preedit.range.0.min(self.buffer.len_bytes());
498 Some((start + cursor.0, start + cursor.1))
499 }
500
501 pub fn apply_edit(
502 &mut self,
503 range: std::ops::Range<usize>,
504 new_text: &str,
505 next_caret: usize,
506 next_anchor: usize,
507 ) -> String {
508 let buffer_len = self.buffer.len_bytes();
509 let start = range.start.min(buffer_len);
510 let end = range.end.min(buffer_len).max(start);
511 let range = start..end;
512 let old_text = self.buffer.slice(range.clone()).to_string();
513 let mut txn = EditTransaction::new();
514 txn.push(TextEdit::new(range, new_text, old_text));
515 apply_transaction(&mut self.buffer, &txn);
516 self.history.record(TextHistoryEntry {
517 transaction: txn,
518 before_caret: self.caret,
519 before_anchor: self.anchor,
520 after_caret: next_caret,
521 after_anchor: next_anchor,
522 });
523 self.caret = next_caret;
524 self.anchor = next_anchor;
525 self.preedit = None;
526 self.pending_model_sync = true;
527 self.buffer.to_string()
528 }
529
530 pub fn undo(&mut self) -> Option<(String, usize, usize)> {
531 let (caret, anchor) = self.history.undo(&mut self.buffer)?;
532 self.caret = caret;
533 self.anchor = anchor;
534 self.preedit = None;
535 self.pending_model_sync = true;
536 Some((self.buffer.to_string(), caret, anchor))
537 }
538
539 pub fn redo(&mut self) -> Option<(String, usize, usize)> {
540 let (caret, anchor) = self.history.redo(&mut self.buffer)?;
541 self.caret = caret;
542 self.anchor = anchor;
543 self.preedit = None;
544 self.pending_model_sync = true;
545 Some((self.buffer.to_string(), caret, anchor))
546 }
547}
548
549fn normalize_preedit_cursor(text: &str, cursor: Option<(usize, usize)>) -> Option<(usize, usize)> {
550 let (mut start, mut end) = cursor?;
551 start = start.min(text.len());
552 end = end.min(text.len());
553 if start > end {
554 std::mem::swap(&mut start, &mut end);
555 }
556 start = floor_char_boundary(text, start);
557 end = floor_char_boundary(text, end);
558 Some((start, end))
559}
560
561fn floor_char_boundary(text: &str, mut idx: usize) -> usize {
562 idx = idx.min(text.len());
563 while idx > 0 && !text.is_char_boundary(idx) {
564 idx -= 1;
565 }
566 idx
567}
568
569#[derive(Clone, Debug, Default)]
570pub struct InteractionStateMap {
571 pub hovered: HashMap<WidgetId, bool>,
572 pub hover_path: Vec<WidgetId>,
573 pub hover_rich_text_annotation: Option<HoveredRichTextAnnotation>,
574 pub pressed: HashMap<WidgetId, bool>,
575 pub focused: Option<WidgetId>,
576 pub cursor: MouseCursor,
577 pub last_down_point: Option<LayoutPoint>,
578}
579
580#[derive(Clone, Debug, PartialEq, Eq)]
581pub struct HoveredRichTextAnnotation {
582 pub node_id: WidgetId,
583 pub annotation: RichTextAnnotation,
584}
585
586impl InteractionStateMap {
587 pub fn is_hovered(&self, id: WidgetId) -> bool {
588 self.hovered.get(&id).copied().unwrap_or(false)
589 }
590 pub fn is_pressed(&self, id: WidgetId) -> bool {
591 self.pressed.get(&id).copied().unwrap_or(false)
592 }
593 pub fn is_focused(&self, id: WidgetId) -> bool {
594 self.focused == Some(id)
595 }
596
597 pub fn hovered_path(&self) -> &[WidgetId] {
598 &self.hover_path
599 }
600
601 pub fn hovered_rich_text_annotation(&self) -> Option<&HoveredRichTextAnnotation> {
602 self.hover_rich_text_annotation.as_ref()
603 }
604
605 pub fn cursor(&self) -> MouseCursor {
606 self.cursor
607 }
608
609 pub fn set_hovered(&mut self, id: WidgetId, value: bool) {
610 if value {
611 self.hovered.insert(id, true);
612 } else {
613 self.hovered.remove(&id);
614 }
615 }
616
617 pub fn set_hover_path(&mut self, path: Vec<WidgetId>) {
618 self.hover_path = path;
619 }
620
621 pub fn set_hovered_rich_text_annotation(
622 &mut self,
623 annotation: Option<HoveredRichTextAnnotation>,
624 ) {
625 self.hover_rich_text_annotation = annotation;
626 }
627
628 pub fn set_pressed(&mut self, id: WidgetId, value: bool) {
629 if value {
630 self.pressed.insert(id, true);
631 } else {
632 self.pressed.remove(&id);
633 }
634 }
635
636 pub fn set_focused(&mut self, id: Option<WidgetId>) {
637 self.focused = id;
638 }
639
640 pub fn set_cursor(&mut self, cursor: MouseCursor) {
641 self.cursor = cursor;
642 }
643}
644
645#[derive(Clone, Debug, Default)]
646pub struct VideoStateMap {
647 pub states: HashMap<WidgetId, VideoState>,
648}
649
650#[derive(Clone, Debug, Default)]
651pub struct WebState {
652 pub url: String,
653 pub user_agent: Option<String>,
654 pub loading: bool,
655 pub can_go_back: bool,
656 pub can_go_forward: bool,
657 pub title: Option<String>,
658}
659
660#[derive(Clone, Debug, Default)]
661pub struct WebStateMap {
662 pub states: HashMap<WidgetId, WebState>,
663}
664
665impl GlobalState for VideoStateMap {}
668
669#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
670pub struct VideoState {
671 pub status: VideoStatus,
672 pub position_ms: u64,
673 pub duration_ms: Option<u64>,
674 pub rate: f32,
675 pub volume: f32,
676 pub muted: bool,
677 pub looped: bool,
678 pub asset_source: String,
679 pub surface_id: Option<u64>,
680 pub pending_seek: Option<u64>,
681}
682
683impl Default for VideoState {
684 fn default() -> Self {
685 Self {
686 status: VideoStatus::Stopped,
687 position_ms: 0,
688 duration_ms: None,
689 rate: 1.0,
690 volume: 1.0,
691 muted: false,
692 looped: false,
693 asset_source: String::new(),
694 surface_id: None,
695 pending_seek: None,
696 }
697 }
698}
699
700#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
701pub enum VideoStatus {
702 Stopped,
703 Playing,
704 Paused,
705 Buffering,
706 Ended,
707 Error,
708}