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