bevy_ui_builders/scroll_view/
builder.rs

1//! ScrollView builder for creating scrollable containers with dynamic sizing
2
3use bevy::prelude::*;
4use super::types::*;
5
6/// Marker component for scrollbar thumbs
7#[derive(Component, Debug, Clone, Default)]
8pub struct ScrollbarThumb;
9
10/// Builder for creating scrollable containers with responsive sizing
11pub struct ScrollViewBuilder {
12    width: Val,
13    height: Val,
14    max_width: Val,
15    max_height: Val,
16    padding: UiRect,
17    margin: UiRect,
18    gap: Val,
19    direction: ScrollDirection,
20    config: ScrollConfig,
21    background_color: Color,
22}
23
24impl ScrollViewBuilder {
25    /// Create a new ScrollViewBuilder with sensible defaults
26    pub fn new() -> Self {
27        Self {
28            width: Val::Percent(100.0),
29            height: Val::Auto,
30            max_width: Val::Percent(100.0),
31            max_height: Val::Vh(90.0), // 90% viewport height by default
32            padding: UiRect::all(Val::Vw(2.0)), // 2% viewport width padding
33            margin: UiRect::ZERO,
34            gap: Val::Vh(2.0), // 2% viewport height gap
35            direction: ScrollDirection::Vertical,
36            config: ScrollConfig::default(),
37            background_color: Color::NONE,
38        }
39    }
40
41    /// Set the width of the scroll container
42    pub fn width(mut self, width: Val) -> Self {
43        self.width = width;
44        self
45    }
46
47    /// Set the height of the scroll container
48    pub fn height(mut self, height: Val) -> Self {
49        self.height = height;
50        self
51    }
52
53    /// Set the maximum width (useful for responsive design)
54    pub fn max_width(mut self, max_width: Val) -> Self {
55        self.max_width = max_width;
56        self
57    }
58
59    /// Set the maximum height (prevents infinite scrolling)
60    pub fn max_height(mut self, max_height: Val) -> Self {
61        self.max_height = max_height;
62        self
63    }
64
65    /// Set padding using viewport height percentage
66    pub fn padding_vh(mut self, vh: f32) -> Self {
67        self.padding = UiRect::all(Val::Vh(vh));
68        self
69    }
70
71    /// Set padding using viewport width percentage
72    pub fn padding_vw(mut self, vw: f32) -> Self {
73        self.padding = UiRect::all(Val::Vw(vw));
74        self
75    }
76
77    /// Set custom padding
78    pub fn padding(mut self, padding: UiRect) -> Self {
79        self.padding = padding;
80        self
81    }
82
83    /// Set margin
84    pub fn margin(mut self, margin: UiRect) -> Self {
85        self.margin = margin;
86        self
87    }
88
89    /// Set the gap between child elements (row_gap for vertical, column_gap for horizontal)
90    pub fn gap(mut self, gap: Val) -> Self {
91        self.gap = gap;
92        self
93    }
94
95    /// Set the scroll direction
96    pub fn direction(mut self, direction: ScrollDirection) -> Self {
97        self.direction = direction;
98        self
99    }
100
101    /// Enable/disable auto-scroll to focused elements
102    pub fn auto_scroll(mut self, enabled: bool) -> Self {
103        self.config.auto_scroll_to_focus = enabled;
104        self
105    }
106
107    /// Set scrollbar visibility mode
108    pub fn scrollbar_visibility(mut self, visibility: ScrollbarVisibility) -> Self {
109        self.config.scrollbar_visibility = visibility;
110        self
111    }
112
113    /// Enable/disable drag-to-scroll
114    pub fn enable_drag_scroll(mut self, enabled: bool) -> Self {
115        self.config.enable_drag_scroll = enabled;
116        self
117    }
118
119    /// Enable/disable kinetic scrolling
120    pub fn enable_kinetic_scroll(mut self, enabled: bool) -> Self {
121        self.config.enable_kinetic_scroll = enabled;
122        self
123    }
124
125    /// Set scroll sensitivity multiplier
126    pub fn scroll_sensitivity(mut self, sensitivity: f32) -> Self {
127        self.config.scroll_sensitivity = sensitivity;
128        self
129    }
130
131    /// Set scrollbar width in pixels
132    pub fn scrollbar_width(mut self, width: f32) -> Self {
133        self.config.scrollbar_width = width;
134        self
135    }
136
137    /// Set minimum scrollbar thumb length in pixels
138    pub fn min_thumb_length(mut self, length: f32) -> Self {
139        self.config.min_thumb_length = length;
140        self
141    }
142
143    /// Show/hide scroll indicators (deprecated - use scrollbar_visibility instead)
144    pub fn show_indicators(mut self, show: bool) -> Self {
145        self.config.scrollbar_visibility = if show {
146            ScrollbarVisibility::AutoHide { timeout_secs: 2.0 }
147        } else {
148            ScrollbarVisibility::Never
149        };
150        self
151    }
152
153    /// Set the background color
154    pub fn background_color(mut self, color: Color) -> Self {
155        self.background_color = color;
156        self
157    }
158
159    /// Build the scroll view container and return its entity
160    pub fn build(self, parent: &mut ChildSpawnerCommands) -> Entity {
161        let overflow = match self.direction {
162            ScrollDirection::Vertical => Overflow::scroll_y(),
163            ScrollDirection::Horizontal => Overflow::scroll_x(),
164            ScrollDirection::Both => Overflow::scroll(),
165        };
166
167        let (flex_direction, row_gap, column_gap) = match self.direction {
168            ScrollDirection::Vertical => (FlexDirection::Column, self.gap, Val::ZERO),
169            ScrollDirection::Horizontal => (FlexDirection::Row, Val::ZERO, self.gap),
170            ScrollDirection::Both => (FlexDirection::Column, self.gap, self.gap),
171        };
172
173        let show_scrollbar = self.config.scrollbar_visibility != ScrollbarVisibility::Never;
174        let enable_drag = self.config.enable_drag_scroll;
175        let enable_kinetic = self.config.enable_kinetic_scroll;
176        let scrollbar_width = self.config.scrollbar_width;
177        let min_thumb_length = self.config.min_thumb_length;
178
179        let mut container_bundle = (
180            Node {
181                width: self.width,
182                height: self.height,
183                max_width: self.max_width,
184                max_height: self.max_height,
185                padding: self.padding,
186                margin: self.margin,
187                flex_direction,
188                row_gap,
189                column_gap,
190                align_items: AlignItems::Stretch,
191                overflow,
192                ..default()
193            },
194            BackgroundColor(self.background_color),
195            ScrollView,
196            ScrollPosition::default(),
197            self.config,
198            Interaction::default(), // Required for hover detection in scroll systems
199        );
200
201        let container = parent.spawn(container_bundle).id();
202
203        // Add drag-to-scroll capability if enabled
204        if enable_drag {
205            parent.commands().entity(container).insert(DragScrollTarget);
206        }
207
208        // Add kinetic scrolling state if enabled
209        if enable_kinetic {
210            parent.commands().entity(container).insert(KineticScrollState::default());
211        }
212
213        // Add visual scrollbar if not Never visibility
214        if show_scrollbar && self.direction != ScrollDirection::Horizontal {
215            parent.spawn((
216                Node {
217                    position_type: PositionType::Absolute,
218                    right: Val::Px(4.0),
219                    top: Val::Px(4.0),
220                    bottom: Val::Px(4.0),
221                    width: Val::Px(scrollbar_width),
222                    ..default()
223                },
224                BackgroundColor(Color::srgba(1.0, 1.0, 1.0, 0.1)),
225                ScrollbarState::new(container),
226            )).with_children(|scrollbar| {
227                scrollbar.spawn((
228                    Node {
229                        width: Val::Percent(100.0),
230                        height: Val::Percent(20.0), // Dynamically updated by Bevy
231                        ..default()
232                    },
233                    BackgroundColor(Color::srgba(1.0, 1.0, 1.0, 0.3)),
234                    BorderRadius::all(Val::Px(scrollbar_width / 2.0)),
235                    ScrollbarThumb,
236                    Interaction::default(), // Required for drag detection
237                ));
238            });
239        }
240
241        container
242    }
243
244    /// Build the scroll view and add children using a closure
245    pub fn build_with_children<F>(self, parent: &mut ChildSpawnerCommands, children_fn: F) -> Entity
246    where
247        F: FnOnce(&mut ChildSpawnerCommands),
248    {
249        let overflow = match self.direction {
250            ScrollDirection::Vertical => Overflow::scroll_y(),
251            ScrollDirection::Horizontal => Overflow::scroll_x(),
252            ScrollDirection::Both => Overflow::scroll(),
253        };
254
255        let (flex_direction, row_gap, column_gap) = match self.direction {
256            ScrollDirection::Vertical => (FlexDirection::Column, self.gap, Val::ZERO),
257            ScrollDirection::Horizontal => (FlexDirection::Row, Val::ZERO, self.gap),
258            ScrollDirection::Both => (FlexDirection::Column, self.gap, self.gap),
259        };
260
261        let show_scrollbar = self.config.scrollbar_visibility != ScrollbarVisibility::Never;
262        let enable_drag = self.config.enable_drag_scroll;
263        let enable_kinetic = self.config.enable_kinetic_scroll;
264        let scrollbar_width = self.config.scrollbar_width;
265        let min_thumb_length = self.config.min_thumb_length;
266
267        let container = parent
268            .spawn((
269                Node {
270                    width: self.width,
271                    height: self.height,
272                    max_width: self.max_width,
273                    max_height: self.max_height,
274                    padding: self.padding,
275                    margin: self.margin,
276                    flex_direction,
277                    row_gap,
278                    column_gap,
279                    align_items: AlignItems::Stretch,
280                    overflow,
281                    ..default()
282                },
283                BackgroundColor(self.background_color),
284                ScrollView,
285                ScrollPosition::default(),
286                self.config,
287                Interaction::default(), // Required for hover detection in scroll systems
288            ))
289            .with_children(children_fn)
290            .id();
291
292        // Add drag-to-scroll capability if enabled
293        if enable_drag {
294            parent.commands().entity(container).insert(DragScrollTarget);
295        }
296
297        // Add kinetic scrolling state if enabled
298        if enable_kinetic {
299            parent.commands().entity(container).insert(KineticScrollState::default());
300        }
301
302        // Add visual scrollbar if not Never visibility
303        if show_scrollbar && self.direction != ScrollDirection::Horizontal {
304            parent.spawn((
305                Node {
306                    position_type: PositionType::Absolute,
307                    right: Val::Px(4.0),
308                    top: Val::Px(4.0),
309                    bottom: Val::Px(4.0),
310                    width: Val::Px(scrollbar_width),
311                    ..default()
312                },
313                BackgroundColor(Color::srgba(1.0, 1.0, 1.0, 0.1)),
314                ScrollbarState::new(container),
315            )).with_children(|scrollbar| {
316                scrollbar.spawn((
317                    Node {
318                        width: Val::Percent(100.0),
319                        height: Val::Percent(20.0), // Dynamically updated by Bevy
320                        ..default()
321                    },
322                    BackgroundColor(Color::srgba(1.0, 1.0, 1.0, 0.3)),
323                    BorderRadius::all(Val::Px(scrollbar_width / 2.0)),
324                    ScrollbarThumb,
325                    Interaction::default(), // Required for drag detection
326                ));
327            });
328        }
329
330        container
331    }
332}
333
334impl Default for ScrollViewBuilder {
335    fn default() -> Self {
336        Self::new()
337    }
338}
339
340/// Convenience function to create a ScrollViewBuilder
341pub fn scroll_view() -> ScrollViewBuilder {
342    ScrollViewBuilder::new()
343}