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