1use crate::action::{ActionEnvelope, ActionId, GlobalState};
2use crate::async_runtime::ServiceStopPayload;
3use crate::effect::{ActionInput, EffectEnvelope};
4use crate::env::{RuntimeState, VideoStatus};
5use crate::registry::{
6 ActionRegistry, ResourcePolicy, RuntimeResourceDeclaration, RuntimeResourceKind, TimerResource,
7 VideoRegistration,
8};
9use crate::BoxedReducer;
10use crate::{
11 Clipboard, Clock, CurrentTime, ImeHandler, InputEvent, KeyCode, KeyEvent, PointerButton,
12 PointerEvent, ResourceExecutionContext,
13};
14use anyhow::{anyhow, Result};
15use fission_diagnostics::prelude as diag;
16use fission_ir::{CoreIR, FlexDirection, LayoutOp, Op, WidgetId};
17use fission_layout::{LayoutPoint, LayoutRect, LayoutSize, LayoutSnapshot, TextMeasurer};
18use glam::{Mat4, Vec4};
19use serde_json;
20use std::any::TypeId;
21use std::collections::{HashMap, HashSet};
22use std::sync::Arc;
23
24#[derive(Debug, Default, Clone)]
25pub struct TickResult {
26 pub changed_motions: Vec<(WidgetId, crate::MotionPropertyId)>,
27}
28
29#[derive(Debug, Clone)]
30enum ActiveResourceKind {
31 Job,
32 Service {
33 service_name: String,
34 slot_key: String,
35 },
36 Timer {
37 interval_ms: u64,
38 payload: Vec<u8>,
39 on_tick: Option<ActionEnvelope>,
40 next_fire_at: CurrentTime,
41 },
42}
43
44#[derive(Debug, Clone)]
45struct ActiveResource {
46 generation: u64,
47 deps: Option<Vec<u8>>,
48 policy: ResourcePolicy,
49 kind: ActiveResourceKind,
50}
51
52pub struct Runtime {
85 pub(crate) reducers: HashMap<ActionId, Vec<BoxedReducer>>,
88 pub(crate) persistent_reducers: HashMap<ActionId, Vec<BoxedReducer>>,
91 pub app_states: HashMap<TypeId, Box<dyn GlobalState>>,
93 pub runtime_state: RuntimeState,
95 pub measurer: Option<Arc<dyn TextMeasurer>>,
97 pub clipboard_backend: Option<Arc<dyn Clipboard>>,
99 pub ime_handler: Option<Arc<dyn ImeHandler>>,
101 pub pending_effects: Vec<EffectEnvelope>,
103 pub next_req_id: u64,
105 active_resources: HashMap<String, ActiveResource>,
107 next_resource_generation: u64,
109}
110
111impl Default for Runtime {
112 fn default() -> Self {
113 let mut runtime = Self {
114 reducers: HashMap::new(),
115 persistent_reducers: HashMap::new(),
116 app_states: HashMap::new(),
117 runtime_state: RuntimeState::default(),
118 measurer: None,
119 clipboard_backend: None,
120 ime_handler: None,
121 pending_effects: Vec::new(),
122 next_req_id: 0,
123 active_resources: HashMap::new(),
124 next_resource_generation: 1,
125 };
126
127 runtime
128 .add_global_state(Box::new(runtime.runtime_state.local_widget_state.clone()))
129 .expect("Failed to add local widget state store");
130
131 runtime
132 .add_global_state(Box::new(Clock::default()))
133 .expect("Failed to add Clock state");
134
135 runtime.register_base_reducers();
136
137 runtime
138 }
139}
140
141impl Runtime {
142 pub fn with_measurer(mut self, measurer: Arc<dyn TextMeasurer>) -> Self {
143 self.measurer = Some(measurer);
144 self
145 }
146
147 pub fn with_clipboard(mut self, backend: Arc<dyn Clipboard>) -> Self {
148 self.clipboard_backend = Some(backend);
149 self
150 }
151
152 pub fn with_ime_handler(mut self, handler: Arc<dyn ImeHandler>) -> Self {
153 self.ime_handler = Some(handler);
154 self
155 }
156
157 pub fn caret_from_point_in_text(
158 &self,
159 value: &str,
160 font_size: f32,
161 viewport_x: f32,
162 viewport_w: f32,
163 content_w: f32,
164 scroll_offset: f32,
165 point_x: f32,
166 ) -> usize {
167 crate::input::text::caret_from_point_in_text(
168 self.measurer.as_ref(),
169 value,
170 font_size,
171 viewport_x,
172 viewport_w,
173 content_w,
174 scroll_offset,
175 point_x,
176 )
177 }
178
179 pub fn register_reducer<S: GlobalState + 'static>(
181 &mut self,
182 action_id: ActionId,
183 reducer_fn: crate::action::Reducer<S>,
184 ) -> Result<()> {
185 let state_type_id = TypeId::of::<S>();
186
187 let boxed_reducer: BoxedReducer = Box::new(
189 move |app_states: &mut HashMap<TypeId, Box<dyn GlobalState>>,
190 action: &ActionEnvelope,
191 target: WidgetId,
192 _effects: &mut Vec<EffectEnvelope>,
193 _input: &ActionInput|
194 -> Result<()> {
195 if let Some(state_box) = app_states.get_mut(&state_type_id) {
196 let concrete_state = state_box.downcast_mut::<S>().ok_or_else(|| {
197 anyhow!("Failed to downcast GlobalState to concrete type for reducer")
198 })?;
199 reducer_fn(concrete_state, action, target.into())
200 } else {
201 anyhow::bail!("Target GlobalState for reducer not found in runtime.");
202 }
203 },
204 );
205
206 self.reducers
207 .entry(action_id)
208 .or_default()
209 .push(boxed_reducer);
210 Ok(())
211 }
212
213 pub fn register_base_reducers(&mut self) {
214 use crate::{AdvanceTo, Tick, ADVANCE_TO_ACTION_ID, TICK_ACTION_ID};
215
216 self.register_reducer::<Clock>(
217 *TICK_ACTION_ID,
218 |state: &mut Clock, action: &ActionEnvelope, _target| {
219 let tick_action: Tick = serde_json::from_slice(&action.payload)
220 .map_err(|e| anyhow!("Failed to deserialize Tick: {}", e))?;
221 state.advance_by(tick_action.dt)
222 },
223 )
224 .expect("Failed to register Tick reducer");
225
226 self.register_reducer::<Clock>(
227 *ADVANCE_TO_ACTION_ID,
228 |state: &mut Clock, action: &ActionEnvelope, _target| {
229 let advance_action: AdvanceTo = serde_json::from_slice(&action.payload)
230 .map_err(|e| anyhow!("Failed to deserialize AdvanceTo: {}", e))?;
231 state.set_to(advance_action.time)
232 },
233 )
234 .expect("Failed to register AdvanceTo reducer");
235 }
236
237 pub fn clear_reducers(&mut self) {
238 self.reducers.clear();
239 self.register_base_reducers();
240 }
241
242 pub fn absorb_registry<S: GlobalState>(&mut self, registry: ActionRegistry<S>) {
243 let new_reducers = registry.into_runtime_reducers();
244 for (id, mut list) in new_reducers {
245 self.reducers.entry(id).or_default().append(&mut list);
246 }
247 }
248
249 pub fn absorb_persistent_registry<S: GlobalState>(&mut self, registry: ActionRegistry<S>) {
255 let new_reducers = registry.into_runtime_reducers();
256 for (id, mut list) in new_reducers {
257 self.persistent_reducers
258 .entry(id)
259 .or_default()
260 .append(&mut list);
261 }
262 }
263
264 pub fn clock(&self) -> &Clock {
265 self.get_global_state::<Clock>()
266 .expect("Clock state must always be present")
267 }
268
269 pub fn get_global_state<S: GlobalState + 'static>(&self) -> Option<&S> {
270 self.app_states
271 .get(&TypeId::of::<S>())
272 .and_then(|s_box| s_box.downcast_ref::<S>())
273 }
274
275 pub fn get_global_state_mut<S: GlobalState + 'static>(&mut self) -> Option<&mut S> {
276 self.app_states
277 .get_mut(&TypeId::of::<S>())
278 .and_then(|s_box| s_box.downcast_mut::<S>())
279 }
280
281 pub fn add_global_state<S: GlobalState + 'static>(&mut self, state: Box<S>) -> Result<()> {
282 let type_id = TypeId::of::<S>();
283 if self.app_states.insert(type_id, state).is_some() {
284 anyhow::bail!("Global state of this type already registered.");
285 }
286 Ok(())
287 }
288
289 pub fn with_global_state<S: GlobalState + 'static>(mut self, state: S) -> Self {
290 self.app_states.insert(TypeId::of::<S>(), Box::new(state));
291 self
292 }
293
294 #[doc(hidden)]
295 pub fn get_app_state<S: GlobalState + 'static>(&self) -> Option<&S> {
296 self.get_global_state::<S>()
297 }
298
299 #[doc(hidden)]
300 pub fn get_app_state_mut<S: GlobalState + 'static>(&mut self) -> Option<&mut S> {
301 self.get_global_state_mut::<S>()
302 }
303
304 #[doc(hidden)]
305 pub fn add_app_state<S: GlobalState + 'static>(&mut self, state: Box<S>) -> Result<()> {
306 self.add_global_state(state)
307 }
308
309 pub fn dispatch(&mut self, action: ActionEnvelope, target: WidgetId) -> Result<()> {
310 self.dispatch_with_input(action, target, &ActionInput::None)
311 }
312
313 fn enqueue_effect(&mut self, mut envelope: EffectEnvelope) {
314 envelope.req_id = self.next_req_id;
315 self.next_req_id += 1;
316 self.pending_effects.push(envelope);
317 }
318
319 pub fn dispatch_with_input(
320 &mut self,
321 action: ActionEnvelope,
322 target: WidgetId,
323 input: &ActionInput,
324 ) -> Result<()> {
325 self.dispatch_node_with_input(action, target.into(), input)
326 }
327
328 fn dispatch_node(&mut self, action: ActionEnvelope, target: WidgetId) -> Result<()> {
329 self.dispatch_node_with_input(action, target, &ActionInput::None)
330 }
331
332 fn dispatch_node_with_input(
333 &mut self,
334 action: ActionEnvelope,
335 target: WidgetId,
336 input: &ActionInput,
337 ) -> Result<()> {
338 diag::emit(
339 diag::DiagCategory::Input,
340 diag::DiagLevel::Debug,
341 diag::DiagEventKind::InputEvent {
342 kind: "dispatch_start".into(),
343 target: Some(target.as_u128()),
344 position: None,
345 },
346 );
347
348 if crate::media::handle_video_action(&mut self.runtime_state.video, &action)? {
350 return Ok(());
351 }
352
353 let action_id = action.id;
354
355 if crate::scoped_action_handlers::dispatch_scoped_action_handler(&action, target, input)? {
356 return Ok(());
357 }
358
359 let mut effects = Vec::new();
361
362 if let Some(reducers) = self.persistent_reducers.get_mut(&action_id) {
363 diag::emit(
364 diag::DiagCategory::Input,
365 diag::DiagLevel::Debug,
366 diag::DiagEventKind::InputEvent {
367 kind: format!("persistent_reducers:{}", reducers.len()),
368 target: Some(target.as_u128()),
369 position: None,
370 },
371 );
372
373 let mut temp_reducers: Vec<BoxedReducer> = reducers.drain(..).collect();
374 for reducer_wrapper in temp_reducers.iter_mut() {
375 reducer_wrapper(&mut self.app_states, &action, target, &mut effects, input)?;
376 }
377 reducers.extend(temp_reducers);
378 }
379
380 if let Some(reducers) = self.reducers.get_mut(&action_id) {
381 diag::emit(
382 diag::DiagCategory::Input,
383 diag::DiagLevel::Debug,
384 diag::DiagEventKind::InputEvent {
385 kind: format!("reducers:{}", reducers.len()),
386 target: Some(target.as_u128()),
387 position: None,
388 },
389 );
390
391 let mut temp_reducers: Vec<BoxedReducer> = reducers.drain(..).collect();
392 for reducer_wrapper in temp_reducers.iter_mut() {
393 reducer_wrapper(&mut self.app_states, &action, target, &mut effects, input)?;
394 }
395 reducers.extend(temp_reducers);
396 }
397
398 for envelope in effects {
399 self.enqueue_effect(envelope);
400 }
401
402 diag::emit(
403 diag::DiagCategory::Input,
404 diag::DiagLevel::Debug,
405 diag::DiagEventKind::InputEvent {
406 kind: "dispatch_end".into(),
407 target: Some(target.as_u128()),
408 position: None,
409 },
410 );
411 Ok(())
412 }
413
414 pub fn tick(&mut self, dt: CurrentTime) -> Result<TickResult> {
415 use crate::Tick;
416 let action = Tick { dt };
417 let envelope: ActionEnvelope = action.into();
418 self.dispatch_node(envelope, WidgetId::derived(0, &[0]))?;
419
420 self.tick_resource_timers()?;
421
422 let current_time = self.clock().current_time();
423 let changed_motions =
424 crate::motion::tick_motion(&mut self.runtime_state.motion, current_time);
425 Ok(TickResult { changed_motions })
426 }
427
428 fn tick_resource_timers(&mut self) -> Result<()> {
429 let now = self.clock().current_time();
430 let mut ticks = Vec::new();
431
432 for resource in self.active_resources.values_mut() {
433 if let ActiveResourceKind::Timer {
434 interval_ms,
435 payload,
436 on_tick,
437 next_fire_at,
438 } = &mut resource.kind
439 {
440 let Some(action) = on_tick.clone() else {
441 continue;
442 };
443
444 let interval_ms = (*interval_ms).max(1);
445 while now >= *next_fire_at {
446 ticks.push((action.clone(), payload.clone()));
447 *next_fire_at = next_fire_at.saturating_add(interval_ms);
448 }
449 }
450 }
451
452 for (action, payload) in ticks {
453 self.dispatch_node_with_input(
454 action,
455 WidgetId::derived(0, &[0]),
456 &ActionInput::TimerTick { payload },
457 )?;
458 }
459
460 Ok(())
461 }
462
463 pub fn sync_motion_declarations(
464 &mut self,
465 declarations: &[crate::MotionDeclaration],
466 layout: Option<&LayoutSnapshot>,
467 ) -> Vec<(WidgetId, crate::MotionPropertyId)> {
468 let current_time = self.clock().current_time();
469 let snapshot = self.runtime_state.clone();
470 let result = crate::motion::sync_motion_declarations(
471 &mut self.runtime_state.motion,
472 declarations,
473 &snapshot,
474 layout,
475 current_time,
476 );
477 result.changed
478 }
479
480 pub fn sync_video_nodes(&mut self, registrations: &[VideoRegistration]) {
481 let mut seen: HashSet<WidgetId> = HashSet::new();
482
483 for reg in registrations {
484 seen.insert(reg.node_id);
485 let entry = self
486 .runtime_state
487 .video
488 .states
489 .entry(reg.node_id)
490 .or_insert_with(crate::env::VideoState::default);
491 entry.asset_source = reg.source.clone();
492 entry.looped = reg.loop_playback;
493 if reg.autoplay && entry.status == VideoStatus::Stopped {
494 entry.status = VideoStatus::Playing;
495 }
496 }
497
498 self.runtime_state
499 .video
500 .states
501 .retain(|node_id, _| seen.contains(node_id));
502 }
503
504 pub fn sync_web_nodes(&mut self, registrations: &[crate::registry::WebRegistration]) {
505 let mut seen: HashSet<WidgetId> = HashSet::new();
506
507 for reg in registrations {
508 seen.insert(reg.node_id);
509 let entry = self
510 .runtime_state
511 .web
512 .states
513 .entry(reg.node_id)
514 .or_insert_with(crate::env::WebState::default);
515
516 if entry.url != reg.url {
518 entry.url = reg.url.clone();
519 entry.loading = true; }
521 entry.user_agent = reg.user_agent.clone();
522 }
523
524 self.runtime_state
525 .web
526 .states
527 .retain(|node_id, _| seen.contains(node_id));
528 }
529
530 pub fn post_layout_hook(&mut self, ir: &CoreIR, layout: &LayoutSnapshot) {
531 let mut current_heroes = HashMap::new();
532
533 for (id, node) in &ir.nodes {
534 if let Op::Semantics(s) = &node.op {
535 if let Some(tag) = &s.hero_tag {
536 if let Some(geom) = layout.get_node_geometry(*id) {
537 current_heroes.insert(tag.clone(), (*id, geom.rect));
538 }
539 }
540 }
541 }
542
543 for (tag, (_new_id, new_rect)) in ¤t_heroes {
545 if let Some((_old_id, old_rect)) = self.runtime_state.hero.positions.get(tag) {
546 if *new_rect != *old_rect {
547 diag::emit(
549 diag::DiagCategory::Layout,
550 diag::DiagLevel::Debug,
551 diag::DiagEventKind::AnchorPlacement {
552 widget: 0,
553 node: 0,
554 rect_x: old_rect.origin.x,
555 rect_y: old_rect.origin.y,
556 rect_w: old_rect.size.width,
557 rect_h: old_rect.size.height,
558 place_left: new_rect.origin.x,
559 place_top: new_rect.origin.y,
560 note: Some(format!("Hero flight: {}", tag)),
561 },
562 );
563 }
564 }
565 }
566
567 self.runtime_state.hero.positions = current_heroes;
568 }
569
570 pub fn handle_input(
571 &mut self,
572 event: InputEvent,
573 ir: &CoreIR,
574 layout: &LayoutSnapshot,
575 ) -> Result<()> {
576 use crate::hit_test::{
577 find_neighbor_focus_node, find_next_focus_node, hit_test_with_scroll, FocusDirection,
578 };
579 use crate::input::gesture::GestureController;
580 use crate::input::hover::HoverController;
581 use crate::input::slider::SliderController;
582 use crate::input::text::TextInputController;
583 use crate::input::{ControllerContext, InputController};
584 use crate::scrollbar::scrollbar_hit_test;
585 use crate::ui::custom_render::downcast_render_object;
586
587 if self.runtime_state.interaction.focused.is_none() {
588 if let Some(autofocus_id) = Self::find_autofocus_node(ir) {
589 self.runtime_state
590 .interaction
591 .set_focused(Some(autofocus_id));
592 if let Some(ime_handler) = &self.ime_handler {
593 let accepts_text = ir
594 .nodes
595 .get(&autofocus_id)
596 .and_then(|node| match &node.op {
597 Op::Semantics(semantics) => {
598 Some(semantics.role == fission_ir::semantics::Role::TextInput)
599 }
600 _ => None,
601 })
602 .unwrap_or(false);
603 ime_handler.set_ime_allowed(accepts_text);
604 }
605 }
606 }
607
608 if matches!(event, InputEvent::Pointer(_)) {
609 let dispatched_actions = {
610 let mut ctx = ControllerContext {
611 ir,
612 layout,
613 text_edit: &mut self.runtime_state.text_edit,
614 interaction: &mut self.runtime_state.interaction,
615 scroll: &mut self.runtime_state.scroll,
616 gesture: &mut self.runtime_state.gesture,
617 clipboard: self.clipboard_backend.as_ref(),
618 measurer: self.measurer.as_ref(),
619 dispatched_actions: Vec::new(),
620 };
621 let mut hover_controller = HoverController;
622 let _ = hover_controller.handle_event(&mut ctx, &event);
623 ctx.dispatched_actions
624 };
625 self.dispatch_input_actions(dispatched_actions)?;
626 }
627
628 let pointer_targets_scrollbar = match &event {
634 InputEvent::Pointer(PointerEvent::Down { point, button, .. })
635 if matches!(button, PointerButton::Primary) =>
636 {
637 scrollbar_hit_test(ir, layout, &self.runtime_state.scroll, *point).is_some()
638 }
639 InputEvent::Pointer(PointerEvent::Move { .. })
640 | InputEvent::Pointer(PointerEvent::Up { .. }) => {
641 self.runtime_state.gesture.scrollbar_drag.is_some()
642 }
643 InputEvent::Pointer(PointerEvent::Scroll { point, .. }) => {
644 scrollbar_hit_test(ir, layout, &self.runtime_state.scroll, *point).is_some()
645 }
646 _ => false,
647 };
648
649 if !pointer_targets_scrollbar {
650 if let Some(point) = Self::event_point(&event) {
651 if let Some(hit_node_id) =
652 hit_test_with_scroll(ir, layout, &self.runtime_state.scroll, point)
653 {
654 let mut target_ro: Option<(WidgetId, &fission_ir::AnyRenderObject)> = None;
659 {
660 let mut walk = Some(hit_node_id);
661 while let Some(nid) = walk {
662 if let Some(ro) = ir.custom_render_objects.get(&nid) {
663 target_ro = Some((nid, ro));
664 break;
665 }
666 walk = ir.nodes.get(&nid).and_then(|n| n.parent);
667 }
668 }
669 if target_ro.is_none() {
670 for (ro_nid, ro) in &ir.custom_render_objects {
671 if let Some(rect) = layout.get_node_rect(*ro_nid) {
672 if rect.contains(point) {
673 target_ro = Some((*ro_nid, ro));
674 break;
675 }
676 }
677 }
678 }
679
680 if let Some((nid, any_ro)) = target_ro {
681 if let Some(render_obj) = downcast_render_object(any_ro) {
682 let mut node_rect = layout
683 .get_node_rect(nid)
684 .unwrap_or(LayoutRect::new(0.0, 0.0, 0.0, 0.0));
685 {
688 let mut walk = ir.nodes.get(&nid).and_then(|n| n.parent);
689 while let Some(pid) = walk {
690 if let Some(pnode) = ir.nodes.get(&pid) {
691 if let fission_ir::Op::Layout(
692 fission_ir::LayoutOp::Scroll { direction, .. },
693 ) = &pnode.op
694 {
695 let off = self.runtime_state.scroll.get_offset(pid);
696 match direction {
697 fission_ir::FlexDirection::Row => {
698 node_rect.origin.x -= off
699 }
700 fission_ir::FlexDirection::Column => {
701 node_rect.origin.y -= off
702 }
703 }
704 }
705 walk = pnode.parent;
706 } else {
707 break;
708 }
709 }
710 }
711 let result = render_obj.handle_event(nid, &event, node_rect);
712 if result.handled {
713 if matches!(event, InputEvent::Pointer(PointerEvent::Down { .. })) {
715 let old_focused_id = self.runtime_state.interaction.focused;
716 if Some(nid) != old_focused_id {
717 self.clear_text_pending_on_blur(old_focused_id, Some(nid));
718 self.dispatch_custom_blur_actions(ir, old_focused_id)?;
719 }
720 self.runtime_state.interaction.set_focused(Some(nid));
721 if let Some(ime_handler) = &self.ime_handler {
722 let accepts_text = render_obj.accepts_text_input();
723 ime_handler.set_ime_allowed(accepts_text);
724 if accepts_text {
725 if let Some(rect) =
726 render_obj.ime_cursor_area(node_rect)
727 {
728 ime_handler.set_ime_cursor_area(rect);
729 }
730 }
731 }
732 }
733 for (target, envelope) in result.actions {
735 self.dispatch_node(envelope, target)?;
736 }
737 return Ok(());
738 }
739 }
740 }
741 }
742 }
743 }
744
745 if matches!(event, InputEvent::Keyboard(_) | InputEvent::Ime(_)) {
751 if let Some(focused_id) = self.runtime_state.interaction.focused {
752 let mut walk_id = Some(focused_id);
753 while let Some(nid) = walk_id {
754 if let Some(any_ro) = ir.custom_render_objects.get(&nid) {
755 if let Some(render_obj) = downcast_render_object(any_ro) {
756 let node_rect = layout
757 .get_node_rect(nid)
758 .unwrap_or(LayoutRect::new(0.0, 0.0, 0.0, 0.0));
759 let result = render_obj.handle_event(nid, &event, node_rect);
760 if result.handled {
761 for (target, envelope) in result.actions {
762 self.dispatch_node(envelope, target)?;
763 }
764 return Ok(());
765 }
766 }
767 }
768 walk_id = ir.nodes.get(&nid).and_then(|n| n.parent);
769 }
770 }
771 }
772
773 let (handled, dispatched_actions) = {
774 let mut ctx = ControllerContext {
775 ir,
776 layout,
777 text_edit: &mut self.runtime_state.text_edit,
778 interaction: &mut self.runtime_state.interaction,
779 scroll: &mut self.runtime_state.scroll,
780 gesture: &mut self.runtime_state.gesture,
781 clipboard: self.clipboard_backend.as_ref(),
782 measurer: self.measurer.as_ref(),
783 dispatched_actions: Vec::new(),
784 };
785
786 let mut hover_controller = HoverController;
787 let _ = hover_controller.handle_event(&mut ctx, &event);
788
789 let mut gesture_controller = GestureController;
790 let handled = if gesture_controller.handle_event(&mut ctx, &event) {
791 true
792 } else {
793 let mut text_controller = TextInputController;
794 if text_controller.handle_event(&mut ctx, &event) {
795 true
796 } else {
797 let mut slider_controller = SliderController;
798 slider_controller.handle_event(&mut ctx, &event)
799 }
800 };
801 (handled, ctx.dispatched_actions)
802 };
803
804 self.dispatch_input_actions(dispatched_actions)?;
805
806 if handled {
807 if matches!(event, InputEvent::Pointer(PointerEvent::Up { .. })) {
808 self.runtime_state.interaction.pressed.clear();
809 self.runtime_state.interaction.last_down_point = None;
810 }
811 return Ok(());
812 }
813
814 match event {
815 InputEvent::Pointer(PointerEvent::Scroll { point, delta, .. }) => {
816 let trace_scroll =
817 std::env::var("FISSION_SCROLL_TRACE").ok().as_deref() == Some("1");
818 if trace_scroll {
819 eprintln!(
820 "[scroll-trace] event point=({:.1},{:.1}) delta=({:.1},{:.1})",
821 point.x, point.y, delta.x, delta.y
822 );
823 }
824 let hit_node_id = scrollbar_hit_test(ir, layout, &self.runtime_state.scroll, point)
825 .map(|hit| hit.geometry.node_id)
826 .or_else(|| {
827 hit_test_with_scroll(ir, layout, &self.runtime_state.scroll, point)
828 });
829 if let Some(hit_node_id) = hit_node_id {
830 if trace_scroll {
831 eprintln!("[scroll-trace] hit_node={}", hit_node_id.as_u128());
832 }
833 let mut current_id = Some(hit_node_id);
834 while let Some(node_id) = current_id {
835 if let Some(node) = ir.nodes.get(&node_id) {
836 if let Op::Layout(LayoutOp::Scroll { direction, .. }) = &node.op {
837 let current_offset = self.runtime_state.scroll.get_offset(node_id);
838 let delta_val = match direction {
839 FlexDirection::Row => delta.x,
840 FlexDirection::Column => delta.y,
841 };
842 let mut new_offset = current_offset + delta_val;
843
844 let mut max_offset = 0.0f32;
845 let mut viewport_w = 0.0f32;
846 let mut viewport_h = 0.0f32;
847 let mut content_w = 0.0f32;
848 let mut content_h = 0.0f32;
849 if let Some(geom) = layout.get_node_geometry(node_id) {
850 viewport_w = geom.rect.width();
851 viewport_h = geom.rect.height();
852 content_w = geom.content_size.width;
853 content_h = geom.content_size.height;
854 max_offset = if matches!(direction, FlexDirection::Row) {
855 (geom.content_size.width - geom.rect.width()).max(0.0)
856 } else {
857 (geom.content_size.height - geom.rect.height()).max(0.0)
858 };
859 new_offset = new_offset.clamp(0.0, max_offset);
860 }
861
862 if trace_scroll {
863 eprintln!(
864 "[scroll-trace] scroll_node={} axis={} offset={:.1}->{:.1} max={:.1} viewport=({:.1},{:.1}) content=({:.1},{:.1})",
865 node_id.as_u128(),
866 match direction { FlexDirection::Row => "x", FlexDirection::Column => "y" },
867 current_offset,
868 new_offset,
869 max_offset,
870 viewport_w,
871 viewport_h,
872 content_w,
873 content_h
874 );
875 }
876
877 {
878 use fission_diagnostics::prelude as diag;
879 diag::emit(
880 diag::DiagCategory::Input,
881 diag::DiagLevel::Debug,
882 diag::DiagEventKind::ScrollUpdate {
883 node: node_id.as_u128(),
884 axis: match direction {
885 FlexDirection::Row => "x".into(),
886 FlexDirection::Column => "y".into(),
887 },
888 point_x: point.x,
889 point_y: point.y,
890 delta: delta_val,
891 old_offset: current_offset,
892 new_offset,
893 max_offset,
894 viewport_w,
895 viewport_h,
896 content_w,
897 content_h,
898 },
899 );
900 }
901
902 self.runtime_state.scroll.set_offset(node_id, new_offset);
903 if (new_offset - current_offset).abs() > 0.001 {
907 break;
908 }
909 }
911 current_id = node.parent;
912 } else {
913 break;
914 }
915 }
916 } else if trace_scroll {
917 eprintln!("[scroll-trace] hit_test: no node");
918 }
919 }
920 InputEvent::Keyboard(KeyEvent::Down {
921 key_code,
922 modifiers,
923 }) => match key_code {
924 KeyCode::Tab => {
925 let reverse = (modifiers & 1) != 0;
926 let old_focus = self.runtime_state.interaction.focused;
927 let next =
928 find_next_focus_node(ir, self.runtime_state.interaction.focused, reverse);
929 if next != old_focus {
930 self.clear_text_pending_on_blur(old_focus, next);
931 self.dispatch_custom_blur_actions(ir, old_focus)?;
932 }
933 self.runtime_state.interaction.set_focused(next);
934 }
935 KeyCode::Up | KeyCode::Down | KeyCode::Left | KeyCode::Right => {
936 if let Some(focused) = self.runtime_state.interaction.focused {
937 let dir = match key_code {
938 KeyCode::Up => FocusDirection::Up,
939 KeyCode::Down => FocusDirection::Down,
940 KeyCode::Left => FocusDirection::Left,
941 KeyCode::Right => FocusDirection::Right,
942 _ => unreachable!(),
943 };
944 if let Some(next) = find_neighbor_focus_node(ir, layout, focused, dir) {
945 self.clear_text_pending_on_blur(Some(focused), Some(next));
946 self.dispatch_custom_blur_actions(ir, Some(focused))?;
947 self.runtime_state.interaction.set_focused(Some(next));
948 }
949 }
950 }
951 KeyCode::Enter | KeyCode::Space => {
952 if let Some(focused_id) = self.runtime_state.interaction.focused {
953 let mut current_id = Some(focused_id);
954 while let Some(node_id) = current_id {
955 if let Some(node) = ir.nodes.get(&node_id) {
956 if let Op::Semantics(semantics) = &node.op {
957 if let Some(action_entry) = semantics.actions.entries.first() {
958 if let Some(payload) = &action_entry.payload_data {
959 let envelope = ActionEnvelope {
960 id: ActionId::from_u128(action_entry.action_id),
961 payload: payload.clone(),
962 };
963 let input = crate::input::scoped_action_input(
964 ir,
965 node_id,
966 ActionInput::None,
967 );
968 return self.dispatch_node_with_input(
969 envelope, node_id, &input,
970 );
971 }
972 }
973 }
974 current_id = node.parent;
975 } else {
976 break;
977 }
978 }
979 }
980 }
981 _ => {}
982 },
983 InputEvent::Pointer(PointerEvent::Down { point, .. }) => {
984 if let Some(hit_node_id) =
985 hit_test_with_scroll(ir, layout, &self.runtime_state.scroll, point)
986 {
987 diag::emit(
988 diag::DiagCategory::Input,
989 diag::DiagLevel::Debug,
990 diag::DiagEventKind::InputEvent {
991 kind: "pointer_down_hit".into(),
992 target: Some(hit_node_id.as_u128()),
993 position: Some((point.x, point.y)),
994 },
995 );
996 let mut focus_candidate = Some(hit_node_id);
997 while let Some(node_id) = focus_candidate {
998 if let Some(node) = ir.nodes.get(&node_id) {
999 if let Op::Semantics(s) = &node.op {
1000 if s.focusable {
1001 let old_focused_id = self.runtime_state.interaction.focused;
1002 if Some(node_id) != old_focused_id {
1003 self.clear_text_pending_on_blur(
1004 old_focused_id,
1005 Some(node_id),
1006 );
1007 self.dispatch_custom_blur_actions(ir, old_focused_id)?;
1008
1009 if s.role == fission_ir::semantics::Role::TextInput {
1010 if let Some(ime_handler) = &self.ime_handler {
1011 ime_handler.set_ime_allowed(true);
1012 }
1013 } else if let Some(ime_handler) = &self.ime_handler {
1014 ime_handler.set_ime_allowed(false);
1015 }
1016 }
1017 self.runtime_state.interaction.set_focused(Some(node_id));
1018 break;
1019 }
1020 }
1021 focus_candidate = node.parent;
1022 } else {
1023 break;
1024 }
1025 }
1026 if focus_candidate.is_none() {
1027 let old_focused_id = self.runtime_state.interaction.focused;
1028 if let Some(old_focused_id) = self.runtime_state.interaction.focused {
1029 if let Some(old_node) = ir.nodes.get(&old_focused_id) {
1030 if let Op::Semantics(s) = &old_node.op {
1031 if s.role == fission_ir::semantics::Role::TextInput {
1032 if let Some(ime_handler) = &self.ime_handler {
1033 ime_handler.set_ime_allowed(false);
1034 }
1035 }
1036 }
1037 }
1038 }
1039 self.clear_text_pending_on_blur(old_focused_id, None);
1040 self.dispatch_custom_blur_actions(ir, old_focused_id)?;
1041 self.runtime_state.interaction.set_focused(None);
1042 }
1043
1044 let mut current_pressed_id = Some(hit_node_id);
1045 while let Some(node_id) = current_pressed_id {
1046 self.runtime_state.interaction.set_pressed(node_id, true);
1047 if let Some(node) = ir.nodes.get(&node_id) {
1048 current_pressed_id = node.parent;
1049 } else {
1050 break;
1051 }
1052 }
1053 self.runtime_state.interaction.last_down_point = Some(point);
1054
1055 if let Some(focused_id) = self.runtime_state.interaction.focused {
1056 if let Some(node) = ir.nodes.get(&focused_id) {
1057 if let Op::Semantics(s) = &node.op {
1058 if s.role == fission_ir::semantics::Role::TextInput {
1059 if let Some(ime_handler) = &self.ime_handler {
1060 ime_handler.set_ime_cursor_area(LayoutRect::new(
1061 point.x, point.y, 2.0, 16.0,
1062 ));
1063 }
1064 }
1065 }
1066 }
1067 }
1068 } else {
1069 let old_focused_id = self.runtime_state.interaction.focused;
1070 if let Some(old_focused_id) = self.runtime_state.interaction.focused {
1071 if let Some(old_node) = ir.nodes.get(&old_focused_id) {
1072 if let Op::Semantics(s) = &old_node.op {
1073 if s.role == fission_ir::semantics::Role::TextInput {
1074 if let Some(ime_handler) = &self.ime_handler {
1075 ime_handler.set_ime_allowed(false);
1076 }
1077 }
1078 }
1079 }
1080 }
1081 self.clear_text_pending_on_blur(old_focused_id, None);
1082 self.dispatch_custom_blur_actions(ir, old_focused_id)?;
1083 self.runtime_state.interaction.set_focused(None);
1084 }
1085 }
1086 InputEvent::Pointer(PointerEvent::Up { point, .. }) => {
1087 self.runtime_state.interaction.pressed.clear();
1088 self.runtime_state.interaction.last_down_point = None;
1089 if let Some(hit_node_id) =
1090 hit_test_with_scroll(ir, layout, &self.runtime_state.scroll, point)
1091 {
1092 let mut current_id = Some(hit_node_id);
1093 while let Some(node_id) = current_id {
1094 if let Some(node) = ir.nodes.get(&node_id) {
1095 if let Op::Semantics(semantics) = &node.op {
1096 if semantics.role == fission_ir::semantics::Role::TextInput {
1097 } else if let Some(action_entry) = semantics.actions.entries.first()
1099 {
1100 if let Some(payload) = &action_entry.payload_data {
1101 let envelope = ActionEnvelope {
1102 id: ActionId::from_u128(action_entry.action_id),
1103 payload: payload.clone(),
1104 };
1105 diag::emit(
1106 diag::DiagCategory::Input,
1107 diag::DiagLevel::Debug,
1108 diag::DiagEventKind::InputEvent {
1109 kind: "pointer_up_dispatch".into(),
1110 target: Some(node_id.as_u128()),
1111 position: Some((point.x, point.y)),
1112 },
1113 );
1114 let input = crate::input::scoped_action_input(
1115 ir,
1116 node_id,
1117 ActionInput::None,
1118 );
1119 return self
1120 .dispatch_node_with_input(envelope, node_id, &input);
1121 }
1122 }
1123 }
1124 current_id = node.parent;
1125 } else {
1126 break;
1127 }
1128 }
1129 }
1130 }
1131 _ => {}
1132 }
1133 Ok(())
1134 }
1135
1136 pub fn clear_hover_state(&mut self, ir: &CoreIR, point: Option<LayoutPoint>) -> Result<bool> {
1137 use crate::input::hover::HoverController;
1138 use crate::input::ControllerContext;
1139
1140 let dispatched_actions = {
1141 let layout = &LayoutSnapshot::new(LayoutSize::ZERO);
1142 let mut ctx = ControllerContext {
1143 ir,
1144 layout,
1145 text_edit: &mut self.runtime_state.text_edit,
1146 interaction: &mut self.runtime_state.interaction,
1147 scroll: &mut self.runtime_state.scroll,
1148 gesture: &mut self.runtime_state.gesture,
1149 clipboard: self.clipboard_backend.as_ref(),
1150 measurer: self.measurer.as_ref(),
1151 dispatched_actions: Vec::new(),
1152 };
1153 let changed = HoverController::clear(&mut ctx, point);
1154 (changed, ctx.dispatched_actions)
1155 };
1156 self.dispatch_input_actions(dispatched_actions.1)?;
1157 Ok(dispatched_actions.0)
1158 }
1159
1160 fn dispatch_input_actions(
1161 &mut self,
1162 dispatched_actions: Vec<(WidgetId, ActionEnvelope, ActionInput)>,
1163 ) -> Result<()> {
1164 for (target, action, input) in dispatched_actions {
1165 self.dispatch_node_with_input(action, target, &input)?;
1166 }
1167 Ok(())
1168 }
1169
1170 fn clear_text_pending_on_blur(
1171 &mut self,
1172 old_focus: Option<WidgetId>,
1173 new_focus: Option<WidgetId>,
1174 ) {
1175 if old_focus == new_focus {
1176 return;
1177 }
1178 if let Some(old_id) = old_focus {
1179 if let Some(st) = self.runtime_state.text_edit.states.get_mut(&old_id) {
1180 st.pending_model_sync = false;
1181 st.clear_preedit();
1182 }
1183 }
1184 }
1185
1186 fn dispatch_custom_blur_actions(
1187 &mut self,
1188 ir: &CoreIR,
1189 old_focus: Option<WidgetId>,
1190 ) -> Result<()> {
1191 if let Some(old_id) = old_focus {
1192 if let Some(any_ro) = ir.custom_render_objects.get(&old_id) {
1193 if let Some(render_obj) = crate::ui::custom_render::downcast_render_object(any_ro) {
1194 if render_obj.accepts_text_input() {
1195 if let Some(ime_handler) = &self.ime_handler {
1196 ime_handler.set_ime_allowed(false);
1197 }
1198 }
1199 for (target, envelope) in render_obj.blur_actions(old_id) {
1200 self.dispatch_node(envelope, target)?;
1201 }
1202 }
1203 }
1204 }
1205 Ok(())
1206 }
1207
1208 pub fn hit_test(
1209 &self,
1210 point: LayoutPoint,
1211 ir: &CoreIR,
1212 snapshot: &LayoutSnapshot,
1213 ) -> Option<WidgetId> {
1214 if let Some(root) = ir.root {
1215 return self.hit_test_recursive(root, point, ir, snapshot);
1216 }
1217 None
1218 }
1219
1220 fn hit_test_recursive(
1221 &self,
1222 node_id: WidgetId,
1223 point: LayoutPoint,
1224 ir: &CoreIR,
1225 snapshot: &LayoutSnapshot,
1226 ) -> Option<WidgetId> {
1227 if let Some(geom) = snapshot.nodes.get(&node_id) {
1228 if geom.rect.contains(point) {
1229 if let Some(node) = ir.nodes.get(&node_id) {
1230 for child in node.children.iter().rev() {
1231 let mut child_point = point;
1232
1233 if let Op::Layout(LayoutOp::Scroll { direction, .. }) = &node.op {
1234 if !geom.rect.contains(point) {
1235 continue;
1236 }
1237 let offset = self.runtime_state.scroll.get_offset(node_id);
1238 match direction {
1239 FlexDirection::Row => child_point.x += offset,
1240 FlexDirection::Column => child_point.y += offset,
1241 }
1242 }
1243
1244 if let Op::Layout(LayoutOp::Transform { transform }) = &node.op {
1245 let mat = Mat4::from_cols_array(transform);
1246 let local_x = point.x - geom.rect.origin.x;
1283 let local_y = point.y - geom.rect.origin.y;
1284
1285 let p = Vec4::new(local_x, local_y, 0.0, 1.0);
1286 let inv = mat.inverse();
1287 let transformed = inv * p;
1288
1289 child_point = LayoutPoint::new(
1290 transformed.x + geom.rect.origin.x,
1291 transformed.y + geom.rect.origin.y,
1292 );
1293 }
1294
1295 if let Some(hit) =
1296 self.hit_test_recursive(*child, child_point, ir, snapshot)
1297 {
1298 return Some(hit);
1299 }
1300 }
1301
1302 match &node.op {
1303 Op::Paint(_)
1304 | Op::Layout(LayoutOp::Scroll { .. })
1305 | Op::Layout(LayoutOp::Embed { .. }) => return Some(node_id),
1306 _ => return None,
1307 }
1308 }
1309 return None;
1310 }
1311 }
1312 None
1313 }
1314
1315 fn event_point(event: &InputEvent) -> Option<LayoutPoint> {
1321 match event {
1322 InputEvent::Pointer(PointerEvent::Down { point, .. })
1323 | InputEvent::Pointer(PointerEvent::Up { point, .. })
1324 | InputEvent::Pointer(PointerEvent::Move { point, .. })
1325 | InputEvent::Pointer(PointerEvent::Scroll { point, .. }) => Some(*point),
1326 _ => None,
1327 }
1328 }
1329
1330 fn find_autofocus_node(ir: &CoreIR) -> Option<WidgetId> {
1331 fn walk(ir: &CoreIR, node_id: WidgetId) -> Option<WidgetId> {
1332 let node = ir.nodes.get(&node_id)?;
1333 if let Op::Semantics(semantics) = &node.op {
1334 if semantics.autofocus && semantics.focusable && !semantics.disabled {
1335 return Some(node_id);
1336 }
1337 }
1338 for child_id in &node.children {
1339 if let Some(found) = walk(ir, *child_id) {
1340 return Some(found);
1341 }
1342 }
1343 None
1344 }
1345
1346 ir.root.and_then(|root| walk(ir, root))
1347 }
1348
1349 pub fn reconcile_resources(
1350 &mut self,
1351 declarations: Vec<RuntimeResourceDeclaration>,
1352 ) -> Result<()> {
1353 let now = self.clock().current_time();
1354 let mut existing = std::mem::take(&mut self.active_resources);
1355 let mut next = HashMap::new();
1356
1357 for declaration in declarations {
1358 let key = declaration.key.clone();
1359 match existing.remove(&key) {
1360 Some(current)
1361 if current.policy == declaration.policy
1362 && current.deps == declaration.deps
1363 && current.matches_kind(&declaration.kind) =>
1364 {
1365 next.insert(key, current);
1366 }
1367 Some(current) if declaration.policy == ResourcePolicy::PreserveOnChange => {
1368 next.insert(key, current);
1369 }
1370 Some(current) => {
1371 self.stop_resource(&key, ¤t);
1372 let replacement = self.start_resource(declaration, now);
1373 next.insert(key, replacement);
1374 }
1375 None => {
1376 let resource = self.start_resource(declaration, now);
1377 next.insert(key, resource);
1378 }
1379 }
1380 }
1381
1382 for (key, resource) in existing {
1383 self.stop_resource(&key, &resource);
1384 }
1385
1386 self.active_resources = next;
1387 Ok(())
1388 }
1389
1390 pub fn resource_generation(&self, key: &str) -> Option<u64> {
1391 self.active_resources
1392 .get(key)
1393 .map(|resource| resource.generation)
1394 }
1395
1396 pub fn is_resource_current(&self, resource: &ResourceExecutionContext) -> bool {
1397 self.resource_generation(&resource.key) == Some(resource.generation)
1398 }
1399
1400 fn start_resource(
1401 &mut self,
1402 declaration: RuntimeResourceDeclaration,
1403 now: CurrentTime,
1404 ) -> ActiveResource {
1405 let generation = self.next_resource_generation;
1406 self.next_resource_generation += 1;
1407
1408 let context = ResourceExecutionContext {
1409 key: declaration.key.clone(),
1410 generation,
1411 };
1412
1413 let kind = match declaration.kind {
1414 RuntimeResourceKind::Job(mut job) => {
1415 job.effect.resource = Some(context);
1416 self.enqueue_effect(job.effect);
1417 ActiveResourceKind::Job
1418 }
1419 RuntimeResourceKind::Service(mut service) => {
1420 service.effect.resource = Some(context);
1421 let (service_name, slot_key) = match &service.effect.effect {
1422 crate::Effect::StartService(payload) => {
1423 (payload.service_name.clone(), payload.slot_key.clone())
1424 }
1425 _ => unreachable!("service resource must lower to StartService"),
1426 };
1427 self.enqueue_effect(service.effect);
1428 ActiveResourceKind::Service {
1429 service_name,
1430 slot_key,
1431 }
1432 }
1433 RuntimeResourceKind::Timer(timer) => self.start_timer_resource(timer, now),
1434 };
1435
1436 ActiveResource {
1437 generation,
1438 deps: declaration.deps,
1439 policy: declaration.policy,
1440 kind,
1441 }
1442 }
1443
1444 fn start_timer_resource(&self, timer: TimerResource, now: CurrentTime) -> ActiveResourceKind {
1445 let interval_ms = timer.interval_ms.max(1);
1446 ActiveResourceKind::Timer {
1447 interval_ms,
1448 payload: timer.payload,
1449 on_tick: timer.on_tick,
1450 next_fire_at: if timer.immediate {
1451 now
1452 } else {
1453 now.saturating_add(interval_ms)
1454 },
1455 }
1456 }
1457
1458 fn stop_resource(&mut self, key: &str, resource: &ActiveResource) {
1459 if let ActiveResourceKind::Service {
1460 service_name,
1461 slot_key,
1462 } = &resource.kind
1463 {
1464 self.enqueue_effect(EffectEnvelope {
1465 req_id: 0,
1466 effect: crate::Effect::StopService(ServiceStopPayload {
1467 service_name: service_name.clone(),
1468 slot_key: slot_key.clone(),
1469 }),
1470 on_ok: None,
1471 on_err: None,
1472 service_bindings: None,
1473 resource: Some(ResourceExecutionContext {
1474 key: key.to_string(),
1475 generation: resource.generation,
1476 }),
1477 });
1478 }
1479 }
1480}
1481
1482impl ActiveResource {
1483 fn matches_kind(&self, kind: &RuntimeResourceKind) -> bool {
1484 matches!(
1485 (&self.kind, kind),
1486 (ActiveResourceKind::Job, RuntimeResourceKind::Job(_))
1487 | (
1488 ActiveResourceKind::Timer { .. },
1489 RuntimeResourceKind::Timer(_)
1490 )
1491 | (
1492 ActiveResourceKind::Service { .. },
1493 RuntimeResourceKind::Service(_)
1494 )
1495 )
1496 }
1497}