1use crate::action::{ActionEnvelope, ActionId, GlobalState};
2use crate::async_runtime::ServiceStopPayload;
3use crate::effect::{
4 ActionInput, Effect, EffectEnvelope, RuntimeEffect, ScrollAlignment, ScrollAxis,
5 ScrollBehavior, ScrollIntoViewRequest,
6};
7use crate::env::{RuntimeState, VideoStatus};
8use crate::registry::{
9 ActionRegistry, ResourcePolicy, RuntimeResourceDeclaration, RuntimeResourceKind, TimerResource,
10 VideoRegistration,
11};
12use crate::BoxedReducer;
13use crate::{
14 Clipboard, Clock, CurrentTime, ImeHandler, InputEvent, KeyCode, KeyEvent, PointerButton,
15 PointerEvent, ResourceExecutionContext,
16};
17use anyhow::{anyhow, Result};
18use fission_diagnostics::prelude as diag;
19use fission_ir::{CoreIR, FlexDirection, FocusPolicy, LayoutOp, Op, WidgetId};
20use fission_layout::{LayoutPoint, LayoutRect, LayoutSize, LayoutSnapshot, TextMeasurer};
21use glam::{Mat4, Vec4};
22use serde_json;
23use std::any::TypeId;
24use std::collections::{HashMap, HashSet};
25use std::sync::Arc;
26
27#[derive(Debug, Default, Clone)]
28pub struct TickResult {
29 pub changed_motions: Vec<(WidgetId, crate::MotionPropertyId)>,
30}
31
32#[derive(Debug, Clone)]
33enum ActiveResourceKind {
34 Job,
35 Service {
36 service_name: String,
37 slot_key: String,
38 },
39 Timer {
40 interval_ms: u64,
41 payload: Vec<u8>,
42 on_tick: Option<ActionEnvelope>,
43 next_fire_at: CurrentTime,
44 },
45}
46
47#[derive(Debug, Clone)]
48struct ActiveResource {
49 generation: u64,
50 deps: Option<Vec<u8>>,
51 policy: ResourcePolicy,
52 kind: ActiveResourceKind,
53}
54
55#[derive(Debug, Clone)]
56struct PendingScrollIntoView {
57 request: ScrollIntoViewRequest,
58 retries_remaining: u8,
59}
60
61#[derive(Debug, Clone, Copy, PartialEq, Eq)]
62enum ScrollIntoViewOutcome {
63 Applied { changed: bool },
64 Retry,
65 Ignored,
66}
67
68pub struct Runtime {
101 pub(crate) reducers: HashMap<ActionId, Vec<BoxedReducer>>,
104 pub(crate) persistent_reducers: HashMap<ActionId, Vec<BoxedReducer>>,
107 pub app_states: HashMap<TypeId, Box<dyn GlobalState>>,
109 pub runtime_state: RuntimeState,
111 pub measurer: Option<Arc<dyn TextMeasurer>>,
113 pub clipboard_backend: Option<Arc<dyn Clipboard>>,
115 pub ime_handler: Option<Arc<dyn ImeHandler>>,
117 pub pending_effects: Vec<EffectEnvelope>,
119 pending_scroll_into_view: Vec<PendingScrollIntoView>,
121 pub next_req_id: u64,
123 active_resources: HashMap<String, ActiveResource>,
125 next_resource_generation: u64,
127}
128
129impl Default for Runtime {
130 fn default() -> Self {
131 let mut runtime = Self {
132 reducers: HashMap::new(),
133 persistent_reducers: HashMap::new(),
134 app_states: HashMap::new(),
135 runtime_state: RuntimeState::default(),
136 measurer: None,
137 clipboard_backend: None,
138 ime_handler: None,
139 pending_effects: Vec::new(),
140 pending_scroll_into_view: Vec::new(),
141 next_req_id: 0,
142 active_resources: HashMap::new(),
143 next_resource_generation: 1,
144 };
145
146 runtime
147 .add_global_state(Box::new(runtime.runtime_state.local_widget_state.clone()))
148 .expect("Failed to add local widget state store");
149
150 runtime
151 .add_global_state(Box::new(Clock::default()))
152 .expect("Failed to add Clock state");
153
154 runtime.register_base_reducers();
155
156 runtime
157 }
158}
159
160impl Runtime {
161 pub fn with_measurer(mut self, measurer: Arc<dyn TextMeasurer>) -> Self {
162 self.measurer = Some(measurer);
163 self
164 }
165
166 pub fn with_clipboard(mut self, backend: Arc<dyn Clipboard>) -> Self {
167 self.clipboard_backend = Some(backend);
168 self
169 }
170
171 pub fn with_ime_handler(mut self, handler: Arc<dyn ImeHandler>) -> Self {
172 self.ime_handler = Some(handler);
173 self
174 }
175
176 pub fn caret_from_point_in_text(
177 &self,
178 value: &str,
179 font_size: f32,
180 viewport_x: f32,
181 viewport_w: f32,
182 content_w: f32,
183 scroll_offset: f32,
184 point_x: f32,
185 ) -> usize {
186 crate::input::text::caret_from_point_in_text(
187 self.measurer.as_ref(),
188 value,
189 font_size,
190 viewport_x,
191 viewport_w,
192 content_w,
193 scroll_offset,
194 point_x,
195 )
196 }
197
198 pub fn register_reducer<S: GlobalState + 'static>(
200 &mut self,
201 action_id: ActionId,
202 reducer_fn: crate::action::Reducer<S>,
203 ) -> Result<()> {
204 let state_type_id = TypeId::of::<S>();
205
206 let boxed_reducer: BoxedReducer = Box::new(
208 move |app_states: &mut HashMap<TypeId, Box<dyn GlobalState>>,
209 action: &ActionEnvelope,
210 target: WidgetId,
211 _effects: &mut Vec<EffectEnvelope>,
212 _input: &ActionInput|
213 -> Result<()> {
214 if let Some(state_box) = app_states.get_mut(&state_type_id) {
215 let concrete_state = state_box.downcast_mut::<S>().ok_or_else(|| {
216 anyhow!("Failed to downcast GlobalState to concrete type for reducer")
217 })?;
218 reducer_fn(concrete_state, action, target.into())
219 } else {
220 anyhow::bail!("Target GlobalState for reducer not found in runtime.");
221 }
222 },
223 );
224
225 self.reducers
226 .entry(action_id)
227 .or_default()
228 .push(boxed_reducer);
229 Ok(())
230 }
231
232 pub fn register_base_reducers(&mut self) {
233 use crate::{AdvanceTo, Tick, ADVANCE_TO_ACTION_ID, TICK_ACTION_ID};
234
235 self.register_reducer::<Clock>(
236 *TICK_ACTION_ID,
237 |state: &mut Clock, action: &ActionEnvelope, _target| {
238 let tick_action: Tick = serde_json::from_slice(&action.payload)
239 .map_err(|e| anyhow!("Failed to deserialize Tick: {}", e))?;
240 state.advance_by(tick_action.dt)
241 },
242 )
243 .expect("Failed to register Tick reducer");
244
245 self.register_reducer::<Clock>(
246 *ADVANCE_TO_ACTION_ID,
247 |state: &mut Clock, action: &ActionEnvelope, _target| {
248 let advance_action: AdvanceTo = serde_json::from_slice(&action.payload)
249 .map_err(|e| anyhow!("Failed to deserialize AdvanceTo: {}", e))?;
250 state.set_to(advance_action.time)
251 },
252 )
253 .expect("Failed to register AdvanceTo reducer");
254 }
255
256 pub fn clear_reducers(&mut self) {
257 self.reducers.clear();
258 self.register_base_reducers();
259 }
260
261 pub fn absorb_registry<S: GlobalState>(&mut self, registry: ActionRegistry<S>) {
262 let new_reducers = registry.into_runtime_reducers();
263 for (id, mut list) in new_reducers {
264 self.reducers.entry(id).or_default().append(&mut list);
265 }
266 }
267
268 pub fn absorb_persistent_registry<S: GlobalState>(&mut self, registry: ActionRegistry<S>) {
274 let new_reducers = registry.into_runtime_reducers();
275 for (id, mut list) in new_reducers {
276 self.persistent_reducers
277 .entry(id)
278 .or_default()
279 .append(&mut list);
280 }
281 }
282
283 pub fn clock(&self) -> &Clock {
284 self.get_global_state::<Clock>()
285 .expect("Clock state must always be present")
286 }
287
288 pub fn get_global_state<S: GlobalState + 'static>(&self) -> Option<&S> {
289 self.app_states
290 .get(&TypeId::of::<S>())
291 .and_then(|s_box| s_box.downcast_ref::<S>())
292 }
293
294 pub fn get_global_state_mut<S: GlobalState + 'static>(&mut self) -> Option<&mut S> {
295 self.app_states
296 .get_mut(&TypeId::of::<S>())
297 .and_then(|s_box| s_box.downcast_mut::<S>())
298 }
299
300 pub fn add_global_state<S: GlobalState + 'static>(&mut self, state: Box<S>) -> Result<()> {
301 let type_id = TypeId::of::<S>();
302 if self.app_states.insert(type_id, state).is_some() {
303 anyhow::bail!("Global state of this type already registered.");
304 }
305 Ok(())
306 }
307
308 pub fn with_global_state<S: GlobalState + 'static>(mut self, state: S) -> Self {
309 self.app_states.insert(TypeId::of::<S>(), Box::new(state));
310 self
311 }
312
313 #[doc(hidden)]
314 pub fn get_app_state<S: GlobalState + 'static>(&self) -> Option<&S> {
315 self.get_global_state::<S>()
316 }
317
318 #[doc(hidden)]
319 pub fn get_app_state_mut<S: GlobalState + 'static>(&mut self) -> Option<&mut S> {
320 self.get_global_state_mut::<S>()
321 }
322
323 #[doc(hidden)]
324 pub fn add_app_state<S: GlobalState + 'static>(&mut self, state: Box<S>) -> Result<()> {
325 self.add_global_state(state)
326 }
327
328 pub fn dispatch(&mut self, action: ActionEnvelope, target: WidgetId) -> Result<()> {
329 self.dispatch_with_input(action, target, &ActionInput::None)
330 }
331
332 fn enqueue_effect(&mut self, mut envelope: EffectEnvelope) {
333 envelope.req_id = self.next_req_id;
334 self.next_req_id += 1;
335 self.pending_effects.push(envelope);
336 }
337
338 pub fn dispatch_with_input(
339 &mut self,
340 action: ActionEnvelope,
341 target: WidgetId,
342 input: &ActionInput,
343 ) -> Result<()> {
344 self.dispatch_node_with_input(action, target.into(), input)
345 }
346
347 fn dispatch_node(&mut self, action: ActionEnvelope, target: WidgetId) -> Result<()> {
348 self.dispatch_node_with_input(action, target, &ActionInput::None)
349 }
350
351 fn dispatch_node_with_input(
352 &mut self,
353 action: ActionEnvelope,
354 target: WidgetId,
355 input: &ActionInput,
356 ) -> Result<()> {
357 diag::emit(
358 diag::DiagCategory::Input,
359 diag::DiagLevel::Debug,
360 diag::DiagEventKind::InputEvent {
361 kind: "dispatch_start".into(),
362 target: Some(target.as_u128()),
363 position: None,
364 },
365 );
366
367 if crate::media::handle_video_action(&mut self.runtime_state.video, &action)? {
369 return Ok(());
370 }
371
372 let action_id = action.id;
373
374 if crate::scoped_action_handlers::dispatch_scoped_action_handler(&action, target, input)? {
375 return Ok(());
376 }
377
378 let mut effects = Vec::new();
380
381 if let Some(reducers) = self.persistent_reducers.get_mut(&action_id) {
382 diag::emit(
383 diag::DiagCategory::Input,
384 diag::DiagLevel::Debug,
385 diag::DiagEventKind::InputEvent {
386 kind: format!("persistent_reducers:{}", reducers.len()),
387 target: Some(target.as_u128()),
388 position: None,
389 },
390 );
391
392 let mut temp_reducers: Vec<BoxedReducer> = reducers.drain(..).collect();
393 for reducer_wrapper in temp_reducers.iter_mut() {
394 reducer_wrapper(&mut self.app_states, &action, target, &mut effects, input)?;
395 }
396 reducers.extend(temp_reducers);
397 }
398
399 if let Some(reducers) = self.reducers.get_mut(&action_id) {
400 diag::emit(
401 diag::DiagCategory::Input,
402 diag::DiagLevel::Debug,
403 diag::DiagEventKind::InputEvent {
404 kind: format!("reducers:{}", reducers.len()),
405 target: Some(target.as_u128()),
406 position: None,
407 },
408 );
409
410 let mut temp_reducers: Vec<BoxedReducer> = reducers.drain(..).collect();
411 for reducer_wrapper in temp_reducers.iter_mut() {
412 reducer_wrapper(&mut self.app_states, &action, target, &mut effects, input)?;
413 }
414 reducers.extend(temp_reducers);
415 }
416
417 for envelope in effects {
418 self.enqueue_effect(envelope);
419 }
420
421 diag::emit(
422 diag::DiagCategory::Input,
423 diag::DiagLevel::Debug,
424 diag::DiagEventKind::InputEvent {
425 kind: "dispatch_end".into(),
426 target: Some(target.as_u128()),
427 position: None,
428 },
429 );
430 Ok(())
431 }
432
433 pub fn tick(&mut self, dt: CurrentTime) -> Result<TickResult> {
434 use crate::Tick;
435 let action = Tick { dt };
436 let envelope: ActionEnvelope = action.into();
437 self.dispatch_node(envelope, WidgetId::derived(0, &[0]))?;
438
439 self.tick_resource_timers()?;
440
441 let current_time = self.clock().current_time();
442 let changed_motions =
443 crate::motion::tick_motion(&mut self.runtime_state.motion, current_time);
444 Ok(TickResult { changed_motions })
445 }
446
447 fn tick_resource_timers(&mut self) -> Result<()> {
448 let now = self.clock().current_time();
449 let mut ticks = Vec::new();
450
451 for resource in self.active_resources.values_mut() {
452 if let ActiveResourceKind::Timer {
453 interval_ms,
454 payload,
455 on_tick,
456 next_fire_at,
457 } = &mut resource.kind
458 {
459 let Some(action) = on_tick.clone() else {
460 continue;
461 };
462
463 let interval_ms = (*interval_ms).max(1);
464 while now >= *next_fire_at {
465 ticks.push((action.clone(), payload.clone()));
466 *next_fire_at = next_fire_at.saturating_add(interval_ms);
467 }
468 }
469 }
470
471 for (action, payload) in ticks {
472 self.dispatch_node_with_input(
473 action,
474 WidgetId::derived(0, &[0]),
475 &ActionInput::TimerTick { payload },
476 )?;
477 }
478
479 Ok(())
480 }
481
482 pub fn sync_motion_declarations(
483 &mut self,
484 declarations: &[crate::MotionDeclaration],
485 layout: Option<&LayoutSnapshot>,
486 ) -> Vec<(WidgetId, crate::MotionPropertyId)> {
487 let current_time = self.clock().current_time();
488 let snapshot = self.runtime_state.clone();
489 let result = crate::motion::sync_motion_declarations(
490 &mut self.runtime_state.motion,
491 declarations,
492 &snapshot,
493 layout,
494 current_time,
495 );
496 result.changed
497 }
498
499 pub fn sync_video_nodes(&mut self, registrations: &[VideoRegistration]) {
500 let mut seen: HashSet<WidgetId> = HashSet::new();
501
502 for reg in registrations {
503 seen.insert(reg.node_id);
504 let entry = self
505 .runtime_state
506 .video
507 .states
508 .entry(reg.node_id)
509 .or_insert_with(crate::env::VideoState::default);
510 entry.asset_source = reg.source.clone();
511 entry.looped = reg.loop_playback;
512 entry.audio = reg.audio.clone();
513 if reg.autoplay && entry.status == VideoStatus::Stopped {
514 entry.status = VideoStatus::Playing;
515 }
516 }
517
518 self.runtime_state
519 .video
520 .states
521 .retain(|node_id, _| seen.contains(node_id));
522 }
523
524 pub fn sync_web_nodes(&mut self, registrations: &[crate::registry::WebRegistration]) {
525 let mut seen: HashSet<WidgetId> = HashSet::new();
526
527 for reg in registrations {
528 seen.insert(reg.node_id);
529 let entry = self
530 .runtime_state
531 .web
532 .states
533 .entry(reg.node_id)
534 .or_insert_with(crate::env::WebState::default);
535
536 if entry.url != reg.url {
538 entry.url = reg.url.clone();
539 entry.loading = true; }
541 entry.user_agent = reg.user_agent.clone();
542 }
543
544 self.runtime_state
545 .web
546 .states
547 .retain(|node_id, _| seen.contains(node_id));
548 }
549
550 pub fn queue_runtime_effect(&mut self, effect: RuntimeEffect) -> bool {
555 match effect {
556 RuntimeEffect::ScrollIntoView(request) => {
557 self.queue_scroll_into_view(request);
558 true
559 }
560 RuntimeEffect::Cancel { .. } | RuntimeEffect::ReleaseResource { .. } => false,
561 }
562 }
563
564 pub fn queue_scroll_into_view(&mut self, request: ScrollIntoViewRequest) {
566 self.pending_scroll_into_view.push(PendingScrollIntoView {
567 request,
568 retries_remaining: 1,
569 });
570 }
571
572 fn drain_scroll_into_view_effects(&mut self) {
573 let pending = std::mem::take(&mut self.pending_effects);
574
575 for env in pending {
576 let EffectEnvelope {
577 req_id,
578 effect,
579 on_ok,
580 on_err,
581 service_bindings,
582 resource,
583 } = env;
584
585 match effect {
586 Effect::Runtime(RuntimeEffect::ScrollIntoView(request)) => {
587 self.queue_scroll_into_view(request);
588 }
589 retained => self.pending_effects.push(EffectEnvelope {
590 req_id,
591 effect: retained,
592 on_ok,
593 on_err,
594 service_bindings,
595 resource,
596 }),
597 }
598 }
599 }
600
601 fn apply_pending_scroll_into_view(&mut self, ir: &CoreIR, layout: &LayoutSnapshot) -> bool {
602 self.drain_scroll_into_view_effects();
603
604 let mut needs_follow_up_frame = false;
605 let pending = std::mem::take(&mut self.pending_scroll_into_view);
606
607 for mut pending_request in pending {
608 match self.apply_scroll_into_view(&pending_request.request, ir, layout) {
609 ScrollIntoViewOutcome::Applied { changed } => {
610 needs_follow_up_frame |= changed;
611 }
612 ScrollIntoViewOutcome::Retry if pending_request.retries_remaining > 0 => {
613 pending_request.retries_remaining -= 1;
614 self.pending_scroll_into_view.push(pending_request);
615 needs_follow_up_frame = true;
616 }
617 ScrollIntoViewOutcome::Retry | ScrollIntoViewOutcome::Ignored => {}
618 }
619 }
620
621 needs_follow_up_frame
622 }
623
624 fn apply_scroll_into_view(
625 &mut self,
626 request: &ScrollIntoViewRequest,
627 ir: &CoreIR,
628 layout: &LayoutSnapshot,
629 ) -> ScrollIntoViewOutcome {
630 let Some(target_geom) = layout.get_node_geometry(request.target) else {
631 Self::emit_scroll_into_view_diag("missing_target", request, None);
632 return ScrollIntoViewOutcome::Retry;
633 };
634
635 let Some(container_id) = self.resolve_scroll_container(request, ir, layout) else {
636 Self::emit_scroll_into_view_diag("missing_container", request, None);
637 return ScrollIntoViewOutcome::Retry;
638 };
639
640 if !Self::is_descendant_or_self(ir, request.target, container_id) {
641 Self::emit_scroll_into_view_diag("target_not_descendant", request, Some(container_id));
642 return ScrollIntoViewOutcome::Ignored;
643 }
644
645 let Some(container_geom) = layout.get_node_geometry(container_id) else {
646 Self::emit_scroll_into_view_diag(
647 "missing_container_layout",
648 request,
649 Some(container_id),
650 );
651 return ScrollIntoViewOutcome::Retry;
652 };
653
654 let Some(direction) = Self::scroll_direction(ir, container_id) else {
655 Self::emit_scroll_into_view_diag("not_scroll_container", request, Some(container_id));
656 return ScrollIntoViewOutcome::Ignored;
657 };
658
659 if !Self::axis_matches(request.axis, direction) {
660 Self::emit_scroll_into_view_diag("axis_mismatch", request, Some(container_id));
661 return ScrollIntoViewOutcome::Ignored;
662 }
663
664 if matches!(request.behavior, ScrollBehavior::Smooth) {
665 Self::emit_scroll_into_view_diag(
666 "smooth_resolved_as_instant",
667 request,
668 Some(container_id),
669 );
670 }
671
672 let current_offset = self.runtime_state.scroll.get_offset(container_id);
673 let new_offset = match direction {
674 FlexDirection::Column => Self::compute_scroll_offset(
675 current_offset,
676 target_geom.rect.y() - container_geom.rect.y(),
677 target_geom.rect.height(),
678 container_geom.rect.height(),
679 container_geom.content_size.height,
680 request.padding[2],
681 request.padding[3],
682 request.alignment,
683 request.if_needed,
684 ),
685 FlexDirection::Row => Self::compute_scroll_offset(
686 current_offset,
687 target_geom.rect.x() - container_geom.rect.x(),
688 target_geom.rect.width(),
689 container_geom.rect.width(),
690 container_geom.content_size.width,
691 request.padding[0],
692 request.padding[1],
693 request.alignment,
694 request.if_needed,
695 ),
696 };
697
698 if (new_offset - current_offset).abs() > f32::EPSILON {
699 self.runtime_state
700 .scroll
701 .set_offset(container_id, new_offset);
702 ScrollIntoViewOutcome::Applied { changed: true }
703 } else {
704 ScrollIntoViewOutcome::Applied { changed: false }
705 }
706 }
707
708 fn resolve_scroll_container(
709 &self,
710 request: &ScrollIntoViewRequest,
711 ir: &CoreIR,
712 layout: &LayoutSnapshot,
713 ) -> Option<WidgetId> {
714 if let Some(container) = request.container {
715 return ir
716 .nodes
717 .contains_key(&container)
718 .then_some(container)
719 .filter(|id| layout.get_node_geometry(*id).is_some());
720 }
721
722 let mut current = ir.nodes.get(&request.target)?.parent;
723 while let Some(node_id) = current {
724 if let Some(direction) = Self::scroll_direction(ir, node_id) {
725 if Self::axis_matches(request.axis, direction)
726 && layout.get_node_geometry(node_id).is_some()
727 {
728 return Some(node_id);
729 }
730 }
731 current = ir.nodes.get(&node_id).and_then(|node| node.parent);
732 }
733
734 None
735 }
736
737 fn scroll_direction(ir: &CoreIR, node_id: WidgetId) -> Option<FlexDirection> {
738 match ir.nodes.get(&node_id).map(|node| &node.op) {
739 Some(Op::Layout(LayoutOp::Scroll { direction, .. })) => Some(*direction),
740 _ => None,
741 }
742 }
743
744 fn axis_matches(axis: ScrollAxis, direction: FlexDirection) -> bool {
745 matches!(
746 (axis, direction),
747 (ScrollAxis::Both, _)
748 | (ScrollAxis::Vertical, FlexDirection::Column)
749 | (ScrollAxis::Horizontal, FlexDirection::Row)
750 )
751 }
752
753 fn is_descendant_or_self(ir: &CoreIR, target: WidgetId, ancestor: WidgetId) -> bool {
754 let mut current = Some(target);
755 while let Some(node_id) = current {
756 if node_id == ancestor {
757 return true;
758 }
759 current = ir.nodes.get(&node_id).and_then(|node| node.parent);
760 }
761 false
762 }
763
764 fn compute_scroll_offset(
765 current_offset: f32,
766 target_content_start: f32,
767 target_size: f32,
768 viewport_size: f32,
769 content_size: f32,
770 padding_start: f32,
771 padding_end: f32,
772 alignment: ScrollAlignment,
773 if_needed: bool,
774 ) -> f32 {
775 let current_offset = Self::finite_or_zero(current_offset).max(0.0);
776 let viewport_size = Self::finite_or_zero(viewport_size).max(0.0);
777 let content_size = Self::finite_or_zero(content_size).max(0.0);
778 let target_size = Self::finite_or_zero(target_size).max(0.0);
779 let padding_start = Self::finite_or_zero(padding_start).max(0.0);
780 let padding_end = Self::finite_or_zero(padding_end).max(0.0);
781 let max_offset = (content_size - viewport_size).max(0.0);
782
783 if viewport_size <= f32::EPSILON || max_offset <= f32::EPSILON {
784 return 0.0;
785 }
786
787 let target_start = Self::finite_or_zero(target_content_start);
788 let target_end = target_start + target_size;
789 let reveal_start = target_start - padding_start;
790 let reveal_end = target_end + padding_end;
791 let viewport_start = current_offset;
792 let viewport_end = current_offset + viewport_size;
793
794 if if_needed && reveal_start >= viewport_start && reveal_end <= viewport_end {
795 return current_offset.min(max_offset);
796 }
797
798 let desired = match alignment {
799 ScrollAlignment::Start => reveal_start,
800 ScrollAlignment::Center => {
801 let padded_viewport = (viewport_size - padding_start - padding_end).max(0.0);
802 target_start - padding_start - (padded_viewport - target_size) * 0.5
803 }
804 ScrollAlignment::End => reveal_end - viewport_size,
805 ScrollAlignment::Nearest => {
806 if reveal_start < viewport_start {
807 reveal_start
808 } else if reveal_end > viewport_end {
809 reveal_end - viewport_size
810 } else {
811 current_offset
812 }
813 }
814 ScrollAlignment::Fraction(fraction) => {
815 let fraction = Self::finite_or_zero(fraction).clamp(0.0, 1.0);
816 let padded_viewport = (viewport_size - padding_start - padding_end).max(0.0);
817 target_start - padding_start - (padded_viewport - target_size) * fraction
818 }
819 };
820
821 Self::finite_or_zero(desired).clamp(0.0, max_offset)
822 }
823
824 fn finite_or_zero(value: f32) -> f32 {
825 if value.is_finite() {
826 value
827 } else {
828 0.0
829 }
830 }
831
832 fn emit_scroll_into_view_diag(
833 kind: &'static str,
834 request: &ScrollIntoViewRequest,
835 container: Option<WidgetId>,
836 ) {
837 diag::emit(
838 diag::DiagCategory::Input,
839 diag::DiagLevel::Debug,
840 diag::DiagEventKind::InputEvent {
841 kind: format!(
842 "scroll_into_view:{kind}:target={:?}:container={:?}",
843 request.target,
844 container.or(request.container)
845 ),
846 target: Some(request.target.as_u128()),
847 position: None,
848 },
849 );
850 }
851
852 pub fn post_layout_hook(&mut self, ir: &CoreIR, layout: &LayoutSnapshot) -> bool {
857 let needs_follow_up_frame = self.apply_pending_scroll_into_view(ir, layout);
858 let mut current_heroes = HashMap::new();
859
860 for (id, node) in &ir.nodes {
861 if let Op::Semantics(s) = &node.op {
862 if let Some(tag) = &s.hero_tag {
863 if let Some(geom) = layout.get_node_geometry(*id) {
864 current_heroes.insert(tag.clone(), (*id, geom.rect));
865 }
866 }
867 }
868 }
869
870 for (tag, (_new_id, new_rect)) in ¤t_heroes {
872 if let Some((_old_id, old_rect)) = self.runtime_state.hero.positions.get(tag) {
873 if *new_rect != *old_rect {
874 diag::emit(
876 diag::DiagCategory::Layout,
877 diag::DiagLevel::Debug,
878 diag::DiagEventKind::AnchorPlacement {
879 widget: 0,
880 node: 0,
881 rect_x: old_rect.origin.x,
882 rect_y: old_rect.origin.y,
883 rect_w: old_rect.size.width,
884 rect_h: old_rect.size.height,
885 place_left: new_rect.origin.x,
886 place_top: new_rect.origin.y,
887 note: Some(format!("Hero flight: {}", tag)),
888 },
889 );
890 }
891 }
892 }
893
894 self.runtime_state.hero.positions = current_heroes;
895 needs_follow_up_frame
896 }
897
898 pub fn handle_input(
899 &mut self,
900 event: InputEvent,
901 ir: &CoreIR,
902 layout: &LayoutSnapshot,
903 ) -> Result<()> {
904 use crate::hit_test::{
905 find_neighbor_focus_node, find_next_focus_node, hit_test_with_scroll, FocusDirection,
906 };
907 use crate::input::gesture::GestureController;
908 use crate::input::hover::HoverController;
909 use crate::input::slider::SliderController;
910 use crate::input::text::TextInputController;
911 use crate::input::{ControllerContext, InputController};
912 use crate::scrollbar::scrollbar_hit_test;
913 use crate::ui::custom_render::downcast_render_object;
914
915 if self.runtime_state.interaction.focused.is_none() {
916 if let Some(autofocus_id) = Self::find_autofocus_node(ir) {
917 self.runtime_state
918 .interaction
919 .set_focused(Some(autofocus_id));
920 if let Some(ime_handler) = &self.ime_handler {
921 let accepts_text = ir
922 .nodes
923 .get(&autofocus_id)
924 .and_then(|node| match &node.op {
925 Op::Semantics(semantics) => {
926 Some(semantics.role == fission_ir::semantics::Role::TextInput)
927 }
928 _ => None,
929 })
930 .unwrap_or(false);
931 ime_handler.set_ime_allowed(accepts_text);
932 }
933 }
934 }
935
936 if matches!(event, InputEvent::Pointer(_)) {
937 let dispatched_actions = {
938 let mut ctx = ControllerContext {
939 ir,
940 layout,
941 text_edit: &mut self.runtime_state.text_edit,
942 interaction: &mut self.runtime_state.interaction,
943 scroll: &mut self.runtime_state.scroll,
944 gesture: &mut self.runtime_state.gesture,
945 clipboard: self.clipboard_backend.as_ref(),
946 measurer: self.measurer.as_ref(),
947 dispatched_actions: Vec::new(),
948 };
949 let mut hover_controller = HoverController;
950 let _ = hover_controller.handle_event(&mut ctx, &event);
951 ctx.dispatched_actions
952 };
953 self.dispatch_input_actions(dispatched_actions)?;
954 }
955
956 let pointer_targets_scrollbar = match &event {
962 InputEvent::Pointer(PointerEvent::Down { point, button, .. })
963 if matches!(button, PointerButton::Primary) =>
964 {
965 scrollbar_hit_test(ir, layout, &self.runtime_state.scroll, *point).is_some()
966 }
967 InputEvent::Pointer(PointerEvent::Move { .. })
968 | InputEvent::Pointer(PointerEvent::Up { .. }) => {
969 self.runtime_state.gesture.scrollbar_drag.is_some()
970 }
971 InputEvent::Pointer(PointerEvent::Scroll { point, .. }) => {
972 scrollbar_hit_test(ir, layout, &self.runtime_state.scroll, *point).is_some()
973 }
974 _ => false,
975 };
976
977 if !pointer_targets_scrollbar {
978 if let Some(point) = Self::event_point(&event) {
979 if let Some(hit_node_id) =
980 hit_test_with_scroll(ir, layout, &self.runtime_state.scroll, point)
981 {
982 let mut target_ro: Option<(WidgetId, &fission_ir::AnyRenderObject)> = None;
987 {
988 let mut walk = Some(hit_node_id);
989 while let Some(nid) = walk {
990 if let Some(ro) = ir.custom_render_objects.get(&nid) {
991 target_ro = Some((nid, ro));
992 break;
993 }
994 walk = ir.nodes.get(&nid).and_then(|n| n.parent);
995 }
996 }
997 if target_ro.is_none() {
998 for (ro_nid, ro) in &ir.custom_render_objects {
999 if let Some(rect) = layout.get_node_rect(*ro_nid) {
1000 if rect.contains(point) {
1001 target_ro = Some((*ro_nid, ro));
1002 break;
1003 }
1004 }
1005 }
1006 }
1007
1008 if let Some((nid, any_ro)) = target_ro {
1009 if let Some(render_obj) = downcast_render_object(any_ro) {
1010 let mut node_rect = layout
1011 .get_node_rect(nid)
1012 .unwrap_or(LayoutRect::new(0.0, 0.0, 0.0, 0.0));
1013 {
1016 let mut walk = ir.nodes.get(&nid).and_then(|n| n.parent);
1017 while let Some(pid) = walk {
1018 if let Some(pnode) = ir.nodes.get(&pid) {
1019 if let fission_ir::Op::Layout(
1020 fission_ir::LayoutOp::Scroll { direction, .. },
1021 ) = &pnode.op
1022 {
1023 let off = self.runtime_state.scroll.get_offset(pid);
1024 match direction {
1025 fission_ir::FlexDirection::Row => {
1026 node_rect.origin.x -= off
1027 }
1028 fission_ir::FlexDirection::Column => {
1029 node_rect.origin.y -= off
1030 }
1031 }
1032 }
1033 walk = pnode.parent;
1034 } else {
1035 break;
1036 }
1037 }
1038 }
1039 let result = render_obj.handle_event(nid, &event, node_rect);
1040 if result.handled {
1041 if matches!(event, InputEvent::Pointer(PointerEvent::Down { .. })) {
1043 let old_focused_id = self.runtime_state.interaction.focused;
1044 if Some(nid) != old_focused_id {
1045 self.clear_text_pending_on_blur(old_focused_id, Some(nid));
1046 self.dispatch_custom_blur_actions(ir, old_focused_id)?;
1047 }
1048 self.runtime_state.interaction.set_focused(Some(nid));
1049 if let Some(ime_handler) = &self.ime_handler {
1050 let accepts_text = render_obj.accepts_text_input();
1051 ime_handler.set_ime_allowed(accepts_text);
1052 if accepts_text {
1053 if let Some(rect) =
1054 render_obj.ime_cursor_area(node_rect)
1055 {
1056 ime_handler.set_ime_cursor_area(rect);
1057 }
1058 }
1059 }
1060 }
1061 for (target, envelope) in result.actions {
1063 self.dispatch_node(envelope, target)?;
1064 }
1065 self.update_focused_ime_state(ir, layout);
1066 return Ok(());
1067 }
1068 }
1069 }
1070 }
1071 }
1072 }
1073
1074 if matches!(event, InputEvent::Keyboard(_) | InputEvent::Ime(_)) {
1080 if let Some(focused_id) = self.runtime_state.interaction.focused {
1081 let mut walk_id = Some(focused_id);
1082 while let Some(nid) = walk_id {
1083 if let Some(any_ro) = ir.custom_render_objects.get(&nid) {
1084 if let Some(render_obj) = downcast_render_object(any_ro) {
1085 let node_rect = layout
1086 .get_node_rect(nid)
1087 .unwrap_or(LayoutRect::new(0.0, 0.0, 0.0, 0.0));
1088 let result = render_obj.handle_event(nid, &event, node_rect);
1089 if result.handled {
1090 for (target, envelope) in result.actions {
1091 self.dispatch_node(envelope, target)?;
1092 }
1093 self.update_focused_ime_state(ir, layout);
1094 return Ok(());
1095 }
1096 }
1097 }
1098 walk_id = ir.nodes.get(&nid).and_then(|n| n.parent);
1099 }
1100 }
1101 }
1102
1103 let (handled, dispatched_actions) = {
1104 let mut ctx = ControllerContext {
1105 ir,
1106 layout,
1107 text_edit: &mut self.runtime_state.text_edit,
1108 interaction: &mut self.runtime_state.interaction,
1109 scroll: &mut self.runtime_state.scroll,
1110 gesture: &mut self.runtime_state.gesture,
1111 clipboard: self.clipboard_backend.as_ref(),
1112 measurer: self.measurer.as_ref(),
1113 dispatched_actions: Vec::new(),
1114 };
1115
1116 let mut hover_controller = HoverController;
1117 let _ = hover_controller.handle_event(&mut ctx, &event);
1118
1119 let mut gesture_controller = GestureController;
1120 let handled = if gesture_controller.handle_event(&mut ctx, &event) {
1121 true
1122 } else {
1123 let mut text_controller = TextInputController;
1124 if text_controller.handle_event(&mut ctx, &event) {
1125 true
1126 } else {
1127 let mut slider_controller = SliderController;
1128 slider_controller.handle_event(&mut ctx, &event)
1129 }
1130 };
1131 (handled, ctx.dispatched_actions)
1132 };
1133
1134 self.dispatch_input_actions(dispatched_actions)?;
1135
1136 if handled {
1137 if matches!(event, InputEvent::Pointer(PointerEvent::Up { .. })) {
1138 self.runtime_state.interaction.pressed.clear();
1139 self.runtime_state.interaction.last_down_point = None;
1140 }
1141 self.update_focused_ime_state(ir, layout);
1142 return Ok(());
1143 }
1144
1145 match event {
1146 InputEvent::Pointer(PointerEvent::Scroll { point, delta, .. }) => {
1147 let trace_scroll =
1148 std::env::var("FISSION_SCROLL_TRACE").ok().as_deref() == Some("1");
1149 if trace_scroll {
1150 eprintln!(
1151 "[scroll-trace] event point=({:.1},{:.1}) delta=({:.1},{:.1})",
1152 point.x, point.y, delta.x, delta.y
1153 );
1154 }
1155 let hit_node_id = scrollbar_hit_test(ir, layout, &self.runtime_state.scroll, point)
1156 .map(|hit| hit.geometry.node_id)
1157 .or_else(|| {
1158 hit_test_with_scroll(ir, layout, &self.runtime_state.scroll, point)
1159 });
1160 if let Some(hit_node_id) = hit_node_id {
1161 if trace_scroll {
1162 eprintln!("[scroll-trace] hit_node={}", hit_node_id.as_u128());
1163 }
1164 let mut current_id = Some(hit_node_id);
1165 while let Some(node_id) = current_id {
1166 if let Some(node) = ir.nodes.get(&node_id) {
1167 if let Op::Layout(LayoutOp::Scroll { direction, .. }) = &node.op {
1168 let current_offset = self.runtime_state.scroll.get_offset(node_id);
1169 let delta_val = match direction {
1170 FlexDirection::Row => delta.x,
1171 FlexDirection::Column => delta.y,
1172 };
1173 let mut new_offset = current_offset + delta_val;
1174
1175 let mut max_offset = 0.0f32;
1176 let mut viewport_w = 0.0f32;
1177 let mut viewport_h = 0.0f32;
1178 let mut content_w = 0.0f32;
1179 let mut content_h = 0.0f32;
1180 if let Some(geom) = layout.get_node_geometry(node_id) {
1181 viewport_w = geom.rect.width();
1182 viewport_h = geom.rect.height();
1183 content_w = geom.content_size.width;
1184 content_h = geom.content_size.height;
1185 max_offset = if matches!(direction, FlexDirection::Row) {
1186 (geom.content_size.width - geom.rect.width()).max(0.0)
1187 } else {
1188 (geom.content_size.height - geom.rect.height()).max(0.0)
1189 };
1190 new_offset = new_offset.clamp(0.0, max_offset);
1191 }
1192
1193 if trace_scroll {
1194 eprintln!(
1195 "[scroll-trace] scroll_node={} axis={} offset={:.1}->{:.1} max={:.1} viewport=({:.1},{:.1}) content=({:.1},{:.1})",
1196 node_id.as_u128(),
1197 match direction { FlexDirection::Row => "x", FlexDirection::Column => "y" },
1198 current_offset,
1199 new_offset,
1200 max_offset,
1201 viewport_w,
1202 viewport_h,
1203 content_w,
1204 content_h
1205 );
1206 }
1207
1208 {
1209 use fission_diagnostics::prelude as diag;
1210 diag::emit(
1211 diag::DiagCategory::Input,
1212 diag::DiagLevel::Debug,
1213 diag::DiagEventKind::ScrollUpdate {
1214 node: node_id.as_u128(),
1215 axis: match direction {
1216 FlexDirection::Row => "x".into(),
1217 FlexDirection::Column => "y".into(),
1218 },
1219 point_x: point.x,
1220 point_y: point.y,
1221 delta: delta_val,
1222 old_offset: current_offset,
1223 new_offset,
1224 max_offset,
1225 viewport_w,
1226 viewport_h,
1227 content_w,
1228 content_h,
1229 },
1230 );
1231 }
1232
1233 self.runtime_state.scroll.set_offset(node_id, new_offset);
1234 if (new_offset - current_offset).abs() > 0.001 {
1238 break;
1239 }
1240 }
1242 current_id = node.parent;
1243 } else {
1244 break;
1245 }
1246 }
1247 } else if trace_scroll {
1248 eprintln!("[scroll-trace] hit_test: no node");
1249 }
1250 }
1251 InputEvent::Keyboard(KeyEvent::Down {
1252 key_code,
1253 modifiers,
1254 }) => match key_code {
1255 KeyCode::Tab => {
1256 let reverse = (modifiers & 1) != 0;
1257 let old_focus = self.runtime_state.interaction.focused;
1258 let next =
1259 find_next_focus_node(ir, self.runtime_state.interaction.focused, reverse);
1260 if next != old_focus {
1261 self.clear_text_pending_on_blur(old_focus, next);
1262 self.dispatch_custom_blur_actions(ir, old_focus)?;
1263 }
1264 self.runtime_state.interaction.set_focused(next);
1265 }
1266 KeyCode::Up | KeyCode::Down | KeyCode::Left | KeyCode::Right => {
1267 let reverse = matches!(key_code, KeyCode::Up | KeyCode::Left);
1268 let old_focus = self.runtime_state.interaction.focused;
1269 let next = if let Some(focused) = old_focus {
1270 let dir = match key_code {
1271 KeyCode::Up => FocusDirection::Up,
1272 KeyCode::Down => FocusDirection::Down,
1273 KeyCode::Left => FocusDirection::Left,
1274 KeyCode::Right => FocusDirection::Right,
1275 _ => unreachable!(),
1276 };
1277 find_neighbor_focus_node(ir, layout, focused, dir)
1278 .or_else(|| find_next_focus_node(ir, Some(focused), reverse))
1279 } else {
1280 find_next_focus_node(ir, None, reverse)
1281 };
1282 if next != old_focus {
1283 self.clear_text_pending_on_blur(old_focus, next);
1284 self.dispatch_custom_blur_actions(ir, old_focus)?;
1285 self.runtime_state.interaction.set_focused(next);
1286 }
1287 }
1288 KeyCode::Enter | KeyCode::Space => {
1289 if let Some(focused_id) = self.runtime_state.interaction.focused {
1290 let mut current_id = Some(focused_id);
1291 while let Some(node_id) = current_id {
1292 if let Some(node) = ir.nodes.get(&node_id) {
1293 if let Op::Semantics(semantics) = &node.op {
1294 if let Some(action_entry) = semantics.actions.entries.first() {
1295 if let Some(payload) = &action_entry.payload_data {
1296 let envelope = ActionEnvelope {
1297 id: ActionId::from_u128(action_entry.action_id),
1298 payload: payload.clone(),
1299 };
1300 let input = crate::input::scoped_action_input(
1301 ir,
1302 node_id,
1303 ActionInput::None,
1304 );
1305 return self.dispatch_node_with_input(
1306 envelope, node_id, &input,
1307 );
1308 }
1309 }
1310 }
1311 current_id = node.parent;
1312 } else {
1313 break;
1314 }
1315 }
1316 }
1317 }
1318 _ => {}
1319 },
1320 InputEvent::Pointer(PointerEvent::Down { point, .. }) => {
1321 if let Some(hit_node_id) =
1322 hit_test_with_scroll(ir, layout, &self.runtime_state.scroll, point)
1323 {
1324 diag::emit(
1325 diag::DiagCategory::Input,
1326 diag::DiagLevel::Debug,
1327 diag::DiagEventKind::InputEvent {
1328 kind: "pointer_down_hit".into(),
1329 target: Some(hit_node_id.as_u128()),
1330 position: Some((point.x, point.y)),
1331 },
1332 );
1333 let mut focus_candidate = Some(hit_node_id);
1334 while let Some(node_id) = focus_candidate {
1335 if let Some(node) = ir.nodes.get(&node_id) {
1336 if let Op::Semantics(s) = &node.op {
1337 if s.focusable {
1338 if s.focus_policy == FocusPolicy::PreserveCurrentOnPointer {
1339 break;
1340 }
1341 let old_focused_id = self.runtime_state.interaction.focused;
1342 if Some(node_id) != old_focused_id {
1343 self.clear_text_pending_on_blur(
1344 old_focused_id,
1345 Some(node_id),
1346 );
1347 self.dispatch_custom_blur_actions(ir, old_focused_id)?;
1348
1349 if s.role == fission_ir::semantics::Role::TextInput {
1350 if let Some(ime_handler) = &self.ime_handler {
1351 ime_handler.set_ime_allowed(true);
1352 }
1353 } else if let Some(ime_handler) = &self.ime_handler {
1354 ime_handler.set_ime_allowed(false);
1355 }
1356 }
1357 self.runtime_state.interaction.set_focused(Some(node_id));
1358 break;
1359 }
1360 }
1361 focus_candidate = node.parent;
1362 } else {
1363 break;
1364 }
1365 }
1366 if focus_candidate.is_none() {
1367 let old_focused_id = self.runtime_state.interaction.focused;
1368 if let Some(old_focused_id) = self.runtime_state.interaction.focused {
1369 if let Some(old_node) = ir.nodes.get(&old_focused_id) {
1370 if let Op::Semantics(s) = &old_node.op {
1371 if s.role == fission_ir::semantics::Role::TextInput {
1372 if let Some(ime_handler) = &self.ime_handler {
1373 ime_handler.set_ime_allowed(false);
1374 }
1375 }
1376 }
1377 }
1378 }
1379 self.clear_text_pending_on_blur(old_focused_id, None);
1380 self.dispatch_custom_blur_actions(ir, old_focused_id)?;
1381 self.runtime_state.interaction.set_focused(None);
1382 }
1383
1384 let mut current_pressed_id = Some(hit_node_id);
1385 while let Some(node_id) = current_pressed_id {
1386 self.runtime_state.interaction.set_pressed(node_id, true);
1387 if let Some(node) = ir.nodes.get(&node_id) {
1388 current_pressed_id = node.parent;
1389 } else {
1390 break;
1391 }
1392 }
1393 self.runtime_state.interaction.last_down_point = Some(point);
1394
1395 if let Some(focused_id) = self.runtime_state.interaction.focused {
1396 if let Some(node) = ir.nodes.get(&focused_id) {
1397 if let Op::Semantics(s) = &node.op {
1398 if s.role == fission_ir::semantics::Role::TextInput {
1399 if let Some(ime_handler) = &self.ime_handler {
1400 ime_handler.set_ime_cursor_area(LayoutRect::new(
1401 point.x, point.y, 2.0, 16.0,
1402 ));
1403 }
1404 }
1405 }
1406 }
1407 }
1408 } else {
1409 let old_focused_id = self.runtime_state.interaction.focused;
1410 if let Some(old_focused_id) = self.runtime_state.interaction.focused {
1411 if let Some(old_node) = ir.nodes.get(&old_focused_id) {
1412 if let Op::Semantics(s) = &old_node.op {
1413 if s.role == fission_ir::semantics::Role::TextInput {
1414 if let Some(ime_handler) = &self.ime_handler {
1415 ime_handler.set_ime_allowed(false);
1416 }
1417 }
1418 }
1419 }
1420 }
1421 self.clear_text_pending_on_blur(old_focused_id, None);
1422 self.dispatch_custom_blur_actions(ir, old_focused_id)?;
1423 self.runtime_state.interaction.set_focused(None);
1424 }
1425 }
1426 InputEvent::Pointer(PointerEvent::Up { point, .. }) => {
1427 self.runtime_state.interaction.pressed.clear();
1428 self.runtime_state.interaction.last_down_point = None;
1429 if let Some(hit_node_id) =
1430 hit_test_with_scroll(ir, layout, &self.runtime_state.scroll, point)
1431 {
1432 let mut current_id = Some(hit_node_id);
1433 while let Some(node_id) = current_id {
1434 if let Some(node) = ir.nodes.get(&node_id) {
1435 if let Op::Semantics(semantics) = &node.op {
1436 if semantics.role == fission_ir::semantics::Role::TextInput {
1437 } else if let Some(action_entry) = semantics.actions.entries.first()
1439 {
1440 if let Some(payload) = &action_entry.payload_data {
1441 let envelope = ActionEnvelope {
1442 id: ActionId::from_u128(action_entry.action_id),
1443 payload: payload.clone(),
1444 };
1445 diag::emit(
1446 diag::DiagCategory::Input,
1447 diag::DiagLevel::Debug,
1448 diag::DiagEventKind::InputEvent {
1449 kind: "pointer_up_dispatch".into(),
1450 target: Some(node_id.as_u128()),
1451 position: Some((point.x, point.y)),
1452 },
1453 );
1454 let input = crate::input::scoped_action_input(
1455 ir,
1456 node_id,
1457 ActionInput::None,
1458 );
1459 return self
1460 .dispatch_node_with_input(envelope, node_id, &input);
1461 }
1462 }
1463 }
1464 current_id = node.parent;
1465 } else {
1466 break;
1467 }
1468 }
1469 }
1470 }
1471 _ => {}
1472 }
1473 self.update_focused_ime_state(ir, layout);
1474 Ok(())
1475 }
1476
1477 pub fn clear_hover_state(&mut self, ir: &CoreIR, point: Option<LayoutPoint>) -> Result<bool> {
1478 use crate::input::hover::HoverController;
1479 use crate::input::ControllerContext;
1480
1481 let dispatched_actions = {
1482 let layout = &LayoutSnapshot::new(LayoutSize::ZERO);
1483 let mut ctx = ControllerContext {
1484 ir,
1485 layout,
1486 text_edit: &mut self.runtime_state.text_edit,
1487 interaction: &mut self.runtime_state.interaction,
1488 scroll: &mut self.runtime_state.scroll,
1489 gesture: &mut self.runtime_state.gesture,
1490 clipboard: self.clipboard_backend.as_ref(),
1491 measurer: self.measurer.as_ref(),
1492 dispatched_actions: Vec::new(),
1493 };
1494 let changed = HoverController::clear(&mut ctx, point);
1495 (changed, ctx.dispatched_actions)
1496 };
1497 self.dispatch_input_actions(dispatched_actions.1)?;
1498 Ok(dispatched_actions.0)
1499 }
1500
1501 fn dispatch_input_actions(
1502 &mut self,
1503 dispatched_actions: Vec<(WidgetId, ActionEnvelope, ActionInput)>,
1504 ) -> Result<()> {
1505 for (target, action, input) in dispatched_actions {
1506 self.dispatch_node_with_input(action, target, &input)?;
1507 }
1508 Ok(())
1509 }
1510
1511 fn update_focused_ime_state(&mut self, ir: &CoreIR, layout: &LayoutSnapshot) {
1512 let Some(ime_handler) = self.ime_handler.clone() else {
1513 return;
1514 };
1515 let Some(focused_id) = self.runtime_state.interaction.focused else {
1516 ime_handler.set_ime_allowed(false);
1517 return;
1518 };
1519
1520 let mut walk = Some(focused_id);
1521 while let Some(node_id) = walk {
1522 if let Some(any_ro) = ir.custom_render_objects.get(&node_id) {
1523 if let Some(render_obj) = crate::ui::custom_render::downcast_render_object(any_ro) {
1524 let accepts_text = render_obj.accepts_text_input();
1525 ime_handler.set_ime_allowed(accepts_text);
1526 if accepts_text {
1527 let rect =
1528 Self::visual_node_rect(ir, layout, &self.runtime_state.scroll, node_id)
1529 .unwrap_or(LayoutRect::new(0.0, 0.0, 0.0, 0.0));
1530 if let Some(cursor_area) = render_obj.ime_cursor_area(rect) {
1531 ime_handler.set_ime_cursor_area(cursor_area);
1532 }
1533 }
1534 return;
1535 }
1536 }
1537 walk = ir.nodes.get(&node_id).and_then(|node| node.parent);
1538 }
1539
1540 let accepts_text = ir
1541 .nodes
1542 .get(&focused_id)
1543 .and_then(|node| match &node.op {
1544 Op::Semantics(semantics) => {
1545 Some(semantics.role == fission_ir::semantics::Role::TextInput)
1546 }
1547 _ => None,
1548 })
1549 .unwrap_or(false);
1550 ime_handler.set_ime_allowed(accepts_text);
1551
1552 if accepts_text {
1553 let cursor_area = {
1554 let mut ctx = crate::input::ControllerContext {
1555 ir,
1556 layout,
1557 text_edit: &mut self.runtime_state.text_edit,
1558 interaction: &mut self.runtime_state.interaction,
1559 scroll: &mut self.runtime_state.scroll,
1560 gesture: &mut self.runtime_state.gesture,
1561 clipboard: self.clipboard_backend.as_ref(),
1562 measurer: self.measurer.as_ref(),
1563 dispatched_actions: Vec::new(),
1564 };
1565 crate::input::text::TextInputController::ime_cursor_area(&mut ctx, focused_id)
1566 };
1567 if let Some(cursor_area) = cursor_area {
1568 ime_handler.set_ime_cursor_area(cursor_area);
1569 }
1570 }
1571 }
1572
1573 fn visual_node_rect(
1574 ir: &CoreIR,
1575 layout: &LayoutSnapshot,
1576 scroll: &crate::env::ScrollStateMap,
1577 node_id: WidgetId,
1578 ) -> Option<LayoutRect> {
1579 let mut rect = layout.get_node_rect(node_id)?;
1580 let mut walk = ir.nodes.get(&node_id).and_then(|node| node.parent);
1581 while let Some(parent_id) = walk {
1582 let Some(parent) = ir.nodes.get(&parent_id) else {
1583 break;
1584 };
1585 if let Op::Layout(LayoutOp::Scroll { direction, .. }) = &parent.op {
1586 let offset = scroll.get_offset(parent_id);
1587 match direction {
1588 FlexDirection::Row => rect.origin.x -= offset,
1589 FlexDirection::Column => rect.origin.y -= offset,
1590 }
1591 }
1592 walk = parent.parent;
1593 }
1594 Some(rect)
1595 }
1596
1597 fn clear_text_pending_on_blur(
1598 &mut self,
1599 old_focus: Option<WidgetId>,
1600 new_focus: Option<WidgetId>,
1601 ) {
1602 if old_focus == new_focus {
1603 return;
1604 }
1605 if let Some(old_id) = old_focus {
1606 if let Some(st) = self.runtime_state.text_edit.states.get_mut(&old_id) {
1607 st.pending_model_sync = false;
1608 st.clear_preedit();
1609 }
1610 }
1611 }
1612
1613 fn dispatch_custom_blur_actions(
1614 &mut self,
1615 ir: &CoreIR,
1616 old_focus: Option<WidgetId>,
1617 ) -> Result<()> {
1618 if let Some(old_id) = old_focus {
1619 if let Some(any_ro) = ir.custom_render_objects.get(&old_id) {
1620 if let Some(render_obj) = crate::ui::custom_render::downcast_render_object(any_ro) {
1621 if render_obj.accepts_text_input() {
1622 if let Some(ime_handler) = &self.ime_handler {
1623 ime_handler.set_ime_allowed(false);
1624 }
1625 }
1626 for (target, envelope) in render_obj.blur_actions(old_id) {
1627 self.dispatch_node(envelope, target)?;
1628 }
1629 }
1630 }
1631 }
1632 Ok(())
1633 }
1634
1635 pub fn hit_test(
1636 &self,
1637 point: LayoutPoint,
1638 ir: &CoreIR,
1639 snapshot: &LayoutSnapshot,
1640 ) -> Option<WidgetId> {
1641 if let Some(root) = ir.root {
1642 return self.hit_test_recursive(root, point, ir, snapshot);
1643 }
1644 None
1645 }
1646
1647 fn hit_test_recursive(
1648 &self,
1649 node_id: WidgetId,
1650 point: LayoutPoint,
1651 ir: &CoreIR,
1652 snapshot: &LayoutSnapshot,
1653 ) -> Option<WidgetId> {
1654 if let Some(geom) = snapshot.nodes.get(&node_id) {
1655 if geom.rect.contains(point) {
1656 if let Some(node) = ir.nodes.get(&node_id) {
1657 for child in node.children.iter().rev() {
1658 let mut child_point = point;
1659
1660 if let Op::Layout(LayoutOp::Scroll { direction, .. }) = &node.op {
1661 if !geom.rect.contains(point) {
1662 continue;
1663 }
1664 let offset = self.runtime_state.scroll.get_offset(node_id);
1665 match direction {
1666 FlexDirection::Row => child_point.x += offset,
1667 FlexDirection::Column => child_point.y += offset,
1668 }
1669 }
1670
1671 if let Op::Layout(LayoutOp::Transform { transform }) = &node.op {
1672 let mat = Mat4::from_cols_array(transform);
1673 let local_x = point.x - geom.rect.origin.x;
1710 let local_y = point.y - geom.rect.origin.y;
1711
1712 let p = Vec4::new(local_x, local_y, 0.0, 1.0);
1713 let inv = mat.inverse();
1714 let transformed = inv * p;
1715
1716 child_point = LayoutPoint::new(
1717 transformed.x + geom.rect.origin.x,
1718 transformed.y + geom.rect.origin.y,
1719 );
1720 }
1721
1722 if let Some(hit) =
1723 self.hit_test_recursive(*child, child_point, ir, snapshot)
1724 {
1725 return Some(hit);
1726 }
1727 }
1728
1729 match &node.op {
1730 Op::Paint(_)
1731 | Op::Layout(LayoutOp::Scroll { .. })
1732 | Op::Layout(LayoutOp::Embed { .. }) => return Some(node_id),
1733 _ => return None,
1734 }
1735 }
1736 return None;
1737 }
1738 }
1739 None
1740 }
1741
1742 fn event_point(event: &InputEvent) -> Option<LayoutPoint> {
1748 match event {
1749 InputEvent::Pointer(PointerEvent::Down { point, .. })
1750 | InputEvent::Pointer(PointerEvent::Up { point, .. })
1751 | InputEvent::Pointer(PointerEvent::Move { point, .. })
1752 | InputEvent::Pointer(PointerEvent::Scroll { point, .. }) => Some(*point),
1753 _ => None,
1754 }
1755 }
1756
1757 fn find_autofocus_node(ir: &CoreIR) -> Option<WidgetId> {
1758 fn walk(ir: &CoreIR, node_id: WidgetId) -> Option<WidgetId> {
1759 let node = ir.nodes.get(&node_id)?;
1760 if let Op::Semantics(semantics) = &node.op {
1761 if semantics.autofocus && semantics.focusable && !semantics.disabled {
1762 return Some(node_id);
1763 }
1764 }
1765 for child_id in &node.children {
1766 if let Some(found) = walk(ir, *child_id) {
1767 return Some(found);
1768 }
1769 }
1770 None
1771 }
1772
1773 ir.root.and_then(|root| walk(ir, root))
1774 }
1775
1776 pub fn reconcile_resources(
1777 &mut self,
1778 declarations: Vec<RuntimeResourceDeclaration>,
1779 ) -> Result<()> {
1780 let now = self.clock().current_time();
1781 let mut existing = std::mem::take(&mut self.active_resources);
1782 let mut next = HashMap::new();
1783
1784 for declaration in declarations {
1785 let key = declaration.key.clone();
1786 match existing.remove(&key) {
1787 Some(current)
1788 if current.policy == declaration.policy
1789 && current.deps == declaration.deps
1790 && current.matches_kind(&declaration.kind) =>
1791 {
1792 next.insert(key, current);
1793 }
1794 Some(current) if declaration.policy == ResourcePolicy::PreserveOnChange => {
1795 next.insert(key, current);
1796 }
1797 Some(current) => {
1798 self.stop_resource(&key, ¤t);
1799 let replacement = self.start_resource(declaration, now);
1800 next.insert(key, replacement);
1801 }
1802 None => {
1803 let resource = self.start_resource(declaration, now);
1804 next.insert(key, resource);
1805 }
1806 }
1807 }
1808
1809 for (key, resource) in existing {
1810 self.stop_resource(&key, &resource);
1811 }
1812
1813 self.active_resources = next;
1814 Ok(())
1815 }
1816
1817 pub fn resource_generation(&self, key: &str) -> Option<u64> {
1818 self.active_resources
1819 .get(key)
1820 .map(|resource| resource.generation)
1821 }
1822
1823 pub fn is_resource_current(&self, resource: &ResourceExecutionContext) -> bool {
1824 self.resource_generation(&resource.key) == Some(resource.generation)
1825 }
1826
1827 fn start_resource(
1828 &mut self,
1829 declaration: RuntimeResourceDeclaration,
1830 now: CurrentTime,
1831 ) -> ActiveResource {
1832 let generation = self.next_resource_generation;
1833 self.next_resource_generation += 1;
1834
1835 let context = ResourceExecutionContext {
1836 key: declaration.key.clone(),
1837 generation,
1838 };
1839
1840 let kind = match declaration.kind {
1841 RuntimeResourceKind::Job(mut job) => {
1842 job.effect.resource = Some(context);
1843 self.enqueue_effect(job.effect);
1844 ActiveResourceKind::Job
1845 }
1846 RuntimeResourceKind::Service(mut service) => {
1847 service.effect.resource = Some(context);
1848 let (service_name, slot_key) = match &service.effect.effect {
1849 crate::Effect::StartService(payload) => {
1850 (payload.service_name.clone(), payload.slot_key.clone())
1851 }
1852 _ => unreachable!("service resource must lower to StartService"),
1853 };
1854 self.enqueue_effect(service.effect);
1855 ActiveResourceKind::Service {
1856 service_name,
1857 slot_key,
1858 }
1859 }
1860 RuntimeResourceKind::Timer(timer) => self.start_timer_resource(timer, now),
1861 };
1862
1863 ActiveResource {
1864 generation,
1865 deps: declaration.deps,
1866 policy: declaration.policy,
1867 kind,
1868 }
1869 }
1870
1871 fn start_timer_resource(&self, timer: TimerResource, now: CurrentTime) -> ActiveResourceKind {
1872 let interval_ms = timer.interval_ms.max(1);
1873 ActiveResourceKind::Timer {
1874 interval_ms,
1875 payload: timer.payload,
1876 on_tick: timer.on_tick,
1877 next_fire_at: if timer.immediate {
1878 now
1879 } else {
1880 now.saturating_add(interval_ms)
1881 },
1882 }
1883 }
1884
1885 fn stop_resource(&mut self, key: &str, resource: &ActiveResource) {
1886 if let ActiveResourceKind::Service {
1887 service_name,
1888 slot_key,
1889 } = &resource.kind
1890 {
1891 self.enqueue_effect(EffectEnvelope {
1892 req_id: 0,
1893 effect: crate::Effect::StopService(ServiceStopPayload {
1894 service_name: service_name.clone(),
1895 slot_key: slot_key.clone(),
1896 }),
1897 on_ok: None,
1898 on_err: None,
1899 service_bindings: None,
1900 resource: Some(ResourceExecutionContext {
1901 key: key.to_string(),
1902 generation: resource.generation,
1903 }),
1904 });
1905 }
1906 }
1907}
1908
1909impl ActiveResource {
1910 fn matches_kind(&self, kind: &RuntimeResourceKind) -> bool {
1911 matches!(
1912 (&self.kind, kind),
1913 (ActiveResourceKind::Job, RuntimeResourceKind::Job(_))
1914 | (
1915 ActiveResourceKind::Timer { .. },
1916 RuntimeResourceKind::Timer(_)
1917 )
1918 | (
1919 ActiveResourceKind::Service { .. },
1920 RuntimeResourceKind::Service(_)
1921 )
1922 )
1923 }
1924}