Skip to main content

fission_core/
env.rs

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