1use hypersig::html::{El, div, input, label, span};
4use hypersig::style::pattern::stack;
5use hypersig::style::value::{
6 AlignItems, BorderRadius, BorderStyle, Cursor, Display, JustifyContent, Opacity, PointerEvents,
7 Position, pct, px, rem,
8};
9use hypersig::style::{Condition, PseudoClass, Style};
10use hypersig::{Expr, IntoExpr, Sig};
11
12use crate::field::label_style;
13use crate::look;
14use crate::token;
15
16#[derive(Debug)]
45pub struct Slider<S> {
46 label: String,
47 value: Sig<S, i32>,
48 min: i32,
49 max: i32,
50 step: i32,
51 value_text: Option<Expr<S, String>>,
52 disabled: Option<Expr<S, bool>>,
53}
54
55impl<S> Slider<S> {
56 #[must_use]
58 pub fn new(label: impl Into<String>, value: Sig<S, i32>) -> Self {
59 Self {
60 label: label.into(),
61 value,
62 min: 0,
63 max: 100,
64 step: 1,
65 value_text: None,
66 disabled: None,
67 }
68 }
69
70 #[must_use]
76 pub fn range(mut self, min: i32, max: i32) -> Self {
77 assert!(min < max, "a slider's minimum is below its maximum");
78 self.min = min;
79 self.max = max;
80 self
81 }
82
83 #[must_use]
89 pub fn step(mut self, step: i32) -> Self {
90 assert!(step > 0, "a slider's step is positive");
91 self.step = step;
92 self
93 }
94
95 #[must_use]
98 pub fn value_text(mut self, text: impl IntoExpr<S, String>) -> Self {
99 self.value_text = Some(text.into_expr());
100 self
101 }
102
103 #[must_use]
105 pub fn disabled(mut self, when: impl IntoExpr<S, bool>) -> Self {
106 self.disabled = Some(when.into_expr());
107 self
108 }
109
110 #[must_use]
112 pub fn render(self) -> El<S> {
113 let id = crate::id::of(self.value, "slider");
114 let span_of_range = f64::from(self.max) - f64::from(self.min);
115 let share = ((self.value.to_f64() - f64::from(self.min)) / span_of_range)
117 .max(0.0)
118 .min(1.0);
119 let shown = self
120 .value_text
121 .clone()
122 .unwrap_or_else(|| self.value.to_text());
123
124 let mut control = input()
125 .attr("type", "range")
126 .id(id.clone())
127 .attr("min", self.min.to_string())
128 .attr("max", self.max.to_string())
129 .attr("step", self.step.to_string())
130 .bind(self.value)
131 .css(input_style());
132 if let Some(text) = self.value_text {
133 control = control.bind_attr("aria-valuetext", text);
134 }
135 let mut out = div().css(stack(rem(0.75)));
136 if let Some(when) = self.disabled {
137 control = control.disabled(when.clone());
138 out = out.css_when(Style::new().opacity(Opacity::Number(0.5)), when);
141 }
142
143 out.child(
144 div()
145 .css(
146 Style::new()
147 .display(Display::Flex)
148 .align_items(AlignItems::Center)
149 .justify_content(JustifyContent::SpaceBetween),
150 )
151 .child(label().attr("for", id).css(label_style()).text(self.label))
152 .child(
153 span()
154 .attr("aria-hidden", "true")
155 .css(look::small_text().color(token::muted_foreground().var()))
156 .bind_text(shown),
157 ),
158 )
159 .child(
160 div()
161 .css(
162 Style::new()
163 .position(Position::Relative)
164 .height(rem(1))
165 .width(pct(100)),
166 )
167 .child(control)
168 .child(
169 div()
170 .css(track_style())
171 .child(div().css(fill_style()).bind_style(
172 hypersig::style::Property::Width,
173 (share.clone() * 100.0).to_fixed(3).concat("%"),
174 )),
175 )
176 .child(
177 div()
178 .css(thumb_style())
179 .bind_style(
182 hypersig::style::Property::Left,
183 Expr::lit(&String::from("calc((100% - 1rem) * "))
184 .concat(share.to_fixed(4))
185 .concat(")"),
186 ),
187 ),
188 )
189 }
190}
191
192fn input_style() -> Style {
195 Style::new()
196 .position(Position::Absolute)
197 .inset(px(0))
198 .width(pct(100))
199 .height(pct(100))
200 .margin(px(0))
201 .opacity(Opacity::Number(0.0))
202 .cursor(Cursor::Pointer)
203 .on(
204 PseudoClass::Disabled,
205 Style::new().cursor(Cursor::NotAllowed),
206 )
207 .peer()
208}
209
210fn track_style() -> Style {
211 Style::new()
212 .position(Position::Absolute)
213 .left(px(0))
214 .right(px(0))
215 .top(rem(0.3125))
216 .height(rem(0.375))
217 .border_radius(BorderRadius::from(px(999)))
218 .background_color(token::muted().var())
219 .pointer_events(PointerEvents::None)
220}
221
222fn fill_style() -> Style {
223 Style::new()
224 .height(pct(100))
225 .border_radius(BorderRadius::from(px(999)))
226 .background_color(token::primary().var())
227}
228
229fn thumb_style() -> Style {
230 Style::new()
231 .position(Position::Absolute)
232 .top(px(0))
233 .width(rem(1))
234 .height(rem(1))
235 .border_width(px(1))
236 .border_style(BorderStyle::Solid)
237 .border_color(token::primary().var())
238 .border_radius(BorderRadius::from(px(999)))
239 .background_color(token::background().var())
240 .box_shadow(hypersig::style::value::raw("0 1px 2px 0 rgb(0 0 0 / 0.05)"))
241 .pointer_events(PointerEvents::None)
242 .merge(look::peer_focus_ring())
243 .on(
244 Condition::Peer(PseudoClass::Disabled),
245 Style::new().opacity(Opacity::Number(0.5)),
246 )
247}
248
249impl<S> From<Slider<S>> for El<S> {
250 fn from(slider: Slider<S>) -> Self {
251 slider.render()
252 }
253}
254
255impl<S> From<Slider<S>> for hypersig::html::Node<S> {
256 fn from(slider: Slider<S>) -> Self {
257 slider.render().into()
258 }
259}