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    web_input_position: WebInputPositionState,
141}
142
143impl<Message, Renderer> Widget<Message, Theme, Renderer> for TextEditor<'_, Message, Renderer>
144where
145    Renderer: core_text::Renderer,
146{
147    fn tag(&self) -> tree::Tag {
148        tree::Tag::of::<TextEditorState>()
149    }
150
151    fn state(&self) -> tree::State {
152        tree::State::new(TextEditorState::default())
153    }
154
155    fn children(&self) -> Vec<Tree> {
156        let inner: &dyn Widget<Message, Theme, Renderer> = &self.inner;
157
158        vec![Tree::new(inner)]
159    }
160
161    fn diff(&self, tree: &mut Tree) {
162        if tree.children.is_empty() {
163            tree.children = self.children();
164        } else {
165            self.inner.diff(&mut tree.children[0]);
166            tree.children.truncate(1);
167        }
168    }
169
170    fn size(&self) -> Size<Length> {
171        Widget::<Message, Theme, Renderer>::size(&self.inner)
172    }
173
174    fn size_hint(&self) -> Size<Length> {
175        Widget::<Message, Theme, Renderer>::size_hint(&self.inner)
176    }
177
178    fn layout(
179        &mut self,
180        tree: &mut Tree,
181        renderer: &Renderer,
182        limits: &layout::Limits,
183    ) -> layout::Node {
184        let inner = self.inner.layout(&mut tree.children[0], renderer, limits);
185
186        layout::Node::with_children(inner.size(), vec![inner])
187    }
188
189    fn operate(
190        &mut self,
191        tree: &mut Tree,
192        layout: Layout<'_>,
193        renderer: &Renderer,
194        operation: &mut dyn core_widget::Operation,
195    ) {
196        self.inner.operate(
197            &mut tree.children[0],
198            layout.children().next().unwrap(),
199            renderer,
200            operation,
201        );
202    }
203
204    fn update(
205        &mut self,
206        tree: &mut Tree,
207        event: &Event,
208        layout: Layout<'_>,
209        cursor: mouse::Cursor,
210        renderer: &Renderer,
211        clipboard: &mut dyn Clipboard,
212        shell: &mut Shell<'_, Message>,
213        viewport: &Rectangle,
214    ) {
215        let bounds = layout.bounds();
216        let visible_bounds = bounds.intersection(viewport);
217        let translated_event = touch_as_mouse_event(event);
218        let inner_event = translated_event.as_ref().unwrap_or(event);
219        let activation = {
220            let state = tree.state.downcast_mut::<TextEditorState>();
221            text_input_activation(
222                self.is_enabled,
223                &mut state.touch_activation,
224                &mut state.web_input_position,
225                event,
226                visible_bounds,
227                cursor,
228            )
229        };
230
231        let started_focused = {
232            let state = tree.children[0]
233                .state
234                .downcast_ref::<iced_text_editor::State<core_text::highlighter::PlainText>>();
235
236            state.is_focused()
237        };
238
239        match activation.inner_touch_handling {
240            TextFieldInnerTouchHandling::Forward => {
241                self.inner.update(
242                    &mut tree.children[0],
243                    inner_event,
244                    layout.children().next().unwrap(),
245                    activation.cursor,
246                    renderer,
247                    clipboard,
248                    shell,
249                    viewport,
250                );
251            }
252            TextFieldInnerTouchHandling::Suppress => {}
253            TextFieldInnerTouchHandling::ConfirmedTap => {
254                let press = Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left));
255                self.inner.update(
256                    &mut tree.children[0],
257                    &press,
258                    layout.children().next().unwrap(),
259                    activation.cursor,
260                    renderer,
261                    clipboard,
262                    shell,
263                    viewport,
264                );
265
266                let release = Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left));
267                self.inner.update(
268                    &mut tree.children[0],
269                    &release,
270                    layout.children().next().unwrap(),
271                    activation.cursor,
272                    renderer,
273                    clipboard,
274                    shell,
275                    viewport,
276                );
277            }
278        }
279
280        if activation.request_mobile_keyboard {
281            shell.request_redraw();
282        }
283
284        normalize_windows_ime_request(shell.input_method_mut(), bounds);
285
286        let is_focused = {
287            let state = tree.children[0]
288                .state
289                .downcast_ref::<iced_text_editor::State<core_text::highlighter::PlainText>>();
290
291            state.is_focused()
292        };
293
294        if is_focused && text_caret_refresh_event(event) {
295            let state = tree.children[0]
296                .state
297                .downcast_mut::<iced_text_editor::State<core_text::highlighter::PlainText>>();
298
299            core_widget::operation::Focusable::focus(state);
300            shell.request_redraw();
301        }
302
303        let input_anchor = web_input_anchor(
304            shell.input_method(),
305            visible_bounds,
306            activation,
307            started_focused,
308            is_focused,
309        );
310
311        sync_mobile_keyboard(
312            started_focused,
313            is_focused,
314            activation.request_mobile_keyboard,
315            input_anchor,
316        );
317    }
318
319    fn mouse_interaction(
320        &self,
321        tree: &Tree,
322        layout: Layout<'_>,
323        cursor: mouse::Cursor,
324        viewport: &Rectangle,
325        renderer: &Renderer,
326    ) -> mouse::Interaction {
327        self.inner.mouse_interaction(
328            &tree.children[0],
329            layout.children().next().unwrap(),
330            cursor,
331            viewport,
332            renderer,
333        )
334    }
335
336    fn draw(
337        &self,
338        tree: &Tree,
339        renderer: &mut Renderer,
340        theme: &Theme,
341        defaults: &renderer::Style,
342        layout: Layout<'_>,
343        cursor: mouse::Cursor,
344        viewport: &Rectangle,
345    ) {
346        register_mobile_text_region(self.is_enabled, layout.bounds(), viewport);
347
348        self.inner.draw(
349            &tree.children[0],
350            renderer,
351            theme,
352            defaults,
353            layout.children().next().unwrap(),
354            cursor,
355            viewport,
356        );
357    }
358
359    fn overlay<'b>(
360        &'b mut self,
361        tree: &'b mut Tree,
362        layout: Layout<'b>,
363        renderer: &Renderer,
364        viewport: &Rectangle,
365        translation: Vector,
366    ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
367        self.inner.overlay(
368            &mut tree.children[0],
369            layout.children().next().unwrap(),
370            renderer,
371            viewport,
372            translation,
373        )
374    }
375}
376
377impl<'a, Message, Renderer> From<TextEditor<'a, Message, Renderer>>
378    for Element<'a, Message, Theme, Renderer>
379where
380    Message: 'a,
381    Renderer: core_text::Renderer + 'a,
382{
383    fn from(text_editor: TextEditor<'a, Message, Renderer>) -> Self {
384        Element::new(text_editor)
385    }
386}
387
388pub fn outlined<'a, Message, Renderer>(
389    content: &'a Content<Renderer>,
390) -> TextEditor<'a, Message, Renderer>
391where
392    Renderer: core_text::Renderer + 'a,
393    Message: 'a,
394{
395    TextEditor::new(content)
396}
397
398pub fn outlined_area<'a, Message, Renderer>(
399    content: &'a Content<Renderer>,
400) -> TextEditor<'a, Message, Renderer>
401where
402    Renderer: core_text::Renderer + 'a,
403    Message: 'a,
404{
405    outlined(content).height(Length::Fixed(OUTLINED_AREA_HEIGHT))
406}