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}
273
274#[derive(Clone, Debug)]
275pub struct TextHistoryEntry {
276 pub transaction: EditTransaction,
277 pub before_caret: usize,
278 pub before_anchor: usize,
279 pub after_caret: usize,
280 pub after_anchor: usize,
281}
282
283#[derive(Clone, Debug)]
284pub struct TextRestorationSnapshot {
285 pub value: String,
286 pub caret: usize,
287 pub anchor: usize,
288}
289
290#[derive(Clone, Debug)]
291pub struct TextEditHistory {
292 pub undo_stack: Vec<TextHistoryEntry>,
293 pub redo_stack: Vec<TextHistoryEntry>,
294 pub capacity: usize, }
296
297impl Default for TextEditHistory {
298 fn default() -> Self {
299 Self {
300 undo_stack: Vec::new(),
301 redo_stack: Vec::new(),
302 capacity: 100,
303 }
304 }
305}
306
307impl TextEditHistory {
308 pub fn record(&mut self, entry: TextHistoryEntry) {
309 self.undo_stack.push(entry);
310 if self.undo_stack.len() > self.capacity {
311 let overflow = self.undo_stack.len() - self.capacity;
312 self.undo_stack.drain(0..overflow);
313 }
314 self.redo_stack.clear();
315 }
316
317 pub fn undo(&mut self, buffer: &mut TextBuffer) -> Option<(usize, usize)> {
318 let entry = self.undo_stack.pop()?;
319 apply_transaction(buffer, &entry.transaction.inverse());
320 let caret = entry.before_caret;
321 let anchor = entry.before_anchor;
322 self.redo_stack.push(entry);
323 Some((caret, anchor))
324 }
325
326 pub fn redo(&mut self, buffer: &mut TextBuffer) -> Option<(usize, usize)> {
327 let entry = self.redo_stack.pop()?;
328 apply_transaction(buffer, &entry.transaction);
329 let caret = entry.after_caret;
330 let anchor = entry.after_anchor;
331 self.undo_stack.push(entry);
332 Some((caret, anchor))
333 }
334}
335
336fn apply_transaction(buffer: &mut TextBuffer, transaction: &EditTransaction) {
337 for edit in &transaction.edits {
338 buffer.replace(edit.range.clone(), &edit.new_text);
339 }
340}
341
342impl TextEditStateMap {
343 pub fn get_mut_or_default(&mut self, id: WidgetId) -> &mut TextEditState {
344 self.states.entry(id).or_default()
345 }
346 pub fn get(&self, id: WidgetId) -> Option<&TextEditState> {
347 self.states.get(&id)
348 }
349 pub fn sync_from_runtime(
350 &mut self,
351 id: WidgetId,
352 semantic_value: &str,
353 restoration_id: Option<&str>,
354 undo_capacity: Option<usize>,
355 ) {
356 let restoration_snapshot = restoration_id.and_then(|rid| {
357 if semantic_value.is_empty() {
358 self.restoration.get(rid).cloned()
359 } else {
360 None
361 }
362 });
363 let st = self.states.entry(id).or_default();
364 st.sync_from_model(semantic_value);
365 if semantic_value.is_empty() && st.buffer.len_bytes() == 0 {
366 if let Some(snapshot) = restoration_snapshot.as_ref() {
367 st.restore_snapshot(snapshot);
368 }
369 }
370 if let Some(capacity) = undo_capacity {
371 st.set_history_capacity(capacity);
372 }
373 if let Some(rid) = restoration_id {
374 self.restoration.insert(rid.to_string(), st.snapshot());
375 }
376 }
377 pub fn persist_restoration(&mut self, id: WidgetId, restoration_id: Option<&str>) {
378 let Some(rid) = restoration_id else {
379 return;
380 };
381 if let Some(st) = self.states.get(&id) {
382 self.restoration.insert(rid.to_string(), st.snapshot());
383 }
384 }
385 pub fn set_caret(&mut self, id: WidgetId, caret: usize, anchor: Option<usize>) {
386 let st = self.states.entry(id).or_default();
387 st.caret = caret;
388 st.anchor = anchor.unwrap_or(caret);
389 st.pending_model_sync = false;
390 }
391}
392
393impl TextEditState {
394 pub fn snapshot(&self) -> TextRestorationSnapshot {
395 TextRestorationSnapshot {
396 value: self.buffer.to_string(),
397 caret: self.caret,
398 anchor: self.anchor,
399 }
400 }
401
402 pub fn restore_snapshot(&mut self, snapshot: &TextRestorationSnapshot) {
403 self.buffer = TextBuffer::from_str(&snapshot.value);
404 self.caret = snapshot.caret.min(snapshot.value.len());
405 self.anchor = snapshot.anchor.min(snapshot.value.len());
406 self.preedit = None;
407 self.pending_model_sync = false;
408 self.last_dispatched_cursor = None;
409 self.history = TextEditHistory::default();
410 }
411
412 pub fn set_history_capacity(&mut self, capacity: usize) {
413 let capacity = capacity.max(1);
414 self.history.capacity = capacity;
415 if self.history.undo_stack.len() > capacity {
416 let overflow = self.history.undo_stack.len() - capacity;
417 self.history.undo_stack.drain(0..overflow);
418 }
419 if self.history.redo_stack.len() > capacity {
420 let overflow = self.history.redo_stack.len() - capacity;
421 self.history.redo_stack.drain(0..overflow);
422 }
423 }
424
425 pub fn committed_text(&self) -> String {
426 self.buffer.to_string()
427 }
428
429 pub fn sync_from_model(&mut self, semantic_value: &str) {
430 if self.pending_model_sync && self.buffer.to_string() == semantic_value {
431 self.pending_model_sync = false;
432 }
433
434 if !self.pending_model_sync && self.buffer.to_string() != semantic_value {
435 self.buffer = TextBuffer::from_str(semantic_value);
436 self.caret = self.caret.min(semantic_value.len());
437 self.anchor = self.anchor.min(semantic_value.len());
438 self.preedit = None;
439 self.history = TextEditHistory::default();
440 }
441 }
442
443 pub fn selection_range(&self) -> (usize, usize) {
444 if self.caret <= self.anchor {
445 (self.caret, self.anchor)
446 } else {
447 (self.anchor, self.caret)
448 }
449 }
450
451 pub fn clear_preedit(&mut self) {
452 self.preedit = None;
453 }
454
455 pub fn set_preedit(&mut self, text: String) {
456 if text.is_empty() {
457 self.preedit = None;
458 return;
459 }
460
461 if let Some(preedit) = &mut self.preedit {
462 preedit.text = text;
463 return;
464 }
465
466 self.preedit = Some(TextPreeditState {
467 text,
468 range: self.selection_range(),
469 });
470 }
471
472 pub fn display_text(&self) -> (String, Option<(usize, usize)>) {
473 let committed = self.buffer.to_string();
474 let Some(preedit) = &self.preedit else {
475 return (committed, None);
476 };
477
478 let start = preedit.range.0.min(committed.len());
479 let end = preedit.range.1.min(committed.len());
480
481 let mut display = String::with_capacity(
482 committed.len() - (end.saturating_sub(start)) + preedit.text.len(),
483 );
484 display.push_str(&committed[..start]);
485 display.push_str(&preedit.text);
486 display.push_str(&committed[end..]);
487 (display, Some((start, start + preedit.text.len())))
488 }
489
490 pub fn apply_edit(
491 &mut self,
492 range: std::ops::Range<usize>,
493 new_text: &str,
494 next_caret: usize,
495 next_anchor: usize,
496 ) -> String {
497 let buffer_len = self.buffer.len_bytes();
498 let start = range.start.min(buffer_len);
499 let end = range.end.min(buffer_len).max(start);
500 let range = start..end;
501 let old_text = self.buffer.slice(range.clone()).to_string();
502 let mut txn = EditTransaction::new();
503 txn.push(TextEdit::new(range, new_text, old_text));
504 apply_transaction(&mut self.buffer, &txn);
505 self.history.record(TextHistoryEntry {
506 transaction: txn,
507 before_caret: self.caret,
508 before_anchor: self.anchor,
509 after_caret: next_caret,
510 after_anchor: next_anchor,
511 });
512 self.caret = next_caret;
513 self.anchor = next_anchor;
514 self.preedit = None;
515 self.pending_model_sync = true;
516 self.buffer.to_string()
517 }
518
519 pub fn undo(&mut self) -> Option<(String, usize, usize)> {
520 let (caret, anchor) = self.history.undo(&mut self.buffer)?;
521 self.caret = caret;
522 self.anchor = anchor;
523 self.preedit = None;
524 self.pending_model_sync = true;
525 Some((self.buffer.to_string(), caret, anchor))
526 }
527
528 pub fn redo(&mut self) -> Option<(String, usize, usize)> {
529 let (caret, anchor) = self.history.redo(&mut self.buffer)?;
530 self.caret = caret;
531 self.anchor = anchor;
532 self.preedit = None;
533 self.pending_model_sync = true;
534 Some((self.buffer.to_string(), caret, anchor))
535 }
536}
537
538#[derive(Clone, Debug, Default)]
539pub struct InteractionStateMap {
540 pub hovered: HashMap<WidgetId, bool>,
541 pub hover_path: Vec<WidgetId>,
542 pub hover_rich_text_annotation: Option<HoveredRichTextAnnotation>,
543 pub pressed: HashMap<WidgetId, bool>,
544 pub focused: Option<WidgetId>,
545 pub cursor: MouseCursor,
546 pub last_down_point: Option<LayoutPoint>,
547}
548
549#[derive(Clone, Debug, PartialEq, Eq)]
550pub struct HoveredRichTextAnnotation {
551 pub node_id: WidgetId,
552 pub annotation: RichTextAnnotation,
553}
554
555impl InteractionStateMap {
556 pub fn is_hovered(&self, id: WidgetId) -> bool {
557 self.hovered.get(&id).copied().unwrap_or(false)
558 }
559 pub fn is_pressed(&self, id: WidgetId) -> bool {
560 self.pressed.get(&id).copied().unwrap_or(false)
561 }
562 pub fn is_focused(&self, id: WidgetId) -> bool {
563 self.focused == Some(id)
564 }
565
566 pub fn hovered_path(&self) -> &[WidgetId] {
567 &self.hover_path
568 }
569
570 pub fn hovered_rich_text_annotation(&self) -> Option<&HoveredRichTextAnnotation> {
571 self.hover_rich_text_annotation.as_ref()
572 }
573
574 pub fn cursor(&self) -> MouseCursor {
575 self.cursor
576 }
577
578 pub fn set_hovered(&mut self, id: WidgetId, value: bool) {
579 if value {
580 self.hovered.insert(id, true);
581 } else {
582 self.hovered.remove(&id);
583 }
584 }
585
586 pub fn set_hover_path(&mut self, path: Vec<WidgetId>) {
587 self.hover_path = path;
588 }
589
590 pub fn set_hovered_rich_text_annotation(
591 &mut self,
592 annotation: Option<HoveredRichTextAnnotation>,
593 ) {
594 self.hover_rich_text_annotation = annotation;
595 }
596
597 pub fn set_pressed(&mut self, id: WidgetId, value: bool) {
598 if value {
599 self.pressed.insert(id, true);
600 } else {
601 self.pressed.remove(&id);
602 }
603 }
604
605 pub fn set_focused(&mut self, id: Option<WidgetId>) {
606 self.focused = id;
607 }
608
609 pub fn set_cursor(&mut self, cursor: MouseCursor) {
610 self.cursor = cursor;
611 }
612}
613
614#[derive(Clone, Debug, Default)]
615pub struct VideoStateMap {
616 pub states: HashMap<WidgetId, VideoState>,
617}
618
619#[derive(Clone, Debug, Default)]
620pub struct WebState {
621 pub url: String,
622 pub user_agent: Option<String>,
623 pub loading: bool,
624 pub can_go_back: bool,
625 pub can_go_forward: bool,
626 pub title: Option<String>,
627}
628
629#[derive(Clone, Debug, Default)]
630pub struct WebStateMap {
631 pub states: HashMap<WidgetId, WebState>,
632}
633
634impl GlobalState for VideoStateMap {}
637
638#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
639pub struct VideoState {
640 pub status: VideoStatus,
641 pub position_ms: u64,
642 pub duration_ms: Option<u64>,
643 pub rate: f32,
644 pub volume: f32,
645 pub muted: bool,
646 pub looped: bool,
647 pub asset_source: String,
648 pub surface_id: Option<u64>,
649 pub pending_seek: Option<u64>,
650}
651
652impl Default for VideoState {
653 fn default() -> Self {
654 Self {
655 status: VideoStatus::Stopped,
656 position_ms: 0,
657 duration_ms: None,
658 rate: 1.0,
659 volume: 1.0,
660 muted: false,
661 looped: false,
662 asset_source: String::new(),
663 surface_id: None,
664 pending_seek: None,
665 }
666 }
667}
668
669#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
670pub enum VideoStatus {
671 Stopped,
672 Playing,
673 Paused,
674 Buffering,
675 Ended,
676 Error,
677}