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 }
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#[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#[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 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, pub anchor: usize, pub history: TextEditHistory,
193 pub preedit: Option<TextPreeditState>,
194 pub pending_model_sync: bool, 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, }
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
603impl 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}