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            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}