1use iced_core::{
5 Alignment, Background, Border, Clipboard, Color, Element, Event, Layout, Length, Padding,
6 Point, Rectangle, Shadow, Shell, Size, Widget,
7 alignment::Vertical,
8 keyboard,
9 layout::{Limits, Node},
10 mouse::{self, Cursor},
11 renderer,
12 widget::{
13 self, Operation, Tree,
14 tree::{State, Tag},
15 },
16};
17use iced_widget::{
18 Column, Container, Row, Text,
19 text::{LineHeight, Wrapping},
20 text_input::{self, Value, cursor},
21};
22use num_traits::{Num, NumAssignOps, bounds::Bounded};
23use std::{
24 fmt::Display,
25 ops::{Bound, RangeBounds},
26 str::FromStr,
27};
28
29use crate::iced_aw_font::advanced_text::{down_open, up_open};
30use crate::style::{self, Status};
31pub use crate::style::{
32 StyleFn,
33 number_input::{self, Catalog, Style},
34};
35use crate::widget::typed_input::TypedInput;
36
37const DEFAULT_PADDING: Padding = Padding::new(5.0);
39
40#[allow(missing_debug_implementations)]
62pub struct NumberInput<'a, T, Message, Theme = iced_widget::Theme, Renderer = iced_widget::Renderer>
63where
64 Renderer: iced_core::text::Renderer<Font = iced_core::Font>,
65 Theme: number_input::ExtendedCatalog,
66{
67 value: T,
69 step: T,
71 min: Bound<T>,
73 max: Bound<T>,
75 padding: iced_core::Padding,
77 size: Option<iced_core::Pixels>,
79 content: TypedInput<'a, T, InternalMessage<T>, Theme, Renderer>,
81 on_change: Option<Box<dyn 'a + Fn(T) -> Message>>,
83 #[allow(clippy::type_complexity)]
85 on_submit: Option<Message>,
86 on_paste: Option<Box<dyn 'a + Fn(T) -> Message>>,
88 class: <Theme as style::number_input::Catalog>::Class<'a>,
90 font: Renderer::Font,
92 ignore_scroll_events: bool,
96 ignore_buttons: bool,
98}
99
100#[derive(Debug, Clone, PartialEq)]
101#[allow(clippy::enum_variant_names)]
102enum InternalMessage<T> {
103 OnChange(T),
104 OnSubmit(Result<T, String>),
105 OnPaste(T),
106}
107
108impl<'a, T, Message, Theme, Renderer> NumberInput<'a, T, Message, Theme, Renderer>
109where
110 T: Num + NumAssignOps + PartialOrd + Display + FromStr + Clone + Bounded + 'a,
111 Message: Clone + 'a,
112 Renderer: iced_core::text::Renderer<Font = iced_core::Font>,
113 Theme: number_input::ExtendedCatalog,
114{
115 pub fn new<F>(value: &T, bounds: impl RangeBounds<T>, on_change: F) -> Self
122 where
123 F: 'static + Fn(T) -> Message + Clone,
124 T: 'static,
125 {
126 let padding = DEFAULT_PADDING;
127
128 Self {
129 value: value.clone(),
130 step: T::one(),
131 min: bounds.start_bound().cloned(),
132 max: bounds.end_bound().cloned(),
133 padding,
134 size: None,
135 content: TypedInput::new("", value)
136 .on_input(InternalMessage::OnChange)
137 .padding(padding)
138 .width(Length::Fixed(127.0))
139 .class(Theme::default_input()),
140 on_change: Some(Box::new(on_change)),
141 on_submit: None,
142 on_paste: None,
143 class: <Theme as style::number_input::Catalog>::default(),
144 font: Renderer::Font::default(),
145 ignore_scroll_events: false,
147 ignore_buttons: false,
148 }
149 }
150
151 #[must_use]
153 pub fn id(mut self, id: impl Into<widget::Id>) -> Self {
154 self.content = self.content.id(id.into());
155 self
156 }
157
158 #[must_use]
162 pub fn on_input<F>(mut self, callback: F) -> Self
163 where
164 F: 'a + Fn(T) -> Message,
165 {
166 self.content = self.content.on_input(InternalMessage::OnChange);
167 self.on_change = Some(Box::new(callback));
168 self
169 }
170
171 #[must_use]
175 pub fn on_input_maybe<F>(mut self, callback: Option<F>) -> Self
176 where
177 F: 'a + Fn(T) -> Message,
178 {
179 if let Some(callback) = callback {
180 self.content = self.content.on_input(InternalMessage::OnChange);
181 self.on_change = Some(Box::new(callback));
182 } else {
183 if self.on_submit.is_none() {
184 #[allow(unused_assignments)]
186 let mut f = Some(InternalMessage::OnChange);
187 f = None;
188 self.content = self.content.on_input_maybe(f);
189 }
190 self.on_change = None;
191 }
192 self
193 }
194
195 #[must_use]
198 pub fn on_submit(mut self, message: Message) -> Self {
199 self.content = self.content.on_submit(InternalMessage::OnSubmit);
200 self.on_submit = Some(message);
201 self
202 }
203
204 #[must_use]
209 pub fn on_submit_maybe(mut self, message: Option<Message>) -> Self {
210 if let Some(message) = message {
211 self.content = self.content.on_submit(InternalMessage::OnSubmit);
212 self.on_submit = Some(message);
213 } else {
214 if self.on_change.is_none() {
215 #[allow(unused_assignments)]
217 let mut f = Some(InternalMessage::OnChange);
218 f = None;
219 self.content = self.content.on_input_maybe(f);
220 }
221 #[allow(unused_assignments)]
223 let mut f = Some(InternalMessage::OnSubmit);
224 f = None;
225 self.content = self.content.on_submit_maybe(f);
226 self.on_change = None;
227 }
228 self
229 }
230
231 #[must_use]
233 pub fn on_paste<F>(mut self, callback: F) -> Self
234 where
235 F: 'a + Fn(T) -> Message,
236 {
237 self.content = self.content.on_paste(InternalMessage::OnPaste);
238 self.on_paste = Some(Box::new(callback));
239 self
240 }
241
242 #[must_use]
244 pub fn on_paste_maybe<F>(mut self, callback: Option<F>) -> Self
245 where
246 F: 'a + Fn(T) -> Message,
247 {
248 if let Some(callback) = callback {
249 self.content = self.content.on_paste(InternalMessage::OnPaste);
250 self.on_paste = Some(Box::new(callback));
251 } else {
252 #[allow(unused_assignments)]
254 let mut f = Some(InternalMessage::OnPaste);
255 f = None;
256 self.content = self.content.on_paste_maybe(f);
257 self.on_paste = None;
258 }
259 self
260 }
261
262 #[allow(clippy::needless_pass_by_value)]
267 #[must_use]
268 pub fn font(mut self, font: Renderer::Font) -> Self {
269 self.font = font;
270 self.content = self.content.font(font);
271 self
272 }
273
274 #[must_use]
276 pub fn icon(mut self, icon: iced_widget::text_input::Icon<Renderer::Font>) -> Self {
277 self.content = self.content.icon(icon);
278 self
279 }
280
281 #[must_use]
283 pub fn width(mut self, width: impl Into<Length>) -> Self {
284 self.content = self.content.width(width);
285 self
286 }
287
288 #[deprecated(since = "0.11.1", note = "use `width` instead")]
290 #[must_use]
291 pub fn content_width(self, width: impl Into<Length>) -> Self {
292 self.width(width)
293 }
294
295 #[must_use]
297 pub fn padding(mut self, padding: impl Into<iced_core::Padding>) -> Self {
298 let padding = padding.into();
299 self.padding = padding;
300 self.content = self.content.padding(padding);
301 self
302 }
303
304 #[must_use]
306 pub fn set_size(mut self, size: impl Into<iced_core::Pixels>) -> Self {
307 let size = size.into();
308 self.size = Some(size);
309 self.content = self.content.size(size);
310 self
311 }
312
313 #[must_use]
315 pub fn line_height(mut self, line_height: impl Into<iced_widget::text::LineHeight>) -> Self {
316 self.content = self.content.line_height(line_height);
317 self
318 }
319
320 #[must_use]
322 pub fn align_x(mut self, alignment: impl Into<iced_core::alignment::Horizontal>) -> Self {
323 self.content = self.content.align_x(alignment);
324 self
325 }
326
327 #[must_use]
329 pub fn style(mut self, style: impl Fn(&Theme, Status) -> Style + 'a) -> Self
330 where
331 <Theme as style::number_input::Catalog>::Class<'a>: From<StyleFn<'a, Theme, Style>>,
332 {
333 self.class = (Box::new(style) as StyleFn<'a, Theme, Style>).into();
334 self
335 }
336 #[must_use]
338 pub fn input_style(
339 mut self,
340 style: impl Fn(&Theme, text_input::Status) -> text_input::Style + 'a,
341 ) -> Self
342 where
343 <Theme as text_input::Catalog>::Class<'a>: From<text_input::StyleFn<'a, Theme>>,
344 {
345 self.content = self.content.style(style);
346 self
347 }
348
349 #[must_use]
351 pub fn class(
352 mut self,
353 class: impl Into<<Theme as style::number_input::Catalog>::Class<'a>>,
354 ) -> Self {
355 self.class = class.into();
356 self
357 }
358
359 #[must_use]
367 pub fn bounds(mut self, bounds: impl RangeBounds<T>) -> Self {
368 self.min = bounds.start_bound().cloned();
369 self.max = bounds.end_bound().cloned();
370
371 self
372 }
373
374 #[must_use]
376 pub fn step(mut self, step: T) -> Self {
377 self.step = step;
378 self
379 }
380
381 #[must_use]
384 pub fn ignore_buttons(mut self, ignore: bool) -> Self {
385 self.ignore_buttons = ignore;
386 self
387 }
388
389 #[must_use]
392 pub fn ignore_scroll(mut self, ignore: bool) -> Self {
393 self.ignore_scroll_events = ignore;
394 self
395 }
396
397 fn decrease_value(&mut self, shell: &mut Shell<Message>) {
399 if self.value.clone() > self.min() + self.step.clone()
400 && self.valid(&(self.value.clone() - self.step.clone()))
401 {
402 self.value -= self.step.clone();
403 } else if self.value > self.min() {
404 self.value = self.min();
405 } else {
406 return;
407 }
408 if let Some(on_change) = &self.on_change {
409 shell.publish(on_change(self.value.clone()));
410 }
411 }
412
413 fn increase_value(&mut self, shell: &mut Shell<Message>) {
415 if self.value < self.max() - self.step.clone()
416 && self.valid(&(self.value.clone() + self.step.clone()))
417 {
418 self.value += self.step.clone();
419 } else if self.value < self.max() {
420 self.value = self.max();
421 } else {
422 return;
423 }
424 if let Some(on_change) = &self.on_change {
425 shell.publish(on_change(self.value.clone()));
426 }
427 }
428
429 fn min(&self) -> T {
432 match &self.min {
433 Bound::Included(n) => n.clone(),
434 Bound::Excluded(n) => n.clone() + self.step.clone(),
435 Bound::Unbounded => T::min_value(),
436 }
437 }
438
439 fn max(&self) -> T {
442 match &self.max {
443 Bound::Included(n) => n.clone(),
444 Bound::Excluded(n) => n.clone() - self.step.clone(),
445 Bound::Unbounded => T::max_value(),
446 }
447 }
448
449 fn valid(&self, value: &T) -> bool {
451 (match &self.min {
452 Bound::Included(n) if *n > *value => false,
453 Bound::Excluded(n) if *n >= *value => false,
454 _ => true,
455 }) && (match &self.max {
456 Bound::Included(n) if *n < *value => false,
457 Bound::Excluded(n) if *n <= *value => false,
458 _ => true,
459 })
460 }
461
462 fn can_increase(&self) -> bool {
464 (self.value < self.max() - self.step.clone()
465 && self.valid(&(self.value.clone() + self.step.clone())))
466 || self.value < self.max()
467 }
468
469 fn can_decrease(&self) -> bool {
471 (self.value.clone() > self.min() + self.step.clone()
472 && self.valid(&(self.value.clone() - self.step.clone())))
473 || self.value > self.min()
474 }
475
476 fn disabled(&self) -> bool {
479 match (&self.min, &self.max) {
480 (Bound::Included(n) | Bound::Excluded(n), Bound::Included(m) | Bound::Excluded(m)) => {
481 *n >= *m
482 }
483 _ => false,
484 }
485 }
486}
487
488impl<'a, T, Message, Theme, Renderer> Widget<Message, Theme, Renderer>
489 for NumberInput<'a, T, Message, Theme, Renderer>
490where
491 T: Num + NumAssignOps + PartialOrd + Display + FromStr + ToString + Clone + Bounded + 'a,
492 Message: 'a + Clone,
493 Renderer: 'a + iced_core::text::Renderer<Font = iced_core::Font>,
494 Theme: number_input::ExtendedCatalog,
495{
496 fn tag(&self) -> Tag {
497 Tag::of::<ModifierState>()
498 }
499 fn state(&self) -> State {
500 State::new(ModifierState::default())
501 }
502
503 fn children(&self) -> Vec<Tree> {
504 vec![Tree {
505 tag: self.content.tag(),
506 state: self.content.state(),
507 children: self.content.children(),
508 }]
509 }
510
511 fn diff(&self, tree: &mut Tree) {
512 tree.diff_children_custom(
513 &[&self.content],
514 |state, content| content.diff(state),
515 |content| Tree {
516 tag: content.tag(),
517 state: content.state(),
518 children: content.children(),
519 },
520 );
521 }
522
523 fn size(&self) -> Size<Length> {
524 Widget::size(&self.content)
525 }
526
527 fn layout(&mut self, tree: &mut Tree, renderer: &Renderer, limits: &Limits) -> Node {
528 let num_size = self.size();
529 let limits = limits.width(num_size.width).height(Length::Shrink);
530 let content = self
531 .content
532 .layout(&mut tree.children[0], renderer, &limits);
533 let limits2 = Limits::new(Size::new(0.0, 0.0), content.size());
534 let txt_size = self.size.unwrap_or_else(|| renderer.default_size());
535
536 let icon_size = txt_size * 2.5 / 4.0;
537 let btn_mod = |c| {
538 Container::<Message, Theme, Renderer>::new(Text::new(format!(" {c} ")).size(icon_size))
539 .center_y(Length::Shrink)
540 .center_x(Length::Shrink)
541 };
542
543 let default_padding = DEFAULT_PADDING;
544
545 let mut element = if self.padding.top < default_padding.top
546 || self.padding.bottom < default_padding.bottom
547 || self.padding.right < default_padding.right
548 {
549 Element::new(
550 Row::<Message, Theme, Renderer>::new()
551 .spacing(1)
552 .width(Length::Shrink)
553 .push(btn_mod('+'))
554 .push(btn_mod('-')),
555 )
556 } else {
557 Element::new(
558 Column::<Message, Theme, Renderer>::new()
559 .spacing(1)
560 .width(Length::Shrink)
561 .push(btn_mod('▲'))
562 .push(btn_mod('▼')),
563 )
564 };
565
566 let input_tree = if let Some(child_tree) = tree.children.get_mut(1) {
567 child_tree.diff(element.as_widget_mut());
568 child_tree
569 } else {
570 let child_tree = Tree::new(element.as_widget());
571 tree.children.insert(1, child_tree);
572 &mut tree.children[1]
573 };
574
575 let mut modifier = element
576 .as_widget_mut()
577 .layout(input_tree, renderer, &limits2.loose());
578 let intrinsic = Size::new(
579 content.size().width - 1.0,
580 content.size().height.max(modifier.size().height),
581 );
582 modifier = modifier.align(Alignment::End, Alignment::Center, intrinsic);
583
584 let size = limits.resolve(num_size.width, Length::Shrink, intrinsic);
585 Node::with_children(size, vec![content, modifier])
586 }
587
588 fn operate(
589 &mut self,
590 tree: &mut Tree,
591 layout: Layout<'_>,
592 renderer: &Renderer,
593 operation: &mut dyn Operation<()>,
594 ) {
595 operation.container(None, layout.bounds());
596 operation.traverse(&mut |operation| {
597 self.content.operate(
598 &mut tree.children[0],
599 layout
600 .children()
601 .next()
602 .expect("NumberInput inner child Textbox was not created."),
603 renderer,
604 operation,
605 );
606 });
607 }
608
609 #[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
610 fn update(
611 &mut self,
612 state: &mut Tree,
613 event: &Event,
614 layout: Layout<'_>,
615 cursor: Cursor,
616 renderer: &Renderer,
617 clipboard: &mut dyn Clipboard,
618 shell: &mut Shell<Message>,
619 viewport: &Rectangle,
620 ) {
621 let mut children = layout.children();
622 let content = children.next().expect("fail to get content layout");
623 let mut mod_children = children
624 .next()
625 .expect("fail to get modifiers layout")
626 .children();
627 let inc_bounds = mod_children
628 .next()
629 .expect("fail to get increase mod layout")
630 .bounds();
631 let dec_bounds = mod_children
632 .next()
633 .expect("fail to get decrease mod layout")
634 .bounds();
635
636 if self.disabled() {
637 return;
638 }
639 let can_decrease = self.can_decrease();
640 let can_increase = self.can_increase();
641
642 let cursor_position = cursor.position().unwrap_or_default();
643 let mouse_over_widget = layout.bounds().contains(cursor_position);
644 let mouse_over_inc = inc_bounds.contains(cursor_position);
645 let mouse_over_dec = dec_bounds.contains(cursor_position);
646 let mouse_over_button = mouse_over_inc || mouse_over_dec;
647
648 let modifiers = state.state.downcast_mut::<ModifierState>();
649 let mut value = self.content.text().to_owned();
650
651 let child = state.children.get_mut(0).expect("fail to get child");
652 let text_input = child
653 .state
654 .downcast_mut::<text_input::State<Renderer::Paragraph>>();
655
656 let mut messages = Vec::new();
658 let mut sub_shell = Shell::new(&mut messages);
659
660 let mut forward_to_text = |widget: &mut Self, child, clipboard| {
662 widget.content.update(
663 child,
664 &event.clone(),
665 content,
666 cursor,
667 renderer,
668 clipboard,
669 &mut sub_shell,
670 viewport,
671 );
672 };
673
674 let supports_negative = self.min() < T::zero();
676 let mut check_value = |value: &str| {
677 if let Ok(value) = T::from_str(value) {
678 self.valid(&value)
679 } else if value.is_empty() || value == "-" && supports_negative {
680 self.value = T::zero();
681 true
682 } else {
683 false
684 }
685 };
686
687 match &event {
688 Event::Keyboard(key) => {
689 if !text_input.is_focused() {
690 return;
691 }
692
693 match key {
694 keyboard::Event::ModifiersChanged(_) => forward_to_text(self, child, clipboard),
695 keyboard::Event::KeyReleased { .. } => return,
696 keyboard::Event::KeyPressed {
697 key,
698 text,
699 modifiers,
700 ..
701 } => {
702 let cursor = text_input.cursor();
703
704 let has_value = !modifiers.command()
708 && text
709 .as_ref()
710 .is_some_and(|t| t.chars().any(|c| !c.is_control()));
711
712 match key.as_ref() {
713 keyboard::Key::Named(keyboard::key::Named::Enter) => {
715 forward_to_text(self, child, clipboard);
716 }
717 keyboard::Key::Character("c" | "a") if modifiers.command() => {
719 forward_to_text(self, child, clipboard);
720 }
721 keyboard::Key::Character("x") if modifiers.command() => {
723 if let Some((start, end)) = cursor.selection(&Value::new(&value)) {
725 let _ = value.drain(start..end);
726 if check_value(&value) {
728 forward_to_text(self, child, clipboard);
729 } else {
730 return;
731 }
732 } else {
733 return;
734 }
735 }
736 keyboard::Key::Character("v") if modifiers.command() => {
738 let Some(paste) =
740 clipboard.read(iced_core::clipboard::Kind::Standard)
741 else {
742 return;
743 };
744 match cursor.state(&Value::new(&value)) {
746 cursor::State::Index(idx) => {
747 value.insert_str(idx, &paste);
748 }
749 cursor::State::Selection { start, end } => {
750 value.replace_range(sorted_range(start, end), &paste);
751 }
752 }
753
754 shell.capture_event();
755
756 if check_value(&value) {
758 forward_to_text(self, child, clipboard);
759 } else {
760 return;
761 }
762 }
763 keyboard::Key::Named(keyboard::key::Named::Backspace) => {
765 match cursor.state(&Value::new(&value)) {
767 cursor::State::Selection { start, end } => {
768 let _ = value.drain(sorted_range(start, end));
769 }
770 cursor::State::Index(idx) if idx > 0 => {
772 if modifiers.command() {
773 let _ =
777 value.drain((value.starts_with('-').into())..idx);
778 } else {
779 let _ = value.remove(idx - 1);
780 }
781 }
782 cursor::State::Index(_) => return,
783 }
784
785 shell.capture_event();
786
787 if check_value(&value) {
789 forward_to_text(self, child, clipboard);
790 } else {
791 return;
792 }
793 }
794 keyboard::Key::Named(keyboard::key::Named::Delete) => {
796 match cursor.state(&Value::new(&value)) {
798 cursor::State::Selection { start, end } => {
799 let _ = value.drain(sorted_range(start, end));
800 }
801 cursor::State::Index(idx) if idx < value.len() => {
803 if idx == 0 && value.starts_with('-') {
804 let _ = value.remove(0);
805 } else if modifiers.command() {
806 let _ = value.drain(idx..);
810 } else {
811 let _ = value.remove(idx);
812 }
813 }
814 cursor::State::Index(_) => return,
815 }
816
817 shell.capture_event();
818
819 if check_value(&value) {
821 forward_to_text(self, child, clipboard);
822 } else {
823 return;
824 }
825 }
826 keyboard::Key::Named(keyboard::key::Named::ArrowDown)
828 if can_decrease && !has_value =>
829 {
830 shell.capture_event();
831 shell.request_redraw();
832 self.decrease_value(shell);
833 }
834 keyboard::Key::Named(keyboard::key::Named::ArrowUp)
836 if can_increase && !has_value =>
837 {
838 shell.capture_event();
839 shell.request_redraw();
840
841 self.increase_value(shell);
842 }
843 keyboard::Key::Named(
845 keyboard::key::Named::ArrowLeft
846 | keyboard::key::Named::ArrowRight
847 | keyboard::key::Named::Home
848 | keyboard::key::Named::End,
849 ) if !has_value => forward_to_text(self, child, clipboard),
850 _ => match text {
852 Some(text) => {
854 match cursor.state(&Value::new(&value)) {
856 cursor::State::Index(idx) => {
857 value.insert_str(idx, text);
858 }
859 cursor::State::Selection { start, end } => {
860 value.replace_range(sorted_range(start, end), text);
861 }
862 }
863
864 shell.capture_event();
865 shell.request_redraw();
866
867 if check_value(&value) {
869 forward_to_text(self, child, clipboard);
870 } else {
871 return;
872 }
873 }
874 None => return,
876 },
877 }
878 }
879 }
880 }
881 Event::Mouse(mouse::Event::WheelScrolled { delta })
883 if mouse_over_widget && !self.ignore_scroll_events =>
884 {
885 match delta {
886 mouse::ScrollDelta::Lines { y, .. } | mouse::ScrollDelta::Pixels { y, .. } => {
887 if y.is_sign_positive() {
888 self.increase_value(shell);
889 } else {
890 self.decrease_value(shell);
891 }
892 }
893 }
894 shell.capture_event();
895 shell.request_redraw();
896 }
897 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
899 if mouse_over_button && !self.ignore_buttons =>
900 {
901 if mouse_over_dec {
902 modifiers.decrease_pressed = true;
903 self.decrease_value(shell);
904 } else {
905 modifiers.increase_pressed = true;
906 self.increase_value(shell);
907 }
908 shell.capture_event();
909 shell.request_redraw();
910 }
911 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
913 if mouse_over_button =>
914 {
915 if mouse_over_dec {
916 modifiers.decrease_pressed = false;
917 } else {
918 modifiers.increase_pressed = false;
919 }
920 shell.capture_event();
921 shell.request_redraw();
922 }
923 _ => forward_to_text(self, child, clipboard),
925 }
926
927 shell.request_redraw_at(sub_shell.redraw_request());
929
930 if sub_shell.is_layout_invalid() {
931 shell.invalidate_layout();
932 }
933 if sub_shell.are_widgets_invalid() {
934 shell.invalidate_widgets();
935 }
936
937 for message in messages {
938 match message {
939 InternalMessage::OnChange(value) => {
940 if self.value != value || self.value.is_zero() {
941 self.value = value.clone();
942 if let Some(on_change) = &self.on_change {
943 shell.publish(on_change(value));
944 }
945 }
946 shell.invalidate_layout();
947 }
948 InternalMessage::OnSubmit(result) => {
949 if let Err(text) = result {
950 assert!(
951 text.is_empty(),
952 "We shouldn't be able to submit a number input with an invalid value"
953 );
954 }
955 if let Some(on_submit) = &self.on_submit {
956 shell.publish(on_submit.clone());
957 }
958 shell.invalidate_layout();
959 }
960 InternalMessage::OnPaste(value) => {
961 if self.value != value {
962 self.value = value.clone();
963 if let Some(on_paste) = &self.on_paste {
964 shell.publish(on_paste(value));
965 }
966 }
967 shell.invalidate_layout();
968 }
969 }
970 }
971 }
972
973 fn mouse_interaction(
974 &self,
975 _state: &Tree,
976 layout: Layout<'_>,
977 cursor: Cursor,
978 _viewport: &Rectangle,
979 _renderer: &Renderer,
980 ) -> mouse::Interaction {
981 let bounds = layout.bounds();
982 let mut children = layout.children();
983 let _content_layout = children.next().expect("fail to get content layout");
984 let mut mod_children = children
985 .next()
986 .expect("fail to get modifiers layout")
987 .children();
988 let inc_bounds = mod_children
989 .next()
990 .expect("fail to get increase mod layout")
991 .bounds();
992 let dec_bounds = mod_children
993 .next()
994 .expect("fail to get decrease mod layout")
995 .bounds();
996 let is_mouse_over = bounds.contains(cursor.position().unwrap_or_default());
997 let is_decrease_disabled = !self.can_decrease();
998 let is_increase_disabled = !self.can_increase();
999 let mouse_over_decrease = dec_bounds.contains(cursor.position().unwrap_or_default());
1000 let mouse_over_increase = inc_bounds.contains(cursor.position().unwrap_or_default());
1001
1002 if ((mouse_over_decrease && !is_decrease_disabled)
1003 || (mouse_over_increase && !is_increase_disabled))
1004 && !self.ignore_buttons
1005 {
1006 mouse::Interaction::Pointer
1007 } else if is_mouse_over {
1008 mouse::Interaction::Text
1009 } else {
1010 mouse::Interaction::default()
1011 }
1012 }
1013
1014 fn draw(
1015 &self,
1016 state: &Tree,
1017 renderer: &mut Renderer,
1018 theme: &Theme,
1019 style: &renderer::Style,
1020 layout: Layout<'_>,
1021 cursor: Cursor,
1022 viewport: &Rectangle,
1023 ) {
1024 let mut children = layout.children();
1025 let content_layout = children.next().expect("fail to get content layout");
1026 let mut mod_children = children
1027 .next()
1028 .expect("fail to get modifiers layout")
1029 .children();
1030 let inc_bounds = mod_children
1031 .next()
1032 .expect("fail to get increase mod layout")
1033 .bounds();
1034 let dec_bounds = mod_children
1035 .next()
1036 .expect("fail to get decrease mod layout")
1037 .bounds();
1038 self.content.draw(
1039 &state.children[0],
1040 renderer,
1041 theme,
1042 style,
1043 content_layout,
1044 cursor,
1045 viewport,
1046 );
1047 let is_decrease_disabled = !self.can_decrease();
1048 let is_increase_disabled = !self.can_increase();
1049
1050 let decrease_btn_style = if is_decrease_disabled {
1051 style::number_input::Catalog::style(theme, &self.class, Status::Disabled)
1052 } else if state.state.downcast_ref::<ModifierState>().decrease_pressed {
1053 style::number_input::Catalog::style(theme, &self.class, Status::Pressed)
1054 } else {
1055 style::number_input::Catalog::style(theme, &self.class, Status::Active)
1056 };
1057
1058 let increase_btn_style = if is_increase_disabled {
1059 style::number_input::Catalog::style(theme, &self.class, Status::Disabled)
1060 } else if state.state.downcast_ref::<ModifierState>().increase_pressed {
1061 style::number_input::Catalog::style(theme, &self.class, Status::Pressed)
1062 } else {
1063 style::number_input::Catalog::style(theme, &self.class, Status::Active)
1064 };
1065
1066 let txt_size = self.size.unwrap_or_else(|| renderer.default_size());
1067
1068 let icon_size = txt_size * 2.5 / 4.0;
1069
1070 if self.ignore_buttons {
1071 return;
1072 }
1073 if dec_bounds.intersects(viewport) {
1075 renderer.fill_quad(
1076 renderer::Quad {
1077 bounds: dec_bounds,
1078 border: Border {
1079 radius: (3.0).into(),
1080 width: 0.0,
1081 color: Color::TRANSPARENT,
1082 },
1083 shadow: Shadow::default(),
1084 snap: false,
1085 },
1086 decrease_btn_style
1087 .button_background
1088 .unwrap_or(Background::Color(Color::TRANSPARENT)),
1089 );
1090 }
1091
1092 let (content, font, shaping) = down_open();
1093 renderer.fill_text(
1094 iced_core::text::Text {
1095 content,
1096 bounds: Size::new(dec_bounds.width, dec_bounds.height),
1097 size: icon_size,
1098 font,
1099 line_height: LineHeight::Relative(1.3),
1100 shaping,
1101 wrapping: Wrapping::default(),
1102 align_x: Alignment::Center.into(),
1103 align_y: Vertical::Center,
1104 },
1105 Point::new(dec_bounds.center_x(), dec_bounds.center_y()),
1106 decrease_btn_style.icon_color,
1107 dec_bounds,
1108 );
1109
1110 if inc_bounds.intersects(viewport) {
1112 renderer.fill_quad(
1113 renderer::Quad {
1114 bounds: inc_bounds,
1115 border: Border {
1116 radius: (3.0).into(),
1117 width: 0.0,
1118 color: Color::TRANSPARENT,
1119 },
1120 shadow: Shadow::default(),
1121 snap: false,
1122 },
1123 increase_btn_style
1124 .button_background
1125 .unwrap_or(Background::Color(Color::TRANSPARENT)),
1126 );
1127 }
1128
1129 let (content, font, shaping) = up_open();
1130 renderer.fill_text(
1131 iced_core::text::Text {
1132 content,
1133 bounds: Size::new(inc_bounds.width, inc_bounds.height),
1134 size: icon_size,
1135 font,
1136 line_height: LineHeight::Relative(1.3),
1137 shaping,
1138 wrapping: Wrapping::default(),
1139 align_x: Alignment::Center.into(),
1140 align_y: Vertical::Center,
1141 },
1142 Point::new(inc_bounds.center_x(), inc_bounds.center_y()),
1143 increase_btn_style.icon_color,
1144 inc_bounds,
1145 );
1146 }
1147}
1148
1149#[derive(Default, Clone, Debug)]
1151pub struct ModifierState {
1152 pub decrease_pressed: bool,
1154 pub increase_pressed: bool,
1156}
1157
1158impl<'a, T, Message, Theme, Renderer> From<NumberInput<'a, T, Message, Theme, Renderer>>
1159 for Element<'a, Message, Theme, Renderer>
1160where
1161 T: 'a + Num + NumAssignOps + PartialOrd + Display + FromStr + Clone + Bounded,
1162 Message: 'a + Clone,
1163 Renderer: 'a + iced_core::text::Renderer<Font = iced_core::Font>,
1164 Theme: 'a + number_input::ExtendedCatalog,
1165{
1166 fn from(num_input: NumberInput<'a, T, Message, Theme, Renderer>) -> Self {
1167 Element::new(num_input)
1168 }
1169}
1170
1171fn sorted_range<T: PartialOrd>(a: T, b: T) -> std::ops::Range<T> {
1172 if a >= b { b..a } else { a..b }
1173}