use crate::action::{ActionEnvelope, ActionId, GlobalState};
use crate::async_runtime::ServiceStopPayload;
use crate::effect::{
ActionInput, Effect, EffectEnvelope, RuntimeEffect, ScrollAlignment, ScrollAxis,
ScrollBehavior, ScrollIntoViewRequest,
};
use crate::env::{RuntimeState, VideoStatus};
use crate::registry::{
ActionRegistry, ResourcePolicy, RuntimeResourceDeclaration, RuntimeResourceKind, TimerResource,
VideoRegistration,
};
use crate::BoxedReducer;
use crate::{
Clipboard, Clock, CurrentTime, ImeHandler, InputEvent, KeyCode, KeyEvent, PointerButton,
PointerEvent, ResourceExecutionContext,
};
use anyhow::{anyhow, Result};
use fission_diagnostics::prelude as diag;
use fission_ir::{CoreIR, FlexDirection, FocusPolicy, LayoutOp, Op, WidgetId};
use fission_layout::{LayoutPoint, LayoutRect, LayoutSize, LayoutSnapshot, TextMeasurer};
use glam::{Mat4, Vec4};
use serde_json;
use std::any::TypeId;
use std::collections::{HashMap, HashSet};
use std::sync::Arc;
#[derive(Debug, Default, Clone)]
pub struct TickResult {
pub changed_motions: Vec<(WidgetId, crate::MotionPropertyId)>,
}
#[derive(Debug, Clone)]
enum ActiveResourceKind {
Job,
Service {
service_name: String,
slot_key: String,
},
Timer {
interval_ms: u64,
payload: Vec<u8>,
on_tick: Option<ActionEnvelope>,
next_fire_at: CurrentTime,
},
}
#[derive(Debug, Clone)]
struct ActiveResource {
generation: u64,
deps: Option<Vec<u8>>,
policy: ResourcePolicy,
kind: ActiveResourceKind,
}
#[derive(Debug, Clone)]
struct PendingScrollIntoView {
request: ScrollIntoViewRequest,
retries_remaining: u8,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
enum ScrollIntoViewOutcome {
Applied { changed: bool },
Retry,
Ignored,
}
pub struct Runtime {
pub(crate) reducers: HashMap<ActionId, Vec<BoxedReducer>>,
pub(crate) persistent_reducers: HashMap<ActionId, Vec<BoxedReducer>>,
pub app_states: HashMap<TypeId, Box<dyn GlobalState>>,
pub runtime_state: RuntimeState,
pub measurer: Option<Arc<dyn TextMeasurer>>,
pub clipboard_backend: Option<Arc<dyn Clipboard>>,
pub ime_handler: Option<Arc<dyn ImeHandler>>,
pub pending_effects: Vec<EffectEnvelope>,
pending_scroll_into_view: Vec<PendingScrollIntoView>,
pub next_req_id: u64,
active_resources: HashMap<String, ActiveResource>,
next_resource_generation: u64,
}
impl Default for Runtime {
fn default() -> Self {
let mut runtime = Self {
reducers: HashMap::new(),
persistent_reducers: HashMap::new(),
app_states: HashMap::new(),
runtime_state: RuntimeState::default(),
measurer: None,
clipboard_backend: None,
ime_handler: None,
pending_effects: Vec::new(),
pending_scroll_into_view: Vec::new(),
next_req_id: 0,
active_resources: HashMap::new(),
next_resource_generation: 1,
};
runtime
.add_global_state(Box::new(runtime.runtime_state.local_widget_state.clone()))
.expect("Failed to add local widget state store");
runtime
.add_global_state(Box::new(Clock::default()))
.expect("Failed to add Clock state");
runtime.register_base_reducers();
runtime
}
}
impl Runtime {
pub fn with_measurer(mut self, measurer: Arc<dyn TextMeasurer>) -> Self {
self.measurer = Some(measurer);
self
}
pub fn with_clipboard(mut self, backend: Arc<dyn Clipboard>) -> Self {
self.clipboard_backend = Some(backend);
self
}
pub fn with_ime_handler(mut self, handler: Arc<dyn ImeHandler>) -> Self {
self.ime_handler = Some(handler);
self
}
pub fn caret_from_point_in_text(
&self,
value: &str,
font_size: f32,
viewport_x: f32,
viewport_w: f32,
content_w: f32,
scroll_offset: f32,
point_x: f32,
) -> usize {
crate::input::text::caret_from_point_in_text(
self.measurer.as_ref(),
value,
font_size,
viewport_x,
viewport_w,
content_w,
scroll_offset,
point_x,
)
}
pub fn register_reducer<S: GlobalState + 'static>(
&mut self,
action_id: ActionId,
reducer_fn: crate::action::Reducer<S>,
) -> Result<()> {
let state_type_id = TypeId::of::<S>();
let boxed_reducer: BoxedReducer = Box::new(
move |app_states: &mut HashMap<TypeId, Box<dyn GlobalState>>,
action: &ActionEnvelope,
target: WidgetId,
_effects: &mut Vec<EffectEnvelope>,
_input: &ActionInput|
-> Result<()> {
if let Some(state_box) = app_states.get_mut(&state_type_id) {
let concrete_state = state_box.downcast_mut::<S>().ok_or_else(|| {
anyhow!("Failed to downcast GlobalState to concrete type for reducer")
})?;
reducer_fn(concrete_state, action, target.into())
} else {
anyhow::bail!("Target GlobalState for reducer not found in runtime.");
}
},
);
self.reducers
.entry(action_id)
.or_default()
.push(boxed_reducer);
Ok(())
}
pub fn register_base_reducers(&mut self) {
use crate::{AdvanceTo, Tick, ADVANCE_TO_ACTION_ID, TICK_ACTION_ID};
self.register_reducer::<Clock>(
*TICK_ACTION_ID,
|state: &mut Clock, action: &ActionEnvelope, _target| {
let tick_action: Tick = serde_json::from_slice(&action.payload)
.map_err(|e| anyhow!("Failed to deserialize Tick: {}", e))?;
state.advance_by(tick_action.dt)
},
)
.expect("Failed to register Tick reducer");
self.register_reducer::<Clock>(
*ADVANCE_TO_ACTION_ID,
|state: &mut Clock, action: &ActionEnvelope, _target| {
let advance_action: AdvanceTo = serde_json::from_slice(&action.payload)
.map_err(|e| anyhow!("Failed to deserialize AdvanceTo: {}", e))?;
state.set_to(advance_action.time)
},
)
.expect("Failed to register AdvanceTo reducer");
}
pub fn clear_reducers(&mut self) {
self.reducers.clear();
self.register_base_reducers();
}
pub fn absorb_registry<S: GlobalState>(&mut self, registry: ActionRegistry<S>) {
let new_reducers = registry.into_runtime_reducers();
for (id, mut list) in new_reducers {
self.reducers.entry(id).or_default().append(&mut list);
}
}
pub fn absorb_persistent_registry<S: GlobalState>(&mut self, registry: ActionRegistry<S>) {
let new_reducers = registry.into_runtime_reducers();
for (id, mut list) in new_reducers {
self.persistent_reducers
.entry(id)
.or_default()
.append(&mut list);
}
}
pub fn clock(&self) -> &Clock {
self.get_global_state::<Clock>()
.expect("Clock state must always be present")
}
pub fn get_global_state<S: GlobalState + 'static>(&self) -> Option<&S> {
self.app_states
.get(&TypeId::of::<S>())
.and_then(|s_box| s_box.downcast_ref::<S>())
}
pub fn get_global_state_mut<S: GlobalState + 'static>(&mut self) -> Option<&mut S> {
self.app_states
.get_mut(&TypeId::of::<S>())
.and_then(|s_box| s_box.downcast_mut::<S>())
}
pub fn add_global_state<S: GlobalState + 'static>(&mut self, state: Box<S>) -> Result<()> {
let type_id = TypeId::of::<S>();
if self.app_states.insert(type_id, state).is_some() {
anyhow::bail!("Global state of this type already registered.");
}
Ok(())
}
pub fn with_global_state<S: GlobalState + 'static>(mut self, state: S) -> Self {
self.app_states.insert(TypeId::of::<S>(), Box::new(state));
self
}
#[doc(hidden)]
pub fn get_app_state<S: GlobalState + 'static>(&self) -> Option<&S> {
self.get_global_state::<S>()
}
#[doc(hidden)]
pub fn get_app_state_mut<S: GlobalState + 'static>(&mut self) -> Option<&mut S> {
self.get_global_state_mut::<S>()
}
#[doc(hidden)]
pub fn add_app_state<S: GlobalState + 'static>(&mut self, state: Box<S>) -> Result<()> {
self.add_global_state(state)
}
pub fn dispatch(&mut self, action: ActionEnvelope, target: WidgetId) -> Result<()> {
self.dispatch_with_input(action, target, &ActionInput::None)
}
fn enqueue_effect(&mut self, mut envelope: EffectEnvelope) {
envelope.req_id = self.next_req_id;
self.next_req_id += 1;
self.pending_effects.push(envelope);
}
pub fn dispatch_with_input(
&mut self,
action: ActionEnvelope,
target: WidgetId,
input: &ActionInput,
) -> Result<()> {
self.dispatch_node_with_input(action, target.into(), input)
}
fn dispatch_node(&mut self, action: ActionEnvelope, target: WidgetId) -> Result<()> {
self.dispatch_node_with_input(action, target, &ActionInput::None)
}
fn dispatch_node_with_input(
&mut self,
action: ActionEnvelope,
target: WidgetId,
input: &ActionInput,
) -> Result<()> {
diag::emit(
diag::DiagCategory::Input,
diag::DiagLevel::Debug,
diag::DiagEventKind::InputEvent {
kind: "dispatch_start".into(),
target: Some(target.as_u128()),
position: None,
},
);
if crate::media::handle_video_action(&mut self.runtime_state.video, &action)? {
return Ok(());
}
let action_id = action.id;
if crate::scoped_action_handlers::dispatch_scoped_action_handler(&action, target, input)? {
return Ok(());
}
let mut effects = Vec::new();
if let Some(reducers) = self.persistent_reducers.get_mut(&action_id) {
diag::emit(
diag::DiagCategory::Input,
diag::DiagLevel::Debug,
diag::DiagEventKind::InputEvent {
kind: format!("persistent_reducers:{}", reducers.len()),
target: Some(target.as_u128()),
position: None,
},
);
let mut temp_reducers: Vec<BoxedReducer> = reducers.drain(..).collect();
for reducer_wrapper in temp_reducers.iter_mut() {
reducer_wrapper(&mut self.app_states, &action, target, &mut effects, input)?;
}
reducers.extend(temp_reducers);
}
if let Some(reducers) = self.reducers.get_mut(&action_id) {
diag::emit(
diag::DiagCategory::Input,
diag::DiagLevel::Debug,
diag::DiagEventKind::InputEvent {
kind: format!("reducers:{}", reducers.len()),
target: Some(target.as_u128()),
position: None,
},
);
let mut temp_reducers: Vec<BoxedReducer> = reducers.drain(..).collect();
for reducer_wrapper in temp_reducers.iter_mut() {
reducer_wrapper(&mut self.app_states, &action, target, &mut effects, input)?;
}
reducers.extend(temp_reducers);
}
for envelope in effects {
self.enqueue_effect(envelope);
}
diag::emit(
diag::DiagCategory::Input,
diag::DiagLevel::Debug,
diag::DiagEventKind::InputEvent {
kind: "dispatch_end".into(),
target: Some(target.as_u128()),
position: None,
},
);
Ok(())
}
pub fn tick(&mut self, dt: CurrentTime) -> Result<TickResult> {
use crate::Tick;
let action = Tick { dt };
let envelope: ActionEnvelope = action.into();
self.dispatch_node(envelope, WidgetId::derived(0, &[0]))?;
self.tick_resource_timers()?;
let current_time = self.clock().current_time();
let changed_motions =
crate::motion::tick_motion(&mut self.runtime_state.motion, current_time);
Ok(TickResult { changed_motions })
}
fn tick_resource_timers(&mut self) -> Result<()> {
let now = self.clock().current_time();
let mut ticks = Vec::new();
for resource in self.active_resources.values_mut() {
if let ActiveResourceKind::Timer {
interval_ms,
payload,
on_tick,
next_fire_at,
} = &mut resource.kind
{
let Some(action) = on_tick.clone() else {
continue;
};
let interval_ms = (*interval_ms).max(1);
while now >= *next_fire_at {
ticks.push((action.clone(), payload.clone()));
*next_fire_at = next_fire_at.saturating_add(interval_ms);
}
}
}
for (action, payload) in ticks {
self.dispatch_node_with_input(
action,
WidgetId::derived(0, &[0]),
&ActionInput::TimerTick { payload },
)?;
}
Ok(())
}
pub fn sync_motion_declarations(
&mut self,
declarations: &[crate::MotionDeclaration],
layout: Option<&LayoutSnapshot>,
) -> Vec<(WidgetId, crate::MotionPropertyId)> {
let current_time = self.clock().current_time();
let snapshot = self.runtime_state.clone();
let result = crate::motion::sync_motion_declarations(
&mut self.runtime_state.motion,
declarations,
&snapshot,
layout,
current_time,
);
result.changed
}
pub fn sync_video_nodes(&mut self, registrations: &[VideoRegistration]) {
let mut seen: HashSet<WidgetId> = HashSet::new();
for reg in registrations {
seen.insert(reg.node_id);
let entry = self
.runtime_state
.video
.states
.entry(reg.node_id)
.or_insert_with(crate::env::VideoState::default);
entry.asset_source = reg.source.clone();
entry.looped = reg.loop_playback;
entry.audio = reg.audio.clone();
if reg.autoplay && entry.status == VideoStatus::Stopped {
entry.status = VideoStatus::Playing;
}
}
self.runtime_state
.video
.states
.retain(|node_id, _| seen.contains(node_id));
}
pub fn sync_web_nodes(&mut self, registrations: &[crate::registry::WebRegistration]) {
let mut seen: HashSet<WidgetId> = HashSet::new();
for reg in registrations {
seen.insert(reg.node_id);
let entry = self
.runtime_state
.web
.states
.entry(reg.node_id)
.or_insert_with(crate::env::WebState::default);
if entry.url != reg.url {
entry.url = reg.url.clone();
entry.loading = true; }
entry.user_agent = reg.user_agent.clone();
}
self.runtime_state
.web
.states
.retain(|node_id, _| seen.contains(node_id));
}
pub fn queue_runtime_effect(&mut self, effect: RuntimeEffect) -> bool {
match effect {
RuntimeEffect::ScrollIntoView(request) => {
self.queue_scroll_into_view(request);
true
}
RuntimeEffect::Cancel { .. } | RuntimeEffect::ReleaseResource { .. } => false,
}
}
pub fn queue_scroll_into_view(&mut self, request: ScrollIntoViewRequest) {
self.pending_scroll_into_view.push(PendingScrollIntoView {
request,
retries_remaining: 1,
});
}
fn drain_scroll_into_view_effects(&mut self) {
let pending = std::mem::take(&mut self.pending_effects);
for env in pending {
let EffectEnvelope {
req_id,
effect,
on_ok,
on_err,
service_bindings,
resource,
} = env;
match effect {
Effect::Runtime(RuntimeEffect::ScrollIntoView(request)) => {
self.queue_scroll_into_view(request);
}
retained => self.pending_effects.push(EffectEnvelope {
req_id,
effect: retained,
on_ok,
on_err,
service_bindings,
resource,
}),
}
}
}
fn apply_pending_scroll_into_view(&mut self, ir: &CoreIR, layout: &LayoutSnapshot) -> bool {
self.drain_scroll_into_view_effects();
let mut needs_follow_up_frame = false;
let pending = std::mem::take(&mut self.pending_scroll_into_view);
for mut pending_request in pending {
match self.apply_scroll_into_view(&pending_request.request, ir, layout) {
ScrollIntoViewOutcome::Applied { changed } => {
needs_follow_up_frame |= changed;
}
ScrollIntoViewOutcome::Retry if pending_request.retries_remaining > 0 => {
pending_request.retries_remaining -= 1;
self.pending_scroll_into_view.push(pending_request);
needs_follow_up_frame = true;
}
ScrollIntoViewOutcome::Retry | ScrollIntoViewOutcome::Ignored => {}
}
}
needs_follow_up_frame
}
fn apply_scroll_into_view(
&mut self,
request: &ScrollIntoViewRequest,
ir: &CoreIR,
layout: &LayoutSnapshot,
) -> ScrollIntoViewOutcome {
let Some(target_geom) = layout.get_node_geometry(request.target) else {
Self::emit_scroll_into_view_diag("missing_target", request, None);
return ScrollIntoViewOutcome::Retry;
};
let Some(container_id) = self.resolve_scroll_container(request, ir, layout) else {
Self::emit_scroll_into_view_diag("missing_container", request, None);
return ScrollIntoViewOutcome::Retry;
};
if !Self::is_descendant_or_self(ir, request.target, container_id) {
Self::emit_scroll_into_view_diag("target_not_descendant", request, Some(container_id));
return ScrollIntoViewOutcome::Ignored;
}
let Some(container_geom) = layout.get_node_geometry(container_id) else {
Self::emit_scroll_into_view_diag(
"missing_container_layout",
request,
Some(container_id),
);
return ScrollIntoViewOutcome::Retry;
};
let Some(direction) = Self::scroll_direction(ir, container_id) else {
Self::emit_scroll_into_view_diag("not_scroll_container", request, Some(container_id));
return ScrollIntoViewOutcome::Ignored;
};
if !Self::axis_matches(request.axis, direction) {
Self::emit_scroll_into_view_diag("axis_mismatch", request, Some(container_id));
return ScrollIntoViewOutcome::Ignored;
}
if matches!(request.behavior, ScrollBehavior::Smooth) {
Self::emit_scroll_into_view_diag(
"smooth_resolved_as_instant",
request,
Some(container_id),
);
}
let current_offset = self.runtime_state.scroll.get_offset(container_id);
let new_offset = match direction {
FlexDirection::Column => Self::compute_scroll_offset(
current_offset,
target_geom.rect.y() - container_geom.rect.y(),
target_geom.rect.height(),
container_geom.rect.height(),
container_geom.content_size.height,
request.padding[2],
request.padding[3],
request.alignment,
request.if_needed,
),
FlexDirection::Row => Self::compute_scroll_offset(
current_offset,
target_geom.rect.x() - container_geom.rect.x(),
target_geom.rect.width(),
container_geom.rect.width(),
container_geom.content_size.width,
request.padding[0],
request.padding[1],
request.alignment,
request.if_needed,
),
};
if (new_offset - current_offset).abs() > f32::EPSILON {
self.runtime_state
.scroll
.set_offset(container_id, new_offset);
ScrollIntoViewOutcome::Applied { changed: true }
} else {
ScrollIntoViewOutcome::Applied { changed: false }
}
}
fn resolve_scroll_container(
&self,
request: &ScrollIntoViewRequest,
ir: &CoreIR,
layout: &LayoutSnapshot,
) -> Option<WidgetId> {
if let Some(container) = request.container {
return ir
.nodes
.contains_key(&container)
.then_some(container)
.filter(|id| layout.get_node_geometry(*id).is_some());
}
let mut current = ir.nodes.get(&request.target)?.parent;
while let Some(node_id) = current {
if let Some(direction) = Self::scroll_direction(ir, node_id) {
if Self::axis_matches(request.axis, direction)
&& layout.get_node_geometry(node_id).is_some()
{
return Some(node_id);
}
}
current = ir.nodes.get(&node_id).and_then(|node| node.parent);
}
None
}
fn scroll_direction(ir: &CoreIR, node_id: WidgetId) -> Option<FlexDirection> {
match ir.nodes.get(&node_id).map(|node| &node.op) {
Some(Op::Layout(LayoutOp::Scroll { direction, .. })) => Some(*direction),
_ => None,
}
}
fn axis_matches(axis: ScrollAxis, direction: FlexDirection) -> bool {
matches!(
(axis, direction),
(ScrollAxis::Both, _)
| (ScrollAxis::Vertical, FlexDirection::Column)
| (ScrollAxis::Horizontal, FlexDirection::Row)
)
}
fn is_descendant_or_self(ir: &CoreIR, target: WidgetId, ancestor: WidgetId) -> bool {
let mut current = Some(target);
while let Some(node_id) = current {
if node_id == ancestor {
return true;
}
current = ir.nodes.get(&node_id).and_then(|node| node.parent);
}
false
}
fn compute_scroll_offset(
current_offset: f32,
target_content_start: f32,
target_size: f32,
viewport_size: f32,
content_size: f32,
padding_start: f32,
padding_end: f32,
alignment: ScrollAlignment,
if_needed: bool,
) -> f32 {
let current_offset = Self::finite_or_zero(current_offset).max(0.0);
let viewport_size = Self::finite_or_zero(viewport_size).max(0.0);
let content_size = Self::finite_or_zero(content_size).max(0.0);
let target_size = Self::finite_or_zero(target_size).max(0.0);
let padding_start = Self::finite_or_zero(padding_start).max(0.0);
let padding_end = Self::finite_or_zero(padding_end).max(0.0);
let max_offset = (content_size - viewport_size).max(0.0);
if viewport_size <= f32::EPSILON || max_offset <= f32::EPSILON {
return 0.0;
}
let target_start = Self::finite_or_zero(target_content_start);
let target_end = target_start + target_size;
let reveal_start = target_start - padding_start;
let reveal_end = target_end + padding_end;
let viewport_start = current_offset;
let viewport_end = current_offset + viewport_size;
if if_needed && reveal_start >= viewport_start && reveal_end <= viewport_end {
return current_offset.min(max_offset);
}
let desired = match alignment {
ScrollAlignment::Start => reveal_start,
ScrollAlignment::Center => {
let padded_viewport = (viewport_size - padding_start - padding_end).max(0.0);
target_start - padding_start - (padded_viewport - target_size) * 0.5
}
ScrollAlignment::End => reveal_end - viewport_size,
ScrollAlignment::Nearest => {
if reveal_start < viewport_start {
reveal_start
} else if reveal_end > viewport_end {
reveal_end - viewport_size
} else {
current_offset
}
}
ScrollAlignment::Fraction(fraction) => {
let fraction = Self::finite_or_zero(fraction).clamp(0.0, 1.0);
let padded_viewport = (viewport_size - padding_start - padding_end).max(0.0);
target_start - padding_start - (padded_viewport - target_size) * fraction
}
};
Self::finite_or_zero(desired).clamp(0.0, max_offset)
}
fn finite_or_zero(value: f32) -> f32 {
if value.is_finite() {
value
} else {
0.0
}
}
fn emit_scroll_into_view_diag(
kind: &'static str,
request: &ScrollIntoViewRequest,
container: Option<WidgetId>,
) {
diag::emit(
diag::DiagCategory::Input,
diag::DiagLevel::Debug,
diag::DiagEventKind::InputEvent {
kind: format!(
"scroll_into_view:{kind}:target={:?}:container={:?}",
request.target,
container.or(request.container)
),
target: Some(request.target.as_u128()),
position: None,
},
);
}
pub fn post_layout_hook(&mut self, ir: &CoreIR, layout: &LayoutSnapshot) -> bool {
let needs_follow_up_frame = self.apply_pending_scroll_into_view(ir, layout);
let mut current_heroes = HashMap::new();
for (id, node) in &ir.nodes {
if let Op::Semantics(s) = &node.op {
if let Some(tag) = &s.hero_tag {
if let Some(geom) = layout.get_node_geometry(*id) {
current_heroes.insert(tag.clone(), (*id, geom.rect));
}
}
}
}
for (tag, (_new_id, new_rect)) in ¤t_heroes {
if let Some((_old_id, old_rect)) = self.runtime_state.hero.positions.get(tag) {
if *new_rect != *old_rect {
diag::emit(
diag::DiagCategory::Layout,
diag::DiagLevel::Debug,
diag::DiagEventKind::AnchorPlacement {
widget: 0,
node: 0,
rect_x: old_rect.origin.x,
rect_y: old_rect.origin.y,
rect_w: old_rect.size.width,
rect_h: old_rect.size.height,
place_left: new_rect.origin.x,
place_top: new_rect.origin.y,
note: Some(format!("Hero flight: {}", tag)),
},
);
}
}
}
self.runtime_state.hero.positions = current_heroes;
needs_follow_up_frame
}
pub fn handle_input(
&mut self,
event: InputEvent,
ir: &CoreIR,
layout: &LayoutSnapshot,
) -> Result<()> {
use crate::hit_test::{
find_neighbor_focus_node, find_next_focus_node, hit_test_with_scroll, FocusDirection,
};
use crate::input::gesture::GestureController;
use crate::input::hover::HoverController;
use crate::input::slider::SliderController;
use crate::input::text::TextInputController;
use crate::input::{ControllerContext, InputController};
use crate::scrollbar::scrollbar_hit_test;
use crate::ui::custom_render::downcast_render_object;
if self.runtime_state.interaction.focused.is_none() {
if let Some(autofocus_id) = Self::find_autofocus_node(ir) {
self.runtime_state
.interaction
.set_focused(Some(autofocus_id));
if let Some(ime_handler) = &self.ime_handler {
let accepts_text = ir
.nodes
.get(&autofocus_id)
.and_then(|node| match &node.op {
Op::Semantics(semantics) => {
Some(semantics.role == fission_ir::semantics::Role::TextInput)
}
_ => None,
})
.unwrap_or(false);
ime_handler.set_ime_allowed(accepts_text);
}
}
}
if matches!(event, InputEvent::Pointer(_)) {
let dispatched_actions = {
let mut ctx = ControllerContext {
ir,
layout,
text_edit: &mut self.runtime_state.text_edit,
interaction: &mut self.runtime_state.interaction,
scroll: &mut self.runtime_state.scroll,
gesture: &mut self.runtime_state.gesture,
clipboard: self.clipboard_backend.as_ref(),
measurer: self.measurer.as_ref(),
dispatched_actions: Vec::new(),
};
let mut hover_controller = HoverController;
let _ = hover_controller.handle_event(&mut ctx, &event);
ctx.dispatched_actions
};
self.dispatch_input_actions(dispatched_actions)?;
}
let pointer_targets_scrollbar = match &event {
InputEvent::Pointer(PointerEvent::Down { point, button, .. })
if matches!(button, PointerButton::Primary) =>
{
scrollbar_hit_test(ir, layout, &self.runtime_state.scroll, *point).is_some()
}
InputEvent::Pointer(PointerEvent::Move { .. })
| InputEvent::Pointer(PointerEvent::Up { .. }) => {
self.runtime_state.gesture.scrollbar_drag.is_some()
}
InputEvent::Pointer(PointerEvent::Scroll { point, .. }) => {
scrollbar_hit_test(ir, layout, &self.runtime_state.scroll, *point).is_some()
}
_ => false,
};
if !pointer_targets_scrollbar {
if let Some(point) = Self::event_point(&event) {
if let Some(hit_node_id) =
hit_test_with_scroll(ir, layout, &self.runtime_state.scroll, point)
{
let mut target_ro: Option<(WidgetId, &fission_ir::AnyRenderObject)> = None;
{
let mut walk = Some(hit_node_id);
while let Some(nid) = walk {
if let Some(ro) = ir.custom_render_objects.get(&nid) {
target_ro = Some((nid, ro));
break;
}
walk = ir.nodes.get(&nid).and_then(|n| n.parent);
}
}
if target_ro.is_none() {
for (ro_nid, ro) in &ir.custom_render_objects {
if let Some(rect) = layout.get_node_rect(*ro_nid) {
if rect.contains(point) {
target_ro = Some((*ro_nid, ro));
break;
}
}
}
}
if let Some((nid, any_ro)) = target_ro {
if let Some(render_obj) = downcast_render_object(any_ro) {
let mut node_rect = layout
.get_node_rect(nid)
.unwrap_or(LayoutRect::new(0.0, 0.0, 0.0, 0.0));
{
let mut walk = ir.nodes.get(&nid).and_then(|n| n.parent);
while let Some(pid) = walk {
if let Some(pnode) = ir.nodes.get(&pid) {
if let fission_ir::Op::Layout(
fission_ir::LayoutOp::Scroll { direction, .. },
) = &pnode.op
{
let off = self.runtime_state.scroll.get_offset(pid);
match direction {
fission_ir::FlexDirection::Row => {
node_rect.origin.x -= off
}
fission_ir::FlexDirection::Column => {
node_rect.origin.y -= off
}
}
}
walk = pnode.parent;
} else {
break;
}
}
}
let result = render_obj.handle_event(nid, &event, node_rect);
if result.handled {
if matches!(event, InputEvent::Pointer(PointerEvent::Down { .. })) {
let old_focused_id = self.runtime_state.interaction.focused;
if Some(nid) != old_focused_id {
self.clear_text_pending_on_blur(old_focused_id, Some(nid));
self.dispatch_custom_blur_actions(ir, old_focused_id)?;
}
self.runtime_state.interaction.set_focused(Some(nid));
if let Some(ime_handler) = &self.ime_handler {
let accepts_text = render_obj.accepts_text_input();
ime_handler.set_ime_allowed(accepts_text);
if accepts_text {
if let Some(rect) =
render_obj.ime_cursor_area(node_rect)
{
ime_handler.set_ime_cursor_area(rect);
}
}
}
}
for (target, envelope) in result.actions {
self.dispatch_node(envelope, target)?;
}
self.update_focused_ime_state(ir, layout);
return Ok(());
}
}
}
}
}
}
if matches!(event, InputEvent::Keyboard(_) | InputEvent::Ime(_)) {
if let Some(focused_id) = self.runtime_state.interaction.focused {
let mut walk_id = Some(focused_id);
while let Some(nid) = walk_id {
if let Some(any_ro) = ir.custom_render_objects.get(&nid) {
if let Some(render_obj) = downcast_render_object(any_ro) {
let node_rect = layout
.get_node_rect(nid)
.unwrap_or(LayoutRect::new(0.0, 0.0, 0.0, 0.0));
let result = render_obj.handle_event(nid, &event, node_rect);
if result.handled {
for (target, envelope) in result.actions {
self.dispatch_node(envelope, target)?;
}
self.update_focused_ime_state(ir, layout);
return Ok(());
}
}
}
walk_id = ir.nodes.get(&nid).and_then(|n| n.parent);
}
}
}
let (handled, dispatched_actions) = {
let mut ctx = ControllerContext {
ir,
layout,
text_edit: &mut self.runtime_state.text_edit,
interaction: &mut self.runtime_state.interaction,
scroll: &mut self.runtime_state.scroll,
gesture: &mut self.runtime_state.gesture,
clipboard: self.clipboard_backend.as_ref(),
measurer: self.measurer.as_ref(),
dispatched_actions: Vec::new(),
};
let mut hover_controller = HoverController;
let _ = hover_controller.handle_event(&mut ctx, &event);
let mut gesture_controller = GestureController;
let handled = if gesture_controller.handle_event(&mut ctx, &event) {
true
} else {
let mut text_controller = TextInputController;
if text_controller.handle_event(&mut ctx, &event) {
true
} else {
let mut slider_controller = SliderController;
slider_controller.handle_event(&mut ctx, &event)
}
};
(handled, ctx.dispatched_actions)
};
self.dispatch_input_actions(dispatched_actions)?;
if handled {
if matches!(event, InputEvent::Pointer(PointerEvent::Up { .. })) {
self.runtime_state.interaction.pressed.clear();
self.runtime_state.interaction.last_down_point = None;
}
self.update_focused_ime_state(ir, layout);
return Ok(());
}
match event {
InputEvent::Pointer(PointerEvent::Scroll { point, delta, .. }) => {
let trace_scroll =
std::env::var("FISSION_SCROLL_TRACE").ok().as_deref() == Some("1");
if trace_scroll {
eprintln!(
"[scroll-trace] event point=({:.1},{:.1}) delta=({:.1},{:.1})",
point.x, point.y, delta.x, delta.y
);
}
let hit_node_id = scrollbar_hit_test(ir, layout, &self.runtime_state.scroll, point)
.map(|hit| hit.geometry.node_id)
.or_else(|| {
hit_test_with_scroll(ir, layout, &self.runtime_state.scroll, point)
});
if let Some(hit_node_id) = hit_node_id {
if trace_scroll {
eprintln!("[scroll-trace] hit_node={}", hit_node_id.as_u128());
}
let mut current_id = Some(hit_node_id);
while let Some(node_id) = current_id {
if let Some(node) = ir.nodes.get(&node_id) {
if let Op::Layout(LayoutOp::Scroll { direction, .. }) = &node.op {
let current_offset = self.runtime_state.scroll.get_offset(node_id);
let delta_val = match direction {
FlexDirection::Row => delta.x,
FlexDirection::Column => delta.y,
};
let mut new_offset = current_offset + delta_val;
let mut max_offset = 0.0f32;
let mut viewport_w = 0.0f32;
let mut viewport_h = 0.0f32;
let mut content_w = 0.0f32;
let mut content_h = 0.0f32;
if let Some(geom) = layout.get_node_geometry(node_id) {
viewport_w = geom.rect.width();
viewport_h = geom.rect.height();
content_w = geom.content_size.width;
content_h = geom.content_size.height;
max_offset = if matches!(direction, FlexDirection::Row) {
(geom.content_size.width - geom.rect.width()).max(0.0)
} else {
(geom.content_size.height - geom.rect.height()).max(0.0)
};
new_offset = new_offset.clamp(0.0, max_offset);
}
if trace_scroll {
eprintln!(
"[scroll-trace] scroll_node={} axis={} offset={:.1}->{:.1} max={:.1} viewport=({:.1},{:.1}) content=({:.1},{:.1})",
node_id.as_u128(),
match direction { FlexDirection::Row => "x", FlexDirection::Column => "y" },
current_offset,
new_offset,
max_offset,
viewport_w,
viewport_h,
content_w,
content_h
);
}
{
use fission_diagnostics::prelude as diag;
diag::emit(
diag::DiagCategory::Input,
diag::DiagLevel::Debug,
diag::DiagEventKind::ScrollUpdate {
node: node_id.as_u128(),
axis: match direction {
FlexDirection::Row => "x".into(),
FlexDirection::Column => "y".into(),
},
point_x: point.x,
point_y: point.y,
delta: delta_val,
old_offset: current_offset,
new_offset,
max_offset,
viewport_w,
viewport_h,
content_w,
content_h,
},
);
}
self.runtime_state.scroll.set_offset(node_id, new_offset);
if (new_offset - current_offset).abs() > 0.001 {
break;
}
}
current_id = node.parent;
} else {
break;
}
}
} else if trace_scroll {
eprintln!("[scroll-trace] hit_test: no node");
}
}
InputEvent::Keyboard(KeyEvent::Down {
key_code,
modifiers,
}) => match key_code {
KeyCode::Tab => {
let reverse = (modifiers & 1) != 0;
let old_focus = self.runtime_state.interaction.focused;
let next =
find_next_focus_node(ir, self.runtime_state.interaction.focused, reverse);
if next != old_focus {
self.clear_text_pending_on_blur(old_focus, next);
self.dispatch_custom_blur_actions(ir, old_focus)?;
}
self.runtime_state.interaction.set_focused(next);
}
KeyCode::Up | KeyCode::Down | KeyCode::Left | KeyCode::Right => {
let reverse = matches!(key_code, KeyCode::Up | KeyCode::Left);
let old_focus = self.runtime_state.interaction.focused;
let next = if let Some(focused) = old_focus {
let dir = match key_code {
KeyCode::Up => FocusDirection::Up,
KeyCode::Down => FocusDirection::Down,
KeyCode::Left => FocusDirection::Left,
KeyCode::Right => FocusDirection::Right,
_ => unreachable!(),
};
find_neighbor_focus_node(ir, layout, focused, dir)
.or_else(|| find_next_focus_node(ir, Some(focused), reverse))
} else {
find_next_focus_node(ir, None, reverse)
};
if next != old_focus {
self.clear_text_pending_on_blur(old_focus, next);
self.dispatch_custom_blur_actions(ir, old_focus)?;
self.runtime_state.interaction.set_focused(next);
}
}
KeyCode::Enter | KeyCode::Space => {
if let Some(focused_id) = self.runtime_state.interaction.focused {
let mut current_id = Some(focused_id);
while let Some(node_id) = current_id {
if let Some(node) = ir.nodes.get(&node_id) {
if let Op::Semantics(semantics) = &node.op {
if let Some(action_entry) = semantics.actions.entries.first() {
if let Some(payload) = &action_entry.payload_data {
let envelope = ActionEnvelope {
id: ActionId::from_u128(action_entry.action_id),
payload: payload.clone(),
};
let input = crate::input::scoped_action_input(
ir,
node_id,
ActionInput::None,
);
return self.dispatch_node_with_input(
envelope, node_id, &input,
);
}
}
}
current_id = node.parent;
} else {
break;
}
}
}
}
_ => {}
},
InputEvent::Pointer(PointerEvent::Down { point, .. }) => {
if let Some(hit_node_id) =
hit_test_with_scroll(ir, layout, &self.runtime_state.scroll, point)
{
diag::emit(
diag::DiagCategory::Input,
diag::DiagLevel::Debug,
diag::DiagEventKind::InputEvent {
kind: "pointer_down_hit".into(),
target: Some(hit_node_id.as_u128()),
position: Some((point.x, point.y)),
},
);
let mut focus_candidate = Some(hit_node_id);
while let Some(node_id) = focus_candidate {
if let Some(node) = ir.nodes.get(&node_id) {
if let Op::Semantics(s) = &node.op {
if s.focusable {
if s.focus_policy == FocusPolicy::PreserveCurrentOnPointer {
break;
}
let old_focused_id = self.runtime_state.interaction.focused;
if Some(node_id) != old_focused_id {
self.clear_text_pending_on_blur(
old_focused_id,
Some(node_id),
);
self.dispatch_custom_blur_actions(ir, old_focused_id)?;
if s.role == fission_ir::semantics::Role::TextInput {
if let Some(ime_handler) = &self.ime_handler {
ime_handler.set_ime_allowed(true);
}
} else if let Some(ime_handler) = &self.ime_handler {
ime_handler.set_ime_allowed(false);
}
}
self.runtime_state.interaction.set_focused(Some(node_id));
break;
}
}
focus_candidate = node.parent;
} else {
break;
}
}
if focus_candidate.is_none() {
let old_focused_id = self.runtime_state.interaction.focused;
if let Some(old_focused_id) = self.runtime_state.interaction.focused {
if let Some(old_node) = ir.nodes.get(&old_focused_id) {
if let Op::Semantics(s) = &old_node.op {
if s.role == fission_ir::semantics::Role::TextInput {
if let Some(ime_handler) = &self.ime_handler {
ime_handler.set_ime_allowed(false);
}
}
}
}
}
self.clear_text_pending_on_blur(old_focused_id, None);
self.dispatch_custom_blur_actions(ir, old_focused_id)?;
self.runtime_state.interaction.set_focused(None);
}
let mut current_pressed_id = Some(hit_node_id);
while let Some(node_id) = current_pressed_id {
self.runtime_state.interaction.set_pressed(node_id, true);
if let Some(node) = ir.nodes.get(&node_id) {
current_pressed_id = node.parent;
} else {
break;
}
}
self.runtime_state.interaction.last_down_point = Some(point);
if let Some(focused_id) = self.runtime_state.interaction.focused {
if let Some(node) = ir.nodes.get(&focused_id) {
if let Op::Semantics(s) = &node.op {
if s.role == fission_ir::semantics::Role::TextInput {
if let Some(ime_handler) = &self.ime_handler {
ime_handler.set_ime_cursor_area(LayoutRect::new(
point.x, point.y, 2.0, 16.0,
));
}
}
}
}
}
} else {
let old_focused_id = self.runtime_state.interaction.focused;
if let Some(old_focused_id) = self.runtime_state.interaction.focused {
if let Some(old_node) = ir.nodes.get(&old_focused_id) {
if let Op::Semantics(s) = &old_node.op {
if s.role == fission_ir::semantics::Role::TextInput {
if let Some(ime_handler) = &self.ime_handler {
ime_handler.set_ime_allowed(false);
}
}
}
}
}
self.clear_text_pending_on_blur(old_focused_id, None);
self.dispatch_custom_blur_actions(ir, old_focused_id)?;
self.runtime_state.interaction.set_focused(None);
}
}
InputEvent::Pointer(PointerEvent::Up { point, .. }) => {
self.runtime_state.interaction.pressed.clear();
self.runtime_state.interaction.last_down_point = None;
if let Some(hit_node_id) =
hit_test_with_scroll(ir, layout, &self.runtime_state.scroll, point)
{
let mut current_id = Some(hit_node_id);
while let Some(node_id) = current_id {
if let Some(node) = ir.nodes.get(&node_id) {
if let Op::Semantics(semantics) = &node.op {
if semantics.role == fission_ir::semantics::Role::TextInput {
} else if let Some(action_entry) = semantics.actions.entries.first()
{
if let Some(payload) = &action_entry.payload_data {
let envelope = ActionEnvelope {
id: ActionId::from_u128(action_entry.action_id),
payload: payload.clone(),
};
diag::emit(
diag::DiagCategory::Input,
diag::DiagLevel::Debug,
diag::DiagEventKind::InputEvent {
kind: "pointer_up_dispatch".into(),
target: Some(node_id.as_u128()),
position: Some((point.x, point.y)),
},
);
let input = crate::input::scoped_action_input(
ir,
node_id,
ActionInput::None,
);
return self
.dispatch_node_with_input(envelope, node_id, &input);
}
}
}
current_id = node.parent;
} else {
break;
}
}
}
}
_ => {}
}
self.update_focused_ime_state(ir, layout);
Ok(())
}
pub fn clear_hover_state(&mut self, ir: &CoreIR, point: Option<LayoutPoint>) -> Result<bool> {
use crate::input::hover::HoverController;
use crate::input::ControllerContext;
let dispatched_actions = {
let layout = &LayoutSnapshot::new(LayoutSize::ZERO);
let mut ctx = ControllerContext {
ir,
layout,
text_edit: &mut self.runtime_state.text_edit,
interaction: &mut self.runtime_state.interaction,
scroll: &mut self.runtime_state.scroll,
gesture: &mut self.runtime_state.gesture,
clipboard: self.clipboard_backend.as_ref(),
measurer: self.measurer.as_ref(),
dispatched_actions: Vec::new(),
};
let changed = HoverController::clear(&mut ctx, point);
(changed, ctx.dispatched_actions)
};
self.dispatch_input_actions(dispatched_actions.1)?;
Ok(dispatched_actions.0)
}
fn dispatch_input_actions(
&mut self,
dispatched_actions: Vec<(WidgetId, ActionEnvelope, ActionInput)>,
) -> Result<()> {
for (target, action, input) in dispatched_actions {
self.dispatch_node_with_input(action, target, &input)?;
}
Ok(())
}
fn update_focused_ime_state(&mut self, ir: &CoreIR, layout: &LayoutSnapshot) {
let Some(ime_handler) = self.ime_handler.clone() else {
return;
};
let Some(focused_id) = self.runtime_state.interaction.focused else {
ime_handler.set_ime_allowed(false);
return;
};
let mut walk = Some(focused_id);
while let Some(node_id) = walk {
if let Some(any_ro) = ir.custom_render_objects.get(&node_id) {
if let Some(render_obj) = crate::ui::custom_render::downcast_render_object(any_ro) {
let accepts_text = render_obj.accepts_text_input();
ime_handler.set_ime_allowed(accepts_text);
if accepts_text {
let rect =
Self::visual_node_rect(ir, layout, &self.runtime_state.scroll, node_id)
.unwrap_or(LayoutRect::new(0.0, 0.0, 0.0, 0.0));
if let Some(cursor_area) = render_obj.ime_cursor_area(rect) {
ime_handler.set_ime_cursor_area(cursor_area);
}
}
return;
}
}
walk = ir.nodes.get(&node_id).and_then(|node| node.parent);
}
let accepts_text = ir
.nodes
.get(&focused_id)
.and_then(|node| match &node.op {
Op::Semantics(semantics) => {
Some(semantics.role == fission_ir::semantics::Role::TextInput)
}
_ => None,
})
.unwrap_or(false);
ime_handler.set_ime_allowed(accepts_text);
if accepts_text {
let cursor_area = {
let mut ctx = crate::input::ControllerContext {
ir,
layout,
text_edit: &mut self.runtime_state.text_edit,
interaction: &mut self.runtime_state.interaction,
scroll: &mut self.runtime_state.scroll,
gesture: &mut self.runtime_state.gesture,
clipboard: self.clipboard_backend.as_ref(),
measurer: self.measurer.as_ref(),
dispatched_actions: Vec::new(),
};
crate::input::text::TextInputController::ime_cursor_area(&mut ctx, focused_id)
};
if let Some(cursor_area) = cursor_area {
ime_handler.set_ime_cursor_area(cursor_area);
}
}
}
fn visual_node_rect(
ir: &CoreIR,
layout: &LayoutSnapshot,
scroll: &crate::env::ScrollStateMap,
node_id: WidgetId,
) -> Option<LayoutRect> {
let mut rect = layout.get_node_rect(node_id)?;
let mut walk = ir.nodes.get(&node_id).and_then(|node| node.parent);
while let Some(parent_id) = walk {
let Some(parent) = ir.nodes.get(&parent_id) else {
break;
};
if let Op::Layout(LayoutOp::Scroll { direction, .. }) = &parent.op {
let offset = scroll.get_offset(parent_id);
match direction {
FlexDirection::Row => rect.origin.x -= offset,
FlexDirection::Column => rect.origin.y -= offset,
}
}
walk = parent.parent;
}
Some(rect)
}
fn clear_text_pending_on_blur(
&mut self,
old_focus: Option<WidgetId>,
new_focus: Option<WidgetId>,
) {
if old_focus == new_focus {
return;
}
if let Some(old_id) = old_focus {
if let Some(st) = self.runtime_state.text_edit.states.get_mut(&old_id) {
st.pending_model_sync = false;
st.clear_preedit();
}
}
}
fn dispatch_custom_blur_actions(
&mut self,
ir: &CoreIR,
old_focus: Option<WidgetId>,
) -> Result<()> {
if let Some(old_id) = old_focus {
if let Some(any_ro) = ir.custom_render_objects.get(&old_id) {
if let Some(render_obj) = crate::ui::custom_render::downcast_render_object(any_ro) {
if render_obj.accepts_text_input() {
if let Some(ime_handler) = &self.ime_handler {
ime_handler.set_ime_allowed(false);
}
}
for (target, envelope) in render_obj.blur_actions(old_id) {
self.dispatch_node(envelope, target)?;
}
}
}
}
Ok(())
}
pub fn hit_test(
&self,
point: LayoutPoint,
ir: &CoreIR,
snapshot: &LayoutSnapshot,
) -> Option<WidgetId> {
if let Some(root) = ir.root {
return self.hit_test_recursive(root, point, ir, snapshot);
}
None
}
fn hit_test_recursive(
&self,
node_id: WidgetId,
point: LayoutPoint,
ir: &CoreIR,
snapshot: &LayoutSnapshot,
) -> Option<WidgetId> {
if let Some(geom) = snapshot.nodes.get(&node_id) {
if geom.rect.contains(point) {
if let Some(node) = ir.nodes.get(&node_id) {
for child in node.children.iter().rev() {
let mut child_point = point;
if let Op::Layout(LayoutOp::Scroll { direction, .. }) = &node.op {
if !geom.rect.contains(point) {
continue;
}
let offset = self.runtime_state.scroll.get_offset(node_id);
match direction {
FlexDirection::Row => child_point.x += offset,
FlexDirection::Column => child_point.y += offset,
}
}
if let Op::Layout(LayoutOp::Transform { transform }) = &node.op {
let mat = Mat4::from_cols_array(transform);
let local_x = point.x - geom.rect.origin.x;
let local_y = point.y - geom.rect.origin.y;
let p = Vec4::new(local_x, local_y, 0.0, 1.0);
let inv = mat.inverse();
let transformed = inv * p;
child_point = LayoutPoint::new(
transformed.x + geom.rect.origin.x,
transformed.y + geom.rect.origin.y,
);
}
if let Some(hit) =
self.hit_test_recursive(*child, child_point, ir, snapshot)
{
return Some(hit);
}
}
match &node.op {
Op::Paint(_)
| Op::Layout(LayoutOp::Scroll { .. })
| Op::Layout(LayoutOp::Embed { .. }) => return Some(node_id),
_ => return None,
}
}
return None;
}
}
None
}
fn event_point(event: &InputEvent) -> Option<LayoutPoint> {
match event {
InputEvent::Pointer(PointerEvent::Down { point, .. })
| InputEvent::Pointer(PointerEvent::Up { point, .. })
| InputEvent::Pointer(PointerEvent::Move { point, .. })
| InputEvent::Pointer(PointerEvent::Scroll { point, .. }) => Some(*point),
_ => None,
}
}
fn find_autofocus_node(ir: &CoreIR) -> Option<WidgetId> {
fn walk(ir: &CoreIR, node_id: WidgetId) -> Option<WidgetId> {
let node = ir.nodes.get(&node_id)?;
if let Op::Semantics(semantics) = &node.op {
if semantics.autofocus && semantics.focusable && !semantics.disabled {
return Some(node_id);
}
}
for child_id in &node.children {
if let Some(found) = walk(ir, *child_id) {
return Some(found);
}
}
None
}
ir.root.and_then(|root| walk(ir, root))
}
pub fn reconcile_resources(
&mut self,
declarations: Vec<RuntimeResourceDeclaration>,
) -> Result<()> {
let now = self.clock().current_time();
let mut existing = std::mem::take(&mut self.active_resources);
let mut next = HashMap::new();
for declaration in declarations {
let key = declaration.key.clone();
match existing.remove(&key) {
Some(current)
if current.policy == declaration.policy
&& current.deps == declaration.deps
&& current.matches_kind(&declaration.kind) =>
{
next.insert(key, current);
}
Some(current) if declaration.policy == ResourcePolicy::PreserveOnChange => {
next.insert(key, current);
}
Some(current) => {
self.stop_resource(&key, ¤t);
let replacement = self.start_resource(declaration, now);
next.insert(key, replacement);
}
None => {
let resource = self.start_resource(declaration, now);
next.insert(key, resource);
}
}
}
for (key, resource) in existing {
self.stop_resource(&key, &resource);
}
self.active_resources = next;
Ok(())
}
pub fn resource_generation(&self, key: &str) -> Option<u64> {
self.active_resources
.get(key)
.map(|resource| resource.generation)
}
pub fn is_resource_current(&self, resource: &ResourceExecutionContext) -> bool {
self.resource_generation(&resource.key) == Some(resource.generation)
}
fn start_resource(
&mut self,
declaration: RuntimeResourceDeclaration,
now: CurrentTime,
) -> ActiveResource {
let generation = self.next_resource_generation;
self.next_resource_generation += 1;
let context = ResourceExecutionContext {
key: declaration.key.clone(),
generation,
};
let kind = match declaration.kind {
RuntimeResourceKind::Job(mut job) => {
job.effect.resource = Some(context);
self.enqueue_effect(job.effect);
ActiveResourceKind::Job
}
RuntimeResourceKind::Service(mut service) => {
service.effect.resource = Some(context);
let (service_name, slot_key) = match &service.effect.effect {
crate::Effect::StartService(payload) => {
(payload.service_name.clone(), payload.slot_key.clone())
}
_ => unreachable!("service resource must lower to StartService"),
};
self.enqueue_effect(service.effect);
ActiveResourceKind::Service {
service_name,
slot_key,
}
}
RuntimeResourceKind::Timer(timer) => self.start_timer_resource(timer, now),
};
ActiveResource {
generation,
deps: declaration.deps,
policy: declaration.policy,
kind,
}
}
fn start_timer_resource(&self, timer: TimerResource, now: CurrentTime) -> ActiveResourceKind {
let interval_ms = timer.interval_ms.max(1);
ActiveResourceKind::Timer {
interval_ms,
payload: timer.payload,
on_tick: timer.on_tick,
next_fire_at: if timer.immediate {
now
} else {
now.saturating_add(interval_ms)
},
}
}
fn stop_resource(&mut self, key: &str, resource: &ActiveResource) {
if let ActiveResourceKind::Service {
service_name,
slot_key,
} = &resource.kind
{
self.enqueue_effect(EffectEnvelope {
req_id: 0,
effect: crate::Effect::StopService(ServiceStopPayload {
service_name: service_name.clone(),
slot_key: slot_key.clone(),
}),
on_ok: None,
on_err: None,
service_bindings: None,
resource: Some(ResourceExecutionContext {
key: key.to_string(),
generation: resource.generation,
}),
});
}
}
}
impl ActiveResource {
fn matches_kind(&self, kind: &RuntimeResourceKind) -> bool {
matches!(
(&self.kind, kind),
(ActiveResourceKind::Job, RuntimeResourceKind::Job(_))
| (
ActiveResourceKind::Timer { .. },
RuntimeResourceKind::Timer(_)
)
| (
ActiveResourceKind::Service { .. },
RuntimeResourceKind::Service(_)
)
)
}
}