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 let fill_length = (available * fraction).round();
198 self.root
199 .width(thumb_size + cross_axis_extra, Unit::Pixel)
200 .height(length, Unit::Pixel);
201 self.track_node
202 .width(track_thickness, Unit::Pixel)
203 .height(available, Unit::Pixel)
204 .position(track_offset, thumb_size * 0.5);
205 self.fill_node
206 .width(track_thickness, Unit::Pixel)
207 .height(fill_length, Unit::Pixel)
208 .position(
209 track_offset,
210 thumb_size * 0.5 + available - fill_length,
211 );
212 self.thumb_node
213 .position(cross_axis_inset, available - (available * fraction));
214 return;
215 }
216
217 self.root
218 .width(length, Unit::Pixel)
219 .height(thumb_size + cross_axis_extra, Unit::Pixel);
220 self.track_node
221 .width(available, Unit::Pixel)
222 .height(track_thickness, Unit::Pixel)
223 .position(thumb_size * 0.5, track_offset);
224 self.fill_node
225 .width(available * fraction, Unit::Pixel)
226 .height(track_thickness, Unit::Pixel)
227 .position(thumb_size * 0.5, track_offset);
228 self.thumb_node
229 .position(available * fraction, cross_axis_inset);
230 }
231
232 fn apply(&self, theme: Theme, state: SliderVisualState, colors: Option<SliderColors>) {
233 let accent = if state.dragging {
234 theme.colors.accent_pressed
235 } else if state.hovered {
236 theme.colors.accent_hovered
237 } else {
238 theme.colors.accent
239 };
240 let metrics = self.metrics;
241 let thumb_size = resolve_thumb_size(metrics);
242 let track_thickness = resolve_track_thickness(metrics);
243 let track_radius = track_thickness * 0.5;
244 self.track_node.corner_radius(track_radius);
245 let track_color = colors
246 .filter(|colors| colors.has_track())
247 .map(|colors| colors.track_color())
248 .unwrap_or(theme.colors.scrollbar_track);
249 self.track_node.bg_color(track_color);
250 self.fill_node.corner_radius(track_radius);
251 let fill_color = colors
252 .filter(|colors| colors.has_fill())
253 .map(|colors| colors.fill_color())
254 .unwrap_or(accent);
255 self.fill_node.bg_color(fill_color);
256 self.thumb_node
257 .width(thumb_size, Unit::Pixel)
258 .height(thumb_size, Unit::Pixel)
259 .corner_radius(thumb_size * 0.5);
260 let thumb_color = colors
261 .filter(|colors| colors.has_thumb())
262 .map(|colors| colors.thumb_color())
263 .unwrap_or(accent);
264 self.thumb_node
265 .bg_color(thumb_color)
266 .border(1.0, theme.colors.surface);
267 }
268}
269
270#[derive(Clone, Copy, Debug, Default)]
271pub struct DefaultSliderTemplate;
272
273impl SliderTemplate for DefaultSliderTemplate {
274 fn create(&self, sizing: Option<SliderSizing>) -> Rc<dyn SliderPresenter> {
275 create_default_slider_presenter(sizing)
276 }
277}
278
279pub const DEFAULT_SLIDER_TEMPLATE: DefaultSliderTemplate = DefaultSliderTemplate;
280
281pub fn create_default_slider_presenter(sizing: Option<SliderSizing>) -> Rc<dyn SliderPresenter> {
282 Rc::new(DefaultSliderPresenter::new(resolve_slider_metrics(sizing)))
283}