use iced_native::{
alignment::{Horizontal, Vertical},
event, keyboard,
layout::{Limits, Node},
mouse, renderer,
widget::{
self, container, text,
text_input::{self, cursor, Value},
tree::{self, Tree},
Column, Container, Operation, Row, Text, TextInput,
},
Alignment, Background, Clipboard, Color, Element, Event, Layout, Length, Padding, Point,
Rectangle, Shell, Size, Widget,
};
use num_traits::{Num, NumAssignOps};
use std::{fmt::Display, str::FromStr};
pub use crate::{
graphics::icons::Icon,
style::number_input::{self, Appearance, StyleSheet},
};
const DEFAULT_PADDING: u16 = 5;
#[allow(missing_debug_implementations)]
pub struct NumberInput<'a, T, Message, Renderer>
where
Renderer: iced_native::text::Renderer<Font = iced_native::Font>,
Renderer::Theme: number_input::StyleSheet
+ text_input::StyleSheet
+ container::StyleSheet
+ text::StyleSheet,
{
value: T,
step: T,
bounds: (T, T),
padding: u16,
size: Option<u16>,
content: TextInput<'a, Message, Renderer>,
on_change: Box<dyn Fn(T) -> Message>,
style: <Renderer::Theme as number_input::StyleSheet>::Style,
font: Renderer::Font,
}
impl<'a, T, Message, Renderer> NumberInput<'a, T, Message, Renderer>
where
T: Num + NumAssignOps + PartialOrd + Display + FromStr + Copy,
Message: Clone,
Renderer: iced_native::text::Renderer<Font = iced_native::Font>,
Renderer::Theme: number_input::StyleSheet
+ text_input::StyleSheet
+ container::StyleSheet
+ text::StyleSheet,
{
pub fn new<F>(value: T, max: T, on_changed: F) -> Self
where
F: 'static + Fn(T) -> Message + Copy,
T: 'static,
{
let padding = DEFAULT_PADDING;
let convert_to_num = move |s: String| {
on_changed(T::from_str(&s).unwrap_or(if s.is_empty() { T::zero() } else { value }))
};
Self {
value,
step: T::one(),
bounds: (T::zero(), max),
padding,
size: None,
content: TextInput::new("", format!("{value}").as_str(), convert_to_num)
.padding(padding)
.width(Length::Units(127)),
on_change: Box::new(on_changed),
style: <Renderer::Theme as number_input::StyleSheet>::Style::default(),
font: iced_graphics::Font::default(),
}
}
#[must_use]
pub fn step(mut self, step: T) -> Self {
self.step = step;
self
}
#[must_use]
pub fn min(mut self, min: T) -> Self {
if min < self.bounds.1 {
self.bounds.0 = min;
}
self
}
#[must_use]
pub fn max(mut self, max: T) -> Self {
if max > self.bounds.0 {
self.bounds.1 = max;
}
self
}
#[must_use]
pub fn bounds(mut self, bounds: (T, T)) -> Self {
if bounds.0 < bounds.1 {
self.bounds = bounds;
}
self
}
#[allow(clippy::needless_pass_by_value)]
#[must_use]
pub fn font(mut self, font: Renderer::Font) -> Self {
self.font = font;
self.content = self.content.font(font);
self
}
#[must_use]
pub fn width(mut self, width: Length) -> Self {
self.content = self.content.width(width);
self
}
#[must_use]
pub fn padding(mut self, units: u16) -> Self {
self.padding = units;
self.content = self.content.padding(units);
self
}
#[must_use]
pub fn size(mut self, size: u16) -> Self {
self.size = Some(size);
self.content = self.content.size(size);
self
}
#[must_use]
pub fn on_submit(mut self, message: Message) -> Self {
self.content = self.content.on_submit(message);
self
}
#[must_use]
pub fn style(
mut self,
style: impl Into<<Renderer::Theme as number_input::StyleSheet>::Style>,
) -> Self {
self.style = style.into();
self
}
#[must_use]
pub fn input_style(
mut self,
style: impl Into<<Renderer::Theme as iced_style::text_input::StyleSheet>::Style>,
) -> Self {
self.content = self.content.style(style);
self
}
fn decrease_val(&mut self, shell: &mut Shell<Message>) {
if self.value > self.bounds.0 {
let new_val = self.value - self.step;
self.value = if new_val > self.bounds.0 {
new_val
} else {
self.bounds.0
};
shell.publish((self.on_change)(self.value));
}
}
fn increase_val(&mut self, shell: &mut Shell<Message>) {
if self.value < self.bounds.1 {
let new_val = self.value + self.step;
self.value = if new_val < self.bounds.1 {
new_val
} else {
self.bounds.1
};
shell.publish((self.on_change)(self.value));
}
}
}
impl<'a, T, Message, Renderer> Widget<Message, Renderer> for NumberInput<'a, T, Message, Renderer>
where
T: Num + NumAssignOps + PartialOrd + Display + FromStr + ToString + Copy,
Message: 'a + Clone,
Renderer: 'a + iced_native::text::Renderer<Font = iced_native::Font>,
Renderer::Theme: number_input::StyleSheet
+ text_input::StyleSheet
+ container::StyleSheet
+ text::StyleSheet,
{
fn tag(&self) -> tree::Tag {
tree::Tag::of::<ModifierState>()
}
fn state(&self) -> tree::State {
tree::State::new(ModifierState::default())
}
fn children(&self) -> Vec<Tree> {
vec![Tree {
tag: self.content.tag(),
state: self.content.state(),
children: self.content.children(),
}]
}
fn diff(&self, tree: &mut Tree) {
tree.diff_children_custom(
&[&self.content],
|state, content| content.diff(state),
|&content| Tree {
tag: content.tag(),
state: content.state(),
children: content.children(),
},
);
}
fn width(&self) -> Length {
Length::Shrink
}
fn height(&self) -> Length {
Length::Shrink
}
fn layout(&self, renderer: &Renderer, limits: &Limits) -> Node {
let padding = Padding::from(self.padding);
let limits = limits
.width(self.width())
.height(Length::Shrink)
.pad(padding);
let content = self.content.layout(renderer, &limits.loose());
let txt_size = self.size.unwrap_or_else(|| renderer.default_size());
let icon_size = txt_size * 3 / 4;
let btn_mod = |c| {
Container::<(), Renderer>::new(Text::new(format!(" {c} ")).size(icon_size))
.center_y()
.center_x()
};
let mut modifier = if self.padding < DEFAULT_PADDING {
Row::<(), Renderer>::new()
.spacing(1)
.width(Length::Shrink)
.push(btn_mod('+'))
.push(btn_mod('-'))
.layout(renderer, &limits.loose())
} else {
Column::<(), Renderer>::new()
.spacing(1)
.width(Length::Shrink)
.push(btn_mod('â–²'))
.push(btn_mod('â–¼'))
.layout(renderer, &limits.loose())
};
let intrinsic = Size::new(
content.size().width - 3.0,
content.size().height.max(modifier.size().height),
);
modifier.align(Alignment::End, Alignment::Center, intrinsic);
let size = limits.resolve(intrinsic);
Node::with_children(size, vec![content, modifier])
}
fn operate(
&self,
tree: &mut Tree,
layout: Layout<'_>,
renderer: &Renderer,
operation: &mut dyn Operation<Message>,
) {
operation.container(None, &mut |operation| {
self.content.operate(
&mut tree.children[0],
layout
.children()
.next()
.expect("NumberInput inner child Textbox was not created."),
renderer,
operation,
);
});
}
#[allow(clippy::too_many_lines, clippy::cognitive_complexity)]
fn on_event(
&mut self,
state: &mut Tree,
event: Event,
layout: Layout<'_>,
cursor_position: Point,
renderer: &Renderer,
clipboard: &mut dyn Clipboard,
shell: &mut Shell<Message>,
) -> event::Status {
let mut children = layout.children();
let content = children.next().expect("fail to get content layout");
let mut mod_children = children
.next()
.expect("fail to get modifiers layout")
.children();
let inc_bounds = mod_children
.next()
.expect("fail to get increase mod layout")
.bounds();
let dec_bounds = mod_children
.next()
.expect("fail to get decreate mod layout")
.bounds();
let mouse_over_inc = inc_bounds.contains(cursor_position);
let mouse_over_dec = dec_bounds.contains(cursor_position);
let modifiers = state.state.downcast_mut::<ModifierState>();
let child = &mut state.children[0];
if layout.bounds().contains(cursor_position) {
if mouse_over_inc || mouse_over_dec {
let mut event_status = event::Status::Captured;
match event {
Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left)) => {
if mouse_over_dec {
modifiers.decrease_pressed = true;
self.decrease_val(shell);
} else if mouse_over_inc {
modifiers.increase_pressed = true;
self.increase_val(shell);
} else {
event_status = event::Status::Ignored;
}
}
Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left)) => {
if mouse_over_dec {
modifiers.decrease_pressed = false;
} else if mouse_over_inc {
modifiers.increase_pressed = false;
} else {
event_status = event::Status::Ignored;
}
}
_ => event_status = event::Status::Ignored,
}
event_status
} else {
match event {
Event::Keyboard(keyboard::Event::CharacterReceived(c))
if child
.state
.downcast_mut::<widget::text_input::State>()
.is_focused()
&& c.is_numeric() =>
{
let mut new_val = self.value.to_string();
match child
.state
.downcast_mut::<widget::text_input::State>()
.cursor()
.state(&Value::new(&new_val))
{
cursor::State::Index(idx) => {
if T::zero().eq(&self.value) {
new_val = c.to_string();
} else {
new_val.insert(idx, c);
}
}
cursor::State::Selection { start, end } => {
if (0..new_val.len()).contains(&start)
&& (0..new_val.len()).contains(&end)
{
new_val.replace_range(
if start > end { end..start } else { start..end },
&c.to_string(),
);
}
}
}
match T::from_str(&new_val) {
Ok(val) => {
if (self.bounds.0..=self.bounds.1).contains(&val) {
self.value = val;
shell.publish((self.on_change)(self.value));
self.content.on_event(
child,
event.clone(),
content,
cursor_position,
renderer,
clipboard,
shell,
)
} else {
event::Status::Ignored
}
}
Err(_) => event::Status::Ignored,
}
}
Event::Keyboard(keyboard::Event::KeyPressed { key_code, .. })
if child
.state
.downcast_mut::<widget::text_input::State>()
.is_focused() =>
{
match key_code {
keyboard::KeyCode::Up => {
self.increase_val(shell);
event::Status::Captured
}
keyboard::KeyCode::Down => {
self.decrease_val(shell);
event::Status::Captured
}
keyboard::KeyCode::Backspace => {
if T::zero().eq(&self.value) {
event::Status::Ignored
} else {
let mut new_val = self.value.to_string();
match child
.state
.downcast_mut::<widget::text_input::State>()
.cursor()
.state(&Value::new(&new_val))
{
cursor::State::Index(idx) => {
if idx >= 1 && idx <= new_val.len() {
if new_val.len() == 1 {
new_val = if self.bounds.0 > T::zero() {
self.bounds.0
} else {
T::zero()
}
.to_string();
} else {
let _ = new_val.remove(idx - 1);
}
}
}
cursor::State::Selection { start, end } => {
if (0..new_val.len()).contains(&start)
&& (0..new_val.len()).contains(&end)
{
new_val.replace_range(
if start > end {
end..start
} else {
start..end
},
"",
);
}
}
}
match T::from_str(&new_val) {
Ok(val) => {
if (self.bounds.0..=self.bounds.1).contains(&val) {
self.value = val;
shell.publish((self.on_change)(self.value));
self.content.on_event(
child,
event.clone(),
content,
cursor_position,
renderer,
clipboard,
shell,
)
} else {
event::Status::Ignored
}
}
Err(_) => event::Status::Ignored,
}
}
}
_ => self.content.on_event(
child,
event.clone(),
content,
cursor_position,
renderer,
clipboard,
shell,
),
}
}
Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
let negative = match delta {
mouse::ScrollDelta::Lines { y, .. }
| mouse::ScrollDelta::Pixels { y, .. } => y.is_sign_negative(),
};
if negative {
self.increase_val(shell);
} else {
self.decrease_val(shell);
}
event::Status::Captured
}
_ => self.content.on_event(
child,
event,
content,
cursor_position,
renderer,
clipboard,
shell,
),
}
}
} else {
match event {
Event::Keyboard(_) => event::Status::Ignored,
_ => self.content.on_event(
child,
event,
content,
cursor_position,
renderer,
clipboard,
shell,
),
}
}
}
fn mouse_interaction(
&self,
_state: &Tree,
layout: Layout<'_>,
cursor_position: Point,
_viewport: &Rectangle,
_renderer: &Renderer,
) -> mouse::Interaction {
let bounds = layout.bounds();
let mut children = layout.children();
let _content_layout = children.next().expect("fail to get content layout");
let mut mod_children = children
.next()
.expect("fail to get modifiers layout")
.children();
let inc_bounds = mod_children
.next()
.expect("fail to get increase mod layout")
.bounds();
let dec_bounds = mod_children
.next()
.expect("fail to get decreate mod layout")
.bounds();
let is_mouse_over = bounds.contains(cursor_position);
let is_decrease_disabled = self.value <= self.bounds.0;
let is_increase_disabled = self.value >= self.bounds.1;
let mouse_over_decrease = dec_bounds.contains(cursor_position);
let mouse_over_increase = inc_bounds.contains(cursor_position);
if (mouse_over_decrease && !is_decrease_disabled)
|| (mouse_over_increase && !is_increase_disabled)
{
mouse::Interaction::Pointer
} else if is_mouse_over {
mouse::Interaction::Text
} else {
mouse::Interaction::default()
}
}
fn draw(
&self,
state: &Tree,
renderer: &mut Renderer,
theme: &Renderer::Theme,
_style: &iced_native::renderer::Style,
layout: iced_native::Layout<'_>,
cursor_position: iced_graphics::Point,
_viewport: &iced_graphics::Rectangle,
) {
let mut children = layout.children();
let content_layout = children.next().expect("fail to get content layout");
let mut mod_children = children
.next()
.expect("fail to get modifiers layout")
.children();
let inc_bounds = mod_children
.next()
.expect("fail to get increase mod layout")
.bounds();
let dec_bounds = mod_children
.next()
.expect("fail to get decreate mod layout")
.bounds();
self.content.draw(
&state.children[0],
renderer,
theme,
content_layout,
cursor_position,
None,
);
let is_decrease_disabled = self.value <= self.bounds.0;
let is_increase_disabled = self.value >= self.bounds.1;
let decrease_btn_style = if is_decrease_disabled {
theme.disabled(self.style)
} else if state.state.downcast_ref::<ModifierState>().decrease_pressed {
theme.pressed(self.style)
} else {
theme.active(self.style)
};
let increase_btn_style = if is_increase_disabled {
theme.disabled(self.style)
} else if state.state.downcast_ref::<ModifierState>().increase_pressed {
theme.pressed(self.style)
} else {
theme.active(self.style)
};
renderer.fill_quad(
renderer::Quad {
bounds: dec_bounds,
border_radius: (3.0).into(),
border_width: 0.,
border_color: Color::TRANSPARENT,
},
decrease_btn_style
.button_background
.unwrap_or(Background::Color(Color::TRANSPARENT)),
);
let mut buffer = [0; 4];
renderer.fill_text(iced_native::text::Text {
content: char::from(Icon::CaretDownFill).encode_utf8(&mut buffer),
bounds: Rectangle {
x: dec_bounds.center_x(),
y: dec_bounds.center_y(),
..dec_bounds
},
size: dec_bounds.height,
color: decrease_btn_style.icon_color,
font: crate::graphics::icons::ICON_FONT,
horizontal_alignment: Horizontal::Center,
vertical_alignment: Vertical::Center,
});
renderer.fill_quad(
renderer::Quad {
bounds: inc_bounds,
border_radius: (3.0).into(),
border_width: 0.,
border_color: Color::TRANSPARENT,
},
increase_btn_style
.button_background
.unwrap_or(Background::Color(Color::TRANSPARENT)),
);
renderer.fill_text(iced_native::text::Text {
content: char::from(Icon::CaretUpFill).encode_utf8(&mut buffer),
bounds: Rectangle {
x: inc_bounds.center_x(),
y: inc_bounds.center_y(),
..inc_bounds
},
size: inc_bounds.height,
color: increase_btn_style.icon_color,
font: crate::graphics::icons::ICON_FONT,
horizontal_alignment: Horizontal::Center,
vertical_alignment: Vertical::Center,
});
}
}
#[derive(Default, Clone, Debug)]
pub struct ModifierState {
pub decrease_pressed: bool,
pub increase_pressed: bool,
}
impl<'a, T, Message, Renderer> From<NumberInput<'a, T, Message, Renderer>>
for Element<'a, Message, Renderer>
where
T: 'a + Num + NumAssignOps + PartialOrd + Display + FromStr + Copy,
Message: 'a + Clone,
Renderer: 'a + iced_native::text::Renderer<Font = iced_native::Font>,
Renderer::Theme: number_input::StyleSheet
+ text_input::StyleSheet
+ container::StyleSheet
+ text::StyleSheet,
{
fn from(num_input: NumberInput<'a, T, Message, Renderer>) -> Self {
Element::new(num_input)
}
}