Skip to main content

fission_core/
env.rs

1use crate::{action::AppState, registry::AnimationPropertyId};
2use fission_i18n::{I18nRegistry, Locale};
3use fission_ir::{NodeId, WidgetNodeId};
4use fission_layout::{LayoutPoint, LayoutSize};
5use fission_theme::Theme;
6use serde::{Deserialize, Serialize};
7use std::collections::HashMap;
8use std::sync::Arc;
9
10#[derive(Clone, Debug, Default, Serialize, Deserialize, PartialEq)]
11pub struct WindowInsets {
12    pub top: f32,
13    pub bottom: f32,
14    pub left: f32,
15    pub right: f32,
16}
17
18// Static environment data (Theme, I18n)
19#[derive(Clone)]
20pub struct Env {
21    pub theme: Theme,
22    pub i18n: I18nRegistry,
23    pub locale: Locale,
24    pub window_insets: WindowInsets,
25    pub viewport_size: LayoutSize,
26    pub measurer: Option<Arc<dyn fission_layout::TextMeasurer>>,
27}
28
29impl Default for Env {
30    fn default() -> Self {
31        Self {
32            theme: Theme::default(),
33            i18n: I18nRegistry::new(),
34            locale: Locale::default(),
35            window_insets: WindowInsets::default(),
36            viewport_size: LayoutSize::default(),
37            measurer: None,
38        }
39    }
40}
41
42impl std::fmt::Debug for Env {
43    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
44        f.debug_struct("Env")
45            .field("theme", &self.theme)
46            .field("locale", &self.locale)
47            .field("window_insets", &self.window_insets)
48            .field("viewport_size", &self.viewport_size)
49            .finish()
50    }
51}
52
53impl Env {
54    pub fn new(measurer: Arc<dyn fission_layout::TextMeasurer>) -> Self {
55        Self {
56            theme: Theme::default(),
57            i18n: I18nRegistry::new(),
58            locale: Locale::default(),
59            window_insets: WindowInsets::default(),
60            viewport_size: LayoutSize::default(),
61            measurer: Some(measurer),
62        }
63    }
64}
65
66pub trait Clipboard: Send + Sync {
67    fn get_text(&self) -> Option<String>;
68    fn set_text(&self, text: &str);
69}
70
71pub trait ImeHandler: Send + Sync {
72    fn set_ime_allowed(&self, allowed: bool);
73    fn set_ime_cursor_area(&self, rect: fission_layout::LayoutRect);
74}
75
76// Runtime state managed by framework (Interaction)
77#[derive(Clone, Debug, Default)]
78pub struct RuntimeState {
79    pub scroll: ScrollStateMap,
80    pub video: VideoStateMap,
81    pub web: WebStateMap,
82    pub animation: AnimationStateMap,
83    pub interaction: InteractionStateMap,
84    pub ime_preedit: Option<(NodeId, String)>,
85    pub text_edit: TextEditStateMap,
86    pub clipboard: String,
87    pub caret_visible: HashMap<NodeId, bool>,
88    pub gesture: GestureState,
89    pub hero: HeroState,
90}
91
92#[derive(Clone, Debug, Default)]
93pub struct HeroState {
94    // tag -> (Last Known NodeId, Last Known Rect)
95    pub positions: HashMap<String, (NodeId, fission_layout::LayoutRect)>,
96}
97
98#[derive(Clone, Debug, Default)]
99pub struct GestureState {
100    pub start_point: Option<LayoutPoint>,
101    pub last_point: Option<LayoutPoint>,
102    pub is_panning: bool,
103    pub target_node: Option<NodeId>,
104    pub dragging_payload: Option<Vec<u8>>,
105    pub pressed_button: Option<crate::event::PointerButton>,
106}
107
108#[derive(Clone, Debug, Default)]
109pub struct AnimationStateMap {
110    pub values: HashMap<(WidgetNodeId, AnimationPropertyId), f32>,
111    pub active: HashMap<(WidgetNodeId, AnimationPropertyId), ActiveAnimation>,
112}
113
114#[derive(Clone, Debug)]
115pub struct ActiveAnimation {
116    pub target: WidgetNodeId,
117    pub property: AnimationPropertyId,
118    pub start_value: f32,
119    pub end_value: f32,
120    pub start_time: u64,
121    pub duration: u64,
122    pub repeat: bool,
123}
124
125#[derive(Clone, Debug, Default)]
126pub struct ScrollStateMap {
127    pub offsets: HashMap<NodeId, f32>,
128}
129
130impl ScrollStateMap {
131    pub fn get_offset(&self, id: NodeId) -> f32 {
132        *self.offsets.get(&id).unwrap_or(&0.0)
133    }
134
135    pub fn set_offset(&mut self, id: NodeId, offset: f32) {
136        self.offsets.insert(id, offset);
137    }
138}
139
140#[derive(Clone, Debug, Default)]
141pub struct TextEditStateMap {
142    pub states: HashMap<NodeId, TextEditState>,
143}
144
145#[derive(Clone, Debug)]
146pub struct TextEditState {
147    pub caret: usize,             // byte index into value
148    pub anchor: usize,            // selection anchor; if equal to caret then no selection
149    pub history: TextEditHistory, // NEW
150    pub last_value: String,       // Store last committed value here for history snapshots
151    pub pending_model_sync: bool, // True when edits are newer than the currently lowered semantics value
152    /// Last cursor position that was dispatched as a CursorChanged action.
153    /// Used to deduplicate dispatches and prevent unnecessary model updates
154    /// that could cause extra rebuild cycles.
155    pub last_dispatched_cursor: Option<(usize, usize)>,
156}
157
158impl Default for TextEditState {
159    fn default() -> Self {
160        Self {
161            caret: 0,
162            anchor: 0,
163            history: TextEditHistory::default(),
164            last_value: String::new(),
165            pending_model_sync: false,
166            last_dispatched_cursor: None,
167        }
168    }
169}
170
171#[derive(Clone, Debug)]
172pub struct TextEditHistory {
173    pub stack: Vec<(String, usize, usize)>,
174    pub index: usize,
175    pub capacity: usize, // Max undo steps
176}
177
178impl Default for TextEditHistory {
179    fn default() -> Self {
180        Self {
181            stack: vec![("".to_string(), 0, 0)],
182            index: 0,
183            capacity: 100,
184        }
185    }
186}
187
188impl TextEditHistory {
189    pub fn push(&mut self, value: String, caret: usize, anchor: usize) {
190        // Don't push if state is identical to current top of stack
191        if let Some((last_val, last_caret, last_anchor)) = self.stack.get(self.index) {
192            if last_val == &value && last_caret == &caret && last_anchor == &anchor {
193                return;
194            }
195        }
196
197        // Clear redo history
198        self.stack.truncate(self.index + 1);
199
200        // Add new state
201        self.stack.push((value, caret, anchor));
202        self.index = self.stack.len() - 1;
203
204        // Enforce capacity
205        if self.stack.len() > self.capacity {
206            let overflow = self.stack.len() - self.capacity;
207            self.stack.drain(0..overflow);
208            self.index = self.stack.len() - 1;
209        }
210    }
211
212    pub fn undo(&mut self) -> Option<&(String, usize, usize)> {
213        if self.index > 0 {
214            self.index -= 1;
215            Some(&self.stack[self.index])
216        } else {
217            None
218        }
219    }
220
221    pub fn redo(&mut self) -> Option<&(String, usize, usize)> {
222        if self.index < self.stack.len() - 1 {
223            self.index += 1;
224            Some(&self.stack[self.index])
225        } else {
226            None
227        }
228    }
229}
230
231impl TextEditStateMap {
232    pub fn get_mut_or_default(&mut self, id: NodeId) -> &mut TextEditState {
233        self.states.entry(id).or_default()
234    }
235    pub fn get(&self, id: NodeId) -> Option<&TextEditState> {
236        self.states.get(&id)
237    }
238    pub fn set_caret(&mut self, id: NodeId, caret: usize, anchor: Option<usize>) {
239        let st = self.states.entry(id).or_default();
240        st.caret = caret;
241        st.anchor = anchor.unwrap_or(caret);
242        st.pending_model_sync = false;
243    }
244}
245
246#[derive(Clone, Debug, Default)]
247pub struct InteractionStateMap {
248    pub hovered: HashMap<NodeId, bool>,
249    pub pressed: HashMap<NodeId, bool>,
250    pub focused: Option<NodeId>,
251    pub last_down_point: Option<LayoutPoint>,
252}
253
254impl InteractionStateMap {
255    pub fn is_hovered(&self, id: NodeId) -> bool {
256        self.hovered.get(&id).copied().unwrap_or(false)
257    }
258    pub fn is_pressed(&self, id: NodeId) -> bool {
259        self.pressed.get(&id).copied().unwrap_or(false)
260    }
261    pub fn is_focused(&self, id: NodeId) -> bool {
262        self.focused == Some(id)
263    }
264
265    pub fn set_hovered(&mut self, id: NodeId, value: bool) {
266        if value {
267            self.hovered.insert(id, true);
268        } else {
269            self.hovered.remove(&id);
270        }
271    }
272
273    pub fn set_pressed(&mut self, id: NodeId, value: bool) {
274        if value {
275            self.pressed.insert(id, true);
276        } else {
277            self.pressed.remove(&id);
278        }
279    }
280
281    pub fn set_focused(&mut self, id: Option<NodeId>) {
282        self.focused = id;
283    }
284}
285
286#[derive(Clone, Debug, Default)]
287pub struct VideoStateMap {
288    pub states: HashMap<WidgetNodeId, VideoState>,
289}
290
291#[derive(Clone, Debug, Default)]
292pub struct WebState {
293    pub url: String,
294    pub user_agent: Option<String>,
295    pub loading: bool,
296    pub can_go_back: bool,
297    pub can_go_forward: bool,
298    pub title: Option<String>,
299}
300
301#[derive(Clone, Debug, Default)]
302pub struct WebStateMap {
303    pub states: HashMap<WidgetNodeId, WebState>,
304}
305
306// Static environment data (Theme, I18n)
307
308impl AppState for VideoStateMap {}
309
310#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
311pub struct VideoState {
312    pub status: VideoStatus,
313    pub position_ms: u64,
314    pub duration_ms: Option<u64>,
315    pub rate: f32,
316    pub volume: f32,
317    pub muted: bool,
318    pub looped: bool,
319    pub asset_source: String,
320    pub surface_id: Option<u64>,
321    pub pending_seek: Option<u64>,
322}
323
324impl Default for VideoState {
325    fn default() -> Self {
326        Self {
327            status: VideoStatus::Stopped,
328            position_ms: 0,
329            duration_ms: None,
330            rate: 1.0,
331            volume: 1.0,
332            muted: false,
333            looped: false,
334            asset_source: String::new(),
335            surface_id: None,
336            pending_seek: None,
337        }
338    }
339}
340
341#[derive(Clone, Copy, Debug, PartialEq, Serialize, Deserialize)]
342pub enum VideoStatus {
343    Stopped,
344    Playing,
345    Paused,
346    Buffering,
347    Ended,
348    Error,
349}