1use crate::env::{
2 Clipboard, ContextMenuState, InteractionStateMap, ScrollStateMap, SelectableTextStateMap,
3 TextEditStateMap,
4};
5use crate::event::InputEvent;
6use crate::{ActionEnvelope, ActionId, ActionInput, UpdateTextInput};
7use fission_ir::semantics::ActionTrigger;
8use fission_ir::{CoreIR, Op, Semantics, WidgetId};
9use fission_layout::{LayoutSnapshot, TextMeasurer};
10use std::sync::Arc;
11
12pub mod canvas;
13pub mod gesture;
14pub mod hover;
15pub mod selectable_text;
16pub mod slider;
17pub mod text;
18pub mod viewport;
19
20mod editing_convention;
21pub use editing_convention::TextEditingConvention;
22
23pub struct ControllerContext<'a> {
24 pub ir: &'a CoreIR,
25 pub layout: &'a LayoutSnapshot,
26 pub text_edit: &'a mut TextEditStateMap,
27 pub selectable_text: &'a mut SelectableTextStateMap,
28 pub context_menu: &'a mut ContextMenuState,
29 pub interaction: &'a mut InteractionStateMap,
30 pub scroll: &'a mut ScrollStateMap,
31 pub viewport: &'a viewport::ViewportStateMap,
32 pub gesture: &'a mut crate::env::GestureState,
33 pub editing_convention: TextEditingConvention,
34 pub current_time: crate::time::CurrentTime,
36 pub clipboard: Option<&'a Arc<dyn Clipboard>>,
37 pub measurer: Option<&'a Arc<dyn TextMeasurer>>,
38 pub dispatched_actions: Vec<(WidgetId, ActionEnvelope, ActionInput)>,
40}
41
42pub trait InputController {
43 fn handle_event(&mut self, ctx: &mut ControllerContext, event: &InputEvent) -> bool;
44}
45
46#[doc(hidden)]
53pub fn prepare_text_input_change(
54 semantics: &Semantics,
55 node_id: WidgetId,
56 new_text: String,
57 new_caret: usize,
58 new_anchor: usize,
59) -> Option<(ActionEnvelope, ActionInput)> {
60 let entry = semantics
61 .actions
62 .entries
63 .iter()
64 .find(|entry| entry.trigger == ActionTrigger::TextChanged)?;
65
66 let payload = entry.payload_data.clone().unwrap_or_default();
70 let selection = crate::TextSelection::new(
71 &new_text,
72 new_anchor,
73 new_caret,
74 crate::TextAffinity::Downstream,
75 )
76 .unwrap_or_else(|_| crate::TextSelection::collapsed(crate::TextPosition::at_end(&new_text)));
77 let new_value = crate::TextEditingValue::new(new_text.clone(), selection, None)
78 .expect("selection was validated against the same text");
79 let old_value =
80 crate::TextEditingValue::from_text(semantics.value.as_deref().unwrap_or_default());
81 let input = ActionInput::TextChanged(UpdateTextInput {
82 node_id,
83 new_text: new_text.clone(),
84 new_caret,
85 new_anchor,
86 old_value,
87 new_value,
88 source: crate::TextEditSource::Programmatic,
89 phase: crate::TextEditPhase::Committed,
90 editing_action: None,
91 validation_state: None,
92 validation_message: None,
93 });
94
95 Some((
96 ActionEnvelope {
97 id: ActionId::from_u128(entry.action_id),
98 payload,
99 },
100 input,
101 ))
102}
103
104#[doc(hidden)]
106pub fn prepare_text_input_edit(
107 semantics: &Semantics,
108 node_id: WidgetId,
109 result: crate::TextEditResult,
110) -> Option<(ActionEnvelope, ActionInput)> {
111 let entry = semantics
112 .actions
113 .entries
114 .iter()
115 .find(|entry| entry.trigger == ActionTrigger::TextChanged)?;
116 let payload = entry.payload_data.clone().unwrap_or_default();
117 Some((
118 ActionEnvelope {
119 id: ActionId::from_u128(entry.action_id),
120 payload,
121 },
122 ActionInput::TextChanged(UpdateTextInput::from_values(
123 node_id,
124 result.old_value,
125 result.new_value,
126 result.source,
127 result.phase,
128 )),
129 ))
130}
131
132#[doc(hidden)]
133pub fn prepare_scoped_text_input_edit(
134 ir: &CoreIR,
135 semantics: &Semantics,
136 node_id: WidgetId,
137 result: crate::TextEditResult,
138) -> Option<(ActionEnvelope, ActionInput)> {
139 let (envelope, input) = prepare_text_input_edit(semantics, node_id, result)?;
140 Some((envelope, scoped_action_input(ir, node_id, input)))
141}
142
143#[doc(hidden)]
149pub fn prepare_scoped_text_session_action(
150 ir: &CoreIR,
151 semantics: &Semantics,
152 node_id: WidgetId,
153 trigger: ActionTrigger,
154 value: crate::TextEditingValue,
155 source: crate::TextEditSource,
156 phase: crate::TextEditPhase,
157) -> Option<(ActionEnvelope, ActionInput)> {
158 let entry = semantics
159 .actions
160 .entries
161 .iter()
162 .find(|entry| entry.trigger == trigger)?;
163 let envelope = ActionEnvelope {
164 id: ActionId::from_u128(entry.action_id),
165 payload: entry.payload_data.clone().unwrap_or_default(),
166 };
167 let input = ActionInput::TextChanged(UpdateTextInput::from_values(
168 node_id,
169 value.clone(),
170 value,
171 source,
172 phase,
173 ));
174 Some((envelope, scoped_action_input(ir, node_id, input)))
175}
176
177#[doc(hidden)]
183pub fn prepare_scoped_text_input_change(
184 ir: &CoreIR,
185 semantics: &Semantics,
186 node_id: WidgetId,
187 new_text: String,
188 new_caret: usize,
189 new_anchor: usize,
190) -> Option<(ActionEnvelope, ActionInput)> {
191 let (envelope, input) =
192 prepare_text_input_change(semantics, node_id, new_text, new_caret, new_anchor)?;
193 Some((envelope, scoped_action_input(ir, node_id, input)))
194}
195
196pub(crate) fn action_scope_for_node(ir: &CoreIR, node_id: WidgetId) -> Option<u128> {
197 let mut current_id = Some(node_id);
198 while let Some(id) = current_id {
199 let Some(node) = ir.nodes.get(&id) else {
200 break;
201 };
202 if let Op::Semantics(semantics) = &node.op {
203 if let Some(scope_id) = semantics.action_scope_id {
204 return Some(scope_id);
205 }
206 }
207 current_id = node.parent;
208 }
209 None
210}
211
212pub(crate) fn scoped_action_input(
213 ir: &CoreIR,
214 target: WidgetId,
215 input: ActionInput,
216) -> ActionInput {
217 if let Some(scope_id) = action_scope_for_node(ir, target) {
218 ActionInput::scoped_raw(scope_id, target, input)
219 } else {
220 input
221 }
222}