Skip to main content

fission_core/
runtime.rs

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
52/// The core runtime that owns application state, reducers, and the effect queue.
53///
54/// `Runtime` is the single entry point for the action/reducer pipeline. Platform
55/// shells create one `Runtime`, register their `GlobalState`, build the widget tree
56/// each frame, absorb the resulting [`ActionRegistry`], and call
57/// [`Runtime::handle_input`] to process user events.
58///
59/// # Lifecycle
60///
61/// ```text
62/// 1. runtime = Runtime::default()
63///        .with_measurer(measurer)
64///        .with_clipboard(clipboard);
65/// 2. runtime.add_global_state(Box::new(MyState::default()))?;
66/// 3. loop {
67///        let mut ctx = BuildCtx::new();
68///        let tree = fission_core::build::enter(&mut ctx, &view, || MyRoot.into());
69///        runtime.clear_reducers();
70///        runtime.absorb_registry(ctx.registry);
71///        // lower tree -> IR -> layout -> render
72///        runtime.handle_input(event, &ir, &layout)?;
73///        runtime.tick(dt)?;
74///    }
75/// ```
76///
77/// # Example
78///
79/// ```rust,ignore
80/// let mut runtime = Runtime::default();
81/// runtime.add_global_state(Box::new(Counter { count: 0 }))?;
82/// runtime.tick(16)?;
83/// ```
84pub struct Runtime {
85    /// Per-frame reducers, cleared and re-populated every frame via
86    /// [`absorb_registry`](Runtime::absorb_registry).
87    pub(crate) reducers: HashMap<ActionId, Vec<BoxedReducer>>,
88    /// Persistent reducers that survive [`clear_reducers`](Runtime::clear_reducers)
89    /// calls, installed once at app startup.
90    pub(crate) persistent_reducers: HashMap<ActionId, Vec<BoxedReducer>>,
91    /// Type-indexed application state store.
92    pub app_states: HashMap<TypeId, Box<dyn GlobalState>>,
93    /// Mutable runtime state (interaction, scroll, text editing, motions).
94    pub runtime_state: RuntimeState,
95    /// Platform-provided text measurer for layout.
96    pub measurer: Option<Arc<dyn TextMeasurer>>,
97    /// Platform-provided clipboard backend.
98    pub clipboard_backend: Option<Arc<dyn Clipboard>>,
99    /// Platform-provided IME (Input Method Editor) handler.
100    pub ime_handler: Option<Arc<dyn ImeHandler>>,
101    /// Effects emitted by reducers, awaiting platform execution.
102    pub pending_effects: Vec<EffectEnvelope>,
103    /// Monotonically increasing counter for deterministic request id generation.
104    pub next_req_id: u64,
105    /// Declarative runtime resources that currently exist.
106    active_resources: HashMap<String, ActiveResource>,
107    /// Monotonically increasing generation counter for runtime resources.
108    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    // Helper for manual reducer registration (internal use)
180    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        // Wrap legacy 3-arg reducer into 5-arg BoxedReducer
188        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    /// Registers reducers that should survive `clear_reducers()` calls.
250    ///
251    /// This is intended for app-level "global" handlers (e.g. system effects) that
252    /// are installed once at app startup, while per-frame widget handlers are
253    /// regenerated every frame via `BuildCtx` and `absorb_registry`.
254    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        // Delegate video actions to media module
349        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        // Collect effects from this dispatch (both persistent and per-frame reducers).
360        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            // Only update URL if it changes to avoid reload loops
517            if entry.url != reg.url {
518                entry.url = reg.url.clone();
519                entry.loading = true; // Assume loading starts
520            }
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        // Detection logic for future flight motions
544        for (tag, (_new_id, new_rect)) in &current_heroes {
545            if let Some((_old_id, old_rect)) = self.runtime_state.hero.positions.get(tag) {
546                if *new_rect != *old_rect {
547                    // Logic to spawn overlay flight ghost would go here
548                    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        // --- Custom render object event handling (runs first) ----------------
629        // For pointer events we hit-test, then walk up from the hit node to
630        // check whether any ancestor carries a custom render object.  The
631        // first one that returns `handled = true` short-circuits the entire
632        // standard controller chain.
633        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                    // Find the custom render object for this click.  Walk up from the
655                    // hit node first; if not found, check all registered render objects
656                    // by rect containment (the hit may be on a wrapper node above the
657                    // InternalRenderNode's lowered subtree).
658                    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                            // Adjust node_rect by ancestor scroll offsets so it reflects
686                            // the VISUAL position, matching the screen-coordinate click.
687                            {
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                                // Set focus to this node so keyboard events route here
714                                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                                // Dispatch any actions the render object produced.
734                                for (target, envelope) in result.actions {
735                                    self.dispatch_node(envelope, target)?;
736                                }
737                                return Ok(());
738                            }
739                        }
740                    }
741                }
742            }
743        }
744
745        // --- Keyboard events → focused node's custom render object -----------
746        // Keyboard events have no point, so we route them to the focused node
747        // (if any) and walk up its ancestor chain looking for a custom render
748        // object.  This allows custom editor nodes to handle arrow keys,
749        // typing, etc. before the framework's default focus-navigation logic.
750        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 scroll actually changed, consume the event.
904                                // If it didn't (clamped to same value, e.g. max_offset==0),
905                                // propagate to parent scroll nodes.
906                                if (new_offset - current_offset).abs() > 0.001 {
907                                    break;
908                                }
909                                // Fall through to parent
910                            }
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                                    // No action
1098                                } 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                            // We need to transform the point relative to the node's origin?
1247                            // Layout coordinates are relative to the parent.
1248                            // In hit_test_recursive, `point` is relative to current `node_id`?
1249                            // No, `point` is relative to the `geom.rect.origin` of `node_id`?
1250                            // Let's check recursion.
1251
1252                            // hit_test starts at root with absolute point.
1253                            // recursion: `child_point = point`.
1254                            // wait, `hit_test_recursive` doesn't subtract location?
1255                            // Ah, I see: `if geom.rect.contains(point)`.
1256                            // This implies `point` is ABSOLUTE.
1257
1258                            // If `point` is absolute, and we want to transform into child local space:
1259                            // 1. Move point to node local space: `point - node_pos`.
1260                            // 2. Apply inverse transform.
1261                            // 3. (Implicitly) Move back or keep local?
1262                            // Recursive call expects absolute point?
1263                            // No, `hit_test_recursive` calls itself with `child_point`.
1264                            // If it expects absolute point, then `Transform` node doesn't work well with absolute recursion.
1265
1266                            // Actually, my `hit_test_recursive` impl seems to assume absolute points for all nodes?
1267                            // `if geom.rect.contains(point)` confirms it.
1268
1269                            // So if I have a Transform, I MUST return a point that looks "absolute" to the child
1270                            // but is logically transformed.
1271                            // Absolute child rect is NOT transformed by LayoutEngine.
1272
1273                            // This means `geom.rect` for children of a Transform is WRONG if they are visually moved.
1274                            // BUT LayoutEngine doesn't know about Matrix4.
1275                            // So the children think they are at `(0,0)` relative to parent.
1276
1277                            // To make hit test work:
1278                            // 1. Convert absolute `point` to `node_local_point`.
1279                            // 2. Apply inverse transform to `node_local_point` -> `transformed_local_point`.
1280                            // 3. Convert `transformed_local_point` back to absolute for children -> `transformed_absolute_point`.
1281
1282                            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    /// Extract the pointer position from an input event, if applicable.
1316    ///
1317    /// Used by the custom-render-object event dispatch to perform a hit-test
1318    /// before delegating to render objects.  Returns `None` for keyboard and
1319    /// other non-positional events.
1320    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, &current);
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}