1use iced_core::layout::{Layout, Limits, Node};
6use iced_core::mouse::{self, Cursor};
7use iced_core::widget::{
8 Operation, Tree, Widget,
9 tree::{State, Tag},
10};
11use iced_core::{Clipboard, Shell, widget};
12use iced_core::{Element, Length, Padding, Pixels, Rectangle};
13use iced_core::{Event, Size};
14use iced_widget::text_input::{self, TextInput};
15
16use std::{fmt::Display, str::FromStr};
17
18const DEFAULT_PADDING: Padding = Padding::new(5.0);
20
21pub struct TypedInput<'a, T, Message, Theme = iced_widget::Theme, Renderer = iced_widget::Renderer>
40where
41 Renderer: iced_core::text::Renderer,
42 Theme: text_input::Catalog,
43{
44 value: T,
46 text_input: text_input::TextInput<'a, InternalMessage, Theme, Renderer>,
48 text: String,
49 on_change: Option<Box<dyn 'a + Fn(T) -> Message>>,
51 #[allow(clippy::type_complexity)]
53 on_submit: Option<Box<dyn 'a + Fn(Result<T, String>) -> Message>>,
54 on_paste: Option<Box<dyn 'a + Fn(T) -> Message>>,
56}
57
58#[derive(Debug, Clone, PartialEq)]
59#[allow(clippy::enum_variant_names)]
60enum InternalMessage {
61 OnChange(String),
62 OnSubmit,
63 OnPaste(String),
64}
65
66impl<'a, T, Message, Theme, Renderer> TypedInput<'a, T, Message, Theme, Renderer>
67where
68 T: Display + FromStr,
69 Message: Clone,
70 Renderer: iced_core::text::Renderer,
71 Theme: text_input::Catalog,
72{
73 #[must_use]
79 pub fn new(placeholder: &str, value: &T) -> Self
80 where
81 T: 'a + Clone,
82 {
83 let padding = DEFAULT_PADDING;
84
85 Self {
86 value: value.clone(),
87 text_input: text_input::TextInput::new(placeholder, format!("{value}").as_str())
88 .padding(padding)
89 .width(Length::Fixed(127.0))
90 .class(<Theme as text_input::Catalog>::default()),
91 text: value.to_string(),
92 on_change: None,
93 on_submit: None,
94 on_paste: None,
95 }
96 }
97
98 #[must_use]
100 pub fn id(mut self, id: impl Into<widget::Id>) -> Self {
101 self.text_input = self.text_input.id(id);
102 self
103 }
104
105 #[must_use]
107 pub fn secure(mut self, is_secure: bool) -> Self {
108 self.text_input = self.text_input.secure(is_secure);
109 self
110 }
111
112 #[must_use]
116 pub fn on_input<F>(mut self, callback: F) -> Self
117 where
118 F: 'a + Fn(T) -> Message,
119 {
120 self.text_input = self.text_input.on_input(InternalMessage::OnChange);
121 self.on_change = Some(Box::new(callback));
122 self
123 }
124
125 #[must_use]
129 pub fn on_input_maybe<F>(mut self, callback: Option<F>) -> Self
130 where
131 F: 'a + Fn(T) -> Message,
132 {
133 if let Some(callback) = callback {
134 self.text_input = self.text_input.on_input(InternalMessage::OnChange);
135 self.on_change = Some(Box::new(callback));
136 } else {
137 if self.on_submit.is_none() {
138 #[allow(unused_assignments)]
140 let mut f = Some(InternalMessage::OnChange);
141 f = None;
142 self.text_input = self.text_input.on_input_maybe(f);
143 }
144 self.on_change = None;
145 }
146 self
147 }
148
149 #[must_use]
154 pub fn on_submit<F>(mut self, callback: F) -> Self
155 where
156 F: 'a + Fn(Result<T, String>) -> Message,
157 {
158 self.text_input = self
159 .text_input
160 .on_input(InternalMessage::OnChange)
161 .on_submit(InternalMessage::OnSubmit);
162 self.on_submit = Some(Box::new(callback));
163 self
164 }
165
166 #[must_use]
171 pub fn on_submit_maybe<F>(mut self, callback: Option<F>) -> Self
172 where
173 F: 'a + Fn(Result<T, String>) -> Message,
174 {
175 if let Some(callback) = callback {
176 self.text_input = self
177 .text_input
178 .on_input(InternalMessage::OnChange)
179 .on_submit(InternalMessage::OnSubmit);
180 self.on_submit = Some(Box::new(callback));
181 } else {
182 if self.on_change.is_none() {
183 #[allow(unused_assignments)]
185 let mut f = Some(InternalMessage::OnChange);
186 f = None;
187 self.text_input = self.text_input.on_input_maybe(f);
188 }
189 #[allow(unused_assignments)]
191 let mut f = Some(InternalMessage::OnSubmit);
192 f = None;
193 self.text_input = self.text_input.on_submit_maybe(f);
194 self.on_change = None;
195 }
196 self
197 }
198
199 #[must_use]
201 pub fn on_paste<F>(mut self, callback: F) -> Self
202 where
203 F: 'a + Fn(T) -> Message,
204 {
205 self.text_input = self.text_input.on_paste(InternalMessage::OnPaste);
206 self.on_paste = Some(Box::new(callback));
207 self
208 }
209
210 #[must_use]
212 pub fn on_paste_maybe<F>(mut self, callback: Option<F>) -> Self
213 where
214 F: 'a + Fn(T) -> Message,
215 {
216 if let Some(callback) = callback {
217 self.text_input = self.text_input.on_paste(InternalMessage::OnPaste);
218 self.on_paste = Some(Box::new(callback));
219 } else {
220 #[allow(unused_assignments)]
222 let mut f = Some(InternalMessage::OnPaste);
223 f = None;
224 self.text_input = self.text_input.on_paste_maybe(f);
225 self.on_paste = None;
226 }
227 self
228 }
229
230 #[must_use]
232 pub fn font(mut self, font: Renderer::Font) -> Self {
233 self.text_input = self.text_input.font(font);
234 self
235 }
236
237 #[must_use]
239 pub fn icon(mut self, icon: iced_widget::text_input::Icon<Renderer::Font>) -> Self {
240 self.text_input = self.text_input.icon(icon);
241 self
242 }
243
244 #[must_use]
246 pub fn width(mut self, width: impl Into<Length>) -> Self {
247 self.text_input = self.text_input.width(width);
248 self
249 }
250
251 #[must_use]
253 pub fn padding(mut self, padding: impl Into<Padding>) -> Self {
254 self.text_input = self.text_input.padding(padding);
255 self
256 }
257
258 #[must_use]
260 pub fn size(mut self, size: impl Into<Pixels>) -> Self {
261 self.text_input = self.text_input.size(size);
262 self
263 }
264
265 #[must_use]
267 pub fn line_height(mut self, line_height: impl Into<iced_widget::text::LineHeight>) -> Self {
268 self.text_input = self.text_input.line_height(line_height);
269 self
270 }
271
272 #[must_use]
274 pub fn align_x(mut self, alignment: impl Into<iced_core::alignment::Horizontal>) -> Self {
275 self.text_input = self.text_input.align_x(alignment);
276 self
277 }
278
279 #[must_use]
281 pub fn style(
282 mut self,
283 style: impl Fn(&Theme, text_input::Status) -> text_input::Style + 'a,
284 ) -> Self
285 where
286 <Theme as text_input::Catalog>::Class<'a>: From<text_input::StyleFn<'a, Theme>>,
287 {
288 self.text_input = self.text_input.style(style);
289 self
290 }
291
292 #[must_use]
294 pub fn class(mut self, class: impl Into<<Theme as text_input::Catalog>::Class<'a>>) -> Self {
295 self.text_input = self.text_input.class(class);
296 self
297 }
298
299 pub fn text(&self) -> &str {
301 &self.text
302 }
303}
304
305impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
306 for TypedInput<'a, T, Message, Theme, Renderer>
307where
308 T: Display + FromStr + Clone + PartialEq,
309 Message: 'a + Clone,
310 Renderer: 'a + iced_core::text::Renderer,
311 Theme: text_input::Catalog,
312{
313 fn tag(&self) -> Tag {
314 <TextInput<_, _, _> as Widget<_, _, _>>::tag(&self.text_input)
315 }
316 fn state(&self) -> State {
317 <TextInput<_, _, _> as Widget<_, _, _>>::state(&self.text_input)
318 }
319
320 fn children(&self) -> Vec<Tree> {
321 <TextInput<_, _, _> as Widget<_, _, _>>::children(&self.text_input)
322 }
323
324 fn diff(&self, state: &mut Tree) {
325 <TextInput<_, _, _> as Widget<_, _, _>>::diff(&self.text_input, state);
326 }
327
328 fn size(&self) -> Size<Length> {
329 <TextInput<_, _, _> as Widget<_, _, _>>::size(&self.text_input)
330 }
331
332 fn layout(&mut self, state: &mut Tree, renderer: &Renderer, limits: &Limits) -> Node {
333 <TextInput<_, _, _> as Widget<_, _, _>>::layout(
334 &mut self.text_input,
335 state,
336 renderer,
337 limits,
338 )
339 }
340
341 fn draw(
342 &self,
343 state: &Tree,
344 renderer: &mut Renderer,
345 theme: &Theme,
346 style: &iced_core::renderer::Style,
347 layout: Layout<'_>,
348 cursor: Cursor,
349 viewport: &Rectangle,
350 ) {
351 <TextInput<_, _, _> as Widget<_, _, _>>::draw(
352 &self.text_input,
353 state,
354 renderer,
355 theme,
356 style,
357 layout,
358 cursor,
359 viewport,
360 );
361 }
362
363 fn mouse_interaction(
364 &self,
365 state: &Tree,
366 layout: Layout<'_>,
367 cursor: Cursor,
368 viewport: &Rectangle,
369 renderer: &Renderer,
370 ) -> mouse::Interaction {
371 <TextInput<_, _, _> as Widget<_, _, _>>::mouse_interaction(
372 &self.text_input,
373 state,
374 layout,
375 cursor,
376 viewport,
377 renderer,
378 )
379 }
380
381 fn operate(
382 &mut self,
383 state: &mut Tree,
384 layout: Layout<'_>,
385 renderer: &Renderer,
386 operation: &mut dyn Operation<()>,
387 ) {
388 <TextInput<_, _, _> as Widget<_, _, _>>::operate(
389 &mut self.text_input,
390 state,
391 layout,
392 renderer,
393 operation,
394 );
395 }
396
397 #[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
398 fn update(
399 &mut self,
400 state: &mut Tree,
401 event: &Event,
402 layout: Layout<'_>,
403 cursor: Cursor,
404 renderer: &Renderer,
405 clipboard: &mut dyn Clipboard,
406 shell: &mut Shell<Message>,
407 viewport: &Rectangle,
408 ) {
409 let mut messages = Vec::new();
410 let mut sub_shell = Shell::new(&mut messages);
411 self.text_input.update(
412 state,
413 event,
414 layout,
415 cursor,
416 renderer,
417 clipboard,
418 &mut sub_shell,
419 viewport,
420 );
421
422 shell.request_redraw_at(sub_shell.redraw_request());
423
424 if sub_shell.is_layout_invalid() {
425 shell.invalidate_layout();
426 }
427 if sub_shell.are_widgets_invalid() {
428 shell.invalidate_widgets();
429 }
430
431 for message in messages {
432 match message {
433 InternalMessage::OnChange(value) => {
434 self.text = value;
435
436 if let Ok(value) = T::from_str(&self.text)
437 && self.value != value
438 {
439 self.value = value.clone();
440 if let Some(on_change) = &self.on_change {
441 shell.publish(on_change(value));
442 }
443 }
444
445 shell.invalidate_layout();
446 }
447 InternalMessage::OnSubmit => {
448 if let Some(on_submit) = &self.on_submit {
449 let value = match T::from_str(&self.text) {
450 Ok(v) => Ok(v),
451 Err(_) => Err(self.text.clone()),
452 };
453 shell.publish(on_submit(value));
454 }
455
456 shell.invalidate_layout();
457 }
458 InternalMessage::OnPaste(value) => {
459 self.text = value;
460
461 if let Ok(value) = T::from_str(&self.text)
462 && self.value != value
463 {
464 self.value = value.clone();
465 if let Some(on_paste) = &self.on_paste {
466 shell.publish(on_paste(value));
467 }
468 }
469
470 shell.invalidate_layout();
471 }
472 }
473 }
474 }
475}
476
477impl<'a, T, Message, Theme, Renderer> From<TypedInput<'a, T, Message, Theme, Renderer>>
478 for Element<'a, Message, Theme, Renderer>
479where
480 T: 'a + Display + FromStr + Clone + PartialEq,
481 Message: 'a + Clone,
482 Renderer: 'a + iced_core::text::Renderer,
483 Theme: 'a + text_input::Catalog,
484{
485 fn from(typed_input: TypedInput<'a, T, Message, Theme, Renderer>) -> Self {
486 Element::new(typed_input)
487 }
488}