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}