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