use leptos::prelude::AddAnyAttr;
use std::fmt::Display;
use std::ops::Add;
use std::ops::Sub;
use std::str::FromStr;
use crate::class_list;
use crate::icon;
use crate::input::InputMode;
use crate::input::InputType;
use crate::input_group::GroupItemContextProvider;
use leptos::logging::debug_log;
use leptos::prelude::ClassAttribute;
use leptos::prelude::Effect;
use leptos::prelude::ElementChild;
use leptos::prelude::Get;
use leptos::prelude::GlobalAttributes;
use leptos::prelude::MaybeProp;
use leptos::prelude::RwSignal;
use leptos::prelude::Set;
use leptos::prelude::Signal;
use leptos::{IntoView, component, view};
use num_traits::Bounded;
use num_traits::ConstOne;
use num_traits::SaturatingAdd;
use num_traits::SaturatingSub;
use crate::{button::Button, input::TextInput};
fn clamp<T>(some: T, min: T, max: T) -> T
where
T: PartialOrd,
{
if some < min {
min
} else if some > max {
max
} else {
some
}
}
#[component]
pub fn ControlledNumberInput<T>(
#[prop(optional, into)]
id: MaybeProp<String>,
#[prop(optional, into)]
name: MaybeProp<String>,
#[prop(optional, into)]
class: MaybeProp<String>,
#[prop(default = T::min_value().into(), into)]
min: Signal<T>,
#[prop(default = T::max_value().into(), into)]
max: Signal<T>,
#[prop(optional, into)]
placeholder: MaybeProp<String>,
#[prop(default = T::one())]
step: T,
) -> impl IntoView
where
T: Send + Sync,
T: ConstOne
+ Add<Output = T>
+ Sub<Output = T>
+ SaturatingAdd<Output = T>
+ SaturatingSub<Output = T>
+ PartialOrd
+ Bounded,
T: Default + Clone + Copy + FromStr + ToString + Display + 'static,
{
let value_binder: RwSignal<String> = RwSignal::new("".to_string());
Effect::watch(
move || value_binder.get(),
move |new_value, prev_value, _| {
if Some(new_value) == prev_value {
return;
}
let Ok(new_value) = new_value.parse::<T>() else {
let text = String::new();
if new_value.is_empty() {
return;
}
if prev_value.unwrap_or(&text).parse::<T>().is_err() {
debug_log!(
"User inputted a non-number {new_value:?} after a non-number, resetting to empty"
);
value_binder.set(text);
return;
}
debug_log!(
"User inputted a non-number {new_value:?} resetting their input to {prev_value:?}"
);
value_binder.set(prev_value.unwrap_or(&text).to_string());
return;
};
if new_value != clamp::<T>(new_value, min.get(), max.get()) {
debug_log!(
"User inputted a number {new_value} outside of ({}, {}) ",
min.get(),
max.get()
);
let text = String::new();
value_binder.set(prev_value.unwrap_or(&text).to_string());
};
},
false,
);
let inc_step = step;
let inc_handler = move |_| {
let unparsed = value_binder.get();
let old_value = unparsed.parse::<T>();
if let Ok(old_value) = old_value {
value_binder.set(format!(
"{}",
clamp(old_value.saturating_add(&inc_step), min.get(), max.get())
));
debug_log!("i++");
} else if unparsed.is_empty() {
value_binder.set("1".to_string());
}
};
let dec_handler = move |_| {
let unparsed = value_binder.get();
let old_value = unparsed.parse::<T>();
if let Ok(old_value) = old_value {
value_binder.set(format!(
"{}",
clamp(old_value.saturating_sub(&step), min.get(), max.get())
));
debug_log!("i--");
} else if unparsed.is_empty() {
value_binder.set("1".to_string());
}
};
view! {
<div id=id.get() class=class_list!(class, "relative flex items-center mb-2")>
<GroupItemContextProvider class="rounded-none rounded-l-lg">
<TextInput name placeholder input_type=InputType::Text input_mode=InputMode::Numeric value=value_binder />
</GroupItemContextProvider>
<GroupItemContextProvider class="rounded-none border-x-0 !mr-0">
<Button icon=icon::DecrementIcon() on_click=dec_handler
attr:data-testid="decrement" />
</GroupItemContextProvider>
<GroupItemContextProvider class="rounded-none rounded-r-lg">
<Button icon=icon::IncrementIcon() on_click=inc_handler
attr:data-testid="increment"/>
</GroupItemContextProvider>
<GroupItemContextProvider class="">
<div/>
</GroupItemContextProvider>
</div>
}
}