danceinterpreter_rs/ui/widget/
dynamic_text_input.rs

1use iced::advanced::graphics::core::{touch, Element};
2use iced::advanced::text::Wrapping;
3use iced::advanced::widget::{tree, Operation, Tree};
4use iced::advanced::{layout, mouse, overlay, renderer, text, Clipboard, Layout, Shell, Widget};
5use iced::keyboard::key::Named;
6use iced::keyboard::Key;
7use iced::widget::text::{LineHeight, Shaping};
8use iced::widget::{text_input, Text, TextInput};
9use iced::{alignment, keyboard, Color, Event, Length, Pixels, Rectangle, Size, Vector};
10
11#[allow(missing_debug_implementations)]
12pub struct DynamicTextInput<'a, Message, Theme = iced::Theme, Renderer = iced::Renderer>
13where
14    Renderer: 'a + text::Renderer,
15    Message: 'a + Clone,
16    Theme: 'a + text_input::Catalog + iced::widget::text::Catalog,
17{
18    content_label: Text<'a, Theme, Renderer>,
19    content_input: TextInput<'a, Message, Theme, Renderer>,
20
21    width: Length,
22    interaction: Option<mouse::Interaction>,
23
24    on_enter: Option<Message>,
25    on_submit: Option<Message>,
26}
27
28impl<'a, Message, Theme, Renderer> DynamicTextInput<'a, Message, Theme, Renderer>
29where
30    Renderer: text::Renderer,
31    Message: Clone,
32    Theme: text_input::Catalog + iced::widget::text::Catalog,
33{
34    pub fn interaction(mut self, interaction: mouse::Interaction) -> Self {
35        self.interaction = Some(interaction);
36        self
37    }
38
39    fn get_widget(&self, is_edit_mode: bool) -> &dyn Widget<Message, Theme, Renderer> {
40        if is_edit_mode {
41            &self.content_input
42        } else {
43            &self.content_label
44        }
45    }
46
47    fn get_widget_mut(&mut self, is_edit_mode: bool) -> &mut dyn Widget<Message, Theme, Renderer> {
48        if is_edit_mode {
49            &mut self.content_input
50        } else {
51            &mut self.content_label
52        }
53    }
54}
55
56#[derive(Default)]
57struct State {
58    is_pressed: bool,
59    is_edit_mode: bool,
60
61    previous_click: Option<mouse::Click>,
62}
63
64impl State {
65    fn get_child_index(&self) -> usize {
66        if self.is_edit_mode { 1 } else { 0 }
67    }
68}
69
70fn get_placeholder_color<Theme: text_input::Catalog>(theme: &Theme) -> Color {
71    let class = Theme::default();
72    let style = theme.style(&class, text_input::Status::Active);
73
74    style.placeholder
75}
76
77impl<'a, Message, Theme, Renderer> DynamicTextInput<'a, Message, Theme, Renderer>
78where
79    Renderer: 'a + text::Renderer,
80    Message: 'a + Clone,
81    Theme: 'a + text_input::Catalog + iced::widget::text::Catalog,
82{
83    pub fn new(placeholder: &str, value: &str) -> Self
84    where
85        <Theme as iced::widget::text::Catalog>::Class<'a>:
86        From<iced::widget::text::StyleFn<'a, Theme>>,
87    {
88        let input = TextInput::new(placeholder, value).padding(0);
89
90        let label = if !value.is_empty() {
91            Text::new(value.to_owned())
92                .wrapping(Wrapping::None)
93                .shaping(Shaping::Advanced)
94        } else {
95            Text::new(placeholder.to_owned())
96                .wrapping(Wrapping::None)
97                .shaping(Shaping::Advanced)
98                .style(|theme| iced::widget::text::Style {
99                    color: Some(get_placeholder_color(theme)),
100                })
101        };
102
103        DynamicTextInput {
104            content_input: input,
105            content_label: label,
106
107            width: Length::Fill,
108            interaction: None,
109
110            on_enter: None,
111            on_submit: None,
112        }
113    }
114
115    pub fn width(mut self, width: impl Into<Length>) -> Self {
116        let width = width.into();
117        self.width = width;
118        self.content_input = self.content_input.width(width);
119        self.content_label = self.content_label.width(width);
120
121        self
122    }
123
124    pub fn size(mut self, size: impl Into<Pixels>) -> Self {
125        let size = size.into();
126        self.content_input = self.content_input.size(size);
127        self.content_label = self.content_label.size(size);
128
129        self
130    }
131
132    pub fn line_height(mut self, line_height: impl Into<LineHeight>) -> Self {
133        let line_height = line_height.into();
134        self.content_input = self.content_input.line_height(line_height);
135        self.content_label = self.content_label.line_height(line_height);
136
137        self
138    }
139
140    pub fn align_x(mut self, alignment: impl Into<alignment::Horizontal>) -> Self {
141        let alignment = alignment.into();
142        self.content_input = self.content_input.align_x(alignment);
143        self.content_label = self.content_label.align_x(alignment);
144
145        self
146    }
147
148    pub fn on_change(mut self, on_change: impl Fn(String) -> Message + 'a) -> Self {
149        self.content_input = self.content_input.on_input(on_change);
150        self
151    }
152
153    pub fn on_enter(mut self, on_enter: Message) -> Self {
154        self.on_enter = Some(on_enter);
155        self
156    }
157
158    pub fn on_submit(mut self, on_submit: Message) -> Self {
159        self.on_submit = Some(on_submit);
160        self
161    }
162}
163
164impl<'a, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
165for DynamicTextInput<'a, Message, Theme, Renderer>
166where
167    Renderer: text::Renderer,
168    Message: Clone,
169    Theme: text_input::Catalog + iced::widget::text::Catalog,
170{
171    fn size(&self) -> Size<Length> {
172        Size::new(self.width, Length::Shrink)
173    }
174
175    fn layout(
176        &mut self,
177        tree: &mut Tree,
178        renderer: &Renderer,
179        limits: &layout::Limits,
180    ) -> layout::Node {
181        let state: &State = tree.state.downcast_ref();
182
183        self.get_widget_mut(state.is_edit_mode).layout(
184            &mut tree.children[state.get_child_index()],
185            renderer,
186            limits,
187        )
188    }
189
190    fn draw(
191        &self,
192        tree: &Tree,
193        renderer: &mut Renderer,
194        theme: &Theme,
195        renderer_style: &renderer::Style,
196        layout: Layout<'_>,
197        cursor: mouse::Cursor,
198        viewport: &Rectangle,
199    ) {
200        let state: &State = tree.state.downcast_ref();
201
202        self.get_widget(state.is_edit_mode).draw(
203            &tree.children[state.get_child_index()],
204            renderer,
205            theme,
206            renderer_style,
207            layout,
208            cursor,
209            viewport,
210        );
211    }
212
213    fn tag(&self) -> tree::Tag {
214        tree::Tag::of::<State>()
215    }
216
217    fn state(&self) -> tree::State {
218        tree::State::new(State::default())
219    }
220
221    fn children(&self) -> Vec<Tree> {
222        vec![
223            Tree::new(&self.content_label as &dyn Widget<Message, Theme, Renderer>),
224            Tree::new(&self.content_input as &dyn Widget<Message, Theme, Renderer>),
225        ]
226    }
227
228    fn diff(&self, tree: &mut Tree) {
229        tree.diff_children(&[
230            &self.content_label as &dyn Widget<Message, Theme, Renderer>,
231            &self.content_input as &dyn Widget<Message, Theme, Renderer>,
232        ]);
233    }
234
235    fn operate(
236        &mut self,
237        tree: &mut Tree,
238        layout: Layout<'_>,
239        renderer: &Renderer,
240        operation: &mut dyn Operation,
241    ) {
242        let state: &State = tree.state.downcast_ref();
243        self.get_widget_mut(state.is_edit_mode).operate(
244            &mut tree.children[state.get_child_index()],
245            layout,
246            renderer,
247            operation,
248        );
249    }
250
251    fn update(
252        &mut self,
253        tree: &mut Tree,
254        event: &Event,
255        layout: Layout<'_>,
256        cursor: mouse::Cursor,
257        renderer: &Renderer,
258        clipboard: &mut dyn Clipboard,
259        shell: &mut Shell<'_, Message>,
260        viewport: &Rectangle,
261    ) {
262        let state: &mut State = tree.state.downcast_mut();
263        let content = self.get_widget_mut(state.is_edit_mode);
264
265        content.update(
266            &mut tree.children[state.get_child_index()],
267            event,
268            layout,
269            cursor,
270            renderer,
271            clipboard,
272            shell,
273            viewport,
274        );
275
276        if state.is_edit_mode {
277            let input_state: &mut text_input::State<Renderer::Paragraph> =
278                tree.children[state.get_child_index()].state.downcast_mut();
279
280            if input_state.is_focused()
281                && let Event::Keyboard(keyboard::Event::KeyPressed { key, .. }) = event.clone()
282                && key == Key::Named(Named::Enter)
283            {
284                input_state.unfocus();
285                shell.capture_event();
286            }
287
288            if !input_state.is_focused() {
289                state.is_edit_mode = false;
290
291                if let Some(message) = self.on_submit.clone() {
292                    shell.publish(message);
293                }
294
295                shell.invalidate_layout();
296                shell.request_redraw();
297            }
298        }
299
300        handle_enter_event::<Message, Renderer>(
301            tree,
302            event,
303            layout,
304            cursor,
305            shell,
306            self.on_enter.clone(),
307        )
308    }
309
310    fn mouse_interaction(
311        &self,
312        tree: &Tree,
313        layout: Layout<'_>,
314        cursor: mouse::Cursor,
315        viewport: &Rectangle,
316        renderer: &Renderer,
317    ) -> mouse::Interaction {
318        let state: &State = tree.state.downcast_ref();
319
320        let content_interaction = self.get_widget(state.is_edit_mode).mouse_interaction(
321            &tree.children[state.get_child_index()],
322            layout,
323            cursor,
324            viewport,
325            renderer,
326        );
327
328        match (self.interaction, content_interaction) {
329            (Some(interaction), mouse::Interaction::None) if cursor.is_over(layout.bounds()) => {
330                interaction
331            }
332            _ => content_interaction,
333        }
334    }
335
336    fn overlay<'b>(
337        &'b mut self,
338        tree: &'b mut Tree,
339        layout: Layout<'b>,
340        renderer: &Renderer,
341        viewport: &Rectangle,
342        translation: Vector,
343    ) -> Option<overlay::Element<'b, Message, Theme, Renderer>> {
344        let state: &State = tree.state.downcast_ref();
345
346        self.get_widget_mut(state.is_edit_mode).overlay(
347            &mut tree.children[state.get_child_index()],
348            layout,
349            renderer,
350            viewport,
351            translation,
352        )
353    }
354}
355
356impl<'a, Message, Theme, Renderer> From<DynamicTextInput<'a, Message, Theme, Renderer>>
357for Element<'a, Message, Theme, Renderer>
358where
359    Message: 'a + Clone,
360    Theme: 'a,
361    Renderer: 'a + text::Renderer,
362    Theme: text_input::Catalog + iced::widget::text::Catalog,
363{
364    fn from(
365        area: DynamicTextInput<'a, Message, Theme, Renderer>,
366    ) -> Element<'a, Message, Theme, Renderer> {
367        Element::new(area)
368    }
369}
370
371fn handle_enter_event<Message: Clone, Renderer: text::Renderer>(
372    tree: &mut Tree,
373    event: &Event,
374    layout: Layout<'_>,
375    cursor: mouse::Cursor,
376    shell: &mut Shell<'_, Message>,
377    on_enter: Option<Message>,
378) {
379    let state: &mut State = tree.state.downcast_mut();
380
381    match event {
382        Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
383        | Event::Touch(touch::Event::FingerPressed { .. }) => {
384            let bounds = layout.bounds();
385
386            if cursor.is_over(bounds) {
387                let state = tree.state.downcast_mut::<State>();
388
389                state.is_pressed = true;
390
391                shell.capture_event();
392            }
393        }
394        Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
395        | Event::Touch(touch::Event::FingerLifted { .. }) => {
396            if state.is_pressed {
397                state.is_pressed = false;
398
399                let bounds = layout.bounds();
400
401                if cursor.is_over(bounds)
402                    && let Some(cursor_position) = cursor.position()
403                {
404                    let new_click = mouse::Click::new(
405                        cursor_position,
406                        mouse::Button::Left,
407                        state.previous_click,
408                    );
409
410                    state.previous_click = Some(new_click);
411
412                    if matches!(new_click.kind(), mouse::click::Kind::Double) {
413                        enter_edit_mode::<Message, Renderer>(tree, shell, on_enter);
414                    }
415                }
416
417                shell.capture_event();
418            }
419        }
420        Event::Touch(touch::Event::FingerLost { .. }) => {
421            state.is_pressed = false;
422        }
423        _ => {}
424    }
425}
426
427fn enter_edit_mode<Message: Clone, Renderer: text::Renderer>(
428    tree: &mut Tree,
429    shell: &mut Shell<'_, Message>,
430    on_enter: Option<Message>,
431) {
432    let state: &mut State = tree.state.downcast_mut();
433    state.is_edit_mode = true;
434
435    if let Some(message) = on_enter {
436        shell.publish(message);
437    }
438
439    shell.invalidate_layout();
440
441    let input_state: &mut text_input::State<Renderer::Paragraph> =
442        tree.children[state.get_child_index()].state.downcast_mut();
443
444    input_state.focus();
445    input_state.select_all();
446
447    shell.request_redraw();
448}