1use std::fmt;
4
5use hypersig::html::{Bindable, El, input};
6use hypersig::style::value::rem;
7use hypersig::{Form, IntoExpr, Sig, Validate};
8
9use crate::field::{Frame, field_style};
10
11#[diagnostic::on_unimplemented(
16 message = "a number field cannot be bound to a `{Self}` signal",
17 note = "bind an `i32` or `f64` signal; text goes in a `TextField`"
18)]
19pub trait Numeric: Bindable + Copy + PartialOrd + fmt::Display + sealed::Sealed {}
20
21impl Numeric for i32 {}
22impl Numeric for f64 {}
23
24mod sealed {
25 pub trait Sealed {
26 const ZERO: Self;
28 const DEFAULT_STEP: Option<&'static str>;
31 const NON_NEGATIVE_MODE: &'static str;
33 fn is_number(&self) -> bool;
35 }
36
37 impl Sealed for i32 {
38 const ZERO: Self = 0;
39 const DEFAULT_STEP: Option<&'static str> = None;
40 const NON_NEGATIVE_MODE: &'static str = "numeric";
41 fn is_number(&self) -> bool {
42 true
43 }
44 }
45
46 impl Sealed for f64 {
47 const ZERO: Self = 0.0;
48 const DEFAULT_STEP: Option<&'static str> = Some("any");
49 const NON_NEGATIVE_MODE: &'static str = "decimal";
50 fn is_number(&self) -> bool {
51 !self.is_nan()
52 }
53 }
54}
55
56#[derive(Debug)]
83pub struct NumberField<S, T> {
84 value: Sig<S, T>,
85 min: Option<T>,
86 max: Option<T>,
87 step: Option<T>,
88 frame: Frame<S>,
89}
90
91impl<S, T: Numeric> NumberField<S, T> {
92 #[must_use]
94 pub fn new(label: impl Into<String>, value: Sig<S, T>) -> Self {
95 Self {
96 value,
97 min: None,
98 max: None,
99 step: None,
100 frame: Frame::new(label.into()),
101 }
102 }
103
104 #[must_use]
110 pub fn in_form<V: Validate>(
111 label: impl Into<String>,
112 form: Sig<S, Form<V>>,
113 field: Sig<V, T>,
114 ) -> Self {
115 let mut out = Self::new(label, form.field(field));
116 out.frame = out.frame.in_form(form, field);
117 out
118 }
119
120 #[must_use]
126 pub fn min(mut self, min: T) -> Self {
127 assert!(min.is_number(), "a number field's minimum is a number");
128 assert!(
129 self.max.is_none_or(|max| min <= max),
130 "a number field's minimum is not above its maximum"
131 );
132 self.min = Some(min);
133 self
134 }
135
136 #[must_use]
142 pub fn max(mut self, max: T) -> Self {
143 assert!(max.is_number(), "a number field's maximum is a number");
144 assert!(
145 self.min.is_none_or(|min| min <= max),
146 "a number field's maximum is not below its minimum"
147 );
148 self.max = Some(max);
149 self
150 }
151
152 #[must_use]
159 pub fn step(mut self, step: T) -> Self {
160 assert!(step > T::ZERO, "a number field's step is above zero");
161 self.step = Some(step);
162 self
163 }
164
165 #[must_use]
167 pub fn description(mut self, text: impl Into<String>) -> Self {
168 self.frame.description = Some(text.into());
169 self
170 }
171
172 #[must_use]
175 pub fn error(mut self, message: impl IntoExpr<S, String>) -> Self {
176 self.frame.error = Some(message.into_expr());
177 self
178 }
179
180 #[must_use]
183 pub fn required(mut self) -> Self {
184 self.frame.required = true;
185 self
186 }
187
188 #[must_use]
190 pub fn disabled(mut self, when: impl IntoExpr<S, bool>) -> Self {
191 self.frame.disabled = Some(when.into_expr());
192 self
193 }
194
195 #[must_use]
197 pub fn render(self) -> El<S> {
198 let mut control = input()
199 .attr("type", "number")
200 .css(field_style().height(rem(2.25)))
201 .bind(self.value);
202 if self.min.is_some_and(|min| min >= T::ZERO) {
203 control = control.attr("inputmode", T::NON_NEGATIVE_MODE);
204 }
205 if let Some(min) = self.min {
206 control = control.attr("min", min.to_string());
207 }
208 if let Some(max) = self.max {
209 control = control.attr("max", max.to_string());
210 }
211 match (self.step, T::DEFAULT_STEP) {
212 (Some(step), _) => control = control.attr("step", step.to_string()),
213 (None, Some(step)) => control = control.attr("step", step),
214 (None, None) => {}
215 }
216 self.frame.render(&crate::id::of(self.value, ""), control)
217 }
218}
219
220impl<S, T: Numeric> From<NumberField<S, T>> for El<S> {
221 fn from(field: NumberField<S, T>) -> Self {
222 field.render()
223 }
224}
225
226impl<S, T: Numeric> From<NumberField<S, T>> for hypersig::html::Node<S> {
227 fn from(field: NumberField<S, T>) -> Self {
228 field.render().into()
229 }
230}