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