tessera_ui_basic_components/
slider.rs

1use std::sync::Arc;
2
3use derive_builder::Builder;
4use parking_lot::Mutex;
5use tessera_ui::{
6    Color, ComputedData, Constraint, CursorEventContent, DimensionValue, Dp, Px, PxPosition,
7    focus_state::Focus, winit::window::CursorIcon,
8};
9use tessera_ui_macros::tessera;
10
11use crate::{
12    shape_def::Shape,
13    surface::{SurfaceArgsBuilder, surface},
14};
15
16/// State for the `slider` component.
17pub struct SliderState {
18    /// True if the user is currently dragging the slider.
19    pub is_dragging: bool,
20    /// The focus handler for the slider.
21    pub focus: Focus,
22}
23
24impl Default for SliderState {
25    fn default() -> Self {
26        Self::new()
27    }
28}
29
30impl SliderState {
31    pub fn new() -> Self {
32        Self {
33            is_dragging: false,
34            focus: Focus::new(),
35        }
36    }
37}
38
39/// Arguments for the `slider` component.
40#[derive(Builder, Clone)]
41#[builder(pattern = "owned")]
42pub struct SliderArgs {
43    /// The current value of the slider, ranging from 0.0 to 1.0.
44    #[builder(default = "0.0")]
45    pub value: f32,
46
47    /// Callback function triggered when the slider's value changes.
48    #[builder(default = "Arc::new(|_| {})")]
49    pub on_change: Arc<dyn Fn(f32) + Send + Sync>,
50
51    /// The width of the slider track.
52    #[builder(default = "Dp(200.0)")]
53    pub width: Dp,
54
55    /// The height of the slider track.
56    #[builder(default = "Dp(12.0)")]
57    pub track_height: Dp,
58
59    /// The color of the active part of the track (progress fill).
60    #[builder(default = "Color::new(0.2, 0.5, 0.8, 1.0)")]
61    pub active_track_color: Color,
62
63    /// The color of the inactive part of the track (background).
64    #[builder(default = "Color::new(0.8, 0.8, 0.8, 1.0)")]
65    pub inactive_track_color: Color,
66
67    /// Disable interaction.
68    #[builder(default = "false")]
69    pub disabled: bool,
70}
71
72#[tessera]
73pub fn slider(args: impl Into<SliderArgs>, state: Arc<Mutex<SliderState>>) {
74    let args: SliderArgs = args.into();
75
76    // Background track (inactive part) - capsule shape
77    surface(
78        SurfaceArgsBuilder::default()
79            .width(DimensionValue::Fixed(args.width.to_px()))
80            .height(DimensionValue::Fixed(args.track_height.to_px()))
81            .color(args.inactive_track_color)
82            .shape(Shape::RoundedRectangle {
83                corner_radius: args.track_height.to_px().to_f32() / 2.0,
84                g2_k_value: 2.0, // Capsule shape
85            })
86            .build()
87            .unwrap(),
88        None,
89        move || {
90            // Progress fill (active part) - capsule shape
91            let progress_width = args.width.to_px().to_f32() * args.value;
92            surface(
93                SurfaceArgsBuilder::default()
94                    .width(DimensionValue::Fixed(Px(progress_width as i32)))
95                    .height(DimensionValue::Fill {
96                        min: None,
97                        max: None,
98                    })
99                    .color(args.active_track_color)
100                    .shape(Shape::RoundedRectangle {
101                        corner_radius: args.track_height.to_px().to_f32() / 2.0,
102                        g2_k_value: 2.0, // Capsule shape
103                    })
104                    .build()
105                    .unwrap(),
106                None,
107                || {},
108            );
109        },
110    );
111
112    let on_change = args.on_change.clone();
113    let state_handler_state = state.clone();
114    let disabled = args.disabled;
115
116    state_handler(Box::new(move |input| {
117        if disabled {
118            return;
119        }
120        let mut state = state_handler_state.lock();
121
122        let is_in_component = input.cursor_position.is_some_and(|cursor_pos| {
123            cursor_pos.x.0 >= 0
124                && cursor_pos.x.0 < input.computed_data.width.0
125                && cursor_pos.y.0 >= 0
126                && cursor_pos.y.0 < input.computed_data.height.0
127        });
128
129        // Set cursor to pointer when hovering over the slider
130        if is_in_component {
131            input.requests.cursor_icon = CursorIcon::Pointer;
132        }
133
134        if !is_in_component && !state.is_dragging {
135            return;
136        }
137
138        let mut new_value = None;
139
140        for event in input.cursor_events.iter() {
141            match &event.content {
142                CursorEventContent::Pressed(_) => {
143                    state.focus.request_focus();
144                    state.is_dragging = true;
145
146                    if let Some(pos) = input.cursor_position {
147                        let v =
148                            (pos.x.0 as f32 / input.computed_data.width.0 as f32).clamp(0.0, 1.0);
149                        new_value = Some(v);
150                    }
151                }
152                CursorEventContent::Released(_) => {
153                    state.is_dragging = false;
154                }
155                _ => {}
156            }
157        }
158
159        if state.is_dragging {
160            if let Some(pos) = input.cursor_position {
161                let v = (pos.x.0 as f32 / input.computed_data.width.0 as f32).clamp(0.0, 1.0);
162                new_value = Some(v);
163            }
164        }
165
166        if let Some(v) = new_value {
167            if (v - args.value).abs() > f32::EPSILON {
168                on_change(v);
169            }
170        }
171    }));
172
173    measure(Box::new(move |input| {
174        let self_width = args.width.to_px();
175        let self_height = args.track_height.to_px();
176
177        let track_id = input.children_ids[0];
178
179        // Measure track
180        let track_constraint = Constraint::new(
181            DimensionValue::Fixed(self_width),
182            DimensionValue::Fixed(self_height),
183        );
184        tessera_ui::measure_node(
185            track_id,
186            &track_constraint,
187            input.tree,
188            input.metadatas,
189            input.compute_resource_manager.clone(),
190            input.gpu,
191        )?;
192        tessera_ui::place_node(track_id, PxPosition::new(Px(0), Px(0)), input.metadatas);
193
194        Ok(ComputedData {
195            width: self_width,
196            height: self_height,
197        })
198    }));
199}