1use crate::core::keyboard;
34use crate::core::layout;
35use crate::core::mouse;
36use crate::core::renderer;
37use crate::core::shell;
38use crate::core::text;
39use crate::core::text::editor;
40use crate::core::text::input;
41use crate::core::widget;
42use crate::core::widget::operation::{self, Focusable, Operation};
43use crate::core::widget::tree::{self, Tree};
44use crate::core::window;
45use crate::core::{
46 Background, Border, Color, Element, Event, Layout, Length, Padding, Pixels, Rectangle, Shell,
47 Size, Theme, Widget,
48};
49
50pub struct TextInput<'a, Message, Theme = crate::Theme, Renderer = crate::Renderer>
83where
84 Theme: Catalog,
85 Renderer: text::Renderer,
86{
87 id: Option<widget::Id>,
88 placeholder: text::Fragment<'a>,
89 value: text::Fragment<'a>,
90 is_secure: bool,
91 font: Option<Renderer::Font>,
92 width: Length,
93 height: Length,
94 padding: Padding,
95 size: Option<Pixels>,
96 line_height: text::LineHeight,
97 alignment: text::Alignment,
98 multiline: Option<text::Wrapping>,
99 on_input: Option<Box<dyn Fn(String) -> Message + 'a>>,
100 on_paste: Option<Box<dyn Fn(String) -> Message + 'a>>,
101 on_submit: Option<Message>,
102 class: Theme::Class<'a>,
103 last_status: Option<Status>,
104}
105
106pub const DEFAULT_PADDING: Padding = Padding::new(5.0);
108
109impl<'a, Message, Theme, Renderer> TextInput<'a, Message, Theme, Renderer>
110where
111 Message: Clone,
112 Theme: Catalog,
113 Renderer: text::Renderer,
114{
115 pub fn new(
118 placeholder: impl text::IntoFragment<'a>,
119 value: impl text::IntoFragment<'a>,
120 ) -> Self {
121 TextInput {
122 id: None,
123 placeholder: placeholder.into_fragment(),
124 value: value.into_fragment(),
125 is_secure: false,
126 font: None,
127 width: Length::Fill,
128 height: Length::Fit,
129 padding: DEFAULT_PADDING,
130 size: None,
131 line_height: text::LineHeight::default(),
132 alignment: text::Alignment::Default,
133 multiline: None,
134 on_input: None,
135 on_paste: None,
136 on_submit: None,
137 class: Theme::default(),
138 last_status: None,
139 }
140 }
141
142 pub fn id(mut self, id: impl Into<widget::Id>) -> Self {
144 self.id = Some(id.into());
145 self
146 }
147
148 pub fn secure(mut self, is_secure: bool) -> Self {
150 self.is_secure = is_secure;
151 self
152 }
153
154 pub fn on_input(mut self, on_input: impl Fn(String) -> Message + 'a) -> Self {
159 self.on_input = Some(Box::new(on_input));
160 self
161 }
162
163 pub fn on_input_maybe(mut self, on_input: Option<impl Fn(String) -> Message + 'a>) -> Self {
168 self.on_input = on_input.map(|f| Box::new(f) as _);
169 self
170 }
171
172 pub fn on_submit(mut self, message: Message) -> Self {
175 self.on_submit = Some(message);
176 self
177 }
178
179 pub fn on_submit_maybe(mut self, on_submit: Option<Message>) -> Self {
182 self.on_submit = on_submit;
183 self
184 }
185
186 pub fn on_paste(mut self, on_paste: impl Fn(String) -> Message + 'a) -> Self {
189 self.on_paste = Some(Box::new(on_paste));
190 self
191 }
192
193 pub fn on_paste_maybe(mut self, on_paste: Option<impl Fn(String) -> Message + 'a>) -> Self {
196 self.on_paste = on_paste.map(|f| Box::new(f) as _);
197 self
198 }
199
200 pub fn font(mut self, font: Renderer::Font) -> Self {
204 self.font = Some(font);
205 self
206 }
207
208 pub fn width(mut self, width: impl Into<Length>) -> Self {
210 self.width = width.into();
211 self
212 }
213
214 pub fn padding<P: Into<Padding>>(mut self, padding: P) -> Self {
216 self.padding = padding.into();
217 self
218 }
219
220 pub fn size(mut self, size: impl Into<Pixels>) -> Self {
222 self.size = Some(size.into());
223 self
224 }
225
226 pub fn line_height(mut self, line_height: impl Into<text::LineHeight>) -> Self {
228 self.line_height = line_height.into();
229 self
230 }
231
232 pub fn align_x(mut self, alignment: impl Into<text::Alignment>) -> Self {
234 self.alignment = alignment.into();
235 self
236 }
237
238 pub fn multiline(mut self, wrapping: Option<text::Wrapping>) -> Self {
242 self.multiline = wrapping;
243 self
244 }
245
246 #[must_use]
248 pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
249 where
250 Theme::Class<'a>: From<StyleFn<'a, Theme>>,
251 {
252 self.class = (Box::new(style) as StyleFn<'a, Theme>).into();
253 self
254 }
255
256 #[must_use]
258 pub fn class(mut self, class: impl Into<Theme::Class<'a>>) -> Self {
259 self.class = class.into();
260 self
261 }
262}
263
264impl<Message, Theme, Renderer> Widget<Message, Theme, Renderer>
265 for TextInput<'_, Message, Theme, Renderer>
266where
267 Message: Clone,
268 Theme: Catalog,
269 Renderer: text::Renderer + 'static,
270{
271 fn tag(&self) -> tree::Tag {
272 tree::Tag::of::<State<Renderer>>()
273 }
274
275 fn state(&self) -> tree::State {
276 tree::State::new(State::<Renderer>::new())
277 }
278
279 fn size(&self) -> Size<Length> {
280 Size {
281 width: self.width,
282 height: Length::Shrink,
283 }
284 }
285
286 fn layout(
287 &mut self,
288 tree: &mut Tree,
289 renderer: &Renderer,
290 limits: &layout::Limits,
291 ) -> layout::Node {
292 let state = tree.state.downcast_mut::<State<Renderer>>();
293
294 if state.value != self.value
295 && state
296 .transaction
297 .as_ref()
298 .is_none_or(shell::Tracking::is_processed)
299 {
300 state.input.overwrite(self.value.as_ref());
301 state.value = self.value.clone().into_owned();
302 }
303
304 state.input.layout(
305 renderer,
306 limits,
307 input::Layout {
308 width: self.width,
309 height: self.height,
310 padding: self.padding,
311 placeholder: self.placeholder.as_ref(),
312 font: self.font,
313 size: self.size,
314 line_height: self.line_height,
315 alignment: self.alignment,
316 multiline: self.multiline,
317 },
318 )
319 }
320
321 fn operate(
322 &mut self,
323 tree: &mut Tree,
324 layout: Layout<'_>,
325 _renderer: &Renderer,
326 operation: &mut dyn Operation,
327 ) {
328 let state = tree.state.downcast_mut::<State<Renderer>>();
329
330 operation.text_input(self.id.as_ref(), layout.bounds(), state);
331 operation.focusable(self.id.as_ref(), layout.bounds(), state);
332 }
333
334 fn update(
335 &mut self,
336 tree: &mut Tree,
337 event: &Event,
338 layout: Layout<'_>,
339 cursor: mouse::Cursor,
340 _renderer: &Renderer,
341 shell: &mut Shell<'_, Message>,
342 _viewport: &Rectangle,
343 ) {
344 let state = state::<Renderer>(tree);
345 let is_disabled = self.on_input.is_none();
346
347 if let Some(on_input) = &self.on_input {
348 let edit = state
349 .input
350 .update(event, layout.bounds(), cursor, shell, |key_press| {
351 if let Some(on_submit) = &self.on_submit
352 && key_press.modified_key
353 == keyboard::Key::Named(keyboard::key::Named::Enter)
354 {
355 return Some(editor::Binding::Custom(on_submit.clone()));
356 }
357
358 editor::Binding::from_key_press(key_press)
359 });
360
361 if let Some(edit) = edit {
362 let on_input = if let Some(on_paste) = &self.on_paste
363 && edit.is_paste
364 {
365 on_paste
366 } else {
367 on_input
368 };
369
370 state.value = state.input.value();
371 state.transaction = Some(shell.publish_and_track(on_input(state.value.clone())));
372 }
373 }
374
375 let status = if is_disabled {
376 Status::Disabled
377 } else if state.input.is_focused() {
378 Status::Focused {
379 is_hovered: cursor.is_over(layout.bounds()),
380 }
381 } else if cursor.is_over(layout.bounds()) {
382 Status::Hovered
383 } else {
384 Status::Active
385 };
386
387 if let Event::Window(window::Event::RedrawRequested(_now)) = event {
388 self.last_status = Some(status);
389
390 shell.request_input_method(
391 &state
392 .input
393 .input_method(layout.bounds().shrink(self.padding).position()),
394 );
395 } else if self
396 .last_status
397 .is_some_and(|last_status| status != last_status)
398 {
399 shell.request_redraw();
400 }
401 }
402
403 fn draw(
404 &self,
405 tree: &Tree,
406 renderer: &mut Renderer,
407 theme: &Theme,
408 _style: &renderer::Style,
409 layout: Layout<'_>,
410 _cursor: mouse::Cursor,
411 viewport: &Rectangle,
412 ) {
413 let state = tree.state.downcast_ref::<State<Renderer>>();
414 let style = theme.style(&self.class, self.last_status.unwrap_or(Status::Disabled));
415 let bounds = layout.bounds();
416
417 renderer.fill_quad(
418 renderer::Quad {
419 bounds,
420 border: style.border,
421 ..renderer::Quad::default()
422 },
423 style.background,
424 );
425
426 state.input.draw(
427 renderer,
428 bounds,
429 *viewport,
430 input::Style {
431 value: style.value,
432 selection: style.selection,
433 placeholder: style.placeholder,
434 },
435 );
436 }
437
438 fn mouse_interaction(
439 &self,
440 _tree: &Tree,
441 layout: Layout<'_>,
442 cursor: mouse::Cursor,
443 _viewport: &Rectangle,
444 _renderer: &Renderer,
445 ) -> mouse::Interaction {
446 if cursor.is_over(layout.bounds()) {
447 if self.on_input.is_none() {
448 mouse::Interaction::Idle
449 } else {
450 mouse::Interaction::Text
451 }
452 } else {
453 mouse::Interaction::default()
454 }
455 }
456}
457
458impl<'a, Message, Theme, Renderer> From<TextInput<'a, Message, Theme, Renderer>>
459 for Element<'a, Message, Theme, Renderer>
460where
461 Message: Clone + 'a,
462 Theme: Catalog + 'a,
463 Renderer: text::Renderer + 'static,
464{
465 fn from(
466 text_input: TextInput<'a, Message, Theme, Renderer>,
467 ) -> Element<'a, Message, Theme, Renderer> {
468 Element::new(text_input)
469 }
470}
471
472struct State<R: text::Renderer> {
474 input: text::Input<R>,
475 value: String,
476 transaction: Option<shell::Tracking>,
477}
478
479fn state<Renderer: text::Renderer + 'static>(tree: &mut Tree) -> &mut State<Renderer> {
480 tree.state.downcast_mut::<State<Renderer>>()
481}
482
483impl<R: text::Renderer> State<R> {
484 fn new() -> Self {
486 Self {
487 input: text::Input::new(),
488 value: String::new(),
489 transaction: None,
490 }
491 }
492}
493
494impl<R: text::Renderer> operation::Focusable for State<R> {
495 fn is_focused(&self) -> bool {
496 self.input.is_focused()
497 }
498
499 fn focus(&mut self) {
500 self.input.focus();
501 }
502
503 fn unfocus(&mut self) {
504 self.input.unfocus();
505 }
506}
507
508impl<R: text::Renderer> operation::TextInput for State<R> {
509 fn text(&self) -> text::Fragment<'_> {
510 if self.input.is_empty() {
511 text::Fragment::Borrowed(self.input.placeholder())
512 } else {
513 text::Fragment::Owned(self.input.value())
514 }
515 }
516
517 fn move_cursor_to_front(&mut self) {
518 self.input.move_cursor_to_front();
519 }
520
521 fn move_cursor_to_end(&mut self) {
522 self.input.move_cursor_to_end();
523 }
524
525 fn move_cursor_to(&mut self, position: text::Position) {
526 self.input.move_cursor_to(position);
527 }
528
529 fn select_all(&mut self) {
530 self.input.select_all();
531 }
532
533 fn select_range(&mut self, start: text::Position, end: text::Position) {
534 self.input.select_range(start, end);
535 }
536}
537
538#[derive(Debug, Clone, Copy, PartialEq, Eq)]
540pub enum Status {
541 Active,
543 Hovered,
545 Focused {
547 is_hovered: bool,
549 },
550 Disabled,
552}
553
554#[derive(Debug, Clone, Copy, PartialEq)]
556pub struct Style {
557 pub background: Background,
559 pub border: Border,
561 pub placeholder: Color,
563 pub value: Color,
565 pub selection: Color,
567}
568
569pub trait Catalog: Sized {
571 type Class<'a>;
573
574 fn default<'a>() -> Self::Class<'a>;
576
577 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style;
579}
580
581pub type StyleFn<'a, Theme> = Box<dyn Fn(&Theme, Status) -> Style + 'a>;
585
586impl Catalog for Theme {
587 type Class<'a> = StyleFn<'a, Self>;
588
589 fn default<'a>() -> Self::Class<'a> {
590 Box::new(default)
591 }
592
593 fn style(&self, class: &Self::Class<'_>, status: Status) -> Style {
594 class(self, status)
595 }
596}
597
598pub fn default(theme: &Theme, status: Status) -> Style {
600 let palette = theme.palette();
601
602 let active = Style {
603 background: Background::Color(palette.background.base.color),
604 border: Border {
605 radius: 2.0.into(),
606 width: 1.0,
607 color: palette.background.strong.color,
608 },
609 placeholder: palette.secondary.base.color,
610 value: palette.background.base.text,
611 selection: palette.primary.weak.color,
612 };
613
614 match status {
615 Status::Active => active,
616 Status::Hovered => Style {
617 border: Border {
618 color: palette.background.base.text,
619 ..active.border
620 },
621 ..active
622 },
623 Status::Focused { .. } => Style {
624 border: Border {
625 color: palette.primary.strong.color,
626 ..active.border
627 },
628 ..active
629 },
630 Status::Disabled => Style {
631 background: Background::Color(palette.background.weak.color),
632 value: active.placeholder,
633 placeholder: palette.background.strongest.color,
634 ..active
635 },
636 }
637}