tessera_ui_basic_components/
glass_slider.rs1use 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 fluid_glass::{FluidGlassArgsBuilder, GlassBorder, fluid_glass},
13 shape_def::Shape,
14 surface::{SurfaceArgsBuilder, surface},
15};
16
17pub struct GlassSliderState {
19 pub is_dragging: bool,
21 pub focus: Focus,
23}
24
25impl Default for GlassSliderState {
26 fn default() -> Self {
27 Self::new()
28 }
29}
30
31impl GlassSliderState {
32 pub fn new() -> Self {
33 Self {
34 is_dragging: false,
35 focus: Focus::new(),
36 }
37 }
38}
39
40#[derive(Builder, Clone)]
42#[builder(pattern = "owned")]
43pub struct GlassSliderArgs {
44 #[builder(default = "0.0")]
46 pub value: f32,
47
48 #[builder(default = "Arc::new(|_| {})")]
50 pub on_change: Arc<dyn Fn(f32) + Send + Sync>,
51
52 #[builder(default = "Dp(200.0)")]
54 pub width: Dp,
55
56 #[builder(default = "Dp(12.0)")]
58 pub track_height: Dp,
59
60 #[builder(default = "Color::new(0.3, 0.3, 0.3, 0.15)")]
62 pub track_tint_color: Color,
63
64 #[builder(default = "Color::new(0.5, 0.7, 1.0, 0.25)")]
66 pub progress_tint_color: Color,
67
68 #[builder(default = "8.0")]
70 pub blur_radius: f32,
71
72 #[builder(default = "Color::new(0.5, 0.5, 0.5, 0.3)")]
74 pub track_border_color: Color,
75
76 #[builder(default = "Dp(2.0)")]
78 pub track_border_width: Dp,
79
80 #[builder(default = "false")]
82 pub disabled: bool,
83}
84
85#[tessera]
86pub fn glass_slider(args: impl Into<GlassSliderArgs>, state: Arc<Mutex<GlassSliderState>>) {
87 let args: GlassSliderArgs = args.into();
88
89 fluid_glass(
91 FluidGlassArgsBuilder::default()
92 .width(DimensionValue::Fixed(args.width.to_px()))
93 .height(DimensionValue::Fixed(args.track_height.to_px()))
94 .tint_color(args.track_tint_color)
95 .blur_radius(args.blur_radius)
96 .shape(Shape::RoundedRectangle {
97 corner_radius: args.track_height.0 as f32 / 2.0,
98 g2_k_value: 2.0, })
100 .border(GlassBorder::new(
101 args.track_border_width,
102 args.track_border_color,
103 ))
104 .padding(args.track_border_width)
105 .build()
106 .unwrap(),
107 None,
108 move || {
109 let progress_width = (args.width.to_px().to_f32() * args.value)
111 - (args.track_border_width.to_px().to_f32() * 2.0);
112 let effective_height = args.track_height.to_px().to_f32()
113 - (args.track_border_width.to_px().to_f32() * 2.0);
114 surface(
115 SurfaceArgsBuilder::default()
116 .width(DimensionValue::Fixed(Px(progress_width as i32)))
117 .height(DimensionValue::Fill {
118 min: None,
119 max: None,
120 })
121 .color(args.progress_tint_color)
122 .shape(Shape::RoundedRectangle {
123 corner_radius: effective_height / 2.0,
124 g2_k_value: 2.0, })
126 .build()
127 .unwrap(),
128 None,
129 || {},
130 );
131 },
132 );
133
134 let on_change = args.on_change.clone();
135 let state_handler_state = state.clone();
136 let disabled = args.disabled;
137
138 state_handler(Box::new(move |input| {
139 if disabled {
140 return;
141 }
142 let mut state = state_handler_state.lock();
143
144 let is_in_component = input.cursor_position.is_some_and(|cursor_pos| {
145 cursor_pos.x.0 >= 0
146 && cursor_pos.x.0 < input.computed_data.width.0
147 && cursor_pos.y.0 >= 0
148 && cursor_pos.y.0 < input.computed_data.height.0
149 });
150
151 if is_in_component {
153 input.requests.cursor_icon = CursorIcon::Pointer;
154 }
155
156 if !is_in_component && !state.is_dragging {
157 return;
158 }
159
160 let mut new_value = None;
161
162 for event in input.cursor_events.iter() {
163 match &event.content {
164 CursorEventContent::Pressed(_) => {
165 state.focus.request_focus();
166 state.is_dragging = true;
167
168 if let Some(pos) = input.cursor_position {
169 let v =
170 (pos.x.0 as f32 / input.computed_data.width.0 as f32).clamp(0.0, 1.0);
171 new_value = Some(v);
172 }
173 }
174 CursorEventContent::Released(_) => {
175 state.is_dragging = false;
176 }
177 _ => {}
178 }
179 }
180
181 if state.is_dragging {
182 if let Some(pos) = input.cursor_position {
183 let v = (pos.x.0 as f32 / input.computed_data.width.0 as f32).clamp(0.0, 1.0);
184 new_value = Some(v);
185 }
186 }
187
188 if let Some(v) = new_value {
189 if (v - args.value).abs() > f32::EPSILON {
190 on_change(v);
191 }
192 }
193 }));
194
195 measure(Box::new(move |input| {
196 let self_width = args.width.to_px();
197 let self_height = args.track_height.to_px();
198
199 let track_id = input.children_ids[0];
200
201 let track_constraint = Constraint::new(
203 DimensionValue::Fixed(self_width),
204 DimensionValue::Fixed(self_height),
205 );
206 tessera_ui::measure_node(
207 track_id,
208 &track_constraint,
209 input.tree,
210 input.metadatas,
211 input.compute_resource_manager.clone(),
212 input.gpu,
213 )?;
214 tessera_ui::place_node(track_id, PxPosition::new(Px(0), Px(0)), input.metadatas);
215
216 Ok(ComputedData {
217 width: self_width,
218 height: self_height,
219 })
220 }));
221}