Skip to main content

fission_core/
env.rs

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    // Rich(WindowTitleContent),
29}
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// Static environment data (Theme, I18n)
55#[derive(Clone)]
56pub struct Env {
57    pub theme: Theme,
58    pub i18n: I18nRegistry,
59    pub locale: Locale,
60    pub window: WindowEnv,
61    pub window_insets: WindowInsets,
62    pub viewport_size: LayoutSize,
63    pub measurer: Option<Arc<dyn fission_layout::TextMeasurer>>,
64}
65
66impl Default for Env {
67    fn default() -> Self {
68        Self {
69            theme: Theme::default(),
70            i18n: I18nRegistry::new(),
71            locale: Locale::default(),
72            window: WindowEnv::default(),
73            window_insets: WindowInsets::default(),
74            viewport_size: LayoutSize::default(),
75            measurer: None,
76        }
77    }
78}
79
80impl std::fmt::Debug for Env {
81    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
82        f.debug_struct("Env")
83            .field("theme", &self.theme)
84            .field("locale", &self.locale)
85            .field("window", &self.window)
86            .field("window_insets", &self.window_insets)
87            .field("viewport_size", &self.viewport_size)
88            .finish()
89    }
90}
91
92impl Env {
93    pub fn new(measurer: Arc<dyn fission_layout::TextMeasurer>) -> Self {
94        Self {
95            theme: Theme::default(),
96            i18n: I18nRegistry::new(),
97            locale: Locale::default(),
98            window: WindowEnv::default(),
99            window_insets: WindowInsets::default(),
100            viewport_size: LayoutSize::default(),
101            measurer: Some(measurer),
102        }
103    }
104}
105
106pub trait Clipboard: Send + Sync {
107    fn get_text(&self) -> Option<String>;
108    fn set_text(&self, text: &str);
109}
110
111pub trait ImeHandler: Send + Sync {
112    fn set_ime_allowed(&self, allowed: bool);
113    fn set_ime_cursor_area(&self, rect: fission_layout::LayoutRect);
114}
115
116// Runtime state managed by framework (Interaction)
117#[derive(Clone, Debug, Default)]
118pub struct RuntimeState {
119    pub local_widget_state: LocalStateStore,
120    pub scroll: ScrollStateMap,
121    pub video: VideoStateMap,
122    pub web: WebStateMap,
123    pub animation: AnimationStateMap,
124    pub interaction: InteractionStateMap,
125    pub text_edit: TextEditStateMap,
126    pub clipboard: String,
127    pub caret_visible: HashMap<WidgetId, bool>,
128    pub gesture: GestureState,
129    pub hero: HeroState,
130}
131
132#[derive(Clone, Debug, Default)]
133pub struct HeroState {
134    // tag -> (Last Known WidgetId, Last Known Rect)
135    pub positions: HashMap<String, (WidgetId, fission_layout::LayoutRect)>,
136}
137
138#[derive(Clone, Debug, Default)]
139pub struct GestureState {
140    pub start_point: Option<LayoutPoint>,
141    pub last_point: Option<LayoutPoint>,
142    pub is_panning: bool,
143    pub target_node: Option<WidgetId>,
144    pub dragging_payload: Option<Vec<u8>>,
145    pub pressed_button: Option<crate::event::PointerButton>,
146    pub scrollbar_drag: Option<crate::scrollbar::ScrollbarDragState>,
147}
148
149#[derive(Clone, Debug, Default)]
150pub struct AnimationStateMap {
151    pub values: HashMap<(WidgetId, AnimationPropertyId), f32>,
152    pub active: HashMap<(WidgetId, AnimationPropertyId), ActiveAnimation>,
153}
154
155#[derive(Clone, Debug)]
156pub struct ActiveAnimation {
157    pub target: WidgetId,
158    pub property: AnimationPropertyId,
159    pub start_value: f32,
160    pub end_value: f32,
161    pub start_time: u64,
162    pub duration: u64,
163    pub repeat: bool,
164    pub frame_interval_ms: Option<u64>,
165    pub easing: EasingFunction,
166}
167
168#[derive(Clone, Debug, Default)]
169pub struct ScrollStateMap {
170    pub offsets: HashMap<WidgetId, f32>,
171}
172
173impl ScrollStateMap {
174    pub fn get_offset(&self, id: WidgetId) -> f32 {
175        *self.offsets.get(&id).unwrap_or(&0.0)
176    }
177
178    pub fn set_offset(&mut self, id: WidgetId, offset: f32) {
179        self.offsets.insert(id, offset);
180    }
181}
182
183#[derive(Clone, Debug, Default)]
184pub struct TextEditStateMap {
185    pub states: HashMap<WidgetId, TextEditState>,
186    pub restoration: HashMap<String, TextRestorationSnapshot>,
187}
188
189#[derive(Clone, Debug)]
190pub struct TextEditState {
191    pub buffer: TextBuffer,
192    pub caret: usize,  // byte index into value
193    pub anchor: usize, // selection anchor; if equal to caret then no selection
194    pub history: TextEditHistory,
195    pub preedit: Option<TextPreeditState>,
196    pub pending_model_sync: bool, // True when edits are newer than the currently lowered semantics value
197    /// Last cursor position that was dispatched as a CursorChanged action.
198    /// Used to deduplicate dispatches and prevent unnecessary model updates
199    /// that could cause extra rebuild cycles.
200    pub last_dispatched_cursor: Option<(usize, usize)>,
201    pub affordances: TextInputAffordanceState,
202}
203
204impl Default for TextEditState {
205    fn default() -> Self {
206        Self {
207            buffer: TextBuffer::new(),
208            caret: 0,
209            anchor: 0,
210            history: TextEditHistory::default(),
211            preedit: None,
212            pending_model_sync: false,
213            last_dispatched_cursor: None,
214            affordances: TextInputAffordanceState::default(),
215        }
216    }
217}
218
219#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
220pub enum TextSelectionHandleKind {
221    #[default]
222    Caret,
223    Start,
224    End,
225}
226
227#[derive(Clone, Debug, Default)]
228pub struct TextInputAffordanceState {
229    pub toolbar_visible: bool,
230    pub toolbar_anchor: Option<LayoutPoint>,
231    pub caret_handle: Option<LayoutPoint>,
232    pub selection_start_handle: Option<LayoutPoint>,
233    pub selection_end_handle: Option<LayoutPoint>,
234    pub active_handle: Option<TextSelectionHandleKind>,
235    pub magnifier_visible: bool,
236    pub magnifier_anchor: Option<LayoutPoint>,
237}
238
239#[derive(Clone, Debug)]
240pub struct TextPreeditState {
241    pub text: String,
242    pub range: (usize, usize),
243}
244
245#[derive(Clone, Debug)]
246pub struct TextHistoryEntry {
247    pub transaction: EditTransaction,
248    pub before_caret: usize,
249    pub before_anchor: usize,
250    pub after_caret: usize,
251    pub after_anchor: usize,
252}
253
254#[derive(Clone, Debug)]
255pub struct TextRestorationSnapshot {
256    pub value: String,
257    pub caret: usize,
258    pub anchor: usize,
259}
260
261#[derive(Clone, Debug)]
262pub struct TextEditHistory {
263    pub undo_stack: Vec<TextHistoryEntry>,
264    pub redo_stack: Vec<TextHistoryEntry>,
265    pub capacity: usize, // Max undo steps
266}
267
268impl Default for TextEditHistory {
269    fn default() -> Self {
270        Self {
271            undo_stack: Vec::new(),
272            redo_stack: Vec::new(),
273            capacity: 100,
274        }
275    }
276}
277
278impl TextEditHistory {
279    pub fn record(&mut self, entry: TextHistoryEntry) {
280        self.undo_stack.push(entry);
281        if self.undo_stack.len() > self.capacity {
282            let overflow = self.undo_stack.len() - self.capacity;
283            self.undo_stack.drain(0..overflow);
284        }
285        self.redo_stack.clear();
286    }
287
288    pub fn undo(&mut self, buffer: &mut TextBuffer) -> Option<(usize, usize)> {
289        let entry = self.undo_stack.pop()?;
290        apply_transaction(buffer, &entry.transaction.inverse());
291        let caret = entry.before_caret;
292        let anchor = entry.before_anchor;
293        self.redo_stack.push(entry);
294        Some((caret, anchor))
295    }
296
297    pub fn redo(&mut self, buffer: &mut TextBuffer) -> Option<(usize, usize)> {
298        let entry = self.redo_stack.pop()?;
299        apply_transaction(buffer, &entry.transaction);
300        let caret = entry.after_caret;
301        let anchor = entry.after_anchor;
302        self.undo_stack.push(entry);
303        Some((caret, anchor))
304    }
305}
306
307fn apply_transaction(buffer: &mut TextBuffer, transaction: &EditTransaction) {
308    for edit in &transaction.edits {
309        buffer.replace(edit.range.clone(), &edit.new_text);
310    }
311}
312
313impl TextEditStateMap {
314    pub fn get_mut_or_default(&mut self, id: WidgetId) -> &mut TextEditState {
315        self.states.entry(id).or_default()
316    }
317    pub fn get(&self, id: WidgetId) -> Option<&TextEditState> {
318        self.states.get(&id)
319    }
320    pub fn sync_from_runtime(
321        &mut self,
322        id: WidgetId,
323        semantic_value: &str,
324        restoration_id: Option<&str>,
325        undo_capacity: Option<usize>,
326    ) {
327        let restoration_snapshot = restoration_id.and_then(|rid| {
328            if semantic_value.is_empty() {
329                self.restoration.get(rid).cloned()
330            } else {
331                None
332            }
333        });
334        let st = self.states.entry(id).or_default();
335        st.sync_from_model(semantic_value);
336        if semantic_value.is_empty() && st.buffer.len_bytes() == 0 {
337            if let Some(snapshot) = restoration_snapshot.as_ref() {
338                st.restore_snapshot(snapshot);
339            }
340        }
341        if let Some(capacity) = undo_capacity {
342            st.set_history_capacity(capacity);
343        }
344        if let Some(rid) = restoration_id {
345            self.restoration.insert(rid.to_string(), st.snapshot());
346        }
347    }
348    pub fn persist_restoration(&mut self, id: WidgetId, restoration_id: Option<&str>) {
349        let Some(rid) = restoration_id else {
350            return;
351        };
352        if let Some(st) = self.states.get(&id) {
353            self.restoration.insert(rid.to_string(), st.snapshot());
354        }
355    }
356    pub fn set_caret(&mut self, id: WidgetId, caret: usize, anchor: Option<usize>) {
357        let st = self.states.entry(id).or_default();
358        st.caret = caret;
359        st.anchor = anchor.unwrap_or(caret);
360        st.pending_model_sync = false;
361    }
362}
363
364impl TextEditState {
365    pub fn snapshot(&self) -> TextRestorationSnapshot {
366        TextRestorationSnapshot {
367            value: self.buffer.to_string(),
368            caret: self.caret,
369            anchor: self.anchor,
370        }
371    }
372
373    pub fn restore_snapshot(&mut self, snapshot: &TextRestorationSnapshot) {
374        self.buffer = TextBuffer::from_str(&snapshot.value);
375        self.caret = snapshot.caret.min(snapshot.value.len());
376        self.anchor = snapshot.anchor.min(snapshot.value.len());
377        self.preedit = None;
378        self.pending_model_sync = false;
379        self.last_dispatched_cursor = None;
380        self.history = TextEditHistory::default();
381    }
382
383    pub fn set_history_capacity(&mut self, capacity: usize) {
384        let capacity = capacity.max(1);
385        self.history.capacity = capacity;
386        if self.history.undo_stack.len() > capacity {
387            let overflow = self.history.undo_stack.len() - capacity;
388            self.history.undo_stack.drain(0..overflow);
389        }
390        if self.history.redo_stack.len() > capacity {
391            let overflow = self.history.redo_stack.len() - capacity;
392            self.history.redo_stack.drain(0..overflow);
393        }
394    }
395
396    pub fn committed_text(&self) -> String {
397        self.buffer.to_string()
398    }
399
400    pub fn sync_from_model(&mut self, semantic_value: &str) {
401        if self.pending_model_sync && self.buffer.to_string() == semantic_value {
402            self.pending_model_sync = false;
403        }
404
405        if !self.pending_model_sync && self.buffer.to_string() != semantic_value {
406            self.buffer = TextBuffer::from_str(semantic_value);
407            self.caret = self.caret.min(semantic_value.len());
408            self.anchor = self.anchor.min(semantic_value.len());
409            self.preedit = None;
410            self.history = TextEditHistory::default();
411        }
412    }
413
414    pub fn selection_range(&self) -> (usize, usize) {
415        if self.caret <= self.anchor {
416            (self.caret, self.anchor)
417        } else {
418            (self.anchor, self.caret)
419        }
420    }
421
422    pub fn clear_preedit(&mut self) {
423        self.preedit = None;
424    }
425
426    pub fn set_preedit(&mut self, text: String) {
427        if text.is_empty() {
428            self.preedit = None;
429            return;
430        }
431
432        if let Some(preedit) = &mut self.preedit {
433            preedit.text = text;
434            return;
435        }
436
437        self.preedit = Some(TextPreeditState {
438            text,
439            range: self.selection_range(),
440        });
441    }
442
443    pub fn display_text(&self) -> (String, Option<(usize, usize)>) {
444        let committed = self.buffer.to_string();
445        let Some(preedit) = &self.preedit else {
446            return (committed, None);
447        };
448
449        let start = preedit.range.0.min(committed.len());
450        let end = preedit.range.1.min(committed.len());
451
452        let mut display = String::with_capacity(
453            committed.len() - (end.saturating_sub(start)) + preedit.text.len(),
454        );
455        display.push_str(&committed[..start]);
456        display.push_str(&preedit.text);
457        display.push_str(&committed[end..]);
458        (display, Some((start, start + preedit.text.len())))
459    }
460
461    pub fn apply_edit(
462        &mut self,
463        range: std::ops::Range<usize>,
464        new_text: &str,
465        next_caret: usize,
466        next_anchor: usize,
467    ) -> String {
468        let buffer_len = self.buffer.len_bytes();
469        let start = range.start.min(buffer_len);
470        let end = range.end.min(buffer_len).max(start);
471        let range = start..end;
472        let old_text = self.buffer.slice(range.clone()).to_string();
473        let mut txn = EditTransaction::new();
474        txn.push(TextEdit::new(range, new_text, old_text));
475        apply_transaction(&mut self.buffer, &txn);
476        self.history.record(TextHistoryEntry {
477            transaction: txn,
478            before_caret: self.caret,
479            before_anchor: self.anchor,
480            after_caret: next_caret,
481            after_anchor: next_anchor,
482        });
483        self.caret = next_caret;
484        self.anchor = next_anchor;
485        self.preedit = None;
486        self.pending_model_sync = true;
487        self.buffer.to_string()
488    }
489
490    pub fn undo(&mut self) -> Option<(String, usize, usize)> {
491        let (caret, anchor) = self.history.undo(&mut self.buffer)?;
492        self.caret = caret;
493        self.anchor = anchor;
494        self.preedit = None;
495        self.pending_model_sync = true;
496        Some((self.buffer.to_string(), caret, anchor))
497    }
498
499    pub fn redo(&mut self) -> Option<(String, usize, usize)> {
500        let (caret, anchor) = self.history.redo(&mut self.buffer)?;
501        self.caret = caret;
502        self.anchor = anchor;
503        self.preedit = None;
504        self.pending_model_sync = true;
505        Some((self.buffer.to_string(), caret, anchor))
506    }
507}
508
509#[derive(Clone, Debug, Default)]
510pub struct InteractionStateMap {
511    pub hovered: HashMap<WidgetId, bool>,
512    pub hover_path: Vec<WidgetId>,
513    pub hover_rich_text_annotation: Option<HoveredRichTextAnnotation>,
514    pub pressed: HashMap<WidgetId, bool>,
515    pub focused: Option<WidgetId>,
516    pub cursor: MouseCursor,
517    pub last_down_point: Option<LayoutPoint>,
518}
519
520#[derive(Clone, Debug, PartialEq, Eq)]
521pub struct HoveredRichTextAnnotation {
522    pub node_id: WidgetId,
523    pub annotation: RichTextAnnotation,
524}
525
526impl InteractionStateMap {
527    pub fn is_hovered(&self, id: WidgetId) -> bool {
528        self.hovered.get(&id).copied().unwrap_or(false)
529    }
530    pub fn is_pressed(&self, id: WidgetId) -> bool {
531        self.pressed.get(&id).copied().unwrap_or(false)
532    }
533    pub fn is_focused(&self, id: WidgetId) -> bool {
534        self.focused == Some(id)
535    }
536
537    pub fn hovered_path(&self) -> &[WidgetId] {
538        &self.hover_path
539    }
540
541    pub fn hovered_rich_text_annotation(&self) -> Option<&HoveredRichTextAnnotation> {
542        self.hover_rich_text_annotation.as_ref()
543    }
544
545    pub fn cursor(&self) -> MouseCursor {
546        self.cursor
547    }
548
549    pub fn set_hovered(&mut self, id: WidgetId, value: bool) {
550        if value {
551            self.hovered.insert(id, true);
552        } else {
553            self.hovered.remove(&id);
554        }
555    }
556
557    pub fn set_hover_path(&mut self, path: Vec<WidgetId>) {
558        self.hover_path = path;
559    }
560
561    pub fn set_hovered_rich_text_annotation(
562        &mut self,
563        annotation: Option<HoveredRichTextAnnotation>,
564    ) {
565        self.hover_rich_text_annotation = annotation;
566    }
567
568    pub fn set_pressed(&mut self, id: WidgetId, value: bool) {
569        if value {
570            self.pressed.insert(id, true);
571        } else {
572            self.pressed.remove(&id);
573        }
574    }
575
576    pub fn set_focused(&mut self, id: Option<WidgetId>) {
577        self.focused = id;
578    }
579
580    pub fn set_cursor(&mut self, cursor: MouseCursor) {
581        self.cursor = cursor;
582    }
583}
584
585#[derive(Clone, Debug, Default)]
586pub struct VideoStateMap {
587    pub states: HashMap<WidgetId, VideoState>,
588}
589
590#[derive(Clone, Debug, Default)]
591pub struct WebState {
592    pub url: String,
593    pub user_agent: Option<String>,
594    pub loading: bool,
595    pub can_go_back: bool,
596    pub can_go_forward: bool,
597    pub title: Option<String>,
598}
599
600#[derive(Clone, Debug, Default)]
601pub struct WebStateMap {
602    pub states: HashMap<WidgetId, WebState>,
603}
604
605// Static environment data (Theme, I18n)
606
607impl GlobalState for VideoStateMap {}
608
609#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
610pub struct VideoState {
611    pub status: VideoStatus,
612    pub position_ms: u64,
613    pub duration_ms: Option<u64>,
614    pub rate: f32,
615    pub volume: f32,
616    pub muted: bool,
617    pub looped: bool,
618    pub asset_source: String,
619    pub surface_id: Option<u64>,
620    pub pending_seek: Option<u64>,
621}
622
623impl Default for VideoState {
624    fn default() -> Self {
625        Self {
626            status: VideoStatus::Stopped,
627            position_ms: 0,
628            duration_ms: None,
629            rate: 1.0,
630            volume: 1.0,
631            muted: false,
632            looped: false,
633            asset_source: String::new(),
634            surface_id: None,
635            pending_seek: None,
636        }
637    }
638}
639
640#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
641pub enum VideoStatus {
642    Stopped,
643    Playing,
644    Paused,
645    Buffering,
646    Ended,
647    Error,
648}