Skip to main content

fret_ui/tree/
ui_tree_text_input.rs

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}