fyrox_ui/
scroll_panel.rs

1// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
2//
3// Permission is hereby granted, free of charge, to any person obtaining a copy
4// of this software and associated documentation files (the "Software"), to deal
5// in the Software without restriction, including without limitation the rights
6// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
7// copies of the Software, and to permit persons to whom the Software is
8// furnished to do so, subject to the following conditions:
9//
10// The above copyright notice and this permission notice shall be included in all
11// copies or substantial portions of the Software.
12//
13// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
14// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
15// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
16// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
17// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
18// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
19// SOFTWARE.
20
21//! Scroll panel widget is used to arrange its children widgets, so they can be offset by a certain amount of units
22//! from top-left corner. It is used to provide basic scrolling functionality. See [`ScrollPanel`] docs for more
23//! info and usage examples.
24
25use crate::{
26    brush::Brush,
27    core::{
28        algebra::Vector2, color::Color, math::Rect, pool::Handle, reflect::prelude::*,
29        type_traits::prelude::*, visitor::prelude::*,
30    },
31    define_constructor,
32    draw::{CommandTexture, Draw, DrawingContext},
33    message::{MessageDirection, UiMessage},
34    widget::{Widget, WidgetBuilder},
35    BuildContext, Control, UiNode, UserInterface,
36};
37
38use fyrox_core::uuid_provider;
39use fyrox_graph::constructor::{ConstructorProvider, GraphNodeConstructor};
40use fyrox_graph::BaseSceneGraph;
41use std::ops::{Deref, DerefMut};
42
43/// A set of messages, that is used to modify the state of a scroll panel.
44#[derive(Debug, Clone, PartialEq)]
45pub enum ScrollPanelMessage {
46    /// Sets the desired scrolling value for the vertical axis.
47    VerticalScroll(f32),
48    /// Sets the desired scrolling value for the horizontal axis.
49    HorizontalScroll(f32),
50    /// Adjusts vertical and horizontal scroll values so given node will be in "view box" of scroll panel.
51    BringIntoView(Handle<UiNode>),
52    /// Scrolls to end of the content.
53    ScrollToEnd,
54}
55
56impl ScrollPanelMessage {
57    define_constructor!(
58        /// Creates [`ScrollPanelMessage::VerticalScroll`] message.
59        ScrollPanelMessage:VerticalScroll => fn vertical_scroll(f32), layout: false
60    );
61    define_constructor!(
62        /// Creates [`ScrollPanelMessage::HorizontalScroll`] message.
63        ScrollPanelMessage:HorizontalScroll => fn horizontal_scroll(f32), layout: false
64    );
65    define_constructor!(
66        /// Creates [`ScrollPanelMessage::BringIntoView`] message.
67        ScrollPanelMessage:BringIntoView => fn bring_into_view(Handle<UiNode>), layout: true
68    );
69    define_constructor!(
70        /// Creates [`ScrollPanelMessage::ScrollToEnd`] message.
71        ScrollPanelMessage:ScrollToEnd => fn scroll_to_end(), layout: true
72    );
73}
74
75/// Scroll panel widget is used to arrange its children widgets, so they can be offset by a certain amount of units
76/// from top-left corner. It is used to provide basic scrolling functionality.
77///
78/// ## Examples
79///
80/// ```rust
81/// # use fyrox_ui::{
82/// #     button::ButtonBuilder,
83/// #     core::{algebra::Vector2, pool::Handle},
84/// #     grid::{Column, GridBuilder, Row},
85/// #     scroll_panel::ScrollPanelBuilder,
86/// #     widget::WidgetBuilder,
87/// #     BuildContext, UiNode,
88/// # };
89/// #
90/// fn create_scroll_panel(ctx: &mut BuildContext) -> Handle<UiNode> {
91///     ScrollPanelBuilder::new(
92///         WidgetBuilder::new().with_child(
93///             GridBuilder::new(
94///                 WidgetBuilder::new()
95///                     .with_child(
96///                         ButtonBuilder::new(WidgetBuilder::new())
97///                             .with_text("Some Button")
98///                             .build(ctx),
99///                     )
100///                     .with_child(
101///                         ButtonBuilder::new(WidgetBuilder::new())
102///                             .with_text("Some Other Button")
103///                             .build(ctx),
104///                     ),
105///             )
106///             .add_row(Row::auto())
107///             .add_row(Row::auto())
108///             .add_column(Column::stretch())
109///             .build(ctx),
110///         ),
111///     )
112///     .with_scroll_value(Vector2::new(100.0, 200.0))
113///     .with_vertical_scroll_allowed(true)
114///     .with_horizontal_scroll_allowed(true)
115///     .build(ctx)
116/// }
117/// ```
118///
119/// ## Scrolling
120///
121/// Scrolling value for both axes can be set via [`ScrollPanelMessage::VerticalScroll`] and [`ScrollPanelMessage::HorizontalScroll`]:
122///
123/// ```rust
124/// use fyrox_ui::{
125///     core::pool::Handle, message::MessageDirection, scroll_panel::ScrollPanelMessage, UiNode,
126///     UserInterface,
127/// };
128/// fn set_scrolling_value(
129///     scroll_panel: Handle<UiNode>,
130///     horizontal: f32,
131///     vertical: f32,
132///     ui: &UserInterface,
133/// ) {
134///     ui.send_message(ScrollPanelMessage::horizontal_scroll(
135///         scroll_panel,
136///         MessageDirection::ToWidget,
137///         horizontal,
138///     ));
139///     ui.send_message(ScrollPanelMessage::vertical_scroll(
140///         scroll_panel,
141///         MessageDirection::ToWidget,
142///         vertical,
143///     ));
144/// }
145/// ```
146///
147/// ## Bringing child into view
148///
149/// Calculates the scroll values to bring a desired child into view, it can be used for automatic navigation:
150///
151/// ```rust
152/// # use fyrox_ui::{
153/// #     core::pool::Handle, message::MessageDirection, scroll_panel::ScrollPanelMessage, UiNode,
154/// #     UserInterface,
155/// # };
156/// fn bring_child_into_view(
157///     scroll_panel: Handle<UiNode>,
158///     child: Handle<UiNode>,
159///     ui: &UserInterface,
160/// ) {
161///     ui.send_message(ScrollPanelMessage::bring_into_view(
162///         scroll_panel,
163///         MessageDirection::ToWidget,
164///         child,
165///     ))
166/// }
167/// ```
168#[derive(Default, Clone, Visit, Reflect, Debug, ComponentProvider)]
169#[reflect(derived_type = "UiNode")]
170pub struct ScrollPanel {
171    /// Base widget of the scroll panel.
172    pub widget: Widget,
173    /// Current scroll value of the scroll panel.
174    pub scroll: Vector2<f32>,
175    /// A flag, that defines whether the vertical scrolling is allowed or not.
176    pub vertical_scroll_allowed: bool,
177    /// A flag, that defines whether the horizontal scrolling is allowed or not.
178    pub horizontal_scroll_allowed: bool,
179}
180
181impl ConstructorProvider<UiNode, UserInterface> for ScrollPanel {
182    fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
183        GraphNodeConstructor::new::<Self>()
184            .with_variant("Scroll Panel", |ui| {
185                ScrollPanelBuilder::new(WidgetBuilder::new().with_name("Scroll Panel"))
186                    .build(&mut ui.build_ctx())
187                    .into()
188            })
189            .with_group("Layout")
190    }
191}
192
193crate::define_widget_deref!(ScrollPanel);
194
195uuid_provider!(ScrollPanel = "1ab4936d-58c8-4cf7-b33c-4b56092f4826");
196
197impl ScrollPanel {
198    fn children_size(&self, ui: &UserInterface) -> Vector2<f32> {
199        let mut children_size = Vector2::<f32>::default();
200        for child_handle in self.widget.children() {
201            let desired_size = ui.node(*child_handle).desired_size();
202            children_size.x = children_size.x.max(desired_size.x);
203            children_size.y = children_size.y.max(desired_size.y);
204        }
205        children_size
206    }
207    fn bring_into_view(&self, ui: &UserInterface, handle: Handle<UiNode>) {
208        let Some(node_to_focus_ref) = ui.try_get_node(handle) else {
209            return;
210        };
211        let mut parent = handle;
212        let mut relative_position = Vector2::default();
213        while parent.is_some() && parent != self.handle {
214            let node = ui.node(parent);
215            relative_position += node.actual_local_position();
216            parent = node.parent();
217        }
218        // This check is needed because it possible that given handle is not in
219        // sub-tree of current scroll panel.
220        if parent != self.handle {
221            return;
222        }
223        let size = node_to_focus_ref.actual_local_size();
224        let children_size = self.children_size(ui);
225        let view_size = self.actual_local_size();
226        // Check if requested item already in "view box", this will prevent weird "jumping" effect
227        // when bring into view was requested on already visible element.
228        if self.vertical_scroll_allowed
229            && (relative_position.y < 0.0 || relative_position.y + size.y > view_size.y)
230        {
231            relative_position.y += self.scroll.y;
232            let scroll_max = (children_size.y - view_size.y).max(0.0);
233            relative_position.y = relative_position.y.clamp(0.0, scroll_max);
234            ui.send_message(ScrollPanelMessage::vertical_scroll(
235                self.handle,
236                MessageDirection::ToWidget,
237                relative_position.y,
238            ));
239        }
240        if self.horizontal_scroll_allowed
241            && (relative_position.x < 0.0 || relative_position.x + size.x > view_size.x)
242        {
243            relative_position.x += self.scroll.x;
244            let scroll_max = (children_size.x - view_size.x).max(0.0);
245            relative_position.x = relative_position.x.clamp(0.0, scroll_max);
246            ui.send_message(ScrollPanelMessage::horizontal_scroll(
247                self.handle,
248                MessageDirection::ToWidget,
249                relative_position.x,
250            ));
251        }
252    }
253}
254
255impl Control for ScrollPanel {
256    fn measure_override(&self, ui: &UserInterface, available_size: Vector2<f32>) -> Vector2<f32> {
257        let size_for_child = Vector2::new(
258            if self.horizontal_scroll_allowed {
259                f32::INFINITY
260            } else {
261                available_size.x
262            },
263            if self.vertical_scroll_allowed {
264                f32::INFINITY
265            } else {
266                available_size.y
267            },
268        );
269
270        let mut desired_size = Vector2::default();
271
272        for child_handle in self.widget.children() {
273            ui.measure_node(*child_handle, size_for_child);
274
275            let child = ui.nodes.borrow(*child_handle);
276            let child_desired_size = child.desired_size();
277            if child_desired_size.x > desired_size.x {
278                desired_size.x = child_desired_size.x;
279            }
280            if child_desired_size.y > desired_size.y {
281                desired_size.y = child_desired_size.y;
282            }
283        }
284
285        desired_size
286    }
287
288    fn arrange_override(&self, ui: &UserInterface, final_size: Vector2<f32>) -> Vector2<f32> {
289        let children_size = self.children_size(ui);
290
291        let child_rect = Rect::new(
292            -self.scroll.x,
293            -self.scroll.y,
294            if self.horizontal_scroll_allowed {
295                children_size.x.max(final_size.x)
296            } else {
297                final_size.x
298            },
299            if self.vertical_scroll_allowed {
300                children_size.y.max(final_size.y)
301            } else {
302                final_size.y
303            },
304        );
305
306        for child_handle in self.widget.children() {
307            ui.arrange_node(*child_handle, &child_rect);
308        }
309
310        final_size
311    }
312
313    fn draw(&self, drawing_context: &mut DrawingContext) {
314        // Emit transparent geometry so panel will receive mouse events.
315        drawing_context.push_rect_filled(&self.widget.bounding_rect(), None);
316        drawing_context.commit(
317            self.clip_bounds(),
318            Brush::Solid(Color::TRANSPARENT),
319            CommandTexture::None,
320            &self.material,
321            None,
322        );
323    }
324
325    fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
326        self.widget.handle_routed_message(ui, message);
327
328        if message.destination() == self.handle() {
329            if let Some(msg) = message.data::<ScrollPanelMessage>() {
330                match *msg {
331                    ScrollPanelMessage::VerticalScroll(scroll) => {
332                        self.scroll.y = scroll;
333                        self.invalidate_arrange();
334                    }
335                    ScrollPanelMessage::HorizontalScroll(scroll) => {
336                        self.scroll.x = scroll;
337                        self.invalidate_arrange();
338                    }
339                    ScrollPanelMessage::BringIntoView(handle) => {
340                        self.bring_into_view(ui, handle);
341                    }
342                    ScrollPanelMessage::ScrollToEnd => {
343                        let max_size = self.children_size(ui);
344                        if self.vertical_scroll_allowed {
345                            ui.send_message(ScrollPanelMessage::vertical_scroll(
346                                self.handle,
347                                MessageDirection::ToWidget,
348                                (max_size.y - self.actual_local_size().y).max(0.0),
349                            ));
350                        }
351                        if self.horizontal_scroll_allowed {
352                            ui.send_message(ScrollPanelMessage::horizontal_scroll(
353                                self.handle,
354                                MessageDirection::ToWidget,
355                                (max_size.x - self.actual_local_size().x).max(0.0),
356                            ));
357                        }
358                    }
359                }
360            }
361        }
362    }
363}
364
365/// Scroll panel builder creates [`ScrollPanel`] widget instances and adds them to the user interface.
366pub struct ScrollPanelBuilder {
367    widget_builder: WidgetBuilder,
368    vertical_scroll_allowed: Option<bool>,
369    horizontal_scroll_allowed: Option<bool>,
370    scroll_value: Vector2<f32>,
371}
372
373impl ScrollPanelBuilder {
374    /// Creates new scroll panel builder.
375    pub fn new(widget_builder: WidgetBuilder) -> Self {
376        Self {
377            widget_builder,
378            vertical_scroll_allowed: None,
379            horizontal_scroll_allowed: None,
380            scroll_value: Default::default(),
381        }
382    }
383
384    /// Enables or disables vertical scrolling.
385    pub fn with_vertical_scroll_allowed(mut self, value: bool) -> Self {
386        self.vertical_scroll_allowed = Some(value);
387        self
388    }
389
390    /// Enables or disables horizontal scrolling.
391    pub fn with_horizontal_scroll_allowed(mut self, value: bool) -> Self {
392        self.horizontal_scroll_allowed = Some(value);
393        self
394    }
395
396    /// Sets the desired scrolling value for both axes at the same time.
397    pub fn with_scroll_value(mut self, scroll_value: Vector2<f32>) -> Self {
398        self.scroll_value = scroll_value;
399        self
400    }
401
402    /// Finishes scroll panel building and adds it to the user interface.
403    pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode> {
404        ctx.add_node(UiNode::new(ScrollPanel {
405            widget: self.widget_builder.build(ctx),
406            scroll: self.scroll_value,
407            vertical_scroll_allowed: self.vertical_scroll_allowed.unwrap_or(true),
408            horizontal_scroll_allowed: self.horizontal_scroll_allowed.unwrap_or(false),
409        }))
410    }
411}
412
413#[cfg(test)]
414mod test {
415    use crate::scroll_panel::ScrollPanelBuilder;
416    use crate::{test::test_widget_deletion, widget::WidgetBuilder};
417
418    #[test]
419    fn test_deletion() {
420        test_widget_deletion(|ctx| ScrollPanelBuilder::new(WidgetBuilder::new()).build(ctx));
421    }
422}