Skip to main content

fui/controls/internal/
slider_presenter.rs

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}