bevy_ui_builders/slider/
builder.rs

1//! SliderBuilder implementation
2
3use 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
10/// Builder for creating sliders
11pub 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    /// Create a new slider builder with range
25    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    /// Set the initial value
40    pub fn value(mut self, value: f32) -> Self {
41        self.value = value.clamp(self.min, self.max);
42        self
43    }
44
45    /// Set the step size for snapping
46    pub fn step(mut self, step: f32) -> Self {
47        self.step = Some(step);
48        self
49    }
50
51    /// Set the width
52    pub fn width(mut self, width: Val) -> Self {
53        self.width = width;
54        self
55    }
56
57    /// Set the value format
58    pub fn format(mut self, format: ValueFormat) -> Self {
59        self.format = format;
60        self
61    }
62
63    /// Set the value format (alias for format)
64    pub fn with_format(self, format: ValueFormat) -> Self {
65        self.format(format)
66    }
67
68    /// Show/hide value preview
69    pub fn with_preview(mut self, show: bool) -> Self {
70        self.with_preview = show;
71        self
72    }
73
74    /// Add increment/decrement buttons
75    pub fn with_buttons(mut self) -> Self {
76        self.with_buttons = true;
77        self
78    }
79
80    /// Add a label
81    pub fn label(mut self, label: impl Into<String>) -> Self {
82        self.label = Some(label.into());
83        self
84    }
85
86    /// Attach a marker component to the slider
87    pub fn with_marker<M: Component>(self, marker: M) -> SliderBuilderWithMarker<M> {
88        SliderBuilderWithMarker {
89            builder: self,
90            marker,
91        }
92    }
93
94    /// Build the slider (alias for build)
95    pub fn build_in(self, parent: &mut ChildSpawnerCommands) -> Entity {
96        self.build(parent)
97    }
98
99    /// Build the slider
100    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            // Label and value row
116            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                    // Label
127                    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                    // Value text
140                    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            // Slider track and handle
156            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),  // Half handle width
163                        right: Val::Px(dimensions::SLIDER_HANDLE_SIZE / 2.0), // Half handle width
164                        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                // Note: Cursor hover effects not available in Bevy 0.16
177            ));
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 background
198                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                // Filled portion
210                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                // Handle
224                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                    // Add hover effects for the handle
240                    HoverScale(1.15), // Grow 15% on hover
241                    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            // Add increment/decrement buttons if requested
252            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                    // Decrement button
265                    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                    // Increment button
278                    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
297/// A SliderBuilder with an attached marker component
298pub struct SliderBuilderWithMarker<M: Component> {
299    builder: SliderBuilder,
300    marker: M,
301}
302
303impl<M: Component> SliderBuilderWithMarker<M> {
304    /// Build the slider with the marker component
305    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    /// Build the slider with the marker component (alias for build)
312    pub fn build_in(self, parent: &mut ChildSpawnerCommands) -> Entity {
313        self.build(parent)
314    }
315}