Rust · 8088 bytes Raw Blame History
1 //! Slider widget for numeric value selection
2
3 use super::{point_in_rect, WidgetEvent, WidgetState};
4 use anyhow::Result;
5 use gartk_core::{Color, Rect, Theme};
6 use gartk_render::{Renderer, TextStyle};
7
8 /// Slider track height
9 const TRACK_HEIGHT: u32 = 6;
10 /// Slider knob radius
11 const KNOB_RADIUS: f64 = 8.0;
12 /// Widget height
13 const SLIDER_HEIGHT: u32 = 32;
14
15 /// A slider widget for selecting numeric values
16 pub struct Slider {
17 pub label: String,
18 pub value: f64,
19 pub min: f64,
20 pub max: f64,
21 pub step: f64,
22 pub bounds: Rect,
23 pub state: WidgetState,
24 pub show_value: bool,
25 dragging: bool,
26 }
27
28 impl Slider {
29 /// Create a new slider
30 pub fn new(label: impl Into<String>, min: f64, max: f64) -> Self {
31 Self {
32 label: label.into(),
33 value: min,
34 min,
35 max,
36 step: 1.0,
37 bounds: Rect::new(0, 0, 300, SLIDER_HEIGHT),
38 state: WidgetState::Normal,
39 show_value: true,
40 dragging: false,
41 }
42 }
43
44 /// Set initial value
45 pub fn with_value(mut self, value: f64) -> Self {
46 self.value = value.clamp(self.min, self.max);
47 self
48 }
49
50 /// Set step size
51 pub fn with_step(mut self, step: f64) -> Self {
52 self.step = step;
53 self
54 }
55
56 /// Enable/disable value display
57 pub fn with_show_value(mut self, show: bool) -> Self {
58 self.show_value = show;
59 self
60 }
61
62 /// Set the value
63 pub fn set_value(&mut self, value: f64) {
64 self.value = value.clamp(self.min, self.max);
65 }
66
67 /// Get value as integer
68 pub fn value_i32(&self) -> i32 {
69 self.value.round() as i32
70 }
71
72 /// Get track bounds (the slidable area)
73 fn track_bounds(&self) -> Rect {
74 let label_width = self.bounds.width / 3;
75 let value_width = if self.show_value { 50 } else { 0 };
76 let track_width = self.bounds.width - label_width - value_width - 20;
77
78 let track_y = self.bounds.y + (SLIDER_HEIGHT as i32 - TRACK_HEIGHT as i32) / 2;
79 Rect::new(
80 self.bounds.x + label_width as i32,
81 track_y,
82 track_width,
83 TRACK_HEIGHT,
84 )
85 }
86
87 /// Get knob position (x coordinate)
88 fn knob_x(&self) -> f64 {
89 let track = self.track_bounds();
90 let ratio = (self.value - self.min) / (self.max - self.min);
91 track.x as f64 + (ratio * (track.width as f64 - KNOB_RADIUS * 2.0)) + KNOB_RADIUS
92 }
93
94 /// Get knob y coordinate
95 fn knob_y(&self) -> f64 {
96 let track = self.track_bounds();
97 track.y as f64 + TRACK_HEIGHT as f64 / 2.0
98 }
99
100 /// Check if point is on/near the knob
101 fn point_on_knob(&self, x: i32, y: i32) -> bool {
102 let knob_x = self.knob_x();
103 let knob_y = self.knob_y();
104 let dx = x as f64 - knob_x;
105 let dy = y as f64 - knob_y;
106 (dx * dx + dy * dy).sqrt() <= KNOB_RADIUS + 4.0
107 }
108
109 /// Convert x position to value
110 fn x_to_value(&self, x: i32) -> f64 {
111 let track = self.track_bounds();
112 let track_start = track.x as f64 + KNOB_RADIUS;
113 let track_end = track.x as f64 + track.width as f64 - KNOB_RADIUS;
114 let ratio = ((x as f64 - track_start) / (track_end - track_start)).clamp(0.0, 1.0);
115 let raw_value = self.min + ratio * (self.max - self.min);
116
117 // Snap to step
118 if self.step > 0.0 {
119 let steps = ((raw_value - self.min) / self.step).round();
120 (self.min + steps * self.step).clamp(self.min, self.max)
121 } else {
122 raw_value.clamp(self.min, self.max)
123 }
124 }
125
126 /// Handle mouse move
127 pub fn on_mouse_move(&mut self, x: i32, y: i32) -> WidgetEvent {
128 if self.state == WidgetState::Disabled {
129 return WidgetEvent::None;
130 }
131
132 if self.dragging {
133 let new_value = self.x_to_value(x);
134 if (new_value - self.value).abs() > f64::EPSILON {
135 self.value = new_value;
136 return WidgetEvent::Changed;
137 }
138 } else {
139 let track = self.track_bounds();
140 if self.point_on_knob(x, y) || point_in_rect(x, y, track) {
141 self.state = WidgetState::Hovered;
142 } else {
143 self.state = WidgetState::Normal;
144 }
145 }
146
147 WidgetEvent::None
148 }
149
150 /// Handle mouse down
151 pub fn on_mouse_down(&mut self, x: i32, y: i32) -> WidgetEvent {
152 if self.state == WidgetState::Disabled {
153 return WidgetEvent::None;
154 }
155
156 let track = self.track_bounds();
157 if self.point_on_knob(x, y) {
158 self.dragging = true;
159 self.state = WidgetState::Focused;
160 return WidgetEvent::Focus;
161 } else if point_in_rect(x, y, track) {
162 // Click on track moves knob to that position
163 self.dragging = true;
164 self.state = WidgetState::Focused;
165 let new_value = self.x_to_value(x);
166 if (new_value - self.value).abs() > f64::EPSILON {
167 self.value = new_value;
168 return WidgetEvent::Changed;
169 }
170 return WidgetEvent::Focus;
171 }
172
173 WidgetEvent::None
174 }
175
176 /// Handle mouse up
177 pub fn on_mouse_up(&mut self) -> WidgetEvent {
178 if self.dragging {
179 self.dragging = false;
180 self.state = WidgetState::Normal;
181 return WidgetEvent::Blur;
182 }
183 WidgetEvent::None
184 }
185
186 /// Check if currently dragging
187 pub fn is_dragging(&self) -> bool {
188 self.dragging
189 }
190
191 /// Render the slider
192 pub fn render(&self, renderer: &mut Renderer, theme: &Theme) -> Result<()> {
193 // Draw label
194 let label_style = TextStyle::new()
195 .font_family(&theme.font_family)
196 .font_size(theme.font_size)
197 .color(theme.foreground);
198
199 let label_y = self.bounds.y + (SLIDER_HEIGHT as i32 - theme.font_size as i32) / 2;
200 renderer.text(&self.label, self.bounds.x as f64, label_y as f64, &label_style)?;
201
202 // Draw track
203 let track = self.track_bounds();
204
205 // Track background
206 let track_bg = Rect::new(
207 track.x,
208 track.y,
209 track.width,
210 TRACK_HEIGHT,
211 );
212 renderer.fill_rounded_rect(track_bg, TRACK_HEIGHT as f64 / 2.0, theme.input_background)?;
213
214 // Filled portion
215 let knob_x = self.knob_x();
216 let filled_width = (knob_x - track.x as f64) as u32;
217 if filled_width > 0 {
218 let filled = Rect::new(track.x, track.y, filled_width.min(track.width), TRACK_HEIGHT);
219 renderer.fill_rounded_rect(filled, TRACK_HEIGHT as f64 / 2.0, theme.item_selected_background)?;
220 }
221
222 // Draw knob
223 let knob_color = match self.state {
224 WidgetState::Hovered | WidgetState::Focused => Color::from_u8(0xff, 0xff, 0xff, 0xff),
225 _ => Color::from_u8(0xea, 0xea, 0xea, 0xff),
226 };
227
228 renderer.fill_circle(knob_x, self.knob_y(), KNOB_RADIUS, knob_color)?;
229
230 // Draw subtle shadow around knob using a slightly larger semi-transparent circle
231 renderer.fill_circle(
232 knob_x,
233 self.knob_y(),
234 KNOB_RADIUS + 1.0,
235 Color::from_u8(0x00, 0x00, 0x00, 0x20),
236 )?;
237 // Redraw knob on top
238 renderer.fill_circle(knob_x, self.knob_y(), KNOB_RADIUS, knob_color)?;
239
240 // Draw value
241 if self.show_value {
242 let value_text = if self.step >= 1.0 {
243 format!("{}", self.value as i32)
244 } else {
245 format!("{:.1}", self.value)
246 };
247
248 let value_style = TextStyle::new()
249 .font_family(&theme.font_family)
250 .font_size(theme.font_size * 0.9)
251 .color(theme.foreground);
252
253 let value_x = track.x + track.width as i32 + 10;
254 renderer.text(&value_text, value_x as f64, label_y as f64, &value_style)?;
255 }
256
257 Ok(())
258 }
259 }
260