1use iced_core::{
6 Border, Clipboard, Color, Element, Event, Layout, Length, Point, Rectangle, Shadow, Shell,
7 Size, Widget,
8 layout::{Limits, Node},
9 mouse::{self, Cursor},
10 renderer, touch,
11 widget::tree::{self, Tree},
12};
13
14use std::ops::RangeInclusive;
15
16pub const DEFAULT_HEIGHT: f32 = 30.0;
18
19#[allow(missing_debug_implementations)]
21pub struct SlideBar<'a, T, Message>
22where
23 Message: Clone,
24{
25 pub width: Length,
27 pub height: Option<Length>,
29 pub color: Color,
31 pub background: Option<Color>,
33 pub border_radius: f32,
35 pub border_width: f32,
37 pub border_color: Color,
39 pub range: RangeInclusive<T>,
41 step: T,
43 value: T,
45 on_change: Box<dyn Fn(T) -> Message + 'a>,
47 on_release: Option<Message>,
49}
50
51impl<'a, T, Message> SlideBar<'a, T, Message>
52where
53 T: Copy + From<u8> + std::cmp::PartialOrd,
54 Message: Clone,
55{
56 pub fn new<F>(range: RangeInclusive<T>, value: T, on_change: F) -> Self
65 where
66 F: 'a + Fn(T) -> Message,
67 {
68 let value = if value >= *range.start() {
69 value
70 } else {
71 *range.start()
72 };
73
74 let value = if value <= *range.end() {
75 value
76 } else {
77 *range.end()
78 };
79
80 Self {
81 width: Length::Fill,
82 height: None,
83 color: Color::from([0.5; 3]),
84 background: None,
85 border_radius: 5.0,
86 border_width: 1.0,
87 border_color: Color::BLACK,
88 step: T::from(1),
89 value,
90 range,
91 on_change: Box::new(on_change),
92 on_release: None,
93 }
94 }
95
96 #[must_use]
103 pub fn on_release(mut self, on_release: Message) -> Self {
104 self.on_release = Some(on_release);
105 self
106 }
107
108 #[must_use]
110 pub fn width(mut self, width: impl Into<Length>) -> Self {
111 self.width = width.into();
112 self
113 }
114
115 #[must_use]
117 pub fn height(mut self, height: Option<Length>) -> Self {
118 self.height = height;
119 self
120 }
121
122 #[must_use]
124 pub fn step(mut self, step: impl Into<T>) -> Self {
125 self.step = step.into();
126 self
127 }
128}
129
130impl<T, Message, Theme, Renderer> Widget<Message, Theme, Renderer> for SlideBar<'_, T, Message>
131where
132 T: Copy + Into<f64> + num_traits::FromPrimitive,
133 Message: Clone,
134 Renderer: renderer::Renderer,
135{
136 fn tag(&self) -> tree::Tag {
137 tree::Tag::of::<State>()
138 }
139
140 fn state(&self) -> tree::State {
141 tree::State::new(State::new())
142 }
143
144 fn size(&self) -> Size<Length> {
145 Size {
146 width: self.width,
147 height: self.height.unwrap_or(Length::Fixed(DEFAULT_HEIGHT)),
148 }
149 }
150
151 fn layout(&mut self, _tree: &mut Tree, _renderer: &Renderer, limits: &Limits) -> Node {
152 let limits = limits
153 .width(self.width)
154 .height(self.height.unwrap_or(Length::Fixed(DEFAULT_HEIGHT)));
155
156 let size = limits.resolve(
157 self.width,
158 self.height.unwrap_or(Length::Fixed(DEFAULT_HEIGHT)),
159 Size::ZERO,
160 );
161
162 Node::new(size)
163 }
164
165 fn update(
166 &mut self,
167 tree: &mut Tree,
168 event: &Event,
169 layout: Layout<'_>,
170 cursor: mouse::Cursor,
171 _renderer: &Renderer,
172 _clipboard: &mut dyn Clipboard,
173 shell: &mut Shell<'_, Message>,
174 _viewport: &Rectangle,
175 ) {
176 update(
177 event,
178 layout,
179 cursor,
180 shell,
181 tree.state.downcast_mut::<State>(),
182 &mut self.value,
183 &self.range,
184 self.step,
185 self.on_change.as_ref(),
186 &self.on_release,
187 );
188 }
189
190 fn draw(
191 &self,
192 _tree: &Tree,
193 renderer: &mut Renderer,
194 _theme: &Theme,
195 _style: &renderer::Style,
196 layout: Layout<'_>,
197 _cursor: Cursor,
198 viewport: &Rectangle,
199 ) {
200 draw(renderer, layout, viewport, self);
201 }
202}
203
204#[allow(clippy::too_many_arguments)]
207pub fn update<Message, T>(
208 event: &Event,
209 layout: Layout<'_>,
210 cursor: mouse::Cursor,
211 shell: &mut Shell<'_, Message>,
212 state: &mut State,
213 value: &mut T,
214 range: &RangeInclusive<T>,
215 step: T,
216 on_change: &dyn Fn(T) -> Message,
217 on_release: &Option<Message>,
218) where
219 T: Copy + Into<f64> + num_traits::FromPrimitive,
220 Message: Clone,
221{
222 let is_dragging = state.is_dragging;
223
224 let mut change = |cursor_position: Point| {
225 let bounds = layout.bounds();
226 let new_value = if cursor_position.x <= bounds.x {
227 *range.start()
228 } else if cursor_position.x >= bounds.x + bounds.width {
229 *range.end()
230 } else {
231 let step = step.into();
232 let start = (*range.start()).into();
233 let end = (*range.end()).into();
234
235 let percent = f64::from(cursor_position.x - bounds.x) / f64::from(bounds.width);
236
237 let steps = (percent * (end - start) / step).round();
238 let value = steps * step + start;
239
240 if let Some(value) = T::from_f64(value) {
241 value
242 } else {
243 return;
244 }
245 };
246
247 if ((*value).into() - new_value.into()).abs() > f64::EPSILON {
248 shell.publish((on_change)(new_value));
249
250 *value = new_value;
251 }
252 };
253
254 match event {
255 Event::Mouse(mouse::Event::ButtonPressed(mouse::Button::Left))
256 | Event::Touch(touch::Event::FingerPressed { .. }) => {
257 if let Some(cursor_position) = cursor.position_over(layout.bounds()) {
258 change(cursor_position);
259 state.is_dragging = true;
260
261 shell.capture_event();
262 }
263 }
264 Event::Mouse(mouse::Event::ButtonReleased(mouse::Button::Left))
265 | Event::Touch(touch::Event::FingerLifted { .. } | touch::Event::FingerLost { .. }) => {
266 if is_dragging {
267 if let Some(on_release) = on_release.clone() {
268 shell.publish(on_release);
269 }
270 state.is_dragging = false;
271
272 shell.capture_event();
273 }
274 }
275 Event::Mouse(mouse::Event::CursorMoved { .. })
276 | Event::Touch(touch::Event::FingerMoved { .. }) => {
277 if is_dragging {
278 let _ = cursor.position().map(change);
279
280 shell.capture_event();
281 }
282 }
283 _ => {}
284 }
285}
286
287pub fn draw<T, R, Message>(
289 renderer: &mut R,
290 layout: Layout<'_>,
291 viewport: &Rectangle,
292 slider: &SlideBar<T, Message>,
293) where
294 T: Into<f64> + Copy,
295 Message: Clone,
296 R: renderer::Renderer,
297{
298 let bounds = layout.bounds();
299 let value = slider.value.into() as f32;
300 let (range_start, range_end) = {
301 let (start, end) = slider.range.clone().into_inner();
302
303 (start.into() as f32, end.into() as f32)
304 };
305
306 let active_progress_bounds = if range_start >= range_end {
307 Rectangle {
308 width: 0.0,
309 ..bounds
310 }
311 } else {
312 Rectangle {
313 width: bounds.width * (value - range_start) / (range_end - range_start),
314 ..bounds
315 }
316 };
317
318 let background = slider.background.unwrap_or_else(|| Color::from([1.0; 3]));
319
320 if bounds.intersects(viewport) {
321 renderer.fill_quad(
322 renderer::Quad {
323 bounds,
324 border: Border {
325 radius: slider.border_radius.into(),
326 width: slider.border_width,
327 color: slider.border_color,
328 },
329 shadow: Shadow::default(),
330 ..Default::default()
331 },
332 background,
333 );
334 }
335
336 if active_progress_bounds.intersects(viewport) {
337 renderer.fill_quad(
338 renderer::Quad {
339 bounds: active_progress_bounds,
340 border: Border {
341 radius: slider.border_radius.into(),
342 width: 0.0,
343 color: Color::TRANSPARENT,
344 },
345 shadow: Shadow::default(),
346 ..Default::default()
347 },
348 slider.color,
349 );
350 }
351}
352
353impl<'a, T, Message, Theme, Renderer> From<SlideBar<'a, T, Message>>
354 for Element<'a, Message, Theme, Renderer>
355where
356 T: 'a + Copy + Into<f64> + num_traits::FromPrimitive,
357 Renderer: 'a + renderer::Renderer,
358 Message: 'a + Clone,
359 Theme: 'a,
360{
361 fn from(value: SlideBar<'a, T, Message>) -> Self {
362 Self::new(value)
363 }
364}
365
366#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
368pub struct State {
369 is_dragging: bool,
370}
371
372impl State {
373 #[must_use]
375 pub fn new() -> Self {
376 Self::default()
377 }
378}