1use crate::controls::{SliderColors, SliderSizing};
2use crate::ffi::{Orientation, PositionType, Unit};
3use crate::node::{flex_box, FlexBox};
4use crate::theme::Theme;
5use std::rc::Rc;
6
7fn clamp(value: f32, min: f32, max: f32) -> f32 {
8 if value < min {
9 return min;
10 }
11 if value > max {
12 return max;
13 }
14 value
15}
16
17fn resolve_thumb_size(metrics: SliderPresenterMetrics) -> f32 {
18 if metrics.thumb_size > 1.0 {
19 metrics.thumb_size
20 } else {
21 1.0
22 }
23}
24
25fn resolve_track_thickness(metrics: SliderPresenterMetrics) -> f32 {
26 clamp(metrics.track_thickness, 1.0, resolve_thumb_size(metrics))
27}
28
29fn resolve_cross_axis_extra(metrics: SliderPresenterMetrics) -> f32 {
30 if metrics.cross_axis_extra > 0.0 {
31 metrics.cross_axis_extra
32 } else {
33 0.0
34 }
35}
36
37#[derive(Clone, Copy, Debug, PartialEq)]
38pub struct SliderPresenterMetrics {
39 pub thumb_size: f32,
40 pub track_thickness: f32,
41 pub cross_axis_extra: f32,
42}
43
44impl SliderPresenterMetrics {
45 pub const fn new(thumb_size: f32, track_thickness: f32) -> Self {
46 Self {
47 thumb_size,
48 track_thickness,
49 cross_axis_extra: 2.0,
50 }
51 }
52
53 pub const fn with_cross_axis_extra(mut self, cross_axis_extra: f32) -> Self {
54 self.cross_axis_extra = cross_axis_extra;
55 self
56 }
57
58 pub const fn with_values(thumb_size: f32, track_thickness: f32, cross_axis_extra: f32) -> Self {
59 Self {
60 thumb_size,
61 track_thickness,
62 cross_axis_extra,
63 }
64 }
65}
66
67const DEFAULT_SLIDER_METRICS: SliderPresenterMetrics = SliderPresenterMetrics::new(18.0, 6.0);
68
69fn resolve_slider_metrics(sizing: Option<SliderSizing>) -> SliderPresenterMetrics {
70 let Some(sizing) = sizing else {
71 return DEFAULT_SLIDER_METRICS;
72 };
73 let thumb_size = if sizing.has_thumb_size() {
74 sizing.thumb_size_px()
75 } else {
76 DEFAULT_SLIDER_METRICS.thumb_size
77 };
78 let track_thickness = if sizing.has_track_thickness() {
79 sizing.track_thickness_px()
80 } else {
81 DEFAULT_SLIDER_METRICS.track_thickness
82 };
83 if (thumb_size - DEFAULT_SLIDER_METRICS.thumb_size).abs() <= f32::EPSILON
84 && (track_thickness - DEFAULT_SLIDER_METRICS.track_thickness).abs() <= f32::EPSILON
85 {
86 return DEFAULT_SLIDER_METRICS;
87 }
88 SliderPresenterMetrics::new(thumb_size, track_thickness)
89}
90
91#[derive(Clone, Copy, Debug, PartialEq)]
92pub struct SliderVisualState {
93 pub value: f32,
94 pub min: f32,
95 pub max: f32,
96 pub normalized_value: f32,
97 pub orientation: Orientation,
98 pub hovered: bool,
99 pub dragging: bool,
100 pub focused: bool,
101 pub enabled: bool,
102}
103
104impl SliderVisualState {
105 #[allow(clippy::too_many_arguments)]
106 pub fn new(
107 value: f32,
108 min: f32,
109 max: f32,
110 normalized_value: f32,
111 orientation: Orientation,
112 hovered: bool,
113 dragging: bool,
114 focused: bool,
115 enabled: bool,
116 ) -> Self {
117 Self {
118 value,
119 min,
120 max,
121 normalized_value,
122 orientation,
123 hovered,
124 dragging,
125 focused,
126 enabled,
127 }
128 }
129}
130
131pub trait SliderPresenter {
132 fn root(&self) -> FlexBox;
133 fn metrics(&self) -> SliderPresenterMetrics;
134 fn layout(&self, state: SliderVisualState, length: f32);
135 fn apply(&self, theme: Theme, state: SliderVisualState, colors: Option<SliderColors>);
136}
137
138pub trait SliderTemplate {
139 fn create(&self, sizing: Option<SliderSizing>) -> Rc<dyn SliderPresenter>;
140}
141
142#[derive(Clone)]
143pub struct DefaultSliderPresenter {
144 root: FlexBox,
145 metrics: SliderPresenterMetrics,
146 track_node: FlexBox,
147 fill_node: FlexBox,
148 thumb_node: FlexBox,
149}
150
151impl DefaultSliderPresenter {
152 pub fn new(metrics: SliderPresenterMetrics) -> Self {
153 let root = flex_box();
154 let track_node = flex_box();
155 track_node.position_type(PositionType::Absolute);
156 let fill_node = flex_box();
157 fill_node.position_type(PositionType::Absolute);
158 let thumb_node = flex_box();
159 thumb_node
160 .position_type(PositionType::Absolute)
161 .width(metrics.thumb_size, Unit::Pixel)
162 .height(metrics.thumb_size, Unit::Pixel);
163 root.child(&track_node).child(&fill_node).child(&thumb_node);
164 Self {
165 root,
166 metrics,
167 track_node,
168 fill_node,
169 thumb_node,
170 }
171 }
172}
173
174impl SliderPresenter for DefaultSliderPresenter {
175 fn root(&self) -> FlexBox {
176 self.root.clone()
177 }
178
179 fn metrics(&self) -> SliderPresenterMetrics {
180 self.metrics
181 }
182
183 fn layout(&self, state: SliderVisualState, length: f32) {
184 let metrics = self.metrics;
185 let thumb_size = resolve_thumb_size(metrics);
186 let track_thickness = resolve_track_thickness(metrics);
187 let cross_axis_extra = resolve_cross_axis_extra(metrics);
188 let available = if length > thumb_size {
189 length - thumb_size
190 } else {
191 0.0
192 };
193 let fraction = clamp(state.normalized_value, 0.0, 1.0);
194 let cross_axis_inset = cross_axis_extra * 0.5;
195 let track_offset = cross_axis_inset + ((thumb_size - track_thickness) * 0.5);
196 if state.orientation == Orientation::Vertical {
197 self.root
198 .width(thumb_size + cross_axis_extra, Unit::Pixel)
199 .height(length, Unit::Pixel);
200 self.track_node
201 .width(track_thickness, Unit::Pixel)
202 .height(available, Unit::Pixel)
203 .position(track_offset, thumb_size * 0.5);
204 self.fill_node
205 .width(track_thickness, Unit::Pixel)
206 .height(available * fraction, Unit::Pixel)
207 .position(
208 track_offset,
209 thumb_size * 0.5 + (available * (1.0 - fraction)),
210 );
211 self.thumb_node
212 .position(cross_axis_inset, available - (available * fraction));
213 return;
214 }
215
216 self.root
217 .width(length, Unit::Pixel)
218 .height(thumb_size + cross_axis_extra, Unit::Pixel);
219 self.track_node
220 .width(available, Unit::Pixel)
221 .height(track_thickness, Unit::Pixel)
222 .position(thumb_size * 0.5, track_offset);
223 self.fill_node
224 .width(available * fraction, Unit::Pixel)
225 .height(track_thickness, Unit::Pixel)
226 .position(thumb_size * 0.5, track_offset);
227 self.thumb_node
228 .position(available * fraction, cross_axis_inset);
229 }
230
231 fn apply(&self, theme: Theme, state: SliderVisualState, colors: Option<SliderColors>) {
232 let accent = if state.dragging {
233 theme.colors.accent_pressed
234 } else if state.hovered {
235 theme.colors.accent_hovered
236 } else {
237 theme.colors.accent
238 };
239 let metrics = self.metrics;
240 let thumb_size = resolve_thumb_size(metrics);
241 let track_thickness = resolve_track_thickness(metrics);
242 let track_radius = track_thickness * 0.5;
243 self.track_node.corner_radius(track_radius);
244 let track_color = colors
245 .filter(|colors| colors.has_track())
246 .map(|colors| colors.track_color())
247 .unwrap_or(theme.colors.scrollbar_track);
248 self.track_node.bg_color(track_color);
249 self.fill_node.corner_radius(track_radius);
250 let fill_color = colors
251 .filter(|colors| colors.has_fill())
252 .map(|colors| colors.fill_color())
253 .unwrap_or(accent);
254 self.fill_node.bg_color(fill_color);
255 self.thumb_node
256 .width(thumb_size, Unit::Pixel)
257 .height(thumb_size, Unit::Pixel)
258 .corner_radius(thumb_size * 0.5);
259 let thumb_color = colors
260 .filter(|colors| colors.has_thumb())
261 .map(|colors| colors.thumb_color())
262 .unwrap_or(accent);
263 self.thumb_node
264 .bg_color(thumb_color)
265 .border(1.0, theme.colors.surface);
266 }
267}
268
269#[derive(Clone, Copy, Debug, Default)]
270pub struct DefaultSliderTemplate;
271
272impl SliderTemplate for DefaultSliderTemplate {
273 fn create(&self, sizing: Option<SliderSizing>) -> Rc<dyn SliderPresenter> {
274 create_default_slider_presenter(sizing)
275 }
276}
277
278pub const DEFAULT_SLIDER_TEMPLATE: DefaultSliderTemplate = DefaultSliderTemplate;
279
280pub fn create_default_slider_presenter(sizing: Option<SliderSizing>) -> Rc<dyn SliderPresenter> {
281 Rc::new(DefaultSliderPresenter::new(resolve_slider_metrics(sizing)))
282}