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