1use bevy::prelude::*;
4use bevy::ui::RelativeCursorPosition;
5use crate::button::{ButtonBuilder, ButtonSize, ButtonStyle};
6use crate::styles::{colors, dimensions};
7use crate::systems::hover::{HoverScale, HoverColors};
8use super::types::*;
9
10pub struct SliderBuilder {
12 value: f32,
13 min: f32,
14 max: f32,
15 step: Option<f32>,
16 width: Val,
17 format: ValueFormat,
18 with_preview: bool,
19 with_buttons: bool,
20 label: Option<String>,
21}
22
23impl SliderBuilder {
24 pub fn new(range: std::ops::Range<f32>) -> Self {
26 Self {
27 value: range.start,
28 min: range.start,
29 max: range.end,
30 step: None,
31 width: Val::Px(200.0),
32 format: ValueFormat::Decimal(1),
33 with_preview: true,
34 with_buttons: false,
35 label: None,
36 }
37 }
38
39 pub fn value(mut self, value: f32) -> Self {
41 self.value = value.clamp(self.min, self.max);
42 self
43 }
44
45 pub fn step(mut self, step: f32) -> Self {
47 self.step = Some(step);
48 self
49 }
50
51 pub fn width(mut self, width: Val) -> Self {
53 self.width = width;
54 self
55 }
56
57 pub fn format(mut self, format: ValueFormat) -> Self {
59 self.format = format;
60 self
61 }
62
63 pub fn with_format(self, format: ValueFormat) -> Self {
65 self.format(format)
66 }
67
68 pub fn with_preview(mut self, show: bool) -> Self {
70 self.with_preview = show;
71 self
72 }
73
74 pub fn with_buttons(mut self) -> Self {
76 self.with_buttons = true;
77 self
78 }
79
80 pub fn label(mut self, label: impl Into<String>) -> Self {
82 self.label = Some(label.into());
83 self
84 }
85
86 pub fn with_marker<M: Component>(self, marker: M) -> SliderBuilderWithMarker<M> {
88 SliderBuilderWithMarker {
89 builder: self,
90 marker,
91 }
92 }
93
94 pub fn build_in(self, parent: &mut ChildSpawnerCommands) -> Entity {
96 self.build(parent)
97 }
98
99 pub fn build(self, parent: &mut ChildSpawnerCommands) -> Entity {
101 let container = parent.spawn((
102 Node {
103 width: self.width,
104 flex_direction: FlexDirection::Column,
105 row_gap: Val::Px(dimensions::SPACING_SMALL),
106 margin: UiRect::bottom(Val::Px(dimensions::SPACING_MEDIUM)),
107 ..default()
108 },
109 BackgroundColor(Color::NONE),
110 )).id();
111
112 let mut value_text_id = None;
113
114 parent.commands().entity(container).with_children(|container| {
115 if self.label.is_some() || self.with_preview {
117 container.spawn((
118 Node {
119 width: Val::Percent(100.0),
120 flex_direction: FlexDirection::Row,
121 justify_content: JustifyContent::SpaceBetween,
122 ..default()
123 },
124 BackgroundColor(Color::NONE),
125 )).with_children(|row| {
126 if let Some(label) = self.label {
128 row.spawn((
129 Text::new(label),
130 TextFont {
131 font_size: dimensions::FONT_SIZE_MEDIUM,
132 ..default()
133 },
134 TextColor(colors::TEXT_SECONDARY),
135 SliderLabel,
136 ));
137 }
138
139 if self.with_preview {
141 let entity = row.spawn((
142 Text::new(self.format.format(self.value)),
143 TextFont {
144 font_size: dimensions::FONT_SIZE_MEDIUM,
145 ..default()
146 },
147 TextColor(colors::TEXT_PRIMARY),
148 SliderValueText,
149 )).id();
150 value_text_id = Some(entity);
151 }
152 });
153 }
154
155 let mut slider_entity = container.spawn((
157 Button,
158 Node {
159 width: Val::Percent(100.0),
160 height: Val::Px(dimensions::SLIDER_TRACK_HEIGHT + dimensions::SLIDER_HANDLE_SIZE),
161 padding: UiRect {
162 left: Val::Px(dimensions::SLIDER_HANDLE_SIZE / 2.0), right: Val::Px(dimensions::SLIDER_HANDLE_SIZE / 2.0), top: Val::Px((dimensions::SLIDER_HANDLE_SIZE - dimensions::SLIDER_TRACK_HEIGHT) / 2.0),
165 bottom: Val::Px((dimensions::SLIDER_HANDLE_SIZE - dimensions::SLIDER_TRACK_HEIGHT) / 2.0),
166 },
167 justify_content: JustifyContent::Start,
168 align_items: AlignItems::Center,
169 position_type: PositionType::Relative,
170 ..default()
171 },
172 BackgroundColor(Color::NONE),
173 Interaction::default(),
174 RelativeCursorPosition::default(),
175 SliderTrack,
176 ));
178
179 let track_entity = slider_entity.id();
180
181 let mut slider = Slider::new(self.min, self.max, self.value);
182 slider.step = self.step;
183 slider.value_text_entity = value_text_id;
184
185 slider_entity.insert(slider.clone());
186 slider_entity.insert(SliderConfig {
187 show_value: self.with_preview,
188 value_format: self.format.clone(),
189 track_height: dimensions::SLIDER_TRACK_HEIGHT,
190 handle_size: dimensions::SLIDER_HANDLE_SIZE,
191 track_color: colors::BACKGROUND_TERTIARY,
192 fill_color: colors::PRIMARY.with_alpha(0.3),
193 handle_color: colors::PRIMARY,
194 });
195
196 slider_entity.with_children(|track| {
197 track.spawn((
199 Node {
200 width: Val::Percent(100.0),
201 height: Val::Px(dimensions::SLIDER_TRACK_HEIGHT),
202 position_type: PositionType::Absolute,
203 ..default()
204 },
205 BackgroundColor(colors::BACKGROUND_TERTIARY),
206 BorderRadius::all(Val::Px(dimensions::SLIDER_TRACK_HEIGHT / 2.0)),
207 ));
208
209 let fill_width = slider.normalized() * 100.0;
211 track.spawn((
212 Node {
213 width: Val::Percent(fill_width),
214 height: Val::Px(dimensions::SLIDER_TRACK_HEIGHT),
215 position_type: PositionType::Absolute,
216 ..default()
217 },
218 BackgroundColor(colors::PRIMARY.with_alpha(0.3)),
219 BorderRadius::all(Val::Px(dimensions::SLIDER_TRACK_HEIGHT / 2.0)),
220 SliderFill,
221 ));
222
223 let handle_offset = slider.normalized() * 100.0;
225 track.spawn((
226 Node {
227 width: Val::Px(dimensions::SLIDER_HANDLE_SIZE),
228 height: Val::Px(dimensions::SLIDER_HANDLE_SIZE),
229 position_type: PositionType::Absolute,
230 left: Val::Percent(handle_offset),
231 top: Val::Px(0.0),
232 border: UiRect::all(Val::Px(dimensions::BORDER_WIDTH_MEDIUM)),
233 ..default()
234 },
235 BackgroundColor(colors::PRIMARY),
236 BorderColor::all(colors::BORDER_LIGHT),
237 BorderRadius::all(Val::Px(dimensions::SLIDER_HANDLE_SIZE / 2.0)),
238 SliderHandle,
239 HoverScale(1.15), HoverColors {
242 normal_bg: colors::PRIMARY,
243 hover_bg: colors::PRIMARY_HOVER,
244 normal_border: colors::BORDER_LIGHT,
245 hover_border: colors::BORDER_FOCUS,
246 },
247 Interaction::default(),
248 ));
249 });
250
251 if self.with_buttons {
253 container.spawn((
254 Node {
255 width: Val::Percent(100.0),
256 flex_direction: FlexDirection::Row,
257 justify_content: JustifyContent::Center,
258 column_gap: Val::Px(dimensions::SPACING_MEDIUM),
259 margin: UiRect::top(Val::Px(dimensions::SPACING_SMALL)),
260 ..default()
261 },
262 BackgroundColor(Color::NONE),
263 )).with_children(|button_row| {
264 let dec_button = ButtonBuilder::new("-")
266 .style(ButtonStyle::Secondary)
267 .size(ButtonSize::Small)
268 .build(button_row);
269
270 button_row.commands()
271 .entity(dec_button)
272 .insert(SliderButtonAction {
273 slider_entity: track_entity,
274 delta: -self.step.unwrap_or((self.max - self.min) / 100.0),
275 });
276
277 let inc_button = ButtonBuilder::new("+")
279 .style(ButtonStyle::Secondary)
280 .size(ButtonSize::Small)
281 .build(button_row);
282
283 button_row.commands()
284 .entity(inc_button)
285 .insert(SliderButtonAction {
286 slider_entity: track_entity,
287 delta: self.step.unwrap_or((self.max - self.min) / 100.0),
288 });
289 });
290 }
291 });
292
293 container
294 }
295}
296
297pub struct SliderBuilderWithMarker<M: Component> {
299 builder: SliderBuilder,
300 marker: M,
301}
302
303impl<M: Component> SliderBuilderWithMarker<M> {
304 pub fn build(self, parent: &mut ChildSpawnerCommands) -> Entity {
306 let entity = self.builder.build(parent);
307 parent.commands().entity(entity).insert(self.marker);
308 entity
309 }
310
311 pub fn build_in(self, parent: &mut ChildSpawnerCommands) -> Entity {
313 self.build(parent)
314 }
315}