StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.
| 1 | use std::{ops::Range, sync::Arc}; |
| 2 | |
| 3 | use crate::platform::Cursor; |
| 4 | use crate::{ |
| 5 | elements::{ |
| 6 | AnchorPair, ConstrainedBox, Container, CornerRadius, DragAxis, Draggable, DraggableState, |
| 7 | DropShadow, Fill, Hoverable, MouseStateHandle, OffsetPositioning, OffsetType, |
| 8 | ParentElement, ParentOffsetBounds, PositionedElementOffsetBounds, PositioningAxis, Radius, |
| 9 | Rect, SavePosition, Stack, XAxisAnchor, YAxisAnchor, |
| 10 | }, |
| 11 | ui_components::components::UiComponentStyles, |
| 12 | AppContext, Element, EventContext, |
| 13 | }; |
| 14 | use lazy_static::lazy_static; |
| 15 | use parking_lot::{Mutex, RwLock}; |
| 16 | use pathfinder_color::ColorU; |
| 17 | use pathfinder_geometry::{rect::RectF, vector::vec2f}; |
| 18 | |
| 19 | use super::components::UiComponent; |
| 20 | |
| 21 | const DEFAULT_THUMB_SIZE: f32 = 18.; |
| 22 | const DEFAULT_TRACK_HEIGHT: f32 = 4.; |
| 23 | const HOVER_OPACITY: u8 = 100; |
| 24 | const HOVER_BORDER_SIZE: f32 = 10.; |
| 25 | |
| 26 | lazy_static! { |
| 27 | pub static ref DEFAULT_TRACK_COLOR: ColorU = ColorU::new(170, 170, 170, 255); |
| 28 | pub static ref DEFAULT_TRACK_FILL: Fill = Fill::Solid(ColorU::new(170, 170, 170, 255)); |
| 29 | pub static ref DEFAULT_THUMB_FILL: Fill = Fill::Solid(ColorU::white()); |
| 30 | static ref THUMB_DROP_SHADOW: DropShadow = DropShadow { |
| 31 | color: ColorU::black(), |
| 32 | offset: vec2f(-0.5, 2.), |
| 33 | blur_radius: 20., |
| 34 | spread_radius: 0., |
| 35 | }; |
| 36 | |
| 37 | /// A static counter of the number of instantiated sliders, which is used to create a unique |
| 38 | /// SavePosition ID to reference the position of the slider track, which is used to position |
| 39 | /// the slider thumb. |
| 40 | static ref TRACK_POSITION_ID_COUNT: RwLock<usize> = RwLock::new(0); |
| 41 | } |
| 42 | |
| 43 | #[derive(Clone, Copy, Default)] |
| 44 | struct SliderState { |
| 45 | // The thumb's current offset from the "beginning" (minimum) x-axis coordinate of the track. |
| 46 | thumb_offset_x: Option<f32>, |
| 47 | } |
| 48 | |
| 49 | #[derive(Clone, Default)] |
| 50 | pub struct SliderStateHandle { |
| 51 | thumb_hoverable_state: MouseStateHandle, |
| 52 | thumb_draggable_state: DraggableState, |
| 53 | track_hoverable_state: MouseStateHandle, |
| 54 | inner: Arc<Mutex<SliderState>>, |
| 55 | } |
| 56 | |
| 57 | impl SliderStateHandle { |
| 58 | // Returns the thumb's current offset from the "beginning" (minimum) x-axis coordinate of the |
| 59 | // track. |
| 60 | fn thumb_offset_x(&self) -> Option<f32> { |
| 61 | self.inner.lock().thumb_offset_x |
| 62 | } |
| 63 | |
| 64 | /// Sets the inner [`SliderState`] to `new_state`. |
| 65 | fn store(&self, new_state: SliderState) { |
| 66 | let mut guard = self.inner.lock(); |
| 67 | *guard = new_state; |
| 68 | } |
| 69 | |
| 70 | /// Resets the thumb's offset to `None`, which causes the default value to be |
| 71 | /// used when the slider is next rendered. |
| 72 | pub fn reset_offset(&self) { |
| 73 | self.store(SliderState { |
| 74 | thumb_offset_x: None, |
| 75 | }); |
| 76 | } |
| 77 | } |
| 78 | |
| 79 | /// Type alias for `on_drag` and `on_change` callbacks, either of which is executed when the slider's |
| 80 | /// value has changed. |
| 81 | type OnValueChangedFn = dyn Fn(&mut EventContext, &AppContext, f32) + 'static; |
| 82 | |
| 83 | /// Shared track geometry and snapping configuration passed to every |
| 84 | /// slider callback registration function. |
| 85 | #[derive(Clone)] |
| 86 | struct SliderTrackConfig { |
| 87 | track_position_id: String, |
| 88 | thumb_size: f32, |
| 89 | value_range: Range<f32>, |
| 90 | step: Option<f32>, |
| 91 | snap_values: Option<Arc<Vec<f32>>>, |
| 92 | state_handle: SliderStateHandle, |
| 93 | } |
| 94 | |
| 95 | /// Slider UiComponent for modulating a value between given bounds. |
| 96 | /// |
| 97 | /// Builder methods allow the caller to configure the styling of the slider, as well as set a |
| 98 | /// callback to be executed when the slider 'thumb' (handle) is dragged, as well as when the thumb |
| 99 | /// is dropped (marking the end of a 'drag'). |
| 100 | pub struct Slider { |
| 101 | state_handle: SliderStateHandle, |
| 102 | track_position_id: String, |
| 103 | on_drag_callback: Option<Box<OnValueChangedFn>>, |
| 104 | on_change_callback: Option<Arc<OnValueChangedFn>>, |
| 105 | thumb_size: f32, |
| 106 | track_height: f32, |
| 107 | track_fill: Fill, |
| 108 | thumb_fill: Fill, |
| 109 | styles: UiComponentStyles, |
| 110 | value_range: Range<f32>, |
| 111 | default_value: Option<f32>, |
| 112 | step: Option<f32>, |
| 113 | snap_values: Option<Arc<Vec<f32>>>, |
| 114 | } |
| 115 | |
| 116 | impl Slider { |
| 117 | pub fn new(slider_state_handle: SliderStateHandle) -> Self { |
| 118 | Self { |
| 119 | track_position_id: new_track_position_id(), |
| 120 | state_handle: slider_state_handle, |
| 121 | on_drag_callback: None, |
| 122 | on_change_callback: None, |
| 123 | thumb_size: DEFAULT_THUMB_SIZE, |
| 124 | track_height: DEFAULT_TRACK_HEIGHT, |
| 125 | track_fill: *DEFAULT_TRACK_FILL, |
| 126 | thumb_fill: *DEFAULT_THUMB_FILL, |
| 127 | value_range: 0.0..1., |
| 128 | default_value: None, |
| 129 | step: None, |
| 130 | snap_values: None, |
| 131 | styles: UiComponentStyles { |
| 132 | ..Default::default() |
| 133 | }, |
| 134 | } |
| 135 | } |
| 136 | |
| 137 | /// Sets a step size so that both the thumb and emitted value snap to |
| 138 | /// discrete increments of `step` from `value_range.start`. `value_range.end` |
| 139 | /// is always reachable even if it isn't step-aligned. |
| 140 | pub fn with_step(mut self, step: f32) -> Self { |
| 141 | self.step = Some(step); |
| 142 | self |
| 143 | } |
| 144 | |
| 145 | /// Sets an explicit list of discrete values that the slider snaps to. |
| 146 | /// Drag/drop/click events snap to the nearest value in the list by |
| 147 | /// absolute distance, and the thumb is positioned **linearly** based on |
| 148 | /// the value (`(value - start) / (end - start)`) — this keeps |
| 149 | /// non-step-aligned inputs from looking logarithmic. Takes precedence |
| 150 | /// over [`Self::with_step`] when set. |
| 151 | pub fn with_snap_values(mut self, values: Vec<f32>) -> Self { |
| 152 | self.snap_values = Some(Arc::new(values)); |
| 153 | self |
| 154 | } |
| 155 | |
| 156 | pub fn with_thumb_size(mut self, thumb_size: f32) -> Self { |
| 157 | self.thumb_size = thumb_size; |
| 158 | self |
| 159 | } |
| 160 | |
| 161 | pub fn with_thumb_fill(mut self, fill: Fill) -> Self { |
| 162 | self.thumb_fill = fill; |
| 163 | self |
| 164 | } |
| 165 | |
| 166 | pub fn with_track_fill(mut self, fill: Fill) -> Self { |
| 167 | self.track_fill = fill; |
| 168 | self |
| 169 | } |
| 170 | |
| 171 | pub fn with_track_height(mut self, height: f32) -> Self { |
| 172 | self.track_height = height; |
| 173 | self |
| 174 | } |
| 175 | |
| 176 | /// Sets the slider's value range. If set, values passed to the `on_change` callback are |
| 177 | /// normalized to the given range. |
| 178 | pub fn with_range(mut self, range: Range<f32>) -> Self { |
| 179 | self.value_range = range; |
| 180 | self |
| 181 | } |
| 182 | |
| 183 | pub fn with_default_value(mut self, value: f32) -> Self { |
| 184 | self.default_value = Some(value); |
| 185 | self |
| 186 | } |
| 187 | |
| 188 | /// Called when the value represented by the slider changes when the user drags the slider |
| 189 | /// thumb. The emitted value is normalized to the slider's value range, the default for |
| 190 | /// which is [0, 1]. |
| 191 | pub fn on_drag<F>(mut self, callback: F) -> Self |
| 192 | where |
| 193 | F: Fn(&mut EventContext, &AppContext, f32) + 'static, |
| 194 | { |
| 195 | self.on_drag_callback = Some(Box::new(callback)); |
| 196 | self |
| 197 | } |
| 198 | |
| 199 | /// Called when the slider thumb is 'dropped' at the end of a drag. The emitted value is |
| 200 | /// normalized to the slider's value range, the default for which is [0, 1]. |
| 201 | pub fn on_change<F>(mut self, callback: F) -> Self |
| 202 | where |
| 203 | F: Fn(&mut EventContext, &AppContext, f32) + 'static, |
| 204 | { |
| 205 | self.on_change_callback = Some(Arc::new(callback)); |
| 206 | self |
| 207 | } |
| 208 | |
| 209 | /// Registers the 'on_drag_start` callback on the `Draggable` element representing the slider |
| 210 | /// thumb. |
| 211 | /// |
| 212 | /// This callback stores the thumb's x-axis offset from the start of the track in the |
| 213 | /// given `SliderStateHandle`, snapping if configured. |
| 214 | fn register_on_drag_start_callback(thumb_draggable: &mut Draggable, config: SliderTrackConfig) { |
| 215 | thumb_draggable.set_on_drag_start(move |event_ctx, _app, thumb_position| { |
| 216 | let track_position = event_ctx |
| 217 | .element_position_by_id(config.track_position_id.as_str()) |
| 218 | .expect("Track should be laid out by the time the slider is dragged."); |
| 219 | |
| 220 | let raw_offset_x = thumb_position.origin_x() - track_position.origin_x(); |
| 221 | let draggable_width = draggable_width(track_position, config.thumb_size); |
| 222 | let (snapped_offset_x, _) = snap_offset_and_value( |
| 223 | raw_offset_x, |
| 224 | draggable_width, |
| 225 | &config.value_range, |
| 226 | config.step, |
| 227 | config.snap_values.as_deref().map(Vec::as_slice), |
| 228 | ); |
| 229 | |
| 230 | config.state_handle.store(SliderState { |
| 231 | thumb_offset_x: Some(snapped_offset_x), |
| 232 | }); |
| 233 | let delta = snapped_offset_x - raw_offset_x; |
| 234 | if delta.abs() > f32::EPSILON { |
| 235 | config |
| 236 | .state_handle |
| 237 | .thumb_draggable_state |
| 238 | .adjust_mouse_position(vec2f(delta, 0.)); |
| 239 | } |
| 240 | }); |
| 241 | } |
| 242 | |
| 243 | /// Registers the 'on_drag` callback on the `Draggable` element representing the slider thumb. |
| 244 | /// |
| 245 | /// The registered callback calls the user's supplied `on_drag` callback if the slider's x-axis |
| 246 | /// position has changed since the last time it was called. The user's callback is called with |
| 247 | /// the slider's current value, which is basically the slider thumb's offset x normalized to |
| 248 | /// the slider's `value_range`. In addition, it updates the `thumb_offset_x` in the slider's |
| 249 | /// state. |
| 250 | fn register_on_drag_callback( |
| 251 | thumb_draggable: &mut Draggable, |
| 252 | config: SliderTrackConfig, |
| 253 | on_drag_callback: Option<Box<OnValueChangedFn>>, |
| 254 | ) { |
| 255 | thumb_draggable.set_on_drag(move |event_ctx, app, thumb_position, _| { |
| 256 | let track_position = event_ctx |
| 257 | .element_position_by_id(config.track_position_id.as_str()) |
| 258 | .expect("Track should be laid out by the time the slider is dragged."); |
| 259 | |
| 260 | let raw_offset_x = thumb_position.origin_x() - track_position.origin_x(); |
| 261 | let draggable_width = draggable_width(track_position, config.thumb_size); |
| 262 | let (snapped_offset_x, snapped_value) = snap_offset_and_value( |
| 263 | raw_offset_x, |
| 264 | draggable_width, |
| 265 | &config.value_range, |
| 266 | config.step, |
| 267 | config.snap_values.as_deref().map(Vec::as_slice), |
| 268 | ); |
| 269 | |
| 270 | let delta = snapped_offset_x - raw_offset_x; |
| 271 | if delta.abs() > f32::EPSILON { |
| 272 | config |
| 273 | .state_handle |
| 274 | .thumb_draggable_state |
| 275 | .adjust_mouse_position(vec2f(delta, 0.)); |
| 276 | } |
| 277 | |
| 278 | if Some(snapped_offset_x) != config.state_handle.thumb_offset_x() { |
| 279 | config.state_handle.store(SliderState { |
| 280 | thumb_offset_x: Some(snapped_offset_x), |
| 281 | }); |
| 282 | |
| 283 | if let Some(callback) = &on_drag_callback { |
| 284 | callback(event_ctx, app, snapped_value); |
| 285 | } |
| 286 | } |
| 287 | }); |
| 288 | } |
| 289 | |
| 290 | /// Registers the 'on_change` callback on the `Draggable` element representing the slider thumb. |
| 291 | /// |
| 292 | /// The registered callback unconditinoally calls the user's supplied `on_change` callback. The |
| 293 | /// user's callback is called with the slider's current value, which is basically the slider |
| 294 | /// thumb's offset x normalized to the slider's `value_range`. In addition, it updates the |
| 295 | /// `thumb_offset_x` in the slider's state. |
| 296 | fn register_on_drop_callback( |
| 297 | thumb_draggable: &mut Draggable, |
| 298 | config: SliderTrackConfig, |
| 299 | on_change_callback: Option<Arc<OnValueChangedFn>>, |
| 300 | ) { |
| 301 | thumb_draggable.set_on_drop(move |event_ctx, app, thumb_position, _| { |
| 302 | let track_position = event_ctx |
| 303 | .element_position_by_id(config.track_position_id.as_str()) |
| 304 | .expect("Track should be laid out by the time the slider is dropped."); |
| 305 | let raw_offset_x = thumb_position.origin_x() - track_position.origin_x(); |
| 306 | let draggable_width = draggable_width(track_position, config.thumb_size); |
| 307 | let (snapped_offset_x, snapped_value) = snap_offset_and_value( |
| 308 | raw_offset_x, |
| 309 | draggable_width, |
| 310 | &config.value_range, |
| 311 | config.step, |
| 312 | config.snap_values.as_deref().map(Vec::as_slice), |
| 313 | ); |
| 314 | config.state_handle.store(SliderState { |
| 315 | thumb_offset_x: Some(snapped_offset_x), |
| 316 | }); |
| 317 | |
| 318 | if let Some(callback) = &on_change_callback { |
| 319 | callback(event_ctx, app, snapped_value); |
| 320 | } |
| 321 | }); |
| 322 | } |
| 323 | |
| 324 | /// Registers the 'on_change_callback` callback on the `Hoverable` element representing the slider track. |
| 325 | /// |
| 326 | /// Whenever the underlying track is clicked, we set the thumb offset to the location of the click, |
| 327 | /// and then call the on_change_callback with the updated value. Basically works as if a user immediately |
| 328 | /// dragged the thumb to that location, without all the intermediate on_drag calls. |
| 329 | fn register_on_click_callback( |
| 330 | track_hoverable: Hoverable, |
| 331 | config: SliderTrackConfig, |
| 332 | on_change_callback: Option<Arc<OnValueChangedFn>>, |
| 333 | ) -> Hoverable { |
| 334 | track_hoverable.on_click(move |event_ctx, app, click_position| { |
| 335 | let Some(track_position) = |
| 336 | event_ctx.element_position_by_id(config.track_position_id.as_str()) |
| 337 | else { |
| 338 | return; |
| 339 | }; |
| 340 | |
| 341 | let click_position_x = click_position.x(); |
| 342 | let padding = config.thumb_size / 2.; |
| 343 | let min_x = track_position.min_x() + padding; |
| 344 | let max_x = track_position.max_x() - padding; |
| 345 | |
| 346 | if min_x > click_position_x || max_x < click_position_x { |
| 347 | return; |
| 348 | } |
| 349 | |
| 350 | let raw_offset_x = click_position_x - min_x; |
| 351 | let draggable_width = draggable_width(track_position, config.thumb_size); |
| 352 | let (snapped_offset_x, snapped_value) = snap_offset_and_value( |
| 353 | raw_offset_x, |
| 354 | draggable_width, |
| 355 | &config.value_range, |
| 356 | config.step, |
| 357 | config.snap_values.as_deref().map(Vec::as_slice), |
| 358 | ); |
| 359 | |
| 360 | config.state_handle.store(SliderState { |
| 361 | thumb_offset_x: Some(snapped_offset_x), |
| 362 | }); |
| 363 | |
| 364 | if let Some(callback) = &on_change_callback { |
| 365 | callback(event_ctx, app, snapped_value); |
| 366 | } |
| 367 | }) |
| 368 | } |
| 369 | } |
| 370 | |
| 371 | /// Snaps `raw_offset_x` (a pixel offset along the slider track) to the |
| 372 | /// nearest discrete position, returning both the snapped pixel offset and |
| 373 | /// the corresponding value. |
| 374 | /// |
| 375 | /// If `snap_values` is provided it takes precedence: the raw value (linearly |
| 376 | /// derived from the pixel position and `value_range`) is snapped to the |
| 377 | /// nearest entry in the list by absolute distance, and the returned pixel |
| 378 | /// offset is positioned **linearly** by the snapped value — so positions |
| 379 | /// along the slider always match the value scale. Otherwise `step` (if any) |
| 380 | /// is used for linear stepping from `value_range.start`. |
| 381 | fn snap_offset_and_value( |
| 382 | raw_offset_x: f32, |
| 383 | draggable_width: f32, |
| 384 | value_range: &Range<f32>, |
| 385 | step: Option<f32>, |
| 386 | snap_values: Option<&[f32]>, |
| 387 | ) -> (f32, f32) { |
| 388 | if draggable_width <= 0. { |
| 389 | return (raw_offset_x, value_range.start); |
| 390 | } |
| 391 | let canonical = (raw_offset_x / draggable_width).clamp(0., 1.); |
| 392 | let raw_value = canonical * (value_range.end - value_range.start) + value_range.start; |
| 393 | |
| 394 | if let Some(values) = snap_values { |
| 395 | if !values.is_empty() { |
| 396 | // Snap to nearest value by absolute distance. |
| 397 | let snapped_value = values.iter().copied().fold(values[0], |best, v| { |
| 398 | if (v - raw_value).abs() < (best - raw_value).abs() { |
| 399 | v |
| 400 | } else { |
| 401 | best |
| 402 | } |
| 403 | }); |
| 404 | let snapped_canonical = value_to_canonical_linear(snapped_value, value_range); |
| 405 | return (snapped_canonical * draggable_width, snapped_value); |
| 406 | } |
| 407 | } |
| 408 | |
| 409 | let Some(step) = step.filter(|s| *s > 0.) else { |
| 410 | return (raw_offset_x, raw_value); |
| 411 | }; |
| 412 | |
| 413 | // Snap to nearest step from `range.start`, with `range.end` always reachable. |
| 414 | let snapped_value = if value_range.end - raw_value < step / 2. { |
| 415 | value_range.end |
| 416 | } else { |
| 417 | let offset_from_start = raw_value - value_range.start; |
| 418 | let steps = (offset_from_start / step).round(); |
| 419 | (value_range.start + steps * step).clamp(value_range.start, value_range.end) |
| 420 | }; |
| 421 | |
| 422 | let snapped_canonical = value_to_canonical_linear(snapped_value, value_range); |
| 423 | (snapped_canonical * draggable_width, snapped_value) |
| 424 | } |
| 425 | |
| 426 | /// Linearly maps `value` to a canonical 0..1 position along the slider |
| 427 | /// track. Used both for snap positioning and for rendering `default_value`, |
| 428 | /// so non-snap values (e.g. typed into a freeform input box) render at a |
| 429 | /// position proportional to their actual magnitude. |
| 430 | fn value_to_canonical_linear(value: f32, value_range: &Range<f32>) -> f32 { |
| 431 | let span = value_range.end - value_range.start; |
| 432 | if span <= 0. { |
| 433 | return 0.; |
| 434 | } |
| 435 | ((value - value_range.start) / span).clamp(0., 1.) |
| 436 | } |
| 437 | |
| 438 | impl UiComponent for Slider { |
| 439 | type ElementType = Container; |
| 440 | |
| 441 | fn build(self) -> Self::ElementType { |
| 442 | let Slider { |
| 443 | state_handle, |
| 444 | track_position_id: slider_track_position_id, |
| 445 | on_drag_callback, |
| 446 | on_change_callback, |
| 447 | thumb_size, |
| 448 | track_height, |
| 449 | track_fill, |
| 450 | thumb_fill, |
| 451 | styles, |
| 452 | value_range, |
| 453 | default_value, |
| 454 | step, |
| 455 | snap_values, |
| 456 | } = self; |
| 457 | |
| 458 | let track_position_id = slider_track_position_id.clone(); |
| 459 | let mut slider_thumb = Draggable::new( |
| 460 | state_handle.thumb_draggable_state.clone(), |
| 461 | render_thumb( |
| 462 | thumb_fill, |
| 463 | thumb_size, |
| 464 | state_handle.thumb_hoverable_state.clone(), |
| 465 | ), |
| 466 | ) |
| 467 | .with_drag_axis(DragAxis::HorizontalOnly) |
| 468 | .with_drag_bounds_callback(move |position_cache, _| { |
| 469 | position_cache |
| 470 | .get_position(track_position_id.as_str()) |
| 471 | .map(|track_position| { |
| 472 | // Set drag bounds so the thumb may only be dragged along the track. |
| 473 | RectF::new( |
| 474 | vec2f(track_position.origin_x(), track_position.origin_y()), |
| 475 | vec2f(track_position.width(), 0.), |
| 476 | ) |
| 477 | }) |
| 478 | }); |
| 479 | |
| 480 | let config = SliderTrackConfig { |
| 481 | track_position_id: slider_track_position_id.clone(), |
| 482 | thumb_size, |
| 483 | value_range: value_range.clone(), |
| 484 | step, |
| 485 | snap_values, |
| 486 | state_handle: state_handle.clone(), |
| 487 | }; |
| 488 | |
| 489 | Self::register_on_drag_start_callback(&mut slider_thumb, config.clone()); |
| 490 | Self::register_on_drag_callback(&mut slider_thumb, config.clone(), on_drag_callback); |
| 491 | Self::register_on_drop_callback( |
| 492 | &mut slider_thumb, |
| 493 | config.clone(), |
| 494 | on_change_callback.clone(), |
| 495 | ); |
| 496 | |
| 497 | let track = Hoverable::new(state_handle.track_hoverable_state.clone(), |_| { |
| 498 | render_track(thumb_size, styles.width, track_height, track_fill) |
| 499 | }); |
| 500 | |
| 501 | let track = Self::register_on_click_callback(track, config, on_change_callback.clone()); |
| 502 | |
| 503 | let mut slider = Stack::new(); |
| 504 | |
| 505 | slider.add_child( |
| 506 | SavePosition::new(track.finish(), slider_track_position_id.as_str()).finish(), |
| 507 | ); |
| 508 | |
| 509 | let offset = match state_handle.thumb_offset_x() { |
| 510 | Some(offset_x) => OffsetType::Pixel(offset_x), |
| 511 | None => OffsetType::Percentage( |
| 512 | default_value |
| 513 | .map(|value| value_to_canonical_linear(value, &value_range)) |
| 514 | .unwrap_or(0.), |
| 515 | ), |
| 516 | }; |
| 517 | |
| 518 | slider.add_positioned_child( |
| 519 | slider_thumb.finish(), |
| 520 | OffsetPositioning::from_axes( |
| 521 | PositioningAxis::relative_to_stack_child( |
| 522 | &slider_track_position_id, |
| 523 | PositionedElementOffsetBounds::AnchoredElement, |
| 524 | // Set the position of the thumb based on the slider's current value. |
| 525 | offset, |
| 526 | AnchorPair::new(XAxisAnchor::Left, XAxisAnchor::Left), |
| 527 | ), |
| 528 | PositioningAxis::relative_to_stack_child( |
| 529 | &slider_track_position_id, |
| 530 | PositionedElementOffsetBounds::Unbounded, |
| 531 | OffsetType::Pixel(0.), |
| 532 | AnchorPair::new(YAxisAnchor::Middle, YAxisAnchor::Middle), |
| 533 | ), |
| 534 | ), |
| 535 | ); |
| 536 | Container::new(slider.finish()) |
| 537 | .with_margin_top(styles.margin.map(|margin| margin.top).unwrap_or(0.)) |
| 538 | .with_margin_bottom(styles.margin.map(|margin| margin.bottom).unwrap_or(0.)) |
| 539 | .with_margin_left(styles.margin.map(|margin| margin.left).unwrap_or(0.)) |
| 540 | .with_margin_right(styles.margin.map(|margin| margin.right).unwrap_or(0.)) |
| 541 | .with_padding_top(styles.padding.map(|padding| padding.top).unwrap_or(0.)) |
| 542 | .with_padding_bottom(styles.padding.map(|padding| padding.bottom).unwrap_or(0.)) |
| 543 | .with_padding_left(styles.padding.map(|padding| padding.left).unwrap_or(0.)) |
| 544 | .with_padding_right(styles.padding.map(|padding| padding.right).unwrap_or(0.)) |
| 545 | } |
| 546 | |
| 547 | fn with_style(self, styles: UiComponentStyles) -> Self { |
| 548 | Self { |
| 549 | state_handle: self.state_handle, |
| 550 | track_position_id: self.track_position_id, |
| 551 | on_drag_callback: self.on_drag_callback, |
| 552 | on_change_callback: self.on_change_callback, |
| 553 | thumb_size: self.thumb_size, |
| 554 | track_height: self.track_height, |
| 555 | track_fill: self.track_fill, |
| 556 | thumb_fill: self.thumb_fill, |
| 557 | value_range: self.value_range, |
| 558 | default_value: self.default_value, |
| 559 | step: self.step, |
| 560 | snap_values: self.snap_values, |
| 561 | styles: self.styles.merge(styles), |
| 562 | } |
| 563 | } |
| 564 | } |
| 565 | |
| 566 | /// Renders the slider 'track', along which the thumb can be dragged. |
| 567 | fn render_track(thumb_size: f32, width: Option<f32>, height: f32, fill: Fill) -> Box<dyn Element> { |
| 568 | let mut track = ConstrainedBox::new( |
| 569 | Container::new( |
| 570 | Rect::new() |
| 571 | .with_background(fill) |
| 572 | .with_corner_radius(CornerRadius::with_all(Radius::Percentage(50.))) |
| 573 | .finish(), |
| 574 | ) |
| 575 | .with_padding_left(thumb_size / 2.) |
| 576 | .with_padding_right(thumb_size / 2.) |
| 577 | .finish(), |
| 578 | ) |
| 579 | .with_height(height); |
| 580 | if let Some(width) = width { |
| 581 | track = track.with_width(width); |
| 582 | } |
| 583 | |
| 584 | // We add a container with extra padding to make the track |
| 585 | // as tall (invisibly) as the thumb. This way, we can detect |
| 586 | // clicks that are slightly above or below the track bar itself. |
| 587 | let vertical_padding = ((thumb_size - height) / 2.).max(0.); |
| 588 | Container::new(track.finish()) |
| 589 | .with_padding_top(vertical_padding) |
| 590 | .with_padding_bottom(vertical_padding) |
| 591 | .finish() |
| 592 | } |
| 593 | |
| 594 | /// Renders the 'thumb' (handle) for the slider. |
| 595 | /// |
| 596 | /// The thumb is a circle with diameter set to `size`. |
| 597 | fn render_thumb(fill: Fill, size: f32, state_handle: MouseStateHandle) -> Box<dyn Element> { |
| 598 | Hoverable::new(state_handle, move |hover_state| { |
| 599 | let thumb = Container::new( |
| 600 | ConstrainedBox::new( |
| 601 | Rect::new() |
| 602 | .with_background(fill) |
| 603 | .with_corner_radius(CornerRadius::with_all(Radius::Percentage(50.))) |
| 604 | .with_drop_shadow(*THUMB_DROP_SHADOW) |
| 605 | .finish(), |
| 606 | ) |
| 607 | .with_width(size) |
| 608 | .with_height(size) |
| 609 | .finish(), |
| 610 | ) |
| 611 | .finish(); |
| 612 | let mut stack = Stack::new(); |
| 613 | |
| 614 | if hover_state.is_hovered() { |
| 615 | let hover_size = size + HOVER_BORDER_SIZE; |
| 616 | let mut hover_background = *DEFAULT_TRACK_COLOR; |
| 617 | hover_background.a = HOVER_OPACITY; |
| 618 | |
| 619 | let thumb_hover = Container::new( |
| 620 | ConstrainedBox::new( |
| 621 | Rect::new() |
| 622 | .with_background_color(hover_background) |
| 623 | .with_corner_radius(CornerRadius::with_all(Radius::Percentage(50.))) |
| 624 | .finish(), |
| 625 | ) |
| 626 | .with_width(hover_size) |
| 627 | .with_height(hover_size) |
| 628 | .finish(), |
| 629 | ) |
| 630 | .finish(); |
| 631 | |
| 632 | // Position the hover so that it's centered around the thumb. Since the hover |
| 633 | // is guaranteed to be larger than the thumb, we position the hover at the top |
| 634 | // left corner of the thumb and then translate it to the left and up so that it |
| 635 | // is centered. |
| 636 | stack.add_positioned_child( |
| 637 | thumb_hover, |
| 638 | OffsetPositioning::from_axes( |
| 639 | PositioningAxis::relative_to_parent( |
| 640 | ParentOffsetBounds::Unbounded, |
| 641 | OffsetType::Pixel(-((hover_size - size) / 2.)), |
| 642 | AnchorPair::new(XAxisAnchor::Left, XAxisAnchor::Left), |
| 643 | ), |
| 644 | PositioningAxis::relative_to_parent( |
| 645 | ParentOffsetBounds::Unbounded, |
| 646 | OffsetType::Pixel(-((hover_size - size) / 2.)), |
| 647 | AnchorPair::new(YAxisAnchor::Top, YAxisAnchor::Top), |
| 648 | ), |
| 649 | ), |
| 650 | ); |
| 651 | } |
| 652 | |
| 653 | stack.add_child(thumb); |
| 654 | stack.finish() |
| 655 | }) |
| 656 | .with_cursor(Cursor::PointingHand) |
| 657 | .finish() |
| 658 | } |
| 659 | |
| 660 | /// Returns a unique position ID for the slider track. |
| 661 | fn new_track_position_id() -> String { |
| 662 | let current_count = *TRACK_POSITION_ID_COUNT.read(); |
| 663 | let position_id = format!("SliderTrack{current_count}"); |
| 664 | *TRACK_POSITION_ID_COUNT.write() = current_count + 1; |
| 665 | position_id |
| 666 | } |
| 667 | |
| 668 | /// Returns total width of the draggable area on the 'track'. |
| 669 | fn draggable_width(track_position: RectF, thumb_size: f32) -> f32 { |
| 670 | track_position.max_x() - track_position.min_x() - thumb_size |
| 671 | } |
| 672 |