1use super::*;
2
3impl<H: UiHost> UiTree<H> {
4 pub fn platform_text_input_query(
5 &mut self,
6 app: &mut H,
7 services: &mut dyn UiServices,
8 scale_factor: f32,
9 query: &fret_runtime::PlatformTextInputQuery,
10 ) -> fret_runtime::PlatformTextInputQueryResult {
11 let focus_is_text_input = self.focus_is_text_input(app);
12 if !focus_is_text_input {
13 return match query {
14 fret_runtime::PlatformTextInputQuery::SelectedTextRange
15 | fret_runtime::PlatformTextInputQuery::MarkedTextRange => {
16 fret_runtime::PlatformTextInputQueryResult::Range(None)
17 }
18 fret_runtime::PlatformTextInputQuery::TextForRange { .. } => {
19 fret_runtime::PlatformTextInputQueryResult::Text(None)
20 }
21 fret_runtime::PlatformTextInputQuery::BoundsForRange { .. } => {
22 fret_runtime::PlatformTextInputQueryResult::Bounds(None)
23 }
24 fret_runtime::PlatformTextInputQuery::CharacterIndexForPoint { .. } => {
25 fret_runtime::PlatformTextInputQueryResult::Index(None)
26 }
27 };
28 }
29
30 let Some(focus) = self.focus else {
31 return match query {
32 fret_runtime::PlatformTextInputQuery::SelectedTextRange
33 | fret_runtime::PlatformTextInputQuery::MarkedTextRange => {
34 fret_runtime::PlatformTextInputQueryResult::Range(None)
35 }
36 fret_runtime::PlatformTextInputQuery::TextForRange { .. } => {
37 fret_runtime::PlatformTextInputQueryResult::Text(None)
38 }
39 fret_runtime::PlatformTextInputQuery::BoundsForRange { .. } => {
40 fret_runtime::PlatformTextInputQueryResult::Bounds(None)
41 }
42 fret_runtime::PlatformTextInputQuery::CharacterIndexForPoint { .. } => {
43 fret_runtime::PlatformTextInputQueryResult::Index(None)
44 }
45 };
46 };
47
48 let bounds = self.nodes.get(focus).map(|n| n.bounds).unwrap_or_default();
49
50 if let Some(window) = self.window
51 && let Some(record) = crate::declarative::element_record_for_node(app, window, focus)
52 {
53 let element = record.element;
54 if let crate::declarative::ElementInstance::TextInputRegion(props) = record.instance {
55 let ctx = TextInputRegionPlatformCtx {
56 window,
57 element,
58 bounds,
59 scale_factor,
60 props: &props,
61 };
62 return text_input_region_platform_text_input_query_with_hooks(
63 app, services, ctx, query,
64 );
65 }
66 }
67
68 match query {
69 fret_runtime::PlatformTextInputQuery::SelectedTextRange => {
70 let range = self
71 .nodes
72 .get(focus)
73 .and_then(|n| n.widget.as_ref())
74 .and_then(|w| w.platform_text_input_selected_range_utf16());
75 fret_runtime::PlatformTextInputQueryResult::Range(range)
76 }
77 fret_runtime::PlatformTextInputQuery::MarkedTextRange => {
78 let range = self
79 .nodes
80 .get(focus)
81 .and_then(|n| n.widget.as_ref())
82 .and_then(|w| w.platform_text_input_marked_range_utf16());
83 fret_runtime::PlatformTextInputQueryResult::Range(range)
84 }
85 fret_runtime::PlatformTextInputQuery::TextForRange { range } => {
86 let text = self
87 .nodes
88 .get(focus)
89 .and_then(|n| n.widget.as_ref())
90 .and_then(|w| w.platform_text_input_text_for_range_utf16(*range));
91 fret_runtime::PlatformTextInputQueryResult::Text(text)
92 }
93 fret_runtime::PlatformTextInputQuery::BoundsForRange { range } => {
94 let out = self.with_widget_mut(focus, |w, _tree| {
95 let mut cx = PlatformTextInputCx {
96 app,
97 services,
98 window: _tree.window,
99 node: focus,
100 bounds,
101 scale_factor,
102 };
103 w.platform_text_input_bounds_for_range_utf16(&mut cx, *range)
104 });
105 fret_runtime::PlatformTextInputQueryResult::Bounds(out)
106 }
107 fret_runtime::PlatformTextInputQuery::CharacterIndexForPoint { point } => {
108 let out = self.with_widget_mut(focus, |w, _tree| {
109 let mut cx = PlatformTextInputCx {
110 app,
111 services,
112 window: _tree.window,
113 node: focus,
114 bounds,
115 scale_factor,
116 };
117 w.platform_text_input_character_index_for_point_utf16(&mut cx, *point)
118 });
119 fret_runtime::PlatformTextInputQueryResult::Index(out)
120 }
121 }
122 }
123
124 pub fn platform_text_input_replace_text_in_range_utf16(
125 &mut self,
126 app: &mut H,
127 services: &mut dyn UiServices,
128 scale_factor: f32,
129 range: fret_runtime::Utf16Range,
130 text: &str,
131 ) -> bool {
132 if !self.focus_is_text_input(app) {
133 return false;
134 }
135 let Some(focus) = self.focus else {
136 return false;
137 };
138 let bounds = self.nodes.get(focus).map(|n| n.bounds).unwrap_or_default();
139
140 if let Some(window) = self.window
141 && let Some(record) = crate::declarative::element_record_for_node(app, window, focus)
142 {
143 let element = record.element;
144 if let crate::declarative::ElementInstance::TextInputRegion(props) = record.instance {
145 let ctx = TextInputRegionPlatformCtx {
146 window,
147 element,
148 bounds,
149 scale_factor,
150 props: &props,
151 };
152 let changed =
153 text_input_region_platform_text_input_replace_text_in_range_utf16_with_hooks(
154 app, services, ctx, range, text,
155 );
156 if changed {
157 self.invalidate(focus, Invalidation::Layout);
158 self.request_redraw_coalesced(app);
159 }
160 return changed;
161 }
162 }
163
164 let changed = self.with_widget_mut(focus, |w, _tree| {
165 let mut cx = PlatformTextInputCx {
166 app,
167 services,
168 window: _tree.window,
169 node: focus,
170 bounds,
171 scale_factor,
172 };
173 w.platform_text_input_replace_text_in_range_utf16(&mut cx, range, text)
174 });
175 if changed {
176 self.invalidate(focus, Invalidation::Layout);
177 self.request_redraw_coalesced(app);
178 }
179 changed
180 }
181
182 pub fn platform_text_input_replace_and_mark_text_in_range_utf16(
183 &mut self,
184 app: &mut H,
185 services: &mut dyn UiServices,
186 scale_factor: f32,
187 range: fret_runtime::Utf16Range,
188 text: &str,
189 marked: Option<fret_runtime::Utf16Range>,
190 selected: Option<fret_runtime::Utf16Range>,
191 ) -> bool {
192 if !self.focus_is_text_input(app) {
193 return false;
194 }
195 let Some(focus) = self.focus else {
196 return false;
197 };
198 let bounds = self.nodes.get(focus).map(|n| n.bounds).unwrap_or_default();
199
200 if let Some(window) = self.window
201 && let Some(record) = crate::declarative::element_record_for_node(app, window, focus)
202 {
203 let element = record.element;
204 if let crate::declarative::ElementInstance::TextInputRegion(props) = record.instance {
205 let ctx = TextInputRegionPlatformCtx {
206 window,
207 element,
208 bounds,
209 scale_factor,
210 props: &props,
211 };
212 let changed =
213 text_input_region_platform_text_input_replace_and_mark_text_in_range_utf16_with_hooks(
214 app, services, ctx, range, text, marked, selected,
215 );
216 if changed {
217 self.invalidate(focus, Invalidation::Layout);
218 self.request_redraw_coalesced(app);
219 }
220 return changed;
221 }
222 }
223
224 let changed = self.with_widget_mut(focus, |w, _tree| {
225 let mut cx = PlatformTextInputCx {
226 app,
227 services,
228 window: _tree.window,
229 node: focus,
230 bounds,
231 scale_factor,
232 };
233 w.platform_text_input_replace_and_mark_text_in_range_utf16(
234 &mut cx, range, text, marked, selected,
235 )
236 });
237 if changed {
238 self.invalidate(focus, Invalidation::Layout);
239 self.request_redraw_coalesced(app);
240 }
241 changed
242 }
243
244 pub(in crate::tree) fn set_ime_allowed(&mut self, app: &mut H, enabled: bool) {
245 if self.ime_allowed == enabled {
246 return;
247 }
248 self.ime_allowed = enabled;
249 let Some(window) = self.window else {
250 return;
251 };
252 app.push_effect(Effect::ImeAllow { window, enabled });
253 }
254}
255
256pub(in crate::tree) fn text_input_region_platform_text_input_snapshot(
257 props: &crate::element::TextInputRegionProps,
258) -> fret_runtime::WindowTextInputSnapshot {
259 let value = props.a11y_value.as_deref().unwrap_or("");
260
261 let len_utf16_usize = fret_core::utf::utf8_byte_offset_to_utf16_offset(
262 value,
263 value.len(),
264 fret_core::utf::UtfIndexClamp::Down,
265 );
266 let len_utf16 = u32::try_from(len_utf16_usize).unwrap_or(u32::MAX);
267
268 let selection_utf16 = props.a11y_text_selection.map(|(anchor, focus)| {
269 let anchor_u16 = fret_core::utf::utf8_byte_offset_to_utf16_offset(
270 value,
271 usize::try_from(anchor).unwrap_or(usize::MAX),
272 fret_core::utf::UtfIndexClamp::Down,
273 );
274 let focus_u16 = fret_core::utf::utf8_byte_offset_to_utf16_offset(
275 value,
276 usize::try_from(focus).unwrap_or(usize::MAX),
277 fret_core::utf::UtfIndexClamp::Down,
278 );
279 (
280 u32::try_from(anchor_u16).unwrap_or(u32::MAX),
281 u32::try_from(focus_u16).unwrap_or(u32::MAX),
282 )
283 });
284
285 let marked_utf16 = props.a11y_text_composition.map(|(start, end)| {
286 let (s, e) = fret_core::utf::utf8_byte_range_to_utf16_range(
287 value,
288 usize::try_from(start).unwrap_or(usize::MAX),
289 usize::try_from(end).unwrap_or(usize::MAX),
290 );
291 (
292 u32::try_from(s).unwrap_or(u32::MAX),
293 u32::try_from(e).unwrap_or(u32::MAX),
294 )
295 });
296
297 fret_runtime::WindowTextInputSnapshot {
298 focus_is_text_input: true,
299 is_composing: marked_utf16.is_some(),
300 text_len_utf16: len_utf16,
301 selection_utf16,
302 marked_utf16,
303 ime_cursor_area: props.ime_cursor_area,
304 surrounding_text: props.ime_surrounding_text.clone(),
305 }
306}
307
308fn text_input_region_platform_text_input_query(
309 props: &crate::element::TextInputRegionProps,
310 query: &fret_runtime::PlatformTextInputQuery,
311) -> fret_runtime::PlatformTextInputQueryResult {
312 let value = props.a11y_value.as_deref().unwrap_or("");
313
314 match query {
315 fret_runtime::PlatformTextInputQuery::SelectedTextRange => {
316 let Some((anchor, focus)) = props.a11y_text_selection else {
317 return fret_runtime::PlatformTextInputQueryResult::Range(None);
318 };
319
320 let anchor_u16 = fret_core::utf::utf8_byte_offset_to_utf16_offset(
321 value,
322 usize::try_from(anchor).unwrap_or(usize::MAX),
323 fret_core::utf::UtfIndexClamp::Down,
324 );
325 let focus_u16 = fret_core::utf::utf8_byte_offset_to_utf16_offset(
326 value,
327 usize::try_from(focus).unwrap_or(usize::MAX),
328 fret_core::utf::UtfIndexClamp::Down,
329 );
330 let range = fret_runtime::Utf16Range::new(
331 u32::try_from(anchor_u16).unwrap_or(u32::MAX),
332 u32::try_from(focus_u16).unwrap_or(u32::MAX),
333 )
334 .normalized();
335
336 fret_runtime::PlatformTextInputQueryResult::Range(Some(range))
337 }
338 fret_runtime::PlatformTextInputQuery::MarkedTextRange => {
339 let Some((start, end)) = props.a11y_text_composition else {
340 return fret_runtime::PlatformTextInputQueryResult::Range(None);
341 };
342
343 let (s, e) = fret_core::utf::utf8_byte_range_to_utf16_range(
344 value,
345 usize::try_from(start).unwrap_or(usize::MAX),
346 usize::try_from(end).unwrap_or(usize::MAX),
347 );
348 let range = fret_runtime::Utf16Range::new(
349 u32::try_from(s).unwrap_or(u32::MAX),
350 u32::try_from(e).unwrap_or(u32::MAX),
351 )
352 .normalized();
353
354 fret_runtime::PlatformTextInputQueryResult::Range(Some(range))
355 }
356 fret_runtime::PlatformTextInputQuery::TextForRange { range } => {
357 if value.is_empty() {
358 return fret_runtime::PlatformTextInputQueryResult::Text(None);
359 }
360
361 let range = range.normalized();
362 let (bs, be) = fret_core::utf::utf16_range_to_utf8_byte_range(
363 value,
364 usize::try_from(range.start).unwrap_or(usize::MAX),
365 usize::try_from(range.end).unwrap_or(usize::MAX),
366 );
367
368 let out = value.get(bs..be).map(ToString::to_string);
369 fret_runtime::PlatformTextInputQueryResult::Text(out)
370 }
371 fret_runtime::PlatformTextInputQuery::BoundsForRange { .. } => {
372 fret_runtime::PlatformTextInputQueryResult::Bounds(None)
373 }
374 fret_runtime::PlatformTextInputQuery::CharacterIndexForPoint { .. } => {
375 fret_runtime::PlatformTextInputQueryResult::Index(None)
376 }
377 }
378}
379
380#[derive(Clone, Copy)]
381pub(in crate::tree) struct TextInputRegionPlatformCtx<'a> {
382 window: fret_core::AppWindowId,
383 element: crate::GlobalElementId,
384 bounds: Rect,
385 scale_factor: f32,
386 props: &'a crate::element::TextInputRegionProps,
387}
388
389pub(in crate::tree) fn text_input_region_platform_text_input_query_with_hooks<H: UiHost>(
390 app: &mut H,
391 services: &mut dyn UiServices,
392 ctx: TextInputRegionPlatformCtx<'_>,
393 query: &fret_runtime::PlatformTextInputQuery,
394) -> fret_runtime::PlatformTextInputQueryResult {
395 match query {
396 fret_runtime::PlatformTextInputQuery::BoundsForRange { .. }
397 | fret_runtime::PlatformTextInputQuery::CharacterIndexForPoint { .. } => {
398 let hook = crate::elements::with_element_state(
399 app,
400 ctx.window,
401 ctx.element,
402 crate::action::TextInputRegionActionHooks::default,
403 |hooks| hooks.on_platform_text_input_query.clone(),
404 );
405
406 if let Some(hook) = hook {
407 struct TextInputRegionPlatformQueryHost<'a, H: UiHost> {
408 app: &'a mut H,
409 }
410
411 impl<H: UiHost> crate::action::UiActionHost for TextInputRegionPlatformQueryHost<'_, H> {
412 fn models_mut(&mut self) -> &mut fret_runtime::ModelStore {
413 self.app.models_mut()
414 }
415
416 fn push_effect(&mut self, effect: fret_runtime::Effect) {
417 self.app.push_effect(effect);
418 }
419
420 fn request_redraw(&mut self, window: fret_core::AppWindowId) {
421 self.app.request_redraw(window);
422 }
423
424 fn next_timer_token(&mut self) -> fret_runtime::TimerToken {
425 self.app.next_timer_token()
426 }
427
428 fn next_clipboard_token(&mut self) -> fret_runtime::ClipboardToken {
429 self.app.next_clipboard_token()
430 }
431
432 fn next_share_sheet_token(&mut self) -> fret_runtime::ShareSheetToken {
433 self.app.next_share_sheet_token()
434 }
435
436 fn notify(&mut self, _cx: crate::action::ActionCx) {}
437 }
438
439 let mut host = TextInputRegionPlatformQueryHost { app };
440 let action_cx = crate::action::ActionCx {
441 window: ctx.window,
442 target: ctx.element,
443 };
444 if let Some(out) = hook(
445 &mut host,
446 action_cx,
447 services,
448 ctx.bounds,
449 ctx.scale_factor,
450 ctx.props,
451 query,
452 ) {
453 return out;
454 }
455 }
456
457 match query {
458 fret_runtime::PlatformTextInputQuery::BoundsForRange { .. } => {
459 fret_runtime::PlatformTextInputQueryResult::Bounds(None)
460 }
461 fret_runtime::PlatformTextInputQuery::CharacterIndexForPoint { .. } => {
462 fret_runtime::PlatformTextInputQueryResult::Index(None)
463 }
464 _ => unreachable!(),
465 }
466 }
467 _ => text_input_region_platform_text_input_query(ctx.props, query),
468 }
469}
470
471pub(in crate::tree) fn text_input_region_platform_text_input_replace_text_in_range_utf16_with_hooks<
472 H: UiHost,
473>(
474 app: &mut H,
475 services: &mut dyn UiServices,
476 ctx: TextInputRegionPlatformCtx<'_>,
477 range: fret_runtime::Utf16Range,
478 text: &str,
479) -> bool {
480 let hook = crate::elements::with_element_state(
481 app,
482 ctx.window,
483 ctx.element,
484 crate::action::TextInputRegionActionHooks::default,
485 |hooks| {
486 hooks
487 .on_platform_text_input_replace_text_in_range_utf16
488 .clone()
489 },
490 );
491
492 let Some(hook) = hook else {
493 return false;
494 };
495
496 struct TextInputRegionPlatformReplaceHost<'a, H: UiHost> {
497 app: &'a mut H,
498 }
499
500 impl<H: UiHost> crate::action::UiActionHost for TextInputRegionPlatformReplaceHost<'_, H> {
501 fn models_mut(&mut self) -> &mut fret_runtime::ModelStore {
502 self.app.models_mut()
503 }
504
505 fn push_effect(&mut self, effect: fret_runtime::Effect) {
506 self.app.push_effect(effect);
507 }
508
509 fn request_redraw(&mut self, window: fret_core::AppWindowId) {
510 self.app.request_redraw(window);
511 }
512
513 fn next_timer_token(&mut self) -> fret_runtime::TimerToken {
514 self.app.next_timer_token()
515 }
516
517 fn next_clipboard_token(&mut self) -> fret_runtime::ClipboardToken {
518 self.app.next_clipboard_token()
519 }
520
521 fn next_share_sheet_token(&mut self) -> fret_runtime::ShareSheetToken {
522 self.app.next_share_sheet_token()
523 }
524
525 fn notify(&mut self, _cx: crate::action::ActionCx) {}
526 }
527
528 let mut host = TextInputRegionPlatformReplaceHost { app };
529 let action_cx = crate::action::ActionCx {
530 window: ctx.window,
531 target: ctx.element,
532 };
533 hook(
534 &mut host,
535 action_cx,
536 services,
537 ctx.bounds,
538 ctx.scale_factor,
539 ctx.props,
540 range,
541 text,
542 )
543}
544
545pub(in crate::tree) fn text_input_region_platform_text_input_replace_and_mark_text_in_range_utf16_with_hooks<
546 H: UiHost,
547>(
548 app: &mut H,
549 services: &mut dyn UiServices,
550 ctx: TextInputRegionPlatformCtx<'_>,
551 range: fret_runtime::Utf16Range,
552 text: &str,
553 marked: Option<fret_runtime::Utf16Range>,
554 selected: Option<fret_runtime::Utf16Range>,
555) -> bool {
556 let hook = crate::elements::with_element_state(
557 app,
558 ctx.window,
559 ctx.element,
560 crate::action::TextInputRegionActionHooks::default,
561 |hooks| {
562 hooks
563 .on_platform_text_input_replace_and_mark_text_in_range_utf16
564 .clone()
565 },
566 );
567
568 let Some(hook) = hook else {
569 return false;
570 };
571
572 struct TextInputRegionPlatformReplaceHost<'a, H: UiHost> {
573 app: &'a mut H,
574 }
575
576 impl<H: UiHost> crate::action::UiActionHost for TextInputRegionPlatformReplaceHost<'_, H> {
577 fn models_mut(&mut self) -> &mut fret_runtime::ModelStore {
578 self.app.models_mut()
579 }
580
581 fn push_effect(&mut self, effect: fret_runtime::Effect) {
582 self.app.push_effect(effect);
583 }
584
585 fn request_redraw(&mut self, window: fret_core::AppWindowId) {
586 self.app.request_redraw(window);
587 }
588
589 fn next_timer_token(&mut self) -> fret_runtime::TimerToken {
590 self.app.next_timer_token()
591 }
592
593 fn next_clipboard_token(&mut self) -> fret_runtime::ClipboardToken {
594 self.app.next_clipboard_token()
595 }
596
597 fn next_share_sheet_token(&mut self) -> fret_runtime::ShareSheetToken {
598 self.app.next_share_sheet_token()
599 }
600
601 fn notify(&mut self, _cx: crate::action::ActionCx) {}
602 }
603
604 let mut host = TextInputRegionPlatformReplaceHost { app };
605 let action_cx = crate::action::ActionCx {
606 window: ctx.window,
607 target: ctx.element,
608 };
609 hook(
610 &mut host,
611 action_cx,
612 services,
613 ctx.bounds,
614 ctx.scale_factor,
615 ctx.props,
616 range,
617 text,
618 marked,
619 selected,
620 )
621}