1use crate::action::{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::ui::custom_render::downcast_render_object;
13use crate::{BoxedReducer, EffectCallbackRegistry};
14use crate::{
15 Clipboard, Clock, CurrentTime, ImeHandler, InputEvent, KeyCode, KeyEvent, PointerButton,
16 PointerEvent, ResourceExecutionContext,
17};
18use anyhow::{anyhow, Context, Result};
19use fission_diagnostics::prelude as diag;
20use fission_ir::{CoreIR, FlexDirection, FocusPolicy, LayoutOp, Op, WidgetId};
21use fission_layout::{LayoutPoint, LayoutRect, LayoutSize, LayoutSnapshot, TextMeasurer};
22use glam::{Mat4, Vec4};
23use serde_json;
24use std::any::TypeId;
25use std::collections::{HashMap, HashSet};
26use std::sync::Arc;
27
28#[derive(Debug, Default, Clone)]
29pub struct TickResult {
30 pub changed_motions: Vec<(WidgetId, crate::MotionPropertyId)>,
31 pub resource_actions_dispatched: usize,
35}
36
37#[derive(Debug, Clone)]
38enum ActiveResourceKind {
39 Job,
40 Service {
41 service_name: String,
42 slot_key: String,
43 },
44 Timer {
45 interval_ms: u64,
46 payload: Vec<u8>,
47 on_tick: Option<ActionEnvelope>,
48 next_fire_at: CurrentTime,
49 },
50}
51
52#[derive(Debug, Clone)]
53struct ActiveResource {
54 generation: u64,
55 deps: Option<Vec<u8>>,
56 policy: ResourcePolicy,
57 kind: ActiveResourceKind,
58}
59
60#[derive(Debug, Clone)]
61struct PendingScrollIntoView {
62 request: ScrollIntoViewRequest,
63 retries_remaining: u8,
64}
65
66#[derive(Debug, Clone, Copy)]
67struct FocusBarrierFrame {
68 id: WidgetId,
69 restore_target: Option<WidgetId>,
70}
71
72#[derive(Debug, Clone, Copy, PartialEq, Eq)]
73enum ScrollIntoViewOutcome {
74 Applied { changed: bool },
75 Retry,
76 Ignored,
77}
78
79pub struct Runtime {
112 pub(crate) reducers: HashMap<ActionId, Vec<BoxedReducer>>,
115 pub(crate) persistent_reducers: HashMap<ActionId, Vec<BoxedReducer>>,
118 effect_callbacks: Arc<EffectCallbackRegistry>,
120 pub app_states: HashMap<TypeId, Box<dyn GlobalState>>,
122 pub runtime_state: RuntimeState,
124 pub measurer: Option<Arc<dyn TextMeasurer>>,
126 pub clipboard_backend: Option<Arc<dyn Clipboard>>,
128 pub ime_handler: Option<Arc<dyn ImeHandler>>,
130 pub editing_convention: crate::input::TextEditingConvention,
132 pub pending_effects: Vec<EffectEnvelope>,
134 pending_scroll_into_view: Vec<PendingScrollIntoView>,
136 pending_navigation: Vec<crate::NavigationCommand>,
138 pending_selection_regions: Vec<(WidgetId, crate::SelectionRegionCommand)>,
140 pending_text_editing: Vec<(WidgetId, crate::TextEditingCommand)>,
141 pending_text_scroll: Vec<(WidgetId, crate::TextScrollCommand)>,
142 pending_text_form_validation: Vec<String>,
143 focus_barriers: Vec<FocusBarrierFrame>,
145 pub next_req_id: u64,
147 active_resources: HashMap<String, ActiveResource>,
149 next_resource_generation: u64,
151}
152
153impl Default for Runtime {
154 fn default() -> Self {
155 let mut runtime = Self {
156 reducers: HashMap::new(),
157 persistent_reducers: HashMap::new(),
158 effect_callbacks: Arc::new(EffectCallbackRegistry::new()),
159 app_states: HashMap::new(),
160 runtime_state: RuntimeState::default(),
161 measurer: None,
162 clipboard_backend: None,
163 ime_handler: None,
164 editing_convention: crate::input::TextEditingConvention::default(),
165 pending_effects: Vec::new(),
166 pending_scroll_into_view: Vec::new(),
167 pending_navigation: Vec::new(),
168 pending_selection_regions: Vec::new(),
169 pending_text_editing: Vec::new(),
170 pending_text_scroll: Vec::new(),
171 pending_text_form_validation: Vec::new(),
172 focus_barriers: Vec::new(),
173 next_req_id: 0,
174 active_resources: HashMap::new(),
175 next_resource_generation: 1,
176 };
177
178 runtime
179 .add_global_state(Box::new(runtime.runtime_state.local_widget_state.clone()))
180 .expect("Failed to add local widget state store");
181
182 runtime
183 .add_global_state(Box::new(Clock::default()))
184 .expect("Failed to add Clock state");
185
186 runtime.register_base_reducers();
187
188 runtime
189 }
190}
191
192impl Runtime {
193 pub fn with_measurer(mut self, measurer: Arc<dyn TextMeasurer>) -> Self {
194 self.measurer = Some(measurer);
195 self
196 }
197
198 pub fn with_clipboard(mut self, backend: Arc<dyn Clipboard>) -> Self {
199 self.clipboard_backend = Some(backend);
200 self
201 }
202
203 pub fn with_ime_handler(mut self, handler: Arc<dyn ImeHandler>) -> Self {
204 self.ime_handler = Some(handler);
205 self
206 }
207
208 pub fn reconcile_focus(&mut self, ir: &CoreIR) -> Result<bool> {
213 use crate::hit_test::{
214 focus_barriers_in_tree_order, get_all_focusable_nodes, is_descendant_or_self,
215 is_enabled_focus_node, preferred_focus_node_in_scope,
216 };
217
218 let active_barriers = focus_barriers_in_tree_order(ir);
219 let common_prefix = self
220 .focus_barriers
221 .iter()
222 .map(|frame| frame.id)
223 .zip(active_barriers.iter().copied())
224 .take_while(|(tracked, active)| tracked == active)
225 .count();
226 let popped_any = self.focus_barriers.len() > common_prefix;
227 let mut restore_target = None;
228 while self.focus_barriers.len() > common_prefix {
229 restore_target = self
230 .focus_barriers
231 .pop()
232 .and_then(|frame| frame.restore_target);
233 }
234
235 for (index, barrier_id) in active_barriers
236 .iter()
237 .copied()
238 .enumerate()
239 .skip(common_prefix)
240 {
241 let restore = if index == 0 {
242 restore_target
243 .filter(|id| is_enabled_focus_node(ir, *id))
244 .or_else(|| {
245 self.runtime_state
246 .interaction
247 .focused
248 .filter(|id| is_enabled_focus_node(ir, *id))
249 })
250 } else {
251 let parent_barrier_id = active_barriers[index - 1];
252 self.runtime_state
253 .interaction
254 .focused
255 .filter(|id| {
256 is_enabled_focus_node(ir, *id)
257 && is_descendant_or_self(ir, *id, parent_barrier_id)
258 && !is_descendant_or_self(ir, *id, barrier_id)
259 })
260 .or_else(|| preferred_focus_node_in_scope(ir, parent_barrier_id))
261 };
262 self.focus_barriers.push(FocusBarrierFrame {
263 id: barrier_id,
264 restore_target: restore,
265 });
266 }
267
268 let current = self.runtime_state.interaction.focused;
269 let next = if let Some(barrier_id) = active_barriers.last().copied() {
270 current
271 .filter(|id| {
272 is_enabled_focus_node(ir, *id) && is_descendant_or_self(ir, *id, barrier_id)
273 })
274 .or_else(|| {
275 restore_target.filter(|id| {
276 is_enabled_focus_node(ir, *id) && is_descendant_or_self(ir, *id, barrier_id)
277 })
278 })
279 .or_else(|| preferred_focus_node_in_scope(ir, barrier_id))
280 } else if popped_any {
281 restore_target
282 .filter(|id| is_enabled_focus_node(ir, *id))
283 .or_else(|| {
284 let nodes = get_all_focusable_nodes(ir);
285 nodes
286 .iter()
287 .copied()
288 .find(|id| {
289 matches!(
290 ir.nodes.get(id).map(|node| &node.op),
291 Some(Op::Semantics(semantics)) if semantics.autofocus
292 )
293 })
294 .or_else(|| nodes.first().copied())
295 })
296 } else {
297 current.filter(|id| is_enabled_focus_node(ir, *id))
298 };
299
300 if current == next {
301 return Ok(false);
302 }
303
304 self.set_focused_widget(ir, next, crate::TextEditSource::Programmatic)
305 }
306
307 pub fn caret_from_point_in_text(
308 &self,
309 value: &str,
310 font_size: f32,
311 viewport_x: f32,
312 viewport_w: f32,
313 content_w: f32,
314 scroll_offset: f32,
315 point_x: f32,
316 ) -> usize {
317 crate::input::text::caret_from_point_in_text(
318 self.measurer.as_ref(),
319 value,
320 font_size,
321 viewport_x,
322 viewport_w,
323 content_w,
324 scroll_offset,
325 point_x,
326 )
327 }
328
329 pub fn register_reducer<S: GlobalState + 'static>(
331 &mut self,
332 action_id: ActionId,
333 reducer_fn: crate::action::Reducer<S>,
334 ) -> Result<()> {
335 let state_type_id = TypeId::of::<S>();
336
337 let boxed_reducer: BoxedReducer = Box::new(
339 move |app_states: &mut HashMap<TypeId, Box<dyn GlobalState>>,
340 action: &ActionEnvelope,
341 target: WidgetId,
342 _effects: &mut Vec<EffectEnvelope>,
343 _input: &ActionInput,
344 _callback_registry|
345 -> Result<()> {
346 if let Some(state_box) = app_states.get_mut(&state_type_id) {
347 let concrete_state = state_box.downcast_mut::<S>().ok_or_else(|| {
348 anyhow!("Failed to downcast GlobalState to concrete type for reducer")
349 })?;
350 reducer_fn(concrete_state, action, target.into())
351 } else {
352 anyhow::bail!("Target GlobalState for reducer not found in runtime.");
353 }
354 },
355 );
356
357 self.reducers
358 .entry(action_id)
359 .or_default()
360 .push(boxed_reducer);
361 Ok(())
362 }
363
364 pub fn register_base_reducers(&mut self) {
365 use crate::{AdvanceTo, Tick, ADVANCE_TO_ACTION_ID, TICK_ACTION_ID};
366
367 self.register_reducer::<Clock>(
368 *TICK_ACTION_ID,
369 |state: &mut Clock, action: &ActionEnvelope, _target| {
370 let tick_action: Tick = serde_json::from_slice(&action.payload)
371 .map_err(|e| anyhow!("Failed to deserialize Tick: {}", e))?;
372 state.advance_by(tick_action.dt)
373 },
374 )
375 .expect("Failed to register Tick reducer");
376
377 self.register_reducer::<Clock>(
378 *ADVANCE_TO_ACTION_ID,
379 |state: &mut Clock, action: &ActionEnvelope, _target| {
380 let advance_action: AdvanceTo = serde_json::from_slice(&action.payload)
381 .map_err(|e| anyhow!("Failed to deserialize AdvanceTo: {}", e))?;
382 state.set_to(advance_action.time)
383 },
384 )
385 .expect("Failed to register AdvanceTo reducer");
386 }
387
388 pub fn clear_reducers(&mut self) {
389 self.reducers.clear();
390 self.register_base_reducers();
391 }
392
393 pub fn absorb_registry<S: GlobalState>(&mut self, registry: ActionRegistry<S>) {
394 let new_reducers = registry.into_runtime_reducers();
395 for (id, mut list) in new_reducers {
396 self.reducers.entry(id).or_default().append(&mut list);
397 }
398 }
399
400 pub fn absorb_persistent_registry<S: GlobalState>(&mut self, registry: ActionRegistry<S>) {
406 let new_reducers = registry.into_runtime_reducers();
407 for (id, mut list) in new_reducers {
408 self.persistent_reducers
409 .entry(id)
410 .or_default()
411 .append(&mut list);
412 }
413 }
414
415 pub fn clock(&self) -> &Clock {
416 self.get_global_state::<Clock>()
417 .expect("Clock state must always be present")
418 }
419
420 pub fn get_global_state<S: GlobalState + 'static>(&self) -> Option<&S> {
421 self.app_states
422 .get(&TypeId::of::<S>())
423 .and_then(|s_box| s_box.downcast_ref::<S>())
424 }
425
426 pub fn get_global_state_mut<S: GlobalState + 'static>(&mut self) -> Option<&mut S> {
427 self.app_states
428 .get_mut(&TypeId::of::<S>())
429 .and_then(|s_box| s_box.downcast_mut::<S>())
430 }
431
432 pub fn add_global_state<S: GlobalState + 'static>(&mut self, state: Box<S>) -> Result<()> {
433 let type_id = TypeId::of::<S>();
434 if self.app_states.insert(type_id, state).is_some() {
435 anyhow::bail!("Global state of this type already registered.");
436 }
437 Ok(())
438 }
439
440 pub fn with_global_state<S: GlobalState + 'static>(mut self, state: S) -> Self {
441 self.app_states.insert(TypeId::of::<S>(), Box::new(state));
442 self
443 }
444
445 #[doc(hidden)]
446 pub fn get_app_state<S: GlobalState + 'static>(&self) -> Option<&S> {
447 self.get_global_state::<S>()
448 }
449
450 #[doc(hidden)]
451 pub fn get_app_state_mut<S: GlobalState + 'static>(&mut self) -> Option<&mut S> {
452 self.get_global_state_mut::<S>()
453 }
454
455 #[doc(hidden)]
456 pub fn add_app_state<S: GlobalState + 'static>(&mut self, state: Box<S>) -> Result<()> {
457 self.add_global_state(state)
458 }
459
460 pub fn dispatch(&mut self, action: ActionEnvelope, target: WidgetId) -> Result<()> {
461 self.dispatch_with_input(action, target, &ActionInput::None)
462 }
463
464 fn enqueue_effect(&mut self, mut envelope: EffectEnvelope) {
465 envelope.req_id = self.next_req_id;
466 self.next_req_id += 1;
467 self.pending_effects.push(envelope);
468 }
469
470 #[doc(hidden)]
478 pub fn discard_pending_effects(&mut self) -> usize {
479 let discarded = self.pending_effects.len() + self.effect_callbacks.clear();
480 self.pending_effects.clear();
481 discarded
482 }
483
484 pub fn dispatch_with_input(
485 &mut self,
486 action: ActionEnvelope,
487 target: WidgetId,
488 input: &ActionInput,
489 ) -> Result<()> {
490 self.dispatch_node_with_input(action, target.into(), input)
491 }
492
493 fn dispatch_node(&mut self, action: ActionEnvelope, target: WidgetId) -> Result<()> {
494 self.dispatch_node_with_input(action, target, &ActionInput::None)
495 }
496
497 fn dispatch_node_with_input(
498 &mut self,
499 action: ActionEnvelope,
500 target: WidgetId,
501 input: &ActionInput,
502 ) -> Result<()> {
503 let action_id = action.id;
504 let result = self.try_dispatch_node_with_input(action, target, input);
505 if let Err(error) = &result {
506 crate::registry::emit_action_dispatch_failure(action_id, target, error);
507 }
508 result
509 }
510
511 fn try_dispatch_node_with_input(
512 &mut self,
513 action: ActionEnvelope,
514 target: WidgetId,
515 input: &ActionInput,
516 ) -> Result<()> {
517 diag::emit(
518 diag::DiagCategory::Input,
519 diag::DiagLevel::Debug,
520 diag::DiagEventKind::InputEvent {
521 kind: "dispatch_start".into(),
522 target: Some(target.as_u128()),
523 position: None,
524 },
525 );
526
527 if action.id == crate::NavigationRequested::static_id() {
528 let request: crate::NavigationRequested = serde_json::from_slice(&action.payload)
529 .context("failed to decode built-in navigation request")?;
530 self.pending_navigation.push(request.command);
531 return Ok(());
532 }
533
534 if crate::media::handle_video_action(&mut self.runtime_state.video, &action)? {
536 return Ok(());
537 }
538
539 let action = if let Some(resolution) =
540 crate::scoped_action_handlers::dispatch_scoped_action_handler(&action, target, input)?
541 {
542 match resolution {
543 crate::scoped_action_handlers::ScopedActionResolution::Handled => return Ok(()),
544 crate::scoped_action_handlers::ScopedActionResolution::Forward(forwarded) => {
545 if crate::media::handle_video_action(&mut self.runtime_state.video, &forwarded)?
546 {
547 return Ok(());
548 }
549 forwarded
550 }
551 }
552 } else {
553 action
554 };
555
556 let action_id = action.id;
557
558 let mut effects = Vec::new();
560 let callback_registry = self.effect_callbacks.clone();
561
562 let mut callback_reducers = callback_registry.take(action_id);
563 for reducer_wrapper in callback_reducers.iter_mut() {
564 reducer_wrapper(
565 &mut self.app_states,
566 &action,
567 target,
568 &mut effects,
569 input,
570 &callback_registry,
571 )?;
572 }
573
574 if let Some(reducers) = self.persistent_reducers.get_mut(&action_id) {
575 diag::emit(
576 diag::DiagCategory::Input,
577 diag::DiagLevel::Debug,
578 diag::DiagEventKind::InputEvent {
579 kind: format!("persistent_reducers:{}", reducers.len()),
580 target: Some(target.as_u128()),
581 position: None,
582 },
583 );
584
585 let mut temp_reducers: Vec<BoxedReducer> = reducers.drain(..).collect();
586 let dispatch_result = temp_reducers.iter_mut().try_for_each(|reducer_wrapper| {
587 reducer_wrapper(
588 &mut self.app_states,
589 &action,
590 target,
591 &mut effects,
592 input,
593 &callback_registry,
594 )
595 });
596 reducers.extend(temp_reducers);
597 dispatch_result?;
598 }
599
600 if let Some(reducers) = self.reducers.get_mut(&action_id) {
601 diag::emit(
602 diag::DiagCategory::Input,
603 diag::DiagLevel::Debug,
604 diag::DiagEventKind::InputEvent {
605 kind: format!("reducers:{}", reducers.len()),
606 target: Some(target.as_u128()),
607 position: None,
608 },
609 );
610
611 let mut temp_reducers: Vec<BoxedReducer> = reducers.drain(..).collect();
612 let dispatch_result = temp_reducers.iter_mut().try_for_each(|reducer_wrapper| {
613 reducer_wrapper(
614 &mut self.app_states,
615 &action,
616 target,
617 &mut effects,
618 input,
619 &callback_registry,
620 )
621 });
622 reducers.extend(temp_reducers);
623 dispatch_result?;
624 }
625
626 for envelope in effects {
627 self.enqueue_effect(envelope);
628 }
629
630 diag::emit(
631 diag::DiagCategory::Input,
632 diag::DiagLevel::Debug,
633 diag::DiagEventKind::InputEvent {
634 kind: "dispatch_end".into(),
635 target: Some(target.as_u128()),
636 position: None,
637 },
638 );
639 Ok(())
640 }
641
642 pub fn tick(&mut self, dt: CurrentTime) -> Result<TickResult> {
643 use crate::Tick;
644 let action = Tick { dt };
645 let envelope: ActionEnvelope = action.into();
646 self.dispatch_node(envelope, WidgetId::derived(0, &[0]))?;
647
648 let resource_actions_dispatched = self.tick_resource_timers()?;
649
650 let current_time = self.clock().current_time();
651 let changed_motions =
652 crate::motion::tick_motion(&mut self.runtime_state.motion, current_time);
653 Ok(TickResult {
654 changed_motions,
655 resource_actions_dispatched,
656 })
657 }
658
659 fn tick_resource_timers(&mut self) -> Result<usize> {
660 let now = self.clock().current_time();
661 let mut ticks = Vec::new();
662
663 for resource in self.active_resources.values_mut() {
664 if let ActiveResourceKind::Timer {
665 interval_ms,
666 payload,
667 on_tick,
668 next_fire_at,
669 } = &mut resource.kind
670 {
671 let Some(action) = on_tick.clone() else {
672 continue;
673 };
674
675 let interval_ms = (*interval_ms).max(1);
676 while now >= *next_fire_at {
677 ticks.push((action.clone(), payload.clone()));
678 *next_fire_at = next_fire_at.saturating_add(interval_ms);
679 }
680 }
681 }
682
683 let dispatched = ticks.len();
684 for (action, payload) in ticks {
685 self.dispatch_node_with_input(
686 action,
687 WidgetId::derived(0, &[0]),
688 &ActionInput::TimerTick { payload },
689 )?;
690 }
691
692 Ok(dispatched)
693 }
694
695 pub fn sync_motion_declarations(
696 &mut self,
697 declarations: &[crate::MotionDeclaration],
698 layout: Option<&LayoutSnapshot>,
699 ) -> Vec<(WidgetId, crate::MotionPropertyId)> {
700 let current_time = self.clock().current_time();
701 let snapshot = self.runtime_state.clone();
702 let result = crate::motion::sync_motion_declarations(
703 &mut self.runtime_state.motion,
704 declarations,
705 &snapshot,
706 layout,
707 current_time,
708 );
709 result.changed
710 }
711
712 pub fn sync_video_nodes(&mut self, registrations: &[VideoRegistration]) {
713 let mut seen: HashSet<WidgetId> = HashSet::new();
714
715 for reg in registrations {
716 seen.insert(reg.node_id);
717 let entry = self
718 .runtime_state
719 .video
720 .states
721 .entry(reg.node_id)
722 .or_insert_with(crate::env::VideoState::default);
723 entry.asset_source = reg.source.clone();
724 entry.looped = reg.loop_playback;
725 entry.audio = reg.audio.clone();
726 if reg.autoplay && entry.status == VideoStatus::Stopped {
727 entry.status = VideoStatus::Playing;
728 }
729 }
730
731 self.runtime_state
732 .video
733 .states
734 .retain(|node_id, _| seen.contains(node_id));
735 }
736
737 pub fn sync_web_nodes(&mut self, registrations: &[crate::registry::WebRegistration]) {
738 let mut seen: HashSet<WidgetId> = HashSet::new();
739
740 for reg in registrations {
741 seen.insert(reg.node_id);
742 let entry = self
743 .runtime_state
744 .web
745 .states
746 .entry(reg.node_id)
747 .or_insert_with(crate::env::WebState::default);
748
749 if entry.url != reg.url {
751 entry.url = reg.url.clone();
752 entry.loading = true; }
754 entry.user_agent = reg.user_agent.clone();
755 }
756
757 self.runtime_state
758 .web
759 .states
760 .retain(|node_id, _| seen.contains(node_id));
761 }
762
763 pub fn queue_runtime_effect(&mut self, effect: RuntimeEffect) -> bool {
768 match effect {
769 RuntimeEffect::ScrollIntoView(request) => {
770 self.queue_scroll_into_view(request);
771 true
772 }
773 RuntimeEffect::Navigate(command) => {
774 self.pending_navigation.push(command);
775 true
776 }
777 RuntimeEffect::SelectionRegion { region_id, command } => {
778 self.pending_selection_regions.push((region_id, command));
779 true
780 }
781 RuntimeEffect::TextEditing { input_id, command } => {
782 self.pending_text_editing.push((input_id, command));
783 true
784 }
785 RuntimeEffect::TextScroll { input_id, command } => {
786 self.pending_text_scroll.push((input_id, command));
787 true
788 }
789 RuntimeEffect::TextFormValidation { form_id } => {
790 self.pending_text_form_validation.push(form_id);
791 true
792 }
793 RuntimeEffect::Cancel { .. } | RuntimeEffect::ReleaseResource { .. } => false,
794 }
795 }
796
797 #[doc(hidden)]
799 pub fn take_pending_navigation(&mut self) -> Vec<crate::NavigationCommand> {
800 std::mem::take(&mut self.pending_navigation)
801 }
802
803 pub fn queue_scroll_into_view(&mut self, request: ScrollIntoViewRequest) {
805 self.pending_scroll_into_view.push(PendingScrollIntoView {
806 request,
807 retries_remaining: 1,
808 });
809 }
810
811 fn drain_post_layout_effects(&mut self) {
812 let pending = std::mem::take(&mut self.pending_effects);
813
814 for env in pending {
815 let EffectEnvelope {
816 req_id,
817 effect,
818 on_ok,
819 on_err,
820 service_bindings,
821 resource,
822 } = env;
823
824 match effect {
825 Effect::Runtime(RuntimeEffect::ScrollIntoView(request)) => {
826 self.queue_scroll_into_view(request);
827 }
828 Effect::Runtime(RuntimeEffect::SelectionRegion { region_id, command }) => {
829 self.pending_selection_regions.push((region_id, command));
830 }
831 Effect::Runtime(RuntimeEffect::TextEditing { input_id, command }) => {
832 self.pending_text_editing.push((input_id, command));
833 }
834 Effect::Runtime(RuntimeEffect::TextScroll { input_id, command }) => {
835 self.pending_text_scroll.push((input_id, command));
836 }
837 Effect::Runtime(RuntimeEffect::TextFormValidation { form_id }) => {
838 self.pending_text_form_validation.push(form_id);
839 }
840 retained => self.pending_effects.push(EffectEnvelope {
841 req_id,
842 effect: retained,
843 on_ok,
844 on_err,
845 service_bindings,
846 resource,
847 }),
848 }
849 }
850 }
851
852 fn apply_pending_scroll_into_view(&mut self, ir: &CoreIR, layout: &LayoutSnapshot) -> bool {
853 self.drain_post_layout_effects();
854
855 let mut needs_follow_up_frame = false;
856 let pending = std::mem::take(&mut self.pending_scroll_into_view);
857
858 for mut pending_request in pending {
859 match self.apply_scroll_into_view(&pending_request.request, ir, layout) {
860 ScrollIntoViewOutcome::Applied { changed } => {
861 needs_follow_up_frame |= changed;
862 }
863 ScrollIntoViewOutcome::Retry if pending_request.retries_remaining > 0 => {
864 pending_request.retries_remaining -= 1;
865 self.pending_scroll_into_view.push(pending_request);
866 needs_follow_up_frame = true;
867 }
868 ScrollIntoViewOutcome::Retry | ScrollIntoViewOutcome::Ignored => {}
869 }
870 }
871
872 needs_follow_up_frame
873 }
874
875 fn apply_scroll_into_view(
876 &mut self,
877 request: &ScrollIntoViewRequest,
878 ir: &CoreIR,
879 layout: &LayoutSnapshot,
880 ) -> ScrollIntoViewOutcome {
881 let Some(target_geom) = layout.get_node_geometry(request.target) else {
882 Self::emit_scroll_into_view_diag("missing_target", request, None);
883 return ScrollIntoViewOutcome::Retry;
884 };
885
886 let Some(container_id) = self.resolve_scroll_container(request, ir, layout) else {
887 Self::emit_scroll_into_view_diag("missing_container", request, None);
888 return ScrollIntoViewOutcome::Retry;
889 };
890
891 if !Self::is_descendant_or_self(ir, request.target, container_id) {
892 Self::emit_scroll_into_view_diag("target_not_descendant", request, Some(container_id));
893 return ScrollIntoViewOutcome::Ignored;
894 }
895
896 let Some(container_geom) = layout.get_node_geometry(container_id) else {
897 Self::emit_scroll_into_view_diag(
898 "missing_container_layout",
899 request,
900 Some(container_id),
901 );
902 return ScrollIntoViewOutcome::Retry;
903 };
904
905 let Some(direction) = Self::scroll_direction(ir, container_id) else {
906 Self::emit_scroll_into_view_diag("not_scroll_container", request, Some(container_id));
907 return ScrollIntoViewOutcome::Ignored;
908 };
909
910 if !Self::axis_matches(request.axis, direction) {
911 Self::emit_scroll_into_view_diag("axis_mismatch", request, Some(container_id));
912 return ScrollIntoViewOutcome::Ignored;
913 }
914
915 if matches!(request.behavior, ScrollBehavior::Smooth) {
916 Self::emit_scroll_into_view_diag(
917 "smooth_resolved_as_instant",
918 request,
919 Some(container_id),
920 );
921 }
922
923 let current_offset = self.runtime_state.scroll.get_offset(container_id);
924 let new_offset = match direction {
925 FlexDirection::Column => Self::compute_scroll_offset(
926 current_offset,
927 target_geom.rect.y() - container_geom.rect.y(),
928 target_geom.rect.height(),
929 container_geom.rect.height(),
930 container_geom.content_size.height,
931 request.padding[2],
932 request.padding[3],
933 request.alignment,
934 request.if_needed,
935 ),
936 FlexDirection::Row => Self::compute_scroll_offset(
937 current_offset,
938 target_geom.rect.x() - container_geom.rect.x(),
939 target_geom.rect.width(),
940 container_geom.rect.width(),
941 container_geom.content_size.width,
942 request.padding[0],
943 request.padding[1],
944 request.alignment,
945 request.if_needed,
946 ),
947 };
948
949 if (new_offset - current_offset).abs() > f32::EPSILON {
950 self.runtime_state
951 .scroll
952 .set_offset(container_id, new_offset);
953 ScrollIntoViewOutcome::Applied { changed: true }
954 } else {
955 ScrollIntoViewOutcome::Applied { changed: false }
956 }
957 }
958
959 fn resolve_scroll_container(
960 &self,
961 request: &ScrollIntoViewRequest,
962 ir: &CoreIR,
963 layout: &LayoutSnapshot,
964 ) -> Option<WidgetId> {
965 if let Some(container) = request.container {
966 return ir
967 .nodes
968 .contains_key(&container)
969 .then_some(container)
970 .filter(|id| layout.get_node_geometry(*id).is_some());
971 }
972
973 let mut current = ir.nodes.get(&request.target)?.parent;
974 while let Some(node_id) = current {
975 if let Some(direction) = Self::scroll_direction(ir, node_id) {
976 if Self::axis_matches(request.axis, direction)
977 && layout.get_node_geometry(node_id).is_some()
978 {
979 return Some(node_id);
980 }
981 }
982 current = ir.nodes.get(&node_id).and_then(|node| node.parent);
983 }
984
985 None
986 }
987
988 fn scroll_direction(ir: &CoreIR, node_id: WidgetId) -> Option<FlexDirection> {
989 match ir.nodes.get(&node_id).map(|node| &node.op) {
990 Some(Op::Layout(LayoutOp::Scroll { direction, .. })) => Some(*direction),
991 _ => None,
992 }
993 }
994
995 fn axis_matches(axis: ScrollAxis, direction: FlexDirection) -> bool {
996 matches!(
997 (axis, direction),
998 (ScrollAxis::Both, _)
999 | (ScrollAxis::Vertical, FlexDirection::Column)
1000 | (ScrollAxis::Horizontal, FlexDirection::Row)
1001 )
1002 }
1003
1004 fn is_descendant_or_self(ir: &CoreIR, target: WidgetId, ancestor: WidgetId) -> bool {
1005 let mut current = Some(target);
1006 while let Some(node_id) = current {
1007 if node_id == ancestor {
1008 return true;
1009 }
1010 current = ir.nodes.get(&node_id).and_then(|node| node.parent);
1011 }
1012 false
1013 }
1014
1015 fn compute_scroll_offset(
1016 current_offset: f32,
1017 target_content_start: f32,
1018 target_size: f32,
1019 viewport_size: f32,
1020 content_size: f32,
1021 padding_start: f32,
1022 padding_end: f32,
1023 alignment: ScrollAlignment,
1024 if_needed: bool,
1025 ) -> f32 {
1026 let current_offset = Self::finite_or_zero(current_offset).max(0.0);
1027 let viewport_size = Self::finite_or_zero(viewport_size).max(0.0);
1028 let content_size = Self::finite_or_zero(content_size).max(0.0);
1029 let target_size = Self::finite_or_zero(target_size).max(0.0);
1030 let padding_start = Self::finite_or_zero(padding_start).max(0.0);
1031 let padding_end = Self::finite_or_zero(padding_end).max(0.0);
1032 let max_offset = (content_size - viewport_size).max(0.0);
1033
1034 if viewport_size <= f32::EPSILON || max_offset <= f32::EPSILON {
1035 return 0.0;
1036 }
1037
1038 let target_start = Self::finite_or_zero(target_content_start);
1039 let target_end = target_start + target_size;
1040 let reveal_start = target_start - padding_start;
1041 let reveal_end = target_end + padding_end;
1042 let viewport_start = current_offset;
1043 let viewport_end = current_offset + viewport_size;
1044
1045 if if_needed && reveal_start >= viewport_start && reveal_end <= viewport_end {
1046 return current_offset.min(max_offset);
1047 }
1048
1049 let desired = match alignment {
1050 ScrollAlignment::Start => reveal_start,
1051 ScrollAlignment::Center => {
1052 let padded_viewport = (viewport_size - padding_start - padding_end).max(0.0);
1053 target_start - padding_start - (padded_viewport - target_size) * 0.5
1054 }
1055 ScrollAlignment::End => reveal_end - viewport_size,
1056 ScrollAlignment::Nearest => {
1057 if reveal_end - reveal_start > viewport_size {
1058 reveal_start
1059 } else if reveal_start < viewport_start {
1060 reveal_start
1061 } else if reveal_end > viewport_end {
1062 reveal_end - viewport_size
1063 } else {
1064 current_offset
1065 }
1066 }
1067 ScrollAlignment::Fraction(fraction) => {
1068 let fraction = Self::finite_or_zero(fraction).clamp(0.0, 1.0);
1069 let padded_viewport = (viewport_size - padding_start - padding_end).max(0.0);
1070 target_start - padding_start - (padded_viewport - target_size) * fraction
1071 }
1072 };
1073
1074 Self::finite_or_zero(desired).clamp(0.0, max_offset)
1075 }
1076
1077 fn finite_or_zero(value: f32) -> f32 {
1078 if value.is_finite() {
1079 value
1080 } else {
1081 0.0
1082 }
1083 }
1084
1085 fn emit_scroll_into_view_diag(
1086 kind: &'static str,
1087 request: &ScrollIntoViewRequest,
1088 container: Option<WidgetId>,
1089 ) {
1090 diag::emit(
1091 diag::DiagCategory::Input,
1092 diag::DiagLevel::Debug,
1093 diag::DiagEventKind::InputEvent {
1094 kind: format!(
1095 "scroll_into_view:{kind}:target={:?}:container={:?}",
1096 request.target,
1097 container.or(request.container)
1098 ),
1099 target: Some(request.target.as_u128()),
1100 position: None,
1101 },
1102 );
1103 }
1104
1105 pub fn reconcile_ir(&mut self, ir: &CoreIR) {
1111 self.runtime_state.viewport.reconcile(ir);
1112 crate::selection::reconcile_selection_state(&mut self.runtime_state.selectable_text, ir);
1113 let active_scroll_nodes: HashSet<WidgetId> = ir
1114 .nodes
1115 .iter()
1116 .filter_map(|(id, node)| match node.op {
1117 Op::Layout(LayoutOp::Scroll { .. }) => Some(*id),
1118 _ => None,
1119 })
1120 .collect();
1121 self.runtime_state
1122 .scroll
1123 .retain_active(&active_scroll_nodes);
1124 if self
1125 .runtime_state
1126 .gesture
1127 .scrollbar_drag
1128 .is_some_and(|drag| !active_scroll_nodes.contains(&drag.node_id))
1129 {
1130 self.runtime_state.gesture.scrollbar_drag = None;
1131 }
1132 }
1133
1134 pub fn post_layout_hook(&mut self, ir: &CoreIR, layout: &LayoutSnapshot) -> bool {
1139 self.reconcile_ir(ir);
1143 let mut needs_follow_up_frame = self.apply_pending_scroll_into_view(ir, layout);
1144 needs_follow_up_frame |= self.apply_pending_selection_regions(ir);
1145 needs_follow_up_frame |= self.apply_pending_text_editing(ir, layout);
1146 needs_follow_up_frame |= self.apply_pending_text_scroll(ir, layout);
1147 needs_follow_up_frame |= self.apply_pending_text_form_validation(ir);
1148 let current_time = self.clock().current_time();
1149 needs_follow_up_frame |=
1150 self.runtime_state
1151 .viewport
1152 .advance_inertia(ir, layout, current_time);
1153
1154 let mut current_heroes = HashMap::new();
1155
1156 for (id, node) in &ir.nodes {
1157 if let Op::Semantics(s) = &node.op {
1158 if let Some(tag) = &s.hero_tag {
1159 if let Some(geom) = layout.get_node_geometry(*id) {
1160 current_heroes.insert(tag.clone(), (*id, geom.rect));
1161 }
1162 }
1163 }
1164 }
1165
1166 for (tag, (_new_id, new_rect)) in ¤t_heroes {
1168 if let Some((_old_id, old_rect)) = self.runtime_state.hero.positions.get(tag) {
1169 if *new_rect != *old_rect {
1170 diag::emit(
1172 diag::DiagCategory::Layout,
1173 diag::DiagLevel::Debug,
1174 diag::DiagEventKind::AnchorPlacement {
1175 widget: 0,
1176 node: 0,
1177 rect_x: old_rect.origin.x,
1178 rect_y: old_rect.origin.y,
1179 rect_w: old_rect.size.width,
1180 rect_h: old_rect.size.height,
1181 place_left: new_rect.origin.x,
1182 place_top: new_rect.origin.y,
1183 note: Some(format!("Hero flight: {}", tag)),
1184 },
1185 );
1186 }
1187 }
1188 }
1189
1190 self.runtime_state.hero.positions = current_heroes;
1191 needs_follow_up_frame
1192 }
1193
1194 fn apply_pending_selection_regions(&mut self, ir: &CoreIR) -> bool {
1195 let pending = std::mem::take(&mut self.pending_selection_regions);
1196 let mut changed = false;
1197 for (region_id, command) in pending {
1198 changed |= crate::selection::apply_region_command(
1199 &mut self.runtime_state.selectable_text,
1200 ir,
1201 region_id,
1202 command,
1203 )
1204 .is_ok();
1205 }
1206 changed
1207 }
1208
1209 fn apply_pending_text_form_validation(&mut self, ir: &CoreIR) -> bool {
1210 let pending = std::mem::take(&mut self.pending_text_form_validation);
1211 let mut dispatched = false;
1212 for form_id in pending {
1213 let fields = ir
1214 .nodes
1215 .iter()
1216 .filter_map(|(id, node)| match &node.op {
1217 Op::Semantics(semantics)
1218 if semantics.role == fission_ir::Role::TextInput
1219 && semantics.text_form_id.as_deref() == Some(form_id.as_str()) =>
1220 {
1221 Some((*id, semantics.clone()))
1222 }
1223 _ => None,
1224 })
1225 .collect::<Vec<_>>();
1226 for (id, semantics) in fields {
1227 let Some(entry) = semantics
1228 .actions
1229 .entries
1230 .iter()
1231 .find(|entry| entry.trigger == fission_ir::ActionTrigger::Validation)
1232 else {
1233 continue;
1234 };
1235 let value = self.text_input_value(ir, id);
1236 let mut update = crate::UpdateTextInput::from_values(
1237 id,
1238 value.clone(),
1239 value,
1240 crate::TextEditSource::Programmatic,
1241 crate::TextEditPhase::Validated,
1242 );
1243 update.validation_state = Some(semantics.validation_state);
1244 update.validation_message = semantics.validation_message.clone();
1245 let input = crate::input::scoped_action_input(
1246 ir,
1247 id,
1248 crate::ActionInput::TextChanged(update),
1249 );
1250 let envelope = crate::ActionEnvelope {
1251 id: crate::ActionId::from_u128(entry.action_id),
1252 payload: entry.payload_data.clone().unwrap_or_else(|| {
1253 serde_json::to_vec(&()).expect("unit action payload must serialize")
1254 }),
1255 };
1256 if let Err(error) = self.dispatch_with_input(envelope, id, &input) {
1257 diag::emit(
1258 diag::DiagCategory::Input,
1259 diag::DiagLevel::Warn,
1260 diag::DiagEventKind::InputEvent {
1261 kind: format!("text_form_validation:{error}"),
1262 target: Some(id.as_u128()),
1263 position: None,
1264 },
1265 );
1266 } else {
1267 dispatched = true;
1268 }
1269 }
1270 }
1271 dispatched
1272 }
1273
1274 fn apply_pending_text_editing(&mut self, ir: &CoreIR, layout: &LayoutSnapshot) -> bool {
1275 let pending = std::mem::take(&mut self.pending_text_editing);
1276 let mut changed = false;
1277 for (input_id, command) in pending {
1278 let result = match command {
1279 crate::TextEditingCommand::Focus => {
1280 self.set_focused_widget(ir, Some(input_id), crate::TextEditSource::Programmatic)
1281 }
1282 crate::TextEditingCommand::Unfocus => {
1283 if self.runtime_state.interaction.focused == Some(input_id) {
1284 self.set_focused_widget(ir, None, crate::TextEditSource::Programmatic)
1285 } else {
1286 Ok(false)
1287 }
1288 }
1289 crate::TextEditingCommand::SelectAll => {
1290 let value = self.text_input_value(ir, input_id);
1291 let end = crate::TextPosition::at_end(&value.text);
1292 self.apply_text_edit_command_to(
1293 ir,
1294 layout,
1295 input_id,
1296 crate::TextEditCommand::SetSelection {
1297 selection: crate::TextSelection {
1298 base: crate::TextPosition::START,
1299 extent: end,
1300 affinity: crate::TextAffinity::Downstream,
1301 },
1302 source: crate::TextEditSource::Programmatic,
1303 },
1304 )
1305 }
1306 crate::TextEditingCommand::SetSelection(selection) => self
1307 .apply_text_edit_command_to(
1308 ir,
1309 layout,
1310 input_id,
1311 crate::TextEditCommand::SetSelection {
1312 selection,
1313 source: crate::TextEditSource::Programmatic,
1314 },
1315 ),
1316 crate::TextEditingCommand::SetValue(value) => self.apply_text_edit_command_to(
1317 ir,
1318 layout,
1319 input_id,
1320 crate::TextEditCommand::SetValue {
1321 value,
1322 source: crate::TextEditSource::Programmatic,
1323 phase: crate::TextValuePhase::Committed,
1324 },
1325 ),
1326 };
1327 match result {
1328 Ok(applied) => changed |= applied,
1329 Err(error) => diag::emit(
1330 diag::DiagCategory::Input,
1331 diag::DiagLevel::Warn,
1332 diag::DiagEventKind::InputEvent {
1333 kind: format!("text_editing_controller:{error}"),
1334 target: Some(input_id.as_u128()),
1335 position: None,
1336 },
1337 ),
1338 }
1339 }
1340 changed
1341 }
1342
1343 #[doc(hidden)]
1346 pub fn apply_text_edit_command_to(
1347 &mut self,
1348 ir: &CoreIR,
1349 layout: &LayoutSnapshot,
1350 input_id: WidgetId,
1351 command: crate::TextEditCommand,
1352 ) -> Result<bool> {
1353 use crate::input::text::TextInputController;
1354 use crate::input::ControllerContext;
1355
1356 let previous_text_state = self.runtime_state.text_edit.get(input_id).cloned();
1357 let current_time = self.clock().current_time();
1358 let (handled, dispatched_actions) = {
1359 let mut context = ControllerContext {
1360 ir,
1361 layout,
1362 text_edit: &mut self.runtime_state.text_edit,
1363 selectable_text: &mut self.runtime_state.selectable_text,
1364 context_menu: &mut self.runtime_state.context_menu,
1365 interaction: &mut self.runtime_state.interaction,
1366 scroll: &mut self.runtime_state.scroll,
1367 viewport: &self.runtime_state.viewport,
1368 gesture: &mut self.runtime_state.gesture,
1369 editing_convention: self.editing_convention,
1370 current_time,
1371 clipboard: self.clipboard_backend.as_ref(),
1372 measurer: self.measurer.as_ref(),
1373 dispatched_actions: Vec::new(),
1374 };
1375 let handled =
1376 TextInputController.handle_text_edit_command_for(&mut context, input_id, command);
1377 (handled, context.dispatched_actions)
1378 };
1379 if let Err(error) = self.dispatch_input_actions(dispatched_actions) {
1380 if let Some(previous) = previous_text_state {
1381 self.runtime_state
1382 .text_edit
1383 .states
1384 .insert(input_id, previous);
1385 } else {
1386 self.runtime_state.text_edit.states.remove(&input_id);
1387 }
1388 return Err(error);
1389 }
1390 if self.runtime_state.interaction.focused == Some(input_id) {
1391 self.update_focused_ime_state(ir, layout);
1392 }
1393 Ok(handled)
1394 }
1395
1396 fn apply_pending_text_scroll(&mut self, ir: &CoreIR, layout: &LayoutSnapshot) -> bool {
1397 let pending = std::mem::take(&mut self.pending_text_scroll);
1398 let mut changed = false;
1399 for (input_id, command) in pending {
1400 changed |= crate::TextScrollController::new(input_id)
1401 .apply(&mut self.runtime_state, ir, layout, command)
1402 .is_ok();
1403 }
1404 changed
1405 }
1406
1407 pub fn handle_input(
1408 &mut self,
1409 event: InputEvent,
1410 ir: &CoreIR,
1411 layout: &LayoutSnapshot,
1412 ) -> Result<()> {
1413 use crate::hit_test::{
1414 find_neighbor_focus_node, find_next_focus_node, hit_test_with_viewports, FocusDirection,
1415 };
1416 use crate::input::gesture::GestureController;
1417 use crate::input::hover::HoverController;
1418 use crate::input::selectable_text::SelectableTextController;
1419 use crate::input::slider::SliderController;
1420 use crate::input::text::TextInputController;
1421 use crate::input::{ControllerContext, InputController};
1422 use crate::scrollbar::scrollbar_hit_test;
1423 use crate::ui::custom_render::downcast_render_object;
1424
1425 self.reconcile_focus(ir)?;
1426 let input_time = self.clock().current_time();
1427
1428 if self.runtime_state.interaction.focused.is_none() {
1429 if let Some(autofocus_id) = Self::find_autofocus_node(ir) {
1430 self.set_focused_widget(
1431 ir,
1432 Some(autofocus_id),
1433 crate::TextEditSource::Programmatic,
1434 )?;
1435 }
1436 }
1437
1438 if matches!(event, InputEvent::Pointer(_)) {
1439 let dispatched_actions = {
1440 let mut ctx = ControllerContext {
1441 ir,
1442 layout,
1443 text_edit: &mut self.runtime_state.text_edit,
1444 selectable_text: &mut self.runtime_state.selectable_text,
1445 context_menu: &mut self.runtime_state.context_menu,
1446 interaction: &mut self.runtime_state.interaction,
1447 scroll: &mut self.runtime_state.scroll,
1448 viewport: &self.runtime_state.viewport,
1449 gesture: &mut self.runtime_state.gesture,
1450 editing_convention: self.editing_convention,
1451 current_time: input_time,
1452 clipboard: self.clipboard_backend.as_ref(),
1453 measurer: self.measurer.as_ref(),
1454 dispatched_actions: Vec::new(),
1455 };
1456 let mut hover_controller = HoverController;
1457 let _ = hover_controller.handle_event(&mut ctx, &event);
1458 ctx.dispatched_actions
1459 };
1460 self.dispatch_input_actions(dispatched_actions)?;
1461 }
1462
1463 let pointer_targets_scrollbar = match &event {
1469 InputEvent::Pointer(PointerEvent::Down { point, button, .. })
1470 if matches!(button, PointerButton::Primary) =>
1471 {
1472 scrollbar_hit_test(ir, layout, &self.runtime_state.scroll, *point).is_some()
1473 }
1474 InputEvent::Pointer(PointerEvent::Move { .. })
1475 | InputEvent::Pointer(PointerEvent::Up { .. }) => {
1476 self.runtime_state.gesture.scrollbar_drag.is_some()
1477 }
1478 InputEvent::Pointer(PointerEvent::Scroll { point, .. }) => {
1479 scrollbar_hit_test(ir, layout, &self.runtime_state.scroll, *point).is_some()
1480 }
1481 _ => false,
1482 };
1483
1484 if !pointer_targets_scrollbar {
1485 if let Some(point) = Self::event_point(&event) {
1486 if let Some(hit_node_id) = hit_test_with_viewports(
1487 ir,
1488 layout,
1489 &self.runtime_state.scroll,
1490 &self.runtime_state.viewport,
1491 point,
1492 ) {
1493 let mut target_ro: Option<(WidgetId, &fission_ir::AnyRenderObject)> = None;
1498 {
1499 let mut walk = Some(hit_node_id);
1500 while let Some(nid) = walk {
1501 if let Some(ro) = ir.custom_render_objects.get(&nid) {
1502 target_ro = Some((nid, ro));
1503 break;
1504 }
1505 walk = ir.nodes.get(&nid).and_then(|n| n.parent);
1506 }
1507 }
1508 if target_ro.is_none() {
1509 for (ro_nid, ro) in &ir.custom_render_objects {
1510 if let Some(rect) = layout.get_node_rect(*ro_nid) {
1511 if rect.contains(point) {
1512 target_ro = Some((*ro_nid, ro));
1513 break;
1514 }
1515 }
1516 }
1517 }
1518
1519 if let Some((nid, any_ro)) = target_ro {
1520 if let Some(render_obj) = downcast_render_object(any_ro) {
1521 let mut node_rect = layout
1522 .get_node_rect(nid)
1523 .unwrap_or(LayoutRect::new(0.0, 0.0, 0.0, 0.0));
1524 {
1527 let mut walk = ir.nodes.get(&nid).and_then(|n| n.parent);
1528 while let Some(pid) = walk {
1529 if let Some(pnode) = ir.nodes.get(&pid) {
1530 if let fission_ir::Op::Layout(
1531 fission_ir::LayoutOp::Scroll { direction, .. },
1532 ) = &pnode.op
1533 {
1534 let off = self.runtime_state.scroll.get_offset(pid);
1535 match direction {
1536 fission_ir::FlexDirection::Row => {
1537 node_rect.origin.x -= off
1538 }
1539 fission_ir::FlexDirection::Column => {
1540 node_rect.origin.y -= off
1541 }
1542 }
1543 }
1544 walk = pnode.parent;
1545 } else {
1546 break;
1547 }
1548 }
1549 }
1550 let result = render_obj.handle_event(nid, &event, node_rect);
1551 if result.handled {
1552 if matches!(
1554 event,
1555 InputEvent::Pointer(PointerEvent::Down {
1556 button: PointerButton::Primary,
1557 ..
1558 })
1559 ) {
1560 self.set_focused_widget(
1561 ir,
1562 Some(nid),
1563 crate::TextEditSource::Pointer,
1564 )?;
1565 if let Some(ime_handler) = &self.ime_handler {
1566 let accepts_text = render_obj.accepts_text_input();
1567 ime_handler.set_ime_allowed(accepts_text);
1568 if accepts_text {
1569 if let Some(rect) =
1570 render_obj.ime_cursor_area(node_rect)
1571 {
1572 ime_handler.set_ime_cursor_area(rect);
1573 }
1574 }
1575 }
1576 }
1577 for (target, envelope) in result.actions {
1579 self.dispatch_node(envelope, target)?;
1580 }
1581 self.update_focused_ime_state(ir, layout);
1582 return Ok(());
1583 }
1584 }
1585 }
1586 }
1587 }
1588 }
1589
1590 if matches!(
1596 event,
1597 InputEvent::Keyboard(_)
1598 | InputEvent::Ime(_)
1599 | InputEvent::Editing(_)
1600 | InputEvent::TextEdit(_)
1601 ) {
1602 if let Some(focused_id) = self.runtime_state.interaction.focused {
1603 let mut walk_id = Some(focused_id);
1604 while let Some(nid) = walk_id {
1605 if let Some(any_ro) = ir.custom_render_objects.get(&nid) {
1606 if let Some(render_obj) = downcast_render_object(any_ro) {
1607 let node_rect = layout
1608 .get_node_rect(nid)
1609 .unwrap_or(LayoutRect::new(0.0, 0.0, 0.0, 0.0));
1610 let result = render_obj.handle_event(nid, &event, node_rect);
1611 if result.handled {
1612 for (target, envelope) in result.actions {
1613 self.dispatch_node(envelope, target)?;
1614 }
1615 self.update_focused_ime_state(ir, layout);
1616 return Ok(());
1617 }
1618 }
1619 }
1620 walk_id = ir.nodes.get(&nid).and_then(|n| n.parent);
1621 }
1622 }
1623 }
1624
1625 let (viewport_handled, viewport_actions) = {
1626 let current_time = self.clock().current_time();
1627 let mut ctx = crate::input::viewport::ViewportControllerContext {
1628 ir,
1629 layout,
1630 scroll: &self.runtime_state.scroll,
1631 viewport: &mut self.runtime_state.viewport,
1632 gesture: &mut self.runtime_state.gesture,
1633 current_time,
1634 dispatched_actions: Vec::new(),
1635 };
1636 let mut controller = crate::input::viewport::ViewportController;
1637 let handled = controller.handle_event(&mut ctx, &event);
1638 (handled, ctx.dispatched_actions)
1639 };
1640 self.dispatch_input_actions(viewport_actions)?;
1641 if viewport_handled {
1642 if matches!(
1643 event,
1644 InputEvent::Pointer(PointerEvent::Up { .. } | PointerEvent::Cancel { .. })
1645 ) {
1646 self.runtime_state.interaction.pressed.clear();
1647 self.runtime_state.interaction.last_down_point = None;
1648 }
1649 self.update_focused_ime_state(ir, layout);
1650 return Ok(());
1651 }
1652
1653 let mut pointer_select_all_target = None;
1654
1655 if let InputEvent::Pointer(PointerEvent::Down {
1660 point,
1661 button: PointerButton::Primary,
1662 ..
1663 }) = &event
1664 {
1665 let mut candidate = hit_test_with_viewports(
1666 ir,
1667 layout,
1668 &self.runtime_state.scroll,
1669 &self.runtime_state.viewport,
1670 *point,
1671 );
1672 let mut preserve_current = false;
1673 let mut next = None;
1674 while let Some(node_id) = candidate {
1675 let Some(node) = ir.nodes.get(&node_id) else {
1676 break;
1677 };
1678 if let Op::Semantics(semantics) = &node.op {
1679 if semantics.focusable {
1680 if semantics.focus_policy == FocusPolicy::PreserveCurrentOnPointer {
1681 preserve_current = true;
1682 } else {
1683 next = Some(node_id);
1684 }
1685 break;
1686 }
1687 }
1688 candidate = node.parent;
1689 }
1690 if !preserve_current {
1691 let changed = self.set_focused_widget(ir, next, crate::TextEditSource::Pointer)?;
1692 if changed
1693 && next.is_some_and(|id| {
1694 ir.custom_render_objects
1695 .get(&id)
1696 .and_then(
1697 crate::ui::widgets::text_input::downcast_text_input_runtime_config,
1698 )
1699 .is_some_and(|config| config.select_all_on_focus)
1700 })
1701 {
1702 pointer_select_all_target = next;
1703 }
1704 }
1705 }
1706
1707 let (handled, dispatched_actions) = {
1708 let mut ctx = ControllerContext {
1709 ir,
1710 layout,
1711 text_edit: &mut self.runtime_state.text_edit,
1712 selectable_text: &mut self.runtime_state.selectable_text,
1713 context_menu: &mut self.runtime_state.context_menu,
1714 interaction: &mut self.runtime_state.interaction,
1715 scroll: &mut self.runtime_state.scroll,
1716 viewport: &self.runtime_state.viewport,
1717 gesture: &mut self.runtime_state.gesture,
1718 editing_convention: self.editing_convention,
1719 current_time: input_time,
1720 clipboard: self.clipboard_backend.as_ref(),
1721 measurer: self.measurer.as_ref(),
1722 dispatched_actions: Vec::new(),
1723 };
1724
1725 let mut hover_controller = HoverController;
1726 let _ = hover_controller.handle_event(&mut ctx, &event);
1727
1728 let mut selectable_text_controller = SelectableTextController;
1729 let handled = if selectable_text_controller.handle_event(&mut ctx, &event) {
1730 true
1731 } else {
1732 let mut gesture_controller = GestureController;
1733 if gesture_controller.handle_event(&mut ctx, &event) {
1734 true
1735 } else {
1736 let text_handled = if pointer_select_all_target.is_some() {
1737 true
1738 } else {
1739 let mut text_controller = TextInputController;
1740 text_controller.handle_event(&mut ctx, &event)
1741 };
1742 if text_handled {
1743 true
1744 } else {
1745 let mut slider_controller = SliderController;
1746 slider_controller.handle_event(&mut ctx, &event)
1747 }
1748 }
1749 };
1750 (handled, ctx.dispatched_actions)
1751 };
1752
1753 self.dispatch_input_actions(dispatched_actions)?;
1754
1755 if handled {
1756 if matches!(
1757 event,
1758 InputEvent::Pointer(PointerEvent::Up { .. } | PointerEvent::Cancel { .. })
1759 ) {
1760 self.runtime_state.interaction.pressed.clear();
1761 self.runtime_state.interaction.last_down_point = None;
1762 }
1763 self.update_focused_ime_state(ir, layout);
1764 return Ok(());
1765 }
1766
1767 match event {
1768 InputEvent::Pointer(PointerEvent::Scroll { point, delta, .. }) => {
1769 let trace_scroll =
1770 std::env::var("FISSION_SCROLL_TRACE").ok().as_deref() == Some("1");
1771 if trace_scroll {
1772 eprintln!(
1773 "[scroll-trace] event point=({:.1},{:.1}) delta=({:.1},{:.1})",
1774 point.x, point.y, delta.x, delta.y
1775 );
1776 }
1777 let hit_node_id = scrollbar_hit_test(ir, layout, &self.runtime_state.scroll, point)
1778 .map(|hit| hit.geometry.node_id)
1779 .or_else(|| {
1780 hit_test_with_viewports(
1781 ir,
1782 layout,
1783 &self.runtime_state.scroll,
1784 &self.runtime_state.viewport,
1785 point,
1786 )
1787 });
1788 if let Some(hit_node_id) = hit_node_id {
1789 if trace_scroll {
1790 eprintln!("[scroll-trace] hit_node={}", hit_node_id.as_u128());
1791 }
1792 let mut current_id = Some(hit_node_id);
1793 while let Some(node_id) = current_id {
1794 if let Some(node) = ir.nodes.get(&node_id) {
1795 if let Op::Layout(LayoutOp::Scroll { direction, .. }) = &node.op {
1796 if crate::ui::widgets::text_input::text_input_scroll_physics_for_node(
1797 ir, node_id,
1798 ) == Some(crate::ui::widgets::text_input::TextScrollPhysics::NeverScrollable)
1799 {
1800 current_id = node.parent;
1801 continue;
1802 }
1803 let current_offset = self.runtime_state.scroll.get_offset(node_id);
1804 let delta_val = match direction {
1805 FlexDirection::Row => delta.x,
1806 FlexDirection::Column => delta.y,
1807 };
1808 let mut new_offset = current_offset + delta_val;
1809
1810 let mut max_offset = 0.0f32;
1811 let mut viewport_w = 0.0f32;
1812 let mut viewport_h = 0.0f32;
1813 let mut content_w = 0.0f32;
1814 let mut content_h = 0.0f32;
1815 if let Some(geom) = layout.get_node_geometry(node_id) {
1816 viewport_w = geom.rect.width();
1817 viewport_h = geom.rect.height();
1818 content_w = geom.content_size.width;
1819 content_h = geom.content_size.height;
1820 max_offset = if matches!(direction, FlexDirection::Row) {
1821 (geom.content_size.width - geom.rect.width()).max(0.0)
1822 } else {
1823 (geom.content_size.height - geom.rect.height()).max(0.0)
1824 };
1825 new_offset = new_offset.clamp(0.0, max_offset);
1826 }
1827
1828 if trace_scroll {
1829 eprintln!(
1830 "[scroll-trace] scroll_node={} axis={} offset={:.1}->{:.1} max={:.1} viewport=({:.1},{:.1}) content=({:.1},{:.1})",
1831 node_id.as_u128(),
1832 match direction { FlexDirection::Row => "x", FlexDirection::Column => "y" },
1833 current_offset,
1834 new_offset,
1835 max_offset,
1836 viewport_w,
1837 viewport_h,
1838 content_w,
1839 content_h
1840 );
1841 }
1842
1843 {
1844 use fission_diagnostics::prelude as diag;
1845 diag::emit(
1846 diag::DiagCategory::Input,
1847 diag::DiagLevel::Debug,
1848 diag::DiagEventKind::ScrollUpdate {
1849 node: node_id.as_u128(),
1850 axis: match direction {
1851 FlexDirection::Row => "x".into(),
1852 FlexDirection::Column => "y".into(),
1853 },
1854 point_x: point.x,
1855 point_y: point.y,
1856 delta: delta_val,
1857 old_offset: current_offset,
1858 new_offset,
1859 max_offset,
1860 viewport_w,
1861 viewport_h,
1862 content_w,
1863 content_h,
1864 },
1865 );
1866 }
1867
1868 self.runtime_state.scroll.set_offset(node_id, new_offset);
1869 if (new_offset - current_offset).abs() > 0.001 {
1873 break;
1874 }
1875 }
1877 current_id = node.parent;
1878 } else {
1879 break;
1880 }
1881 }
1882 } else if trace_scroll {
1883 eprintln!("[scroll-trace] hit_test: no node");
1884 }
1885 }
1886 InputEvent::Keyboard(KeyEvent::Down {
1887 key_code,
1888 modifiers,
1889 }) => match key_code {
1890 KeyCode::Tab => {
1891 let reverse = (modifiers & 1) != 0;
1892 let old_focus = self.runtime_state.interaction.focused;
1893 let next =
1894 find_next_focus_node(ir, self.runtime_state.interaction.focused, reverse);
1895 if next != old_focus {
1896 self.set_focused_widget(ir, next, crate::TextEditSource::Keyboard)?;
1897 }
1898 }
1899 KeyCode::Up | KeyCode::Down | KeyCode::Left | KeyCode::Right => {
1900 let reverse = matches!(key_code, KeyCode::Up | KeyCode::Left);
1901 let old_focus = self.runtime_state.interaction.focused;
1902 let next = if let Some(focused) = old_focus {
1903 let dir = match key_code {
1904 KeyCode::Up => FocusDirection::Up,
1905 KeyCode::Down => FocusDirection::Down,
1906 KeyCode::Left => FocusDirection::Left,
1907 KeyCode::Right => FocusDirection::Right,
1908 _ => unreachable!(),
1909 };
1910 find_neighbor_focus_node(ir, layout, focused, dir)
1911 .or_else(|| find_next_focus_node(ir, Some(focused), reverse))
1912 } else {
1913 find_next_focus_node(ir, None, reverse)
1914 };
1915 if next != old_focus {
1916 self.set_focused_widget(ir, next, crate::TextEditSource::Keyboard)?;
1917 }
1918 }
1919 KeyCode::Enter | KeyCode::Space => {
1920 if let Some(focused_id) = self.runtime_state.interaction.focused {
1921 let mut current_id = Some(focused_id);
1922 while let Some(node_id) = current_id {
1923 if let Some(node) = ir.nodes.get(&node_id) {
1924 if let Op::Semantics(semantics) = &node.op {
1925 let action_entry = semantics
1926 .actions
1927 .entries
1928 .iter()
1929 .find(|entry| {
1930 entry.trigger
1931 == fission_ir::semantics::ActionTrigger::Default
1932 })
1933 .and_then(|entry| {
1934 entry.payload_data.as_ref().map(|payload| {
1935 ActionEnvelope {
1936 id: ActionId::from_u128(entry.action_id),
1937 payload: payload.clone(),
1938 }
1939 })
1940 });
1941 let hyperlink = semantics.hyperlink.clone();
1942 if action_entry.is_some() || hyperlink.is_some() {
1943 let input = crate::input::scoped_action_input(
1944 ir,
1945 node_id,
1946 ActionInput::None,
1947 );
1948 let navigation_already_bound =
1949 action_entry.as_ref().is_some_and(|entry| {
1950 entry.id == crate::NavigationRequested::static_id()
1951 });
1952 if let Some(envelope) = action_entry {
1953 self.dispatch_node_with_input(
1954 envelope, node_id, &input,
1955 )?;
1956 }
1957 if let Some(hyperlink) =
1958 hyperlink.filter(|_| !navigation_already_bound)
1959 {
1960 self.dispatch_node_with_input(
1961 crate::NavigationRequested::new(
1962 crate::NavigationCommand::Open(hyperlink),
1963 )
1964 .into(),
1965 node_id,
1966 &input,
1967 )?;
1968 }
1969 return Ok(());
1970 }
1971 }
1972 current_id = node.parent;
1973 } else {
1974 break;
1975 }
1976 }
1977 }
1978 }
1979 _ => {}
1980 },
1981 InputEvent::Pointer(PointerEvent::Down {
1982 point,
1983 button: PointerButton::Primary,
1984 ..
1985 }) => {
1986 if let Some(hit_node_id) = hit_test_with_viewports(
1987 ir,
1988 layout,
1989 &self.runtime_state.scroll,
1990 &self.runtime_state.viewport,
1991 point,
1992 ) {
1993 diag::emit(
1994 diag::DiagCategory::Input,
1995 diag::DiagLevel::Debug,
1996 diag::DiagEventKind::InputEvent {
1997 kind: "pointer_down_hit".into(),
1998 target: Some(hit_node_id.as_u128()),
1999 position: Some((point.x, point.y)),
2000 },
2001 );
2002 let mut focus_candidate = Some(hit_node_id);
2003 while let Some(node_id) = focus_candidate {
2004 if let Some(node) = ir.nodes.get(&node_id) {
2005 if let Op::Semantics(s) = &node.op {
2006 if s.focusable {
2007 if s.focus_policy == FocusPolicy::PreserveCurrentOnPointer {
2008 break;
2009 }
2010 self.set_focused_widget(
2011 ir,
2012 Some(node_id),
2013 crate::TextEditSource::Pointer,
2014 )?;
2015 break;
2016 }
2017 }
2018 focus_candidate = node.parent;
2019 } else {
2020 break;
2021 }
2022 }
2023 if focus_candidate.is_none() {
2024 self.set_focused_widget(ir, None, crate::TextEditSource::Pointer)?;
2025 }
2026
2027 let mut current_pressed_id = Some(hit_node_id);
2028 while let Some(node_id) = current_pressed_id {
2029 self.runtime_state.interaction.set_pressed(node_id, true);
2030 if let Some(node) = ir.nodes.get(&node_id) {
2031 current_pressed_id = node.parent;
2032 } else {
2033 break;
2034 }
2035 }
2036 self.runtime_state.interaction.last_down_point = Some(point);
2037
2038 if let Some(focused_id) = self.runtime_state.interaction.focused {
2039 if let Some(node) = ir.nodes.get(&focused_id) {
2040 if let Op::Semantics(s) = &node.op {
2041 if s.role == fission_ir::semantics::Role::TextInput {
2042 if let Some(ime_handler) = &self.ime_handler {
2043 ime_handler.set_ime_cursor_area(LayoutRect::new(
2044 point.x, point.y, 2.0, 16.0,
2045 ));
2046 }
2047 }
2048 }
2049 }
2050 }
2051 } else {
2052 self.set_focused_widget(ir, None, crate::TextEditSource::Pointer)?;
2053 }
2054 }
2055 InputEvent::Pointer(PointerEvent::Up {
2056 point,
2057 button: PointerButton::Primary,
2058 ..
2059 }) => {
2060 self.runtime_state.interaction.pressed.clear();
2061 let had_primary_down = self
2062 .runtime_state
2063 .interaction
2064 .last_down_point
2065 .take()
2066 .is_some();
2067 if had_primary_down {
2068 if let Some(hit_node_id) = hit_test_with_viewports(
2069 ir,
2070 layout,
2071 &self.runtime_state.scroll,
2072 &self.runtime_state.viewport,
2073 point,
2074 ) {
2075 let mut current_id = Some(hit_node_id);
2076 while let Some(node_id) = current_id {
2077 if let Some(node) = ir.nodes.get(&node_id) {
2078 if let Op::Semantics(semantics) = &node.op {
2079 if semantics.role == fission_ir::semantics::Role::TextInput {
2080 } else if let Some(action_entry) =
2082 semantics.actions.entries.iter().find(|entry| {
2083 entry.trigger
2084 == fission_ir::semantics::ActionTrigger::Default
2085 })
2086 {
2087 if let Some(payload) = &action_entry.payload_data {
2088 let envelope = ActionEnvelope {
2089 id: ActionId::from_u128(action_entry.action_id),
2090 payload: payload.clone(),
2091 };
2092 diag::emit(
2093 diag::DiagCategory::Input,
2094 diag::DiagLevel::Debug,
2095 diag::DiagEventKind::InputEvent {
2096 kind: "pointer_up_dispatch".into(),
2097 target: Some(node_id.as_u128()),
2098 position: Some((point.x, point.y)),
2099 },
2100 );
2101 let input = crate::input::scoped_action_input(
2102 ir,
2103 node_id,
2104 ActionInput::None,
2105 );
2106 return self.dispatch_node_with_input(
2107 envelope, node_id, &input,
2108 );
2109 }
2110 }
2111 }
2112 current_id = node.parent;
2113 } else {
2114 break;
2115 }
2116 }
2117 }
2118 }
2119 }
2120 _ => {}
2121 }
2122 self.update_focused_ime_state(ir, layout);
2123 Ok(())
2124 }
2125
2126 pub fn clear_hover_state(&mut self, ir: &CoreIR, point: Option<LayoutPoint>) -> Result<bool> {
2127 use crate::input::hover::HoverController;
2128 use crate::input::ControllerContext;
2129
2130 let input_time = self.clock().current_time();
2131 let dispatched_actions = {
2132 let layout = &LayoutSnapshot::new(LayoutSize::ZERO);
2133 let mut ctx = ControllerContext {
2134 ir,
2135 layout,
2136 text_edit: &mut self.runtime_state.text_edit,
2137 selectable_text: &mut self.runtime_state.selectable_text,
2138 context_menu: &mut self.runtime_state.context_menu,
2139 interaction: &mut self.runtime_state.interaction,
2140 scroll: &mut self.runtime_state.scroll,
2141 viewport: &self.runtime_state.viewport,
2142 gesture: &mut self.runtime_state.gesture,
2143 editing_convention: self.editing_convention,
2144 current_time: input_time,
2145 clipboard: self.clipboard_backend.as_ref(),
2146 measurer: self.measurer.as_ref(),
2147 dispatched_actions: Vec::new(),
2148 };
2149 let changed = HoverController::clear(&mut ctx, point);
2150 (changed, ctx.dispatched_actions)
2151 };
2152 self.dispatch_input_actions(dispatched_actions.1)?;
2153 Ok(dispatched_actions.0)
2154 }
2155
2156 fn dispatch_input_actions(
2157 &mut self,
2158 dispatched_actions: Vec<(WidgetId, ActionEnvelope, ActionInput)>,
2159 ) -> Result<()> {
2160 for (target, action, input) in dispatched_actions {
2161 self.dispatch_node_with_input(action, target, &input)?;
2162 }
2163 Ok(())
2164 }
2165
2166 #[doc(hidden)]
2167 pub fn update_focused_ime_state(&mut self, ir: &CoreIR, layout: &LayoutSnapshot) {
2168 let Some(ime_handler) = self.ime_handler.clone() else {
2169 return;
2170 };
2171 let Some(focused_id) = self.runtime_state.interaction.focused else {
2172 ime_handler.set_ime_allowed(false);
2173 return;
2174 };
2175
2176 let mut walk = Some(focused_id);
2177 while let Some(node_id) = walk {
2178 if let Some(any_ro) = ir.custom_render_objects.get(&node_id) {
2179 if let Some(render_obj) = crate::ui::custom_render::downcast_render_object(any_ro) {
2180 let accepts_text = render_obj.accepts_text_input();
2181 ime_handler.set_ime_allowed(accepts_text);
2182 if accepts_text {
2183 let rect =
2184 Self::visual_node_rect(ir, layout, &self.runtime_state.scroll, node_id)
2185 .unwrap_or(LayoutRect::new(0.0, 0.0, 0.0, 0.0));
2186 if let Some(cursor_area) = render_obj.ime_cursor_area(rect) {
2187 ime_handler.set_ime_cursor_area(cursor_area);
2188 }
2189 }
2190 return;
2191 }
2192 }
2193 walk = ir.nodes.get(&node_id).and_then(|node| node.parent);
2194 }
2195
2196 let accepts_text = ir
2197 .nodes
2198 .get(&focused_id)
2199 .and_then(|node| match &node.op {
2200 Op::Semantics(semantics) => {
2201 Some(semantics.role == fission_ir::semantics::Role::TextInput)
2202 }
2203 _ => None,
2204 })
2205 .unwrap_or(false);
2206 ime_handler.set_ime_allowed(accepts_text);
2207
2208 if accepts_text {
2209 let editing_value = self
2210 .runtime_state
2211 .text_edit
2212 .get(focused_id)
2213 .map(crate::env::TextEditState::editing_value)
2214 .or_else(|| {
2215 let semantics = ir.nodes.get(&focused_id).and_then(|node| match &node.op {
2216 Op::Semantics(semantics) => Some(semantics),
2217 _ => None,
2218 })?;
2219 let text = semantics.value.clone().unwrap_or_default();
2220 let (anchor, caret) =
2221 semantics.text_selection.unwrap_or((text.len(), text.len()));
2222 let selection = crate::TextSelection::new(
2223 &text,
2224 anchor,
2225 caret,
2226 crate::TextAffinity::Downstream,
2227 )
2228 .unwrap_or_else(|_| {
2229 crate::TextSelection::collapsed(crate::TextPosition::at_end(&text))
2230 });
2231 Some(crate::TextEditingValue {
2232 text,
2233 selection,
2234 composing: None,
2235 })
2236 });
2237 if let Some(editing_value) = editing_value.as_ref() {
2238 ime_handler.set_editing_value(editing_value);
2239 }
2240
2241 let input_time = self.clock().current_time();
2242 let cursor_area = {
2243 let mut ctx = crate::input::ControllerContext {
2244 ir,
2245 layout,
2246 text_edit: &mut self.runtime_state.text_edit,
2247 selectable_text: &mut self.runtime_state.selectable_text,
2248 context_menu: &mut self.runtime_state.context_menu,
2249 interaction: &mut self.runtime_state.interaction,
2250 scroll: &mut self.runtime_state.scroll,
2251 viewport: &self.runtime_state.viewport,
2252 gesture: &mut self.runtime_state.gesture,
2253 editing_convention: self.editing_convention,
2254 current_time: input_time,
2255 clipboard: self.clipboard_backend.as_ref(),
2256 measurer: self.measurer.as_ref(),
2257 dispatched_actions: Vec::new(),
2258 };
2259 crate::input::text::TextInputController::ime_cursor_area(&mut ctx, focused_id)
2260 };
2261 if let Some(cursor_area) = cursor_area {
2262 ime_handler.set_ime_cursor_area(cursor_area);
2263 }
2264 }
2265 }
2266
2267 fn visual_node_rect(
2268 ir: &CoreIR,
2269 layout: &LayoutSnapshot,
2270 scroll: &crate::env::ScrollStateMap,
2271 node_id: WidgetId,
2272 ) -> Option<LayoutRect> {
2273 let mut rect = layout.get_node_rect(node_id)?;
2274 let mut walk = ir.nodes.get(&node_id).and_then(|node| node.parent);
2275 while let Some(parent_id) = walk {
2276 let Some(parent) = ir.nodes.get(&parent_id) else {
2277 break;
2278 };
2279 if let Op::Layout(LayoutOp::Scroll { direction, .. }) = &parent.op {
2280 let offset = scroll.get_offset(parent_id);
2281 match direction {
2282 FlexDirection::Row => rect.origin.x -= offset,
2283 FlexDirection::Column => rect.origin.y -= offset,
2284 }
2285 }
2286 walk = parent.parent;
2287 }
2288 Some(rect)
2289 }
2290
2291 fn clear_text_pending_on_blur(
2292 &mut self,
2293 old_focus: Option<WidgetId>,
2294 new_focus: Option<WidgetId>,
2295 ) {
2296 if old_focus == new_focus {
2297 return;
2298 }
2299 if let Some(old_id) = old_focus {
2300 if let Some(st) = self.runtime_state.text_edit.states.get_mut(&old_id) {
2301 st.pending_model_sync = false;
2302 st.clear_preedit();
2303 }
2304 }
2305 }
2306
2307 fn text_input_value(&self, ir: &CoreIR, id: WidgetId) -> crate::TextEditingValue {
2308 self.runtime_state
2309 .text_edit
2310 .get(id)
2311 .map(|state| state.editing_value())
2312 .unwrap_or_else(|| {
2313 let text = ir
2314 .nodes
2315 .get(&id)
2316 .and_then(|node| match &node.op {
2317 Op::Semantics(semantics) => semantics.value.clone(),
2318 _ => None,
2319 })
2320 .unwrap_or_default();
2321 crate::TextEditingValue::from_text(text)
2322 })
2323 }
2324
2325 fn dispatch_text_session_action(
2326 &mut self,
2327 ir: &CoreIR,
2328 id: WidgetId,
2329 trigger: fission_ir::semantics::ActionTrigger,
2330 source: crate::TextEditSource,
2331 phase: crate::TextEditPhase,
2332 ) -> Result<()> {
2333 let Some(semantics) = ir.nodes.get(&id).and_then(|node| match &node.op {
2334 Op::Semantics(semantics)
2335 if semantics.role == fission_ir::semantics::Role::TextInput =>
2336 {
2337 Some(semantics)
2338 }
2339 _ => None,
2340 }) else {
2341 return Ok(());
2342 };
2343 let value = self.text_input_value(ir, id);
2344 if let Some((envelope, input)) = crate::input::prepare_scoped_text_session_action(
2345 ir, semantics, id, trigger, value, source, phase,
2346 ) {
2347 self.dispatch_node_with_input(envelope, id, &input)?;
2348 }
2349 Ok(())
2350 }
2351
2352 #[doc(hidden)]
2358 pub fn set_focused_widget(
2359 &mut self,
2360 ir: &CoreIR,
2361 next: Option<WidgetId>,
2362 source: crate::TextEditSource,
2363 ) -> Result<bool> {
2364 let current = self.runtime_state.interaction.focused;
2365 if current == next {
2366 return Ok(false);
2367 }
2368
2369 self.clear_text_pending_on_blur(current, next);
2370 self.dispatch_custom_blur_actions(ir, current)?;
2371 if let Some(old_id) = current {
2372 if source == crate::TextEditSource::Pointer {
2373 self.dispatch_text_session_action(
2374 ir,
2375 old_id,
2376 fission_ir::semantics::ActionTrigger::TapOutside,
2377 source,
2378 crate::TextEditPhase::TapOutside,
2379 )?;
2380 }
2381 self.dispatch_text_session_action(
2382 ir,
2383 old_id,
2384 fission_ir::semantics::ActionTrigger::Blur,
2385 source,
2386 crate::TextEditPhase::Blurred,
2387 )?;
2388 }
2389
2390 self.runtime_state.interaction.set_focused(next);
2391 if let Some(new_id) = next {
2392 let select_all = ir
2393 .custom_render_objects
2394 .get(&new_id)
2395 .and_then(crate::ui::widgets::text_input::downcast_text_input_runtime_config)
2396 .is_some_and(|config| config.select_all_on_focus);
2397 if select_all {
2398 self.pending_text_editing
2399 .push((new_id, crate::TextEditingCommand::SelectAll));
2400 }
2401 }
2402
2403 if let Some(ime_handler) = &self.ime_handler {
2404 let accepts_text = next.is_some_and(|id| {
2405 matches!(
2406 ir.nodes.get(&id).map(|node| &node.op),
2407 Some(Op::Semantics(semantics))
2408 if semantics.role == fission_ir::semantics::Role::TextInput
2409 && !semantics.disabled
2410 && !semantics.read_only
2411 ) || ir
2412 .custom_render_objects
2413 .get(&id)
2414 .and_then(downcast_render_object)
2415 .is_some_and(|render_object| render_object.accepts_text_input())
2416 });
2417 ime_handler.set_ime_allowed(accepts_text);
2418 }
2419
2420 if let Some(new_id) = next {
2421 self.dispatch_text_session_action(
2422 ir,
2423 new_id,
2424 fission_ir::semantics::ActionTrigger::Focus,
2425 source,
2426 crate::TextEditPhase::Focused,
2427 )?;
2428 }
2429 Ok(true)
2430 }
2431
2432 fn dispatch_custom_blur_actions(
2433 &mut self,
2434 ir: &CoreIR,
2435 old_focus: Option<WidgetId>,
2436 ) -> Result<()> {
2437 if let Some(old_id) = old_focus {
2438 if let Some(any_ro) = ir.custom_render_objects.get(&old_id) {
2439 if let Some(render_obj) = crate::ui::custom_render::downcast_render_object(any_ro) {
2440 if render_obj.accepts_text_input() {
2441 if let Some(ime_handler) = &self.ime_handler {
2442 ime_handler.set_ime_allowed(false);
2443 }
2444 }
2445 for (target, envelope) in render_obj.blur_actions(old_id) {
2446 self.dispatch_node(envelope, target)?;
2447 }
2448 }
2449 }
2450 }
2451 Ok(())
2452 }
2453
2454 pub fn hit_test(
2455 &self,
2456 point: LayoutPoint,
2457 ir: &CoreIR,
2458 snapshot: &LayoutSnapshot,
2459 ) -> Option<WidgetId> {
2460 if let Some(root) = ir.root {
2461 return self.hit_test_recursive(root, point, ir, snapshot);
2462 }
2463 None
2464 }
2465
2466 fn hit_test_recursive(
2467 &self,
2468 node_id: WidgetId,
2469 point: LayoutPoint,
2470 ir: &CoreIR,
2471 snapshot: &LayoutSnapshot,
2472 ) -> Option<WidgetId> {
2473 if let Some(geom) = snapshot.nodes.get(&node_id) {
2474 if geom.rect.contains(point) {
2475 if let Some(node) = ir.nodes.get(&node_id) {
2476 for child in node.children.iter().rev() {
2477 let mut child_point = point;
2478
2479 if let Op::Layout(LayoutOp::Scroll { direction, .. }) = &node.op {
2480 if !geom.rect.contains(point) {
2481 continue;
2482 }
2483 let offset = self.runtime_state.scroll.get_offset(node_id);
2484 match direction {
2485 FlexDirection::Row => child_point.x += offset,
2486 FlexDirection::Column => child_point.y += offset,
2487 }
2488 }
2489
2490 if let Op::Layout(LayoutOp::Transform { transform }) = &node.op {
2491 let mat = Mat4::from_cols_array(transform);
2492 let local_x = point.x - geom.rect.origin.x;
2529 let local_y = point.y - geom.rect.origin.y;
2530
2531 let p = Vec4::new(local_x, local_y, 0.0, 1.0);
2532 let inv = mat.inverse();
2533 let transformed = inv * p;
2534
2535 child_point = LayoutPoint::new(
2536 transformed.x + geom.rect.origin.x,
2537 transformed.y + geom.rect.origin.y,
2538 );
2539 }
2540
2541 if let Some(hit) =
2542 self.hit_test_recursive(*child, child_point, ir, snapshot)
2543 {
2544 return Some(hit);
2545 }
2546 }
2547
2548 match &node.op {
2549 Op::Paint(_)
2550 | Op::Layout(LayoutOp::Scroll { .. })
2551 | Op::Layout(LayoutOp::Embed { .. }) => return Some(node_id),
2552 _ => return None,
2553 }
2554 }
2555 return None;
2556 }
2557 }
2558 None
2559 }
2560
2561 fn event_point(event: &InputEvent) -> Option<LayoutPoint> {
2567 match event {
2568 InputEvent::Pointer(PointerEvent::Down { point, .. })
2569 | InputEvent::Pointer(PointerEvent::Up { point, .. })
2570 | InputEvent::Pointer(PointerEvent::Move { point, .. })
2571 | InputEvent::Pointer(PointerEvent::Cancel { point, .. })
2572 | InputEvent::Pointer(PointerEvent::Scroll { point, .. })
2573 | InputEvent::Pointer(PointerEvent::Magnify { point, .. }) => Some(*point),
2574 _ => None,
2575 }
2576 }
2577
2578 fn find_autofocus_node(ir: &CoreIR) -> Option<WidgetId> {
2579 fn walk(ir: &CoreIR, node_id: WidgetId) -> Option<WidgetId> {
2580 let node = ir.nodes.get(&node_id)?;
2581 if let Op::Semantics(semantics) = &node.op {
2582 if semantics.autofocus && semantics.focusable && !semantics.disabled {
2583 return Some(node_id);
2584 }
2585 }
2586 for child_id in &node.children {
2587 if let Some(found) = walk(ir, *child_id) {
2588 return Some(found);
2589 }
2590 }
2591 None
2592 }
2593
2594 ir.root.and_then(|root| walk(ir, root))
2595 }
2596
2597 pub fn reconcile_resources(
2598 &mut self,
2599 declarations: Vec<RuntimeResourceDeclaration>,
2600 ) -> Result<()> {
2601 let now = self.clock().current_time();
2602 let mut existing = std::mem::take(&mut self.active_resources);
2603 let mut next = HashMap::new();
2604
2605 for declaration in declarations {
2606 let key = declaration.key.clone();
2607 match existing.remove(&key) {
2608 Some(current)
2609 if current.policy == declaration.policy
2610 && current.deps == declaration.deps
2611 && current.matches_kind(&declaration.kind) =>
2612 {
2613 next.insert(key, current);
2614 }
2615 Some(current) if declaration.policy == ResourcePolicy::PreserveOnChange => {
2616 next.insert(key, current);
2617 }
2618 Some(current) => {
2619 self.stop_resource(&key, ¤t);
2620 let replacement = self.start_resource(declaration, now);
2621 next.insert(key, replacement);
2622 }
2623 None => {
2624 let resource = self.start_resource(declaration, now);
2625 next.insert(key, resource);
2626 }
2627 }
2628 }
2629
2630 for (key, resource) in existing {
2631 self.stop_resource(&key, &resource);
2632 }
2633
2634 self.active_resources = next;
2635 Ok(())
2636 }
2637
2638 pub fn resource_generation(&self, key: &str) -> Option<u64> {
2639 self.active_resources
2640 .get(key)
2641 .map(|resource| resource.generation)
2642 }
2643
2644 pub fn is_resource_current(&self, resource: &ResourceExecutionContext) -> bool {
2645 self.resource_generation(&resource.key) == Some(resource.generation)
2646 }
2647
2648 fn start_resource(
2649 &mut self,
2650 declaration: RuntimeResourceDeclaration,
2651 now: CurrentTime,
2652 ) -> ActiveResource {
2653 let generation = self.next_resource_generation;
2654 self.next_resource_generation += 1;
2655
2656 let context = ResourceExecutionContext {
2657 key: declaration.key.clone(),
2658 generation,
2659 };
2660
2661 let kind = match declaration.kind {
2662 RuntimeResourceKind::Job(mut job) => {
2663 job.effect.resource = Some(context);
2664 self.enqueue_effect(job.effect);
2665 ActiveResourceKind::Job
2666 }
2667 RuntimeResourceKind::Service(mut service) => {
2668 service.effect.resource = Some(context);
2669 let (service_name, slot_key) = match &service.effect.effect {
2670 crate::Effect::StartService(payload) => {
2671 (payload.service_name.clone(), payload.slot_key.clone())
2672 }
2673 _ => unreachable!("service resource must lower to StartService"),
2674 };
2675 self.enqueue_effect(service.effect);
2676 ActiveResourceKind::Service {
2677 service_name,
2678 slot_key,
2679 }
2680 }
2681 RuntimeResourceKind::Timer(timer) => self.start_timer_resource(timer, now),
2682 };
2683
2684 ActiveResource {
2685 generation,
2686 deps: declaration.deps,
2687 policy: declaration.policy,
2688 kind,
2689 }
2690 }
2691
2692 fn start_timer_resource(&self, timer: TimerResource, now: CurrentTime) -> ActiveResourceKind {
2693 let interval_ms = timer.interval_ms.max(1);
2694 ActiveResourceKind::Timer {
2695 interval_ms,
2696 payload: timer.payload,
2697 on_tick: timer.on_tick,
2698 next_fire_at: if timer.immediate {
2699 now
2700 } else {
2701 now.saturating_add(interval_ms)
2702 },
2703 }
2704 }
2705
2706 fn stop_resource(&mut self, key: &str, resource: &ActiveResource) {
2707 if let ActiveResourceKind::Service {
2708 service_name,
2709 slot_key,
2710 } = &resource.kind
2711 {
2712 self.enqueue_effect(EffectEnvelope {
2713 req_id: 0,
2714 effect: crate::Effect::StopService(ServiceStopPayload {
2715 service_name: service_name.clone(),
2716 slot_key: slot_key.clone(),
2717 }),
2718 on_ok: None,
2719 on_err: None,
2720 service_bindings: None,
2721 resource: Some(ResourceExecutionContext {
2722 key: key.to_string(),
2723 generation: resource.generation,
2724 }),
2725 });
2726 }
2727 }
2728}
2729
2730impl ActiveResource {
2731 fn matches_kind(&self, kind: &RuntimeResourceKind) -> bool {
2732 matches!(
2733 (&self.kind, kind),
2734 (ActiveResourceKind::Job, RuntimeResourceKind::Job(_))
2735 | (
2736 ActiveResourceKind::Timer { .. },
2737 RuntimeResourceKind::Timer(_)
2738 )
2739 | (
2740 ActiveResourceKind::Service { .. },
2741 RuntimeResourceKind::Service(_)
2742 )
2743 )
2744 }
2745}