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)]
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#[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 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, pub anchor: usize, pub history: TextEditHistory,
195 pub preedit: Option<TextPreeditState>,
196 pub pending_model_sync: bool, 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, }
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
605impl 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}