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(mut 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::vertical(Val::Px(
162                        (dimensions::SLIDER_HANDLE_SIZE - dimensions::SLIDER_TRACK_HEIGHT) / 2.0
163                    )),
164                    justify_content: JustifyContent::Start,
165                    align_items: AlignItems::Center,
166                    position_type: PositionType::Relative,
167                    ..default()
168                },
169                BackgroundColor(Color::NONE),
170                Interaction::default(),
171                RelativeCursorPosition::default(),
172                SliderTrack,
173                // Note: Cursor hover effects not available in Bevy 0.16
174            ));
175
176            let track_entity = slider_entity.id();
177
178            let mut slider = Slider::new(self.min, self.max, self.value);
179            slider.step = self.step;
180            slider.value_text_entity = value_text_id;
181
182            slider_entity.insert(slider.clone());
183            slider_entity.insert(SliderConfig {
184                show_value: self.with_preview,
185                value_format: self.format.clone(),
186                track_height: dimensions::SLIDER_TRACK_HEIGHT,
187                handle_size: dimensions::SLIDER_HANDLE_SIZE,
188                track_color: colors::BACKGROUND_TERTIARY,
189                fill_color: colors::PRIMARY.with_alpha(0.3),
190                handle_color: colors::PRIMARY,
191            });
192
193            slider_entity.with_children(|track| {
194                // Track background
195                track.spawn((
196                    Node {
197                        width: Val::Percent(100.0),
198                        height: Val::Px(dimensions::SLIDER_TRACK_HEIGHT),
199                        position_type: PositionType::Absolute,
200                        ..default()
201                    },
202                    BackgroundColor(colors::BACKGROUND_TERTIARY),
203                    BorderRadius::all(Val::Px(dimensions::SLIDER_TRACK_HEIGHT / 2.0)),
204                ));
205
206                // Filled portion
207                let fill_width = slider.normalized() * 100.0;
208                track.spawn((
209                    Node {
210                        width: Val::Percent(fill_width),
211                        height: Val::Px(dimensions::SLIDER_TRACK_HEIGHT),
212                        position_type: PositionType::Absolute,
213                        ..default()
214                    },
215                    BackgroundColor(colors::PRIMARY.with_alpha(0.3)),
216                    BorderRadius::all(Val::Px(dimensions::SLIDER_TRACK_HEIGHT / 2.0)),
217                    SliderFill,
218                ));
219
220                // Handle
221                let handle_offset = slider.normalized() * 100.0;
222                track.spawn((
223                    Node {
224                        width: Val::Px(dimensions::SLIDER_HANDLE_SIZE),
225                        height: Val::Px(dimensions::SLIDER_HANDLE_SIZE),
226                        position_type: PositionType::Absolute,
227                        left: Val::Percent(handle_offset.min(95.0)),
228                        top: Val::Px(0.0),
229                        border: UiRect::all(Val::Px(dimensions::BORDER_WIDTH_MEDIUM)),
230                        ..default()
231                    },
232                    BackgroundColor(colors::PRIMARY),
233                    BorderColor(colors::BORDER_LIGHT),
234                    BorderRadius::all(Val::Px(dimensions::SLIDER_HANDLE_SIZE / 2.0)),
235                    SliderHandle,
236                    // Add hover effects for the handle
237                    HoverScale(1.15), // Grow 15% on hover
238                    HoverColors {
239                        normal_bg: colors::PRIMARY,
240                        hover_bg: colors::PRIMARY_HOVER,
241                        normal_border: colors::BORDER_LIGHT,
242                        hover_border: colors::BORDER_FOCUS,
243                    },
244                    Interaction::default(),
245                ));
246            });
247
248            // Add increment/decrement buttons if requested
249            if self.with_buttons {
250                container.spawn((
251                    Node {
252                        width: Val::Percent(100.0),
253                        flex_direction: FlexDirection::Row,
254                        justify_content: JustifyContent::Center,
255                        column_gap: Val::Px(dimensions::SPACING_MEDIUM),
256                        margin: UiRect::top(Val::Px(dimensions::SPACING_SMALL)),
257                        ..default()
258                    },
259                    BackgroundColor(Color::NONE),
260                )).with_children(|button_row| {
261                    // Decrement button
262                    let dec_button = ButtonBuilder::new("-")
263                        .style(ButtonStyle::Secondary)
264                        .size(ButtonSize::Small)
265                        .build(button_row);
266
267                    button_row.commands()
268                        .entity(dec_button)
269                        .insert(SliderButtonAction {
270                            slider_entity: track_entity,
271                            delta: -self.step.unwrap_or((self.max - self.min) / 100.0),
272                        });
273
274                    // Increment button
275                    let inc_button = ButtonBuilder::new("+")
276                        .style(ButtonStyle::Secondary)
277                        .size(ButtonSize::Small)
278                        .build(button_row);
279
280                    button_row.commands()
281                        .entity(inc_button)
282                        .insert(SliderButtonAction {
283                            slider_entity: track_entity,
284                            delta: self.step.unwrap_or((self.max - self.min) / 100.0),
285                        });
286                });
287            }
288        });
289
290        container
291    }
292}
293
294/// A SliderBuilder with an attached marker component
295pub struct SliderBuilderWithMarker<M: Component> {
296    builder: SliderBuilder,
297    marker: M,
298}
299
300impl<M: Component> SliderBuilderWithMarker<M> {
301    /// Build the slider with the marker component
302    pub fn build(self, parent: &mut ChildSpawnerCommands) -> Entity {
303        let entity = self.builder.build(parent);
304        parent.commands().entity(entity).insert(self.marker);
305        entity
306    }
307
308    /// Build the slider with the marker component (alias for build)
309    pub fn build_in(self, parent: &mut ChildSpawnerCommands) -> Entity {
310        self.build(parent)
311    }
312}