Skip to main content

material_ui_rs/widget/component/
text_editor.rs

1//! Material 3 outlined multi-line text field constructors.
2
3use super::*;
4
5pub use iced_text_editor::{Action, Binding, Content, KeyPress};
6
7/// Default height for a compact outlined text area preview.
8pub const OUTLINED_AREA_HEIGHT: f32 = tokens::component::text_field::CONTAINER_HEIGHT * 2.0;
9
10/// A Material 3 outlined multi-line text field.
11pub struct TextEditor<'a, Message, Renderer = iced_widget::Renderer>
12where
13    Renderer: core_text::Renderer,
14{
15    is_enabled: bool,
16    inner: IcedTextEditor<'a, core_text::highlighter::PlainText, Message, Theme, Renderer>,
17}
18
19impl<Message, Renderer> std::fmt::Debug for TextEditor<'_, Message, Renderer>
20where
21    Renderer: core_text::Renderer,
22{
23    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
24        f.debug_struct("TextEditor")
25            .field("is_enabled", &self.is_enabled)
26            .finish_non_exhaustive()
27    }
28}
29
30impl<'a, Message, Renderer> TextEditor<'a, Message, Renderer>
31where
32    Message: 'a,
33    Renderer: core_text::Renderer + 'a,
34{
35    pub fn new(content: &'a Content<Renderer>) -> Self {
36        let inner = IcedTextEditor::new(content)
37            .padding(Padding {
38                top: tokens::component::text_field::TOP_SPACE,
39                right: tokens::component::text_field::TRAILING_SPACE,
40                bottom: tokens::component::text_field::BOTTOM_SPACE,
41                left: tokens::component::text_field::LEADING_SPACE,
42            })
43            .size(tokens::component::text_field::INPUT_TEXT_SIZE)
44            .line_height(absolute_line_height(
45                tokens::component::text_field::INPUT_TEXT_LINE_HEIGHT,
46            ))
47            .min_height(tokens::component::text_field::CONTAINER_HEIGHT)
48            .style(text_editor_style::default);
49
50        Self {
51            is_enabled: false,
52            inner,
53        }
54    }
55
56    pub fn id(mut self, id: impl Into<core_widget::Id>) -> Self {
57        self.inner = self.inner.id(id);
58        self
59    }
60
61    pub fn placeholder(mut self, placeholder: impl text::IntoFragment<'a>) -> Self {
62        self.inner = self.inner.placeholder(placeholder);
63        self
64    }
65
66    pub fn width(mut self, width: impl Into<Pixels>) -> Self {
67        self.inner = self.inner.width(width);
68        self
69    }
70
71    pub fn height(mut self, height: impl Into<Length>) -> Self {
72        self.inner = self.inner.height(height);
73        self
74    }
75
76    pub fn min_height(mut self, min_height: impl Into<Pixels>) -> Self {
77        self.inner = self.inner.min_height(min_height);
78        self
79    }
80
81    pub fn max_height(mut self, max_height: impl Into<Pixels>) -> Self {
82        self.inner = self.inner.max_height(max_height);
83        self
84    }
85
86    pub fn on_action(mut self, on_edit: impl Fn(Action) -> Message + 'a) -> Self {
87        self.is_enabled = true;
88        self.inner = self.inner.on_action(on_edit);
89        self
90    }
91
92    pub fn font(mut self, font: impl Into<Renderer::Font>) -> Self {
93        self.inner = self.inner.font(font);
94        self
95    }
96
97    pub fn size(mut self, size: impl Into<Pixels>) -> Self {
98        self.inner = self.inner.size(size);
99        self
100    }
101
102    pub fn line_height(mut self, line_height: impl Into<core_text::LineHeight>) -> Self {
103        self.inner = self.inner.line_height(line_height);
104        self
105    }
106
107    pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
108        self.inner = self.inner.padding(padding);
109        self
110    }
111
112    pub fn wrapping(mut self, wrapping: core_text::Wrapping) -> Self {
113        self.inner = self.inner.wrapping(wrapping);
114        self
115    }
116
117    pub fn key_binding(
118        mut self,
119        key_binding: impl Fn(KeyPress) -> Option<Binding<Message>> + 'a,
120    ) -> Self {
121        self.inner = self.inner.key_binding(key_binding);
122        self
123    }
124
125    pub fn style(
126        mut self,
127        style: impl Fn(&Theme, iced_text_editor::Status) -> iced_text_editor::Style + 'a,
128    ) -> Self
129    where
130        <Theme as iced_text_editor::Catalog>::Class<'a>: From<iced_text_editor::StyleFn<'a, Theme>>,
131    {
132        self.inner = self.inner.style(style);
133        self
134    }
135}
136
137#[derive(Debug, Default)]
138struct TextEditorState {
139    touch_activation: Option<TextFieldTouchActivation>,
140}
141
142impl<Message, Renderer> Widget<Message, Theme, Renderer> for TextEditor<'_, Message, Renderer>
143where
144    Renderer: core_text::Renderer,
145{
146    fn tag(&self) -> tree::Tag {
147        tree::Tag::of::<TextEditorState>()
148    }
149
150    fn state(&self) -> tree::State {
151        tree::State::new(TextEditorState::default())
152    }
153
154    fn children(&self) -> Vec<Tree> {
155        let inner: &dyn Widget<Message, Theme, Renderer> = &self.inner;
156
157        vec![Tree::new(inner)]
158    }
159
160    fn diff(&self, tree: &mut Tree) {
161        if tree.children.is_empty() {
162            tree.children = self.children();
163        } else {
164            self.inner.diff(&mut tree.children[0]);
165            tree.children.truncate(1);
166        }
167    }
168
169    fn size(&self) -> Size<Length> {
170        Widget::<Message, Theme, Renderer>::size(&self.inner)
171    }
172
173    fn size_hint(&self) -> Size<Length> {
174        Widget::<Message, Theme, Renderer>::size_hint(&self.inner)
175    }
176
177    fn layout(
178        &mut self,
179        tree: &mut Tree,
180        renderer: &Renderer,
181        limits: &layout::Limits,
182    ) -> layout::Node {
183        let inner = self.inner.layout(&mut tree.children[0], renderer, limits);
184
185        layout::Node::with_children(inner.size(), vec![inner])
186    }
187
188    fn operate(
189        &mut self,
190        tree: &mut Tree,
191        layout: Layout<'_>,
192        renderer: &Renderer,
193        operation: &mut dyn core_widget::Operation,
194    ) {
195        self.inner.operate(
196            &mut tree.children[0],
197            layout.children().next().unwrap(),
198            renderer,
199            operation,
200        );
201    }
202
203    fn update(
204        &mut self,
205        tree: &mut Tree,
206        event: &Event,
207        layout: Layout<'_>,
208        cursor: mouse::Cursor,
209        renderer: &Renderer,
210        clipboard: &mut dyn Clipboard,
211        shell: &mut Shell<'_, Message>,
212        viewport: &Rectangle,
213    ) {
214        let bounds = layout.bounds();
215        let visible_bounds = bounds.intersection(viewport);
216        let translated_event = touch_as_mouse_event(event);
217        let inner_event = translated_event.as_ref().unwrap_or(event);
218        let activation = {
219            let state = tree.state.downcast_mut::<TextEditorState>();
220            text_input_activation(
221                self.is_enabled,
222                &mut state.touch_activation,
223                event,
224                visible_bounds,
225                cursor,
226            )
227        };
228
229        let started_focused = {
230            let state = tree.children[0]
231                .state
232                .downcast_ref::<iced_text_editor::State<core_text::highlighter::PlainText>>();
233
234            state.is_focused()
235        };
236
237        match activation.inner_touch_handling {
238            TextFieldInnerTouchHandling::Forward => {
239                self.inner.update(
240                    &mut tree.children[0],
241                    inner_event,
242                    layout.children().next().unwrap(),
243                    activation.cursor,
244                    renderer,
245                    clipboard,
246                    shell,
247                    viewport,
248                );
249            }
250            TextFieldInnerTouchHandling::Suppress => {}
251            TextFieldInnerTouchHandling::ConfirmedTap => {
252                let press = Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left));
253                self.inner.update(
254                    &mut tree.children[0],
255                    &press,
256                    layout.children().next().unwrap(),
257                    activation.cursor,
258                    renderer,
259                    clipboard,
260                    shell,
261                    viewport,
262                );
263
264                let release = Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left));
265                self.inner.update(
266                    &mut tree.children[0],
267                    &release,
268                    layout.children().next().unwrap(),
269                    activation.cursor,
270                    renderer,
271                    clipboard,
272                    shell,
273                    viewport,
274                );
275            }
276        }
277
278        if activation.request_mobile_keyboard {
279            shell.request_redraw();
280        }
281
282        normalize_windows_ime_request(shell.input_method_mut(), bounds);
283
284        let is_focused = {
285            let state = tree.children[0]
286                .state
287                .downcast_ref::<iced_text_editor::State<core_text::highlighter::PlainText>>();
288
289            state.is_focused()
290        };
291
292        if is_focused && text_caret_refresh_event(event) {
293            let state = tree.children[0]
294                .state
295                .downcast_mut::<iced_text_editor::State<core_text::highlighter::PlainText>>();
296
297            core_widget::operation::Focusable::focus(state);
298            shell.request_redraw();
299        }
300
301        if started_focused != is_focused {
302            sync_mobile_keyboard(
303                started_focused,
304                is_focused,
305                activation.request_mobile_keyboard,
306            );
307        }
308
309        if started_focused == is_focused {
310            sync_mobile_keyboard(
311                started_focused,
312                is_focused,
313                activation.request_mobile_keyboard,
314            );
315        }
316    }
317
318    fn mouse_interaction(
319        &self,
320        tree: &Tree,
321        layout: Layout<'_>,
322        cursor: mouse::Cursor,
323        viewport: &Rectangle,
324        renderer: &Renderer,
325    ) -> mouse::Interaction {
326        self.inner.mouse_interaction(
327            &tree.children[0],
328            layout.children().next().unwrap(),
329            cursor,
330            viewport,
331            renderer,
332        )
333    }
334
335    fn draw(
336        &self,
337        tree: &Tree,
338        renderer: &mut Renderer,
339        theme: &Theme,
340        defaults: &renderer::Style,
341        layout: Layout<'_>,
342        cursor: mouse::Cursor,
343        viewport: &Rectangle,
344    ) {
345        register_mobile_text_region(self.is_enabled, layout.bounds(), viewport);
346
347        self.inner.draw(
348            &tree.children[0],
349            renderer,
350            theme,
351            defaults,
352            layout.children().next().unwrap(),
353            cursor,
354            viewport,
355        );
356    }
357
358    fn overlay<'b>(
359        &'b mut self,
360        tree: &'b mut Tree,
361        layout: Layout<'b>,
362        renderer: &Renderer,
363        viewport: &Rectangle,
364        translation: Vector,
365    ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
366        self.inner.overlay(
367            &mut tree.children[0],
368            layout.children().next().unwrap(),
369            renderer,
370            viewport,
371            translation,
372        )
373    }
374}
375
376impl<'a, Message, Renderer> From<TextEditor<'a, Message, Renderer>>
377    for Element<'a, Message, Theme, Renderer>
378where
379    Message: 'a,
380    Renderer: core_text::Renderer + 'a,
381{
382    fn from(text_editor: TextEditor<'a, Message, Renderer>) -> Self {
383        Element::new(text_editor)
384    }
385}
386
387pub fn outlined<'a, Message, Renderer>(
388    content: &'a Content<Renderer>,
389) -> TextEditor<'a, Message, Renderer>
390where
391    Renderer: core_text::Renderer + 'a,
392    Message: 'a,
393{
394    TextEditor::new(content)
395}
396
397pub fn outlined_area<'a, Message, Renderer>(
398    content: &'a Content<Renderer>,
399) -> TextEditor<'a, Message, Renderer>
400where
401    Renderer: core_text::Renderer + 'a,
402    Message: 'a,
403{
404    outlined(content).height(Length::Fixed(OUTLINED_AREA_HEIGHT))
405}