Seregon/StratoSDK

StratoSDK is a framework with a declarative approach similar to Flutter/React, written and designed entirely for Rust.

Rust/27.3 KB/No license
crates/strato-widgets/src/slider.rs
1//! Slider and Progress widgets implementation for StratoUI
2 
3use crate::control::{ControlRole, ControlState};
4use crate::widget::{generate_id, Widget, WidgetContext, WidgetId, WidgetState};
5use std::any::Any;
6use strato_core::{
7 event::{Event, EventResult, MouseButton},
8 layout::{Constraints, Layout, Size},
9 state::Signal,
10 types::{Color, Point, Rect, Transform},
11};
12use strato_renderer::batch::RenderBatch;
13 
14/// Slider widget for numeric value selection
15#[derive(Debug, Clone)]
16pub struct Slider {
17 id: WidgetId,
18 value: Signal<f32>,
19 min: f32,
20 max: f32,
21 step: f32,
22 width: f32,
23 height: f32,
24 enabled: bool,
25 style: SliderStyle,
26 dragging: Signal<bool>,
27 bounds: Signal<Rect>,
28 control: ControlState,
29}
30 
31/// Styling options for slider
32#[derive(Debug, Clone)]
33pub struct SliderStyle {
34 pub track_height: f32,
35 pub thumb_size: f32,
36 pub track_color: [f32; 4],
37 pub track_fill_color: [f32; 4],
38 pub thumb_color: [f32; 4],
39 pub thumb_hover_color: [f32; 4],
40 pub thumb_active_color: [f32; 4],
41 pub disabled_color: [f32; 4],
42 pub border_radius: f32,
43}
44 
45impl Default for SliderStyle {
46 fn default() -> Self {
47 Self {
48 track_height: 4.0,
49 thumb_size: 20.0,
50 track_color: [0.8, 0.8, 0.8, 1.0], // Light gray
51 track_fill_color: [0.2, 0.6, 1.0, 1.0], // Blue
52 thumb_color: [1.0, 1.0, 1.0, 1.0], // White
53 thumb_hover_color: [0.95, 0.95, 0.95, 1.0], // Light gray
54 thumb_active_color: [0.9, 0.9, 0.9, 1.0], // Darker gray
55 disabled_color: [0.7, 0.7, 0.7, 1.0], // Gray
56 border_radius: 2.0,
57 }
58 }
59}
60 
61fn color_from(values: [f32; 4]) -> Color {
62 Color::rgba(values[0], values[1], values[2], values[3])
63}
64 
65fn blend_color(a: Color, b: Color, t: f32) -> Color {
66 let mix = |from: f32, to: f32| from + (to - from) * t;
67 Color::rgba(mix(a.r, b.r), mix(a.g, b.g), mix(a.b, b.b), mix(a.a, b.a))
68}
69 
70impl Slider {
71 /// Create a new slider
72 pub fn new(min: f32, max: f32) -> Self {
73 let mut control = ControlState::new(ControlRole::Slider);
74 control.set_value(format!("{}", min));
75 Self {
76 id: generate_id(),
77 value: Signal::new(min),
78 min,
79 max,
80 step: 1.0,
81 width: 200.0,
82 height: 40.0,
83 enabled: true,
84 style: SliderStyle::default(),
85 dragging: Signal::new(false),
86 bounds: Signal::new(Rect::new(0.0, 0.0, 0.0, 0.0)),
87 control,
88 }
89 }
90 
91 /// Set the initial value
92 pub fn value(mut self, value: f32) -> Self {
93 let clamped = value.clamp(self.min, self.max);
94 self.value.set(clamped);
95 self.control.set_value(format!("{:.2}", clamped));
96 self
97 }
98 
99 /// Set the step size
100 pub fn step(mut self, step: f32) -> Self {
101 self.step = step.max(0.01); // Minimum step
102 self
103 }
104 
105 /// Set the slider dimensions
106 pub fn size(mut self, width: f32, height: f32) -> Self {
107 self.width = width;
108 self.height = height;
109 self
110 }
111 
112 /// Set enabled state
113 pub fn enabled(mut self, enabled: bool) -> Self {
114 self.enabled = enabled;
115 self.control.set_disabled(!enabled);
116 self
117 }
118 
119 /// Set custom style
120 pub fn style(mut self, style: SliderStyle) -> Self {
121 self.style = style;
122 self
123 }
124 
125 /// Get the value signal
126 pub fn value_signal(&self) -> &Signal<f32> {
127 &self.value
128 }
129 
130 /// Get current value
131 pub fn get_value(&self) -> f32 {
132 self.value.get()
133 }
134 
135 /// Set the value
136 pub fn set_value(&mut self, value: f32) {
137 let clamped = value.clamp(self.min, self.max);
138 let stepped = if self.step > 0.0 {
139 (clamped / self.step).round() * self.step
140 } else {
141 clamped
142 };
143 self.value.set(stepped);
144 self.control.set_value(format!("{:.2}", stepped));
145 }
146 
147 /// Calculate value from position
148 fn value_from_position(&self, x: f32, track_width: f32) -> f32 {
149 let ratio = (x / track_width).clamp(0.0, 1.0);
150 let value = self.min + ratio * (self.max - self.min);
151 
152 if self.step > 0.0 {
153 (value / self.step).round() * self.step
154 } else {
155 value
156 }
157 }
158 
159 /// Calculate thumb position from value
160 fn thumb_position(&self, track_width: f32) -> f32 {
161 let ratio = if self.max > self.min {
162 (self.value.get() - self.min) / (self.max - self.min)
163 } else {
164 0.0
165 };
166 ratio * track_width
167 }
168 
169 /// Handle mouse events using stored bounds
170 fn handle_mouse_event(&mut self, event: &Event) -> EventResult {
171 if !self.enabled {
172 return EventResult::Ignored;
173 }
174 
175 let bounds = self.bounds.get();
176 let track_width = (bounds.width - self.style.thumb_size).max(0.0);
177 let track_start_x = bounds.x + self.style.thumb_size * 0.5;
178 
179 match event {
180 Event::MouseDown(mouse_event) => {
181 let point = Point::new(mouse_event.position.x, mouse_event.position.y);
182 if !bounds.contains(point) {
183 return EventResult::Ignored;
184 }
185 
186 if let Some(MouseButton::Left) = mouse_event.button {
187 self.control.press(point, bounds);
188 let local_x = mouse_event.position.x - track_start_x;
189 let new_value = self.value_from_position(local_x, track_width);
190 self.set_value(new_value);
191 self.dragging.set(true);
192 EventResult::Handled
193 } else {
194 EventResult::Ignored
195 }
196 }
197 Event::MouseMove(mouse_event) if self.dragging.get() => {
198 let local_x = mouse_event.position.x - track_start_x;
199 let new_value = self.value_from_position(local_x, track_width);
200 self.set_value(new_value);
201 self.control.set_state(WidgetState::Pressed);
202 EventResult::Handled
203 }
204 Event::MouseMove(mouse_event) => {
205 let point = Point::new(mouse_event.position.x, mouse_event.position.y);
206 self.control.hover(bounds.contains(point));
207 EventResult::Ignored
208 }
209 Event::MouseUp(mouse_event) => {
210 if let Some(MouseButton::Left) = mouse_event.button {
211 self.dragging.set(false);
212 let point = Point::new(mouse_event.position.x, mouse_event.position.y);
213 self.control.release(point, bounds);
214 EventResult::Handled
215 } else {
216 EventResult::Ignored
217 }
218 }
219 _ => EventResult::Ignored,
220 }
221 }
222}
223 
224impl Default for Slider {
225 fn default() -> Self {
226 Self::new(0.0, 100.0)
227 }
228}
229 
230impl Widget for Slider {
231 fn id(&self) -> WidgetId {
232 self.id
233 }
234 
235 fn layout(&mut self, constraints: Constraints) -> Size {
236 constraints.constrain(Size::new(self.width, self.height))
237 }
238 
239 fn render(&self, batch: &mut RenderBatch, layout: Layout) {
240 let bounds = Rect::new(
241 layout.position.x,
242 layout.position.y,
243 layout.size.width,
244 layout.size.height,
245 );
246 self.bounds.set(bounds);
247 
248 let track_width = (bounds.width - self.style.thumb_size).max(0.0);
249 let track_x = bounds.x + self.style.thumb_size * 0.5;
250 let track_y = bounds.y + (bounds.height - self.style.track_height) * 0.5;
251 
252 let track_rect = Rect::new(track_x, track_y, track_width, self.style.track_height);
253 
254 let state = if !self.enabled {
255 WidgetState::Disabled
256 } else if self.dragging.get() {
257 WidgetState::Pressed
258 } else {
259 self.control.state()
260 };
261 let interaction = if self.dragging.get() {
262 1.0
263 } else {
264 self.control.interaction_factor()
265 };
266 
267 let track_color = match state {
268 WidgetState::Disabled => color_from(self.style.disabled_color),
269 _ => color_from(self.style.track_color),
270 };
271 
272 batch.add_rect(track_rect, track_color, Transform::identity());
273 
274 let thumb_offset = self.thumb_position(track_width);
275 let fill_rect = Rect::new(
276 track_x,
277 track_y,
278 thumb_offset.clamp(0.0, track_width),
279 self.style.track_height,
280 );
281 
282 let mut fill_color = color_from(self.style.track_fill_color);
283 if matches!(state, WidgetState::Disabled) {
284 fill_color = blend_color(fill_color, track_color, 0.6);
285 }
286 
287 batch.add_rect(fill_rect, fill_color, Transform::identity());
288 
289 let thumb_center_x = track_x + thumb_offset;
290 let thumb_center_y = bounds.y + bounds.height * 0.5;
291 let thumb_radius = self.style.thumb_size * 0.5;
292 
293 let thumb_base = color_from(self.style.thumb_color);
294 let thumb_target = match state {
295 WidgetState::Pressed => color_from(self.style.thumb_active_color),
296 WidgetState::Hovered => color_from(self.style.thumb_hover_color),
297 WidgetState::Disabled => color_from(self.style.disabled_color),
298 _ => thumb_base,
299 };
300 let thumb_color = blend_color(thumb_base, thumb_target, interaction);
301 
302 batch.add_circle(
303 (thumb_center_x, thumb_center_y),
304 thumb_radius,
305 thumb_color, // Use state-aware color
306 16,
307 strato_core::types::Transform::default(),
308 );
309 }
310 
311 fn handle_event(&mut self, event: &Event) -> EventResult {
312 if matches!(
313 event,
314 Event::MouseDown(_) | Event::MouseUp(_) | Event::MouseMove(_)
315 ) {
316 let result = self.handle_mouse_event(event);
317 if let EventResult::Handled = result {
318 return result;
319 }
320 }
321 
322 if let EventResult::Handled = self.control.handle_keyboard_activation(event) {
323 return EventResult::Handled;
324 }
325 
326 EventResult::Ignored
327 }
328 
329 fn update(&mut self, ctx: &WidgetContext) {
330 self.control.update(ctx.delta_time);
331 }
332 
333 fn as_any(&self) -> &dyn Any {
334 self
335 }
336 
337 fn as_any_mut(&mut self) -> &mut dyn Any {
338 self
339 }
340 
341 fn clone_widget(&self) -> Box<dyn Widget> {
342 Box::new(self.clone())
343 }
344 
345 // Removed state method as it's not part of Widget trait
346}
347 
348/// Progress bar widget for showing completion status
349#[derive(Debug, Clone)]
350pub struct ProgressBar {
351 id: WidgetId,
352 value: Signal<f32>,
353 max: f32,
354 width: f32,
355 height: f32,
356 indeterminate: bool,
357 style: ProgressStyle,
358}
359 
360/// Styling options for progress bar
361#[derive(Debug, Clone)]
362pub struct ProgressStyle {
363 pub background_color: [f32; 4],
364 pub fill_color: [f32; 4],
365 pub border_radius: f32,
366 pub border_width: f32,
367 pub border_color: [f32; 4],
368}
369 
370impl Default for ProgressStyle {
371 fn default() -> Self {
372 Self {
373 background_color: [0.9, 0.9, 0.9, 1.0], // Light gray
374 fill_color: [0.2, 0.6, 1.0, 1.0], // Blue
375 border_radius: 4.0,
376 border_width: 1.0,
377 border_color: [0.8, 0.8, 0.8, 1.0], // Gray
378 }
379 }
380}
381 
382impl ProgressBar {
383 /// Create a new progress bar
384 pub fn new(max: f32) -> Self {
385 Self {
386 id: generate_id(),
387 value: Signal::new(0.0),
388 max,
389 width: 200.0,
390 height: 20.0,
391 indeterminate: false,
392 style: ProgressStyle::default(),
393 }
394 }
395 
396 /// Set the current value
397 pub fn value(mut self, value: f32) -> Self {
398 let clamped = value.clamp(0.0, self.max);
399 self.value.set(clamped);
400 self
401 }
402 
403 /// Set the progress bar dimensions
404 pub fn size(mut self, width: f32, height: f32) -> Self {
405 self.width = width;
406 self.height = height;
407 self
408 }
409 
410 /// Set indeterminate mode (animated)
411 pub fn indeterminate(mut self, indeterminate: bool) -> Self {
412 self.indeterminate = indeterminate;
413 self
414 }
415 
416 /// Set custom style
417 pub fn style(mut self, style: ProgressStyle) -> Self {
418 self.style = style;
419 self
420 }
421 
422 /// Get the value signal
423 pub fn value_signal(&self) -> &Signal<f32> {
424 &self.value
425 }
426 
427 /// Get current value
428 pub fn get_value(&self) -> f32 {
429 self.value.get()
430 }
431 
432 /// Set the value
433 pub fn set_value(&self, value: f32) {
434 let clamped = value.clamp(0.0, self.max);
435 self.value.set(clamped);
436 }
437 
438 /// Get progress percentage (0.0 to 1.0)
439 pub fn progress(&self) -> f32 {
440 if self.max > 0.0 {
441 (self.value.get() / self.max).clamp(0.0, 1.0)
442 } else {
443 0.0
444 }
445 }
446}
447 
448impl Default for ProgressBar {
449 fn default() -> Self {
450 Self::new(100.0)
451 }
452}
453 
454impl Widget for ProgressBar {
455 fn id(&self) -> WidgetId {
456 self.id
457 }
458 
459 fn layout(&mut self, constraints: Constraints) -> Size {
460 constraints.constrain(Size::new(self.width, self.height))
461 }
462 
463 fn render(&self, batch: &mut RenderBatch, layout: Layout) {
464 let bounds = Rect::new(
465 layout.position.x,
466 layout.position.y,
467 layout.size.width,
468 layout.size.height,
469 );
470 
471 let bg_color = Color::rgba(
472 self.style.background_color[0],
473 self.style.background_color[1],
474 self.style.background_color[2],
475 self.style.background_color[3],
476 );
477 
478 batch.add_rect(bounds, bg_color, Transform::identity());
479 
480 let progress = self.progress();
481 if progress > 0.0 {
482 let fill_width = bounds.width * progress;
483 let fill_rect = Rect::new(bounds.x, bounds.y, fill_width, bounds.height);
484 
485 let fill_color = Color::rgba(
486 self.style.fill_color[0],
487 self.style.fill_color[1],
488 self.style.fill_color[2],
489 self.style.fill_color[3],
490 );
491 
492 batch.add_rect(fill_rect, fill_color, Transform::identity());
493 }
494 }
495 
496 fn handle_event(&mut self, _event: &Event) -> EventResult {
497 EventResult::Ignored // Progress bars don't handle events
498 }
499 
500 fn as_any(&self) -> &dyn Any {
501 self
502 }
503 
504 fn as_any_mut(&mut self) -> &mut dyn Any {
505 self
506 }
507 
508 fn clone_widget(&self) -> Box<dyn Widget> {
509 Box::new(self.clone())
510 }
511 
512 // Removed state method as it's not part of Widget trait
513}
514 
515#[cfg(test)]
516mod tests {
517 use super::*;
518 
519 #[test]
520 fn test_slider_creation() {
521 let slider = Slider::new(0.0, 100.0);
522 assert_eq!(slider.get_value(), 0.0);
523 assert_eq!(slider.min, 0.0);
524 assert_eq!(slider.max, 100.0);
525 }
526 
527 #[test]
528 fn test_slider_value_clamping() {
529 let mut slider = Slider::new(0.0, 100.0);
530 
531 slider.set_value(150.0);
532 assert_eq!(slider.get_value(), 100.0);
533 
534 slider.set_value(-50.0);
535 assert_eq!(slider.get_value(), 0.0);
536 }
537 
538 #[test]
539 fn test_slider_step() {
540 let mut slider = Slider::new(0.0, 100.0).step(10.0);
541 
542 slider.set_value(23.0);
543 assert_eq!(slider.get_value(), 20.0); // Rounded to nearest step
544 
545 slider.set_value(27.0);
546 assert_eq!(slider.get_value(), 30.0);
547 }
548 
549 #[test]
550 fn test_progress_bar_creation() {
551 let progress = ProgressBar::new(100.0);
552 assert_eq!(progress.get_value(), 0.0);
553 assert_eq!(progress.max, 100.0);
554 assert_eq!(progress.progress(), 0.0);
555 }
556 
557 #[test]
558 fn test_progress_bar_progress() {
559 let progress = ProgressBar::new(100.0);
560 
561 progress.set_value(50.0);
562 assert_eq!(progress.progress(), 0.5);
563 
564 progress.set_value(100.0);
565 assert_eq!(progress.progress(), 1.0);
566 
567 progress.set_value(150.0); // Should clamp
568 assert_eq!(progress.progress(), 1.0);
569 }
570}
571