Skip to main content

material_ui_rs/
widget.rs

1//! Material 3 sized widget constructors.
2//!
3//! The style traits exposed by `iced` control colors, borders, and shadows, but
4//! not layout defaults like button height or checkbox size. These helpers apply
5//! the Material 3 component metrics from [`crate::tokens`] at construction time.
6
7use iced_widget::checkbox as iced_checkbox;
8use iced_widget::container as iced_container;
9use iced_widget::core::svg as core_svg;
10use iced_widget::core::text as core_text;
11use iced_widget::core::time::Instant;
12use iced_widget::core::widget as core_widget;
13use iced_widget::core::widget::tree::{self, Tree};
14use iced_widget::core::{
15    Background, Border, Clipboard, Color, Element, Event, Layout, Length, Padding, Pixels, Point,
16    Rectangle, Shell, Size, Vector, Widget, alignment, border, input_method, layout, mouse,
17    overlay, renderer, touch, window,
18};
19use iced_widget::radio as iced_radio;
20use iced_widget::rule as iced_rule;
21use iced_widget::text::{self, LineHeight};
22use iced_widget::text_editor as iced_text_editor;
23use iced_widget::text_input as iced_text_input;
24use iced_widget::toggler as iced_toggler;
25use iced_widget::tooltip as iced_tooltip;
26use iced_widget::{
27    Container, Row, Rule, Text, TextEditor as IcedTextEditor, TextInput as IcedTextInput,
28};
29
30use crate::style::{
31    button as button_style, checkbox as checkbox_style, container as container_style,
32    rule as rule_style, slider as slider_style, text_editor as text_editor_style,
33    text_input as text_input_style, toggler as toggler_style, tooltip as tooltip_style,
34};
35use crate::utils::mix;
36use crate::{Theme, fonts, tokens, web_input};
37
38#[path = "widget/component/app_bar.rs"]
39pub mod app_bar;
40#[path = "widget/component/badge.rs"]
41pub mod badge;
42#[path = "widget/component/card.rs"]
43pub mod card;
44#[path = "widget/component/combobox.rs"]
45pub mod combobox;
46#[path = "widget/component/data_table.rs"]
47pub mod data_table;
48#[path = "widget/component/dialog.rs"]
49pub mod dialog;
50#[path = "widget/component/list.rs"]
51pub mod list;
52#[path = "widget/component/log_viewer.rs"]
53pub mod log_viewer;
54#[path = "widget/internal/menu_overlay.rs"]
55mod menu_overlay;
56#[path = "widget/component/navigation.rs"]
57pub mod navigation;
58#[path = "widget/component/page.rs"]
59pub mod page;
60#[path = "widget/component/picker.rs"]
61pub mod picker;
62#[path = "widget/component/progress_bar.rs"]
63pub mod progress_bar;
64#[path = "widget/internal/reveal.rs"]
65mod reveal;
66#[path = "widget/internal/ripple.rs"]
67mod ripple;
68#[path = "widget/component/search.rs"]
69pub mod search;
70#[path = "widget/component/segmented_button.rs"]
71pub mod segmented_button;
72#[path = "widget/component/select.rs"]
73pub mod select;
74#[path = "widget/component/sheet.rs"]
75pub mod sheet;
76#[path = "widget/component/snackbar.rs"]
77pub mod snackbar;
78#[path = "widget/internal/support.rs"]
79mod support;
80#[path = "widget/component/tabs.rs"]
81pub mod tabs;
82#[path = "widget/component/theme_picker.rs"]
83pub mod theme_picker;
84#[path = "widget/component/toolbar.rs"]
85pub mod toolbar;
86#[path = "widget/component/viewport.rs"]
87pub mod viewport;
88
89use support::{
90    AnimatedScalar, SelectionState, TextFieldState, TextFieldTouchActivation, alpha_border,
91    alpha_color, bool_value, draw_text_field_notched, draw_text_field_outline, duration_ms, lerp,
92    scaled_rect, solid_color, text_field_floating_label_notch,
93};
94
95const TEXT_FIELD_TOUCH_SLOP: f32 = 8.0;
96
97fn absolute_line_height(value: f32) -> LineHeight {
98    LineHeight::Absolute(value.into())
99}
100
101#[cfg(target_os = "windows")]
102fn normalize_windows_ime_request(
103    input_method: &mut input_method::InputMethod,
104    avoid_bounds: Rectangle,
105) {
106    let input_method::InputMethod::Enabled {
107        cursor, preedit, ..
108    } = input_method
109    else {
110        return;
111    };
112
113    if !preedit
114        .as_ref()
115        .is_some_and(|preedit| !preedit.content.is_empty())
116    {
117        return;
118    }
119
120    *preedit = None;
121
122    let bounds_right = avoid_bounds.x + avoid_bounds.width;
123    let bounds_bottom = avoid_bounds.y + avoid_bounds.height;
124    let cursor_right = cursor.x + cursor.width;
125    let cursor_bottom = cursor.y + cursor.height;
126
127    if cursor.x < bounds_right
128        && cursor_right > avoid_bounds.x
129        && cursor.y < bounds_bottom
130        && cursor_bottom > avoid_bounds.y
131    {
132        cursor.x = avoid_bounds.x;
133        cursor.width = avoid_bounds.width;
134        cursor.height = (bounds_bottom - cursor.y).max(cursor.height);
135    }
136}
137
138#[cfg(not(target_os = "windows"))]
139fn normalize_windows_ime_request(
140    _input_method: &mut input_method::InputMethod,
141    _avoid_bounds: Rectangle,
142) {
143}
144
145fn text_with_metrics<'a, Renderer>(
146    content: impl text::IntoFragment<'a>,
147    size: f32,
148    line_height: f32,
149) -> Text<'a, Theme, Renderer>
150where
151    Renderer: core_text::Renderer,
152{
153    Text::new(content)
154        .size(size)
155        .line_height(absolute_line_height(line_height))
156}
157
158fn centered_icon_text<'a, Renderer>(
159    icon: impl text::IntoFragment<'a>,
160    size: f32,
161) -> Text<'a, Theme, Renderer>
162where
163    Renderer: core_text::Renderer,
164    iced_widget::core::Font: Into<Renderer::Font>,
165{
166    fonts::icon(icon, size)
167        .width(Length::Fixed(size))
168        .height(Length::Fixed(size))
169        .center()
170}
171
172fn text_field_touch_cursor(event: &Event, cursor: mouse::Cursor) -> mouse::Cursor {
173    match event {
174        Event::Touch(
175            touch::Event::FingerPressed { position, .. }
176            | touch::Event::FingerMoved { position, .. }
177            | touch::Event::FingerLifted { position, .. }
178            | touch::Event::FingerLost { position, .. },
179        ) if cursor.position().is_none() && !cursor.is_levitating() => {
180            mouse::Cursor::Available(*position)
181        }
182        _ => cursor,
183    }
184}
185
186fn touch_as_mouse_event(event: &Event) -> Option<Event> {
187    match event {
188        Event::Touch(touch::Event::FingerPressed { .. }) => Some(Event::Mouse(
189            mouse::Event::ButtonPressed(mouse::Button::Left),
190        )),
191        Event::Touch(touch::Event::FingerMoved { position, .. }) => {
192            Some(Event::Mouse(mouse::Event::CursorMoved {
193                position: *position,
194            }))
195        }
196        Event::Touch(touch::Event::FingerLifted { .. } | touch::Event::FingerLost { .. }) => Some(
197            Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)),
198        ),
199        _ => None,
200    }
201}
202
203fn text_field_touch_position(position: Point, cursor: mouse::Cursor) -> Option<Point> {
204    if let Some(cursor_position) = cursor.position() {
205        return Some(cursor_position);
206    }
207
208    if cursor.is_levitating() {
209        return None;
210    }
211
212    Some(position)
213}
214
215fn text_field_keyboard_activation(
216    touch_activation: &mut Option<TextFieldTouchActivation>,
217    event: &Event,
218    bounds: Rectangle,
219    cursor: mouse::Cursor,
220) -> bool {
221    match event {
222        Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => cursor.is_over(bounds),
223        Event::Touch(touch::Event::FingerPressed { id, position }) => {
224            if let Some(position) = text_field_touch_position(*position, cursor)
225                && bounds.contains(position)
226            {
227                *touch_activation = Some(TextFieldTouchActivation::new(*id, position));
228            } else {
229                *touch_activation = None;
230            }
231
232            false
233        }
234        Event::Touch(touch::Event::FingerMoved { id, position }) => {
235            if let Some(position) = text_field_touch_position(*position, cursor)
236                && touch_activation.is_some_and(|activation| {
237                    activation.matches(*id)
238                        && activation.moved_beyond_slop(position, TEXT_FIELD_TOUCH_SLOP)
239                })
240            {
241                *touch_activation = None;
242            }
243
244            false
245        }
246        Event::Touch(touch::Event::FingerLifted { id, position }) => {
247            let position = text_field_touch_position(*position, cursor);
248
249            touch_activation.take().is_some_and(|activation| {
250                position
251                    .is_some_and(|position| activation.matches(*id) && bounds.contains(position))
252            })
253        }
254        Event::Touch(touch::Event::FingerLost { id, .. }) => {
255            if touch_activation.is_some_and(|activation| activation.matches(*id)) {
256                *touch_activation = None;
257            }
258
259            false
260        }
261        _ => false,
262    }
263}
264
265#[derive(Debug, Clone, Copy, PartialEq, Eq)]
266enum TextFieldInnerTouchHandling {
267    Forward,
268    Suppress,
269    ConfirmedTap,
270}
271
272#[derive(Debug, Clone, Copy)]
273enum TextFieldTouchBounds {
274    Visible(Rectangle),
275    Hidden,
276}
277
278impl TextFieldTouchBounds {
279    fn visible(bounds: Option<Rectangle>) -> Self {
280        bounds.map(Self::Visible).unwrap_or(Self::Hidden)
281    }
282}
283
284#[derive(Debug, Clone, Copy)]
285struct TextFieldTouchContext<'a> {
286    is_enabled: bool,
287    event: &'a Event,
288    bounds: TextFieldTouchBounds,
289    cursor: mouse::Cursor,
290    activation_before: Option<TextFieldTouchActivation>,
291    confirmed_tap: bool,
292}
293
294impl TextFieldTouchContext<'_> {
295    fn keyboard_activation(&self, touch_activation: &mut Option<TextFieldTouchActivation>) -> bool {
296        let TextFieldTouchBounds::Visible(bounds) = self.bounds else {
297            if matches!(self.event, Event::Touch(_)) {
298                *touch_activation = None;
299            }
300
301            return false;
302        };
303
304        text_field_keyboard_activation(touch_activation, self.event, bounds, self.cursor)
305    }
306
307    fn inner_handling(self) -> TextFieldInnerTouchHandling {
308        if !self.is_enabled {
309            return TextFieldInnerTouchHandling::Forward;
310        }
311
312        if self.confirmed_tap {
313            return TextFieldInnerTouchHandling::ConfirmedTap;
314        }
315
316        let TextFieldTouchBounds::Visible(bounds) = self.bounds else {
317            return if matches!(self.event, Event::Touch(_)) {
318                TextFieldInnerTouchHandling::Suppress
319            } else {
320                TextFieldInnerTouchHandling::Forward
321            };
322        };
323
324        if self.press_is_over(bounds) || self.matches_activation() {
325            TextFieldInnerTouchHandling::Suppress
326        } else {
327            TextFieldInnerTouchHandling::Forward
328        }
329    }
330
331    fn press_is_over(self, bounds: Rectangle) -> bool {
332        matches!(
333            self.event,
334            Event::Touch(touch::Event::FingerPressed { position, .. })
335                if text_field_touch_position(*position, self.cursor)
336                    .is_some_and(|position| bounds.contains(position))
337        )
338    }
339
340    fn matches_activation(self) -> bool {
341        let Some(activation) = self.activation_before else {
342            return false;
343        };
344
345        match self.event {
346            Event::Touch(
347                touch::Event::FingerMoved { id, .. }
348                | touch::Event::FingerLifted { id, .. }
349                | touch::Event::FingerLost { id, .. },
350            ) => activation.matches(*id),
351            _ => false,
352        }
353    }
354}
355
356#[derive(Debug, Clone, Copy)]
357struct TextInputActivation {
358    cursor: mouse::Cursor,
359    request_mobile_keyboard: bool,
360    web_input_anchor: Option<Rectangle>,
361    web_input_translation: Vector,
362    inner_touch_handling: TextFieldInnerTouchHandling,
363}
364
365#[derive(Debug, Clone, Copy, Default)]
366struct WebInputPositionState {
367    // Scrollable parents translate the cursor before child updates and only
368    // translate InputMethod::cursor back after the child returns. Remember the
369    // event-space pointer so the Web bridge can undo the cumulative offset now.
370    raw_pointer_position: Option<Point>,
371    translation: Vector,
372}
373
374impl WebInputPositionState {
375    fn update(&mut self, event: &Event, cursor: mouse::Cursor) {
376        let raw_pointer_position = match event {
377            Event::Mouse(mouse::Event::CursorMoved { position })
378            | Event::Touch(
379                touch::Event::FingerPressed { position, .. }
380                | touch::Event::FingerMoved { position, .. }
381                | touch::Event::FingerLifted { position, .. }
382                | touch::Event::FingerLost { position, .. },
383            ) => Some(*position),
384            _ => None,
385        };
386
387        if let Some(position) = raw_pointer_position {
388            self.raw_pointer_position = Some(position);
389        }
390
391        if let (Some(raw), Some(translated)) = (self.raw_pointer_position, cursor.land().position())
392        {
393            self.translation = translated - raw;
394        }
395    }
396}
397
398#[derive(Debug, Clone, Copy, Default)]
399struct MobileTextInputState {
400    touch_activation: Option<TextFieldTouchActivation>,
401    web_input_position: WebInputPositionState,
402}
403
404fn mobile_text_input_activation(
405    is_enabled: bool,
406    state: &mut MobileTextInputState,
407    event: &Event,
408    visible_bounds: Option<Rectangle>,
409    cursor: mouse::Cursor,
410) -> TextInputActivation {
411    text_input_activation(
412        is_enabled,
413        &mut state.touch_activation,
414        &mut state.web_input_position,
415        event,
416        visible_bounds,
417        cursor,
418    )
419}
420
421fn text_input_activation(
422    is_enabled: bool,
423    touch_activation: &mut Option<TextFieldTouchActivation>,
424    web_input_position: &mut WebInputPositionState,
425    event: &Event,
426    visible_bounds: Option<Rectangle>,
427    cursor: mouse::Cursor,
428) -> TextInputActivation {
429    web_input_position.update(event, cursor);
430    let inner_cursor = text_field_touch_cursor(event, cursor);
431    let touch = TextFieldTouchContext {
432        is_enabled,
433        event,
434        bounds: TextFieldTouchBounds::visible(visible_bounds),
435        cursor: inner_cursor,
436        activation_before: *touch_activation,
437        confirmed_tap: false,
438    };
439    let request_mobile_keyboard = is_enabled && touch.keyboard_activation(touch_activation);
440    let web_input_anchor = if request_mobile_keyboard {
441        visible_bounds.map(|bounds| {
442            let position = inner_cursor.position().unwrap_or(bounds.position());
443
444            Rectangle::new(position, Size::UNIT)
445        })
446    } else {
447        None
448    };
449    let inner_touch_handling = TextFieldTouchContext {
450        confirmed_tap: request_mobile_keyboard,
451        ..touch
452    }
453    .inner_handling();
454
455    TextInputActivation {
456        cursor: inner_cursor,
457        request_mobile_keyboard,
458        web_input_anchor,
459        web_input_translation: web_input_position.translation,
460        inner_touch_handling,
461    }
462}
463
464fn web_input_anchor(
465    input_method: &input_method::InputMethod,
466    visible_bounds: Option<Rectangle>,
467    activation: TextInputActivation,
468    started_focused: bool,
469    is_focused: bool,
470) -> Option<Rectangle> {
471    if !is_focused {
472        return None;
473    }
474
475    let anchor = match input_method {
476        input_method::InputMethod::Enabled { cursor, .. } => Some(*cursor),
477        input_method::InputMethod::Disabled if activation.web_input_anchor.is_some() => {
478            activation.web_input_anchor
479        }
480        input_method::InputMethod::Disabled if started_focused != is_focused => {
481            visible_bounds.map(|bounds| {
482                Rectangle::new(bounds.position(), Size::new(1.0, bounds.height.max(1.0)))
483            })
484        }
485        input_method::InputMethod::Disabled => None,
486    }?;
487
488    let anchor = visible_bounds.map_or(anchor, |bounds| {
489        let right = bounds.x + bounds.width;
490        let bottom = bounds.y + bounds.height;
491
492        Rectangle::new(
493            Point::new(
494                anchor.x.clamp(bounds.x, right),
495                anchor.y.clamp(bounds.y, bottom),
496            ),
497            Size::new(anchor.width.max(1.0), anchor.height.max(1.0)),
498        )
499    });
500
501    Some(anchor - activation.web_input_translation)
502}
503
504fn sync_mobile_keyboard(
505    started_focused: bool,
506    is_focused: bool,
507    request_mobile_keyboard: bool,
508    input_anchor: Option<Rectangle>,
509) {
510    if let Some(anchor) = input_anchor {
511        web_input::position_mobile_keyboard(anchor);
512    }
513
514    if started_focused != is_focused {
515        if is_focused {
516            if !request_mobile_keyboard {
517                web_input::show_mobile_keyboard();
518            }
519        } else {
520            web_input::hide_mobile_keyboard();
521        }
522    }
523
524    if request_mobile_keyboard {
525        web_input::show_mobile_keyboard();
526    }
527}
528
529fn register_mobile_text_region(is_enabled: bool, bounds: Rectangle, viewport: &Rectangle) {
530    if is_enabled && let Some(visible_bounds) = bounds.intersection(viewport) {
531        web_input::register_text_region(visible_bounds);
532    }
533}
534
535struct TextInputUpdateContext<'a, 'b, Message, Renderer> {
536    renderer: &'a Renderer,
537    clipboard: &'a mut dyn Clipboard,
538    shell: &'a mut Shell<'b, Message>,
539    viewport: &'a Rectangle,
540}
541
542fn update_mobile_text_input<'a, Message, Renderer>(
543    input: &mut IcedTextInput<'a, Message, Theme, Renderer>,
544    tree: &mut Tree,
545    event: &Event,
546    layout: Layout<'_>,
547    activation: TextInputActivation,
548    context: TextInputUpdateContext<'_, '_, Message, Renderer>,
549) where
550    Message: Clone,
551    Renderer: iced_widget::core::Renderer + core_text::Renderer,
552{
553    match activation.inner_touch_handling {
554        TextFieldInnerTouchHandling::Forward => {
555            input.update(
556                tree,
557                event,
558                layout,
559                activation.cursor,
560                context.renderer,
561                &mut *context.clipboard,
562                &mut *context.shell,
563                context.viewport,
564            );
565        }
566        TextFieldInnerTouchHandling::Suppress => {}
567        TextFieldInnerTouchHandling::ConfirmedTap => {
568            let press = Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left));
569            input.update(
570                tree,
571                &press,
572                layout,
573                activation.cursor,
574                context.renderer,
575                &mut *context.clipboard,
576                &mut *context.shell,
577                context.viewport,
578            );
579
580            let release = Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left));
581            input.update(
582                tree,
583                &release,
584                layout,
585                activation.cursor,
586                context.renderer,
587                &mut *context.clipboard,
588                &mut *context.shell,
589                context.viewport,
590            );
591        }
592    }
593
594    refresh_text_input_caret(
595        tree.state
596            .downcast_mut::<iced_text_input::State<Renderer::Paragraph>>(),
597        event,
598        &mut *context.shell,
599    );
600}
601
602fn press_is_over(event: &Event, bounds: Rectangle, cursor: mouse::Cursor) -> bool {
603    match event {
604        Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => cursor.is_over(bounds),
605        Event::Touch(touch::Event::FingerPressed { position, .. }) => {
606            touch_event_is_over(*position, bounds, cursor)
607        }
608        _ => false,
609    }
610}
611
612fn release_is_over(event: &Event, bounds: Rectangle, cursor: mouse::Cursor) -> bool {
613    match event {
614        Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => cursor.is_over(bounds),
615        Event::Touch(touch::Event::FingerLifted { position, .. }) => {
616            touch_event_is_over(*position, bounds, cursor)
617        }
618        _ => false,
619    }
620}
621
622fn touch_event_is_over(position: Point, bounds: Rectangle, cursor: mouse::Cursor) -> bool {
623    if cursor.position().is_some() {
624        return cursor.is_over(bounds);
625    }
626
627    if cursor.is_levitating() {
628        return false;
629    }
630
631    bounds.contains(position)
632}
633
634fn selection_control_hit_bounds(layout: Layout<'_>, target_size: f32) -> Rectangle {
635    let content_bounds = layout.bounds();
636    let control_bounds = layout
637        .children()
638        .next()
639        .map_or(content_bounds, |control| control.bounds());
640
641    SelectionControlHitTarget {
642        content: content_bounds,
643        control: control_bounds,
644        target_size,
645    }
646    .bounds()
647}
648
649#[derive(Debug, Clone, Copy)]
650struct SelectionControlHitTarget {
651    content: Rectangle,
652    control: Rectangle,
653    target_size: f32,
654}
655
656impl SelectionControlHitTarget {
657    fn bounds(self) -> Rectangle {
658        let target_height = self.content.height.max(self.target_size);
659        let content_target = Rectangle {
660            y: self.content.center_y() - target_height / 2.0,
661            height: target_height,
662            ..self.content
663        };
664        let control_padding =
665            ((self.target_size - self.control.width.min(self.control.height)) / 2.0).max(0.0);
666        let control_target = Rectangle {
667            x: self.control.x - control_padding,
668            y: self.control.y - control_padding,
669            width: self.control.width + control_padding * 2.0,
670            height: self.control.height + control_padding * 2.0,
671        };
672
673        union_bounds(content_target, control_target)
674    }
675}
676
677fn union_bounds(a: Rectangle, b: Rectangle) -> Rectangle {
678    let x = a.x.min(b.x);
679    let y = a.y.min(b.y);
680    let right = (a.x + a.width).max(b.x + b.width);
681    let bottom = (a.y + a.height).max(b.y + b.height);
682
683    Rectangle {
684        x,
685        y,
686        width: right - x,
687        height: bottom - y,
688    }
689}
690
691fn should_suppress_ime_caret() -> bool {
692    !cfg!(any(
693        target_arch = "wasm32",
694        target_os = "android",
695        target_os = "windows"
696    ))
697}
698
699fn text_caret_refresh_event(event: &Event) -> bool {
700    match event {
701        Event::Keyboard(iced_widget::core::keyboard::Event::KeyPressed { key, text, .. }) => {
702            text.as_ref()
703                .is_some_and(|text| text.chars().any(|c| !c.is_control()))
704                || matches!(
705                    key.as_ref(),
706                    iced_widget::core::keyboard::Key::Named(
707                        iced_widget::core::keyboard::key::Named::Enter
708                            | iced_widget::core::keyboard::key::Named::Backspace
709                            | iced_widget::core::keyboard::key::Named::Delete
710                    )
711                )
712        }
713        Event::InputMethod(input_method::Event::Preedit(content, _)) => !content.is_empty(),
714        Event::InputMethod(input_method::Event::Commit(content)) => !content.is_empty(),
715        _ => false,
716    }
717}
718
719fn refresh_text_input_caret<Message, P>(
720    state: &mut iced_text_input::State<P>,
721    event: &Event,
722    shell: &mut Shell<'_, Message>,
723) where
724    P: core_text::Paragraph,
725{
726    if !state.is_focused() || !text_caret_refresh_event(event) {
727        return;
728    }
729
730    let value = {
731        let text = <iced_text_input::State<P> as core_widget::operation::TextInput>::text(state);
732        iced_text_input::Value::new(text)
733    };
734    let cursor = state.cursor().state(&value);
735
736    core_widget::operation::Focusable::focus(state);
737
738    match cursor {
739        iced_text_input::cursor::State::Index(index) => {
740            state.move_cursor_to(index);
741        }
742        iced_text_input::cursor::State::Selection { start, end } => {
743            state.select_range(start, end);
744        }
745    }
746
747    shell.request_redraw();
748}
749
750fn mobile_text_input<'a, Message, Renderer>(
751    input: IcedTextInput<'a, Message, Theme, Renderer>,
752    is_enabled: bool,
753) -> Element<'a, Message, Theme, Renderer>
754where
755    Message: Clone + 'a,
756    Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
757{
758    Element::new(MobileTextInput { input, is_enabled })
759}
760
761struct MobileTextInput<'a, Message, Renderer>
762where
763    Renderer: iced_widget::core::Renderer + core_text::Renderer,
764{
765    input: IcedTextInput<'a, Message, Theme, Renderer>,
766    is_enabled: bool,
767}
768
769impl<Message, Renderer> Widget<Message, Theme, Renderer> for MobileTextInput<'_, Message, Renderer>
770where
771    Message: Clone,
772    Renderer: iced_widget::core::Renderer + core_text::Renderer,
773{
774    fn tag(&self) -> tree::Tag {
775        tree::Tag::of::<MobileTextInputState>()
776    }
777
778    fn state(&self) -> tree::State {
779        tree::State::new(MobileTextInputState::default())
780    }
781
782    fn children(&self) -> Vec<Tree> {
783        let input: &dyn Widget<Message, Theme, Renderer> = &self.input;
784
785        vec![Tree::new(input)]
786    }
787
788    fn diff(&self, tree: &mut Tree) {
789        if tree.children.is_empty() {
790            tree.children = self.children();
791        } else {
792            self.input.diff(&mut tree.children[0]);
793            tree.children.truncate(1);
794        }
795    }
796
797    fn size(&self) -> Size<Length> {
798        Widget::<Message, Theme, Renderer>::size(&self.input)
799    }
800
801    fn size_hint(&self) -> Size<Length> {
802        Widget::<Message, Theme, Renderer>::size_hint(&self.input)
803    }
804
805    fn layout(
806        &mut self,
807        tree: &mut Tree,
808        renderer: &Renderer,
809        limits: &layout::Limits,
810    ) -> layout::Node {
811        let input = <IcedTextInput<'_, Message, Theme, Renderer> as Widget<
812            Message,
813            Theme,
814            Renderer,
815        >>::layout(&mut self.input, &mut tree.children[0], renderer, limits);
816
817        layout::Node::with_children(input.size(), vec![input])
818    }
819
820    fn operate(
821        &mut self,
822        tree: &mut Tree,
823        layout: Layout<'_>,
824        renderer: &Renderer,
825        operation: &mut dyn core_widget::Operation,
826    ) {
827        self.input.operate(
828            &mut tree.children[0],
829            layout.children().next().unwrap(),
830            renderer,
831            operation,
832        );
833    }
834
835    fn update(
836        &mut self,
837        tree: &mut Tree,
838        event: &Event,
839        layout: Layout<'_>,
840        cursor: mouse::Cursor,
841        renderer: &Renderer,
842        clipboard: &mut dyn Clipboard,
843        shell: &mut Shell<'_, Message>,
844        viewport: &Rectangle,
845    ) {
846        let bounds = layout.bounds();
847        let visible_bounds = bounds.intersection(viewport);
848        let input_layout = layout.children().next().unwrap();
849
850        let started_focused = {
851            let state = tree.children[0]
852                .state
853                .downcast_ref::<iced_text_input::State<Renderer::Paragraph>>();
854
855            state.is_focused()
856        };
857
858        let activation = mobile_text_input_activation(
859            self.is_enabled,
860            tree.state.downcast_mut::<MobileTextInputState>(),
861            event,
862            visible_bounds,
863            cursor,
864        );
865
866        update_mobile_text_input(
867            &mut self.input,
868            &mut tree.children[0],
869            event,
870            input_layout,
871            activation,
872            TextInputUpdateContext {
873                renderer,
874                clipboard,
875                shell,
876                viewport,
877            },
878        );
879
880        normalize_windows_ime_request(shell.input_method_mut(), bounds);
881
882        let is_focused = {
883            let state = tree.children[0]
884                .state
885                .downcast_ref::<iced_text_input::State<Renderer::Paragraph>>();
886
887            state.is_focused()
888        };
889
890        let input_anchor = web_input_anchor(
891            shell.input_method(),
892            visible_bounds,
893            activation,
894            started_focused,
895            is_focused,
896        );
897
898        sync_mobile_keyboard(
899            started_focused,
900            is_focused,
901            activation.request_mobile_keyboard,
902            input_anchor,
903        );
904    }
905
906    fn mouse_interaction(
907        &self,
908        tree: &Tree,
909        layout: Layout<'_>,
910        cursor: mouse::Cursor,
911        viewport: &Rectangle,
912        renderer: &Renderer,
913    ) -> mouse::Interaction {
914        self.input.mouse_interaction(
915            &tree.children[0],
916            layout.children().next().unwrap(),
917            cursor,
918            viewport,
919            renderer,
920        )
921    }
922
923    fn draw(
924        &self,
925        tree: &Tree,
926        renderer: &mut Renderer,
927        theme: &Theme,
928        defaults: &renderer::Style,
929        layout: Layout<'_>,
930        cursor: mouse::Cursor,
931        viewport: &Rectangle,
932    ) {
933        register_mobile_text_region(self.is_enabled, layout.bounds(), viewport);
934
935        <IcedTextInput<'_, Message, Theme, Renderer> as Widget<Message, Theme, Renderer>>::draw(
936            &self.input,
937            &tree.children[0],
938            renderer,
939            theme,
940            defaults,
941            layout.children().next().unwrap(),
942            cursor,
943            viewport,
944        );
945    }
946}
947
948fn checkbox_checkmark_svg(mark_progress: f32) -> Vec<u8> {
949    let progress = mark_progress.clamp(0.0, 1.0);
950    let short_height = lerp(
951        tokens::component::checkbox::CHECKMARK_STROKE_WIDTH,
952        tokens::component::checkbox::CHECKMARK_SHORT_MARK_SIZE,
953        progress,
954    );
955    let long_width = tokens::component::checkbox::CHECKMARK_LONG_MARK_SIZE * progress;
956
957    format!(
958        r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 18 18"><g transform="scale(1 -1) translate({} {}) rotate(45)"><rect width="{}" height="{short_height}"/><rect width="{long_width}" height="{}"/></g></svg>"#,
959        tokens::component::checkbox::CHECKMARK_BOTTOM_LEFT_X,
960        tokens::component::checkbox::CHECKMARK_BOTTOM_LEFT_Y,
961        tokens::component::checkbox::CHECKMARK_STROKE_WIDTH,
962        tokens::component::checkbox::CHECKMARK_STROKE_WIDTH,
963    )
964    .into_bytes()
965}
966
967#[path = "widget/component/button.rs"]
968pub mod button;
969#[path = "widget/component/slider.rs"]
970pub mod slider;
971
972#[path = "widget/component/rule.rs"]
973pub mod rule;
974
975#[path = "widget/component/container.rs"]
976pub mod container;
977
978#[path = "widget/component/text_input.rs"]
979pub mod text_input;
980#[path = "widget/component/tooltip.rs"]
981pub mod tooltip;
982
983#[path = "widget/component/text_editor.rs"]
984pub mod text_editor;
985
986#[path = "widget/component/radio.rs"]
987pub mod radio;
988
989#[path = "widget/component/checkbox.rs"]
990pub mod checkbox;
991
992#[path = "widget/component/toggler.rs"]
993pub mod toggler;
994
995#[cfg(test)]
996#[path = "../tests/widget.rs"]
997mod tests;