Skip to main content

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    draw::{CommandTexture, Draw, DrawingContext},
32    message::UiMessage,
33    widget::{Widget, WidgetBuilder},
34    BuildContext, Control, UiNode, UserInterface,
35};
36
37use crate::message::MessageData;
38use fyrox_core::uuid_provider;
39use fyrox_graph::constructor::{ConstructorProvider, GraphNodeConstructor};
40use fyrox_graph::SceneGraph;
41
42/// A set of messages, that is used to modify the state of a scroll panel.
43#[derive(Debug, Clone, PartialEq)]
44pub enum ScrollPanelMessage {
45    /// Sets the desired scrolling value for the vertical axis.
46    VerticalScroll(f32),
47    /// Sets the desired scrolling value for the horizontal axis.
48    HorizontalScroll(f32),
49    /// Adjusts vertical and horizontal scroll values so given node will be in "view box" of scroll panel.
50    BringIntoView(Handle<UiNode>),
51    /// Scrolls to end of the content.
52    ScrollToEnd,
53}
54
55impl MessageData for ScrollPanelMessage {
56    fn need_perform_layout(&self) -> bool {
57        matches!(
58            self,
59            ScrollPanelMessage::BringIntoView(_) | ScrollPanelMessage::ScrollToEnd
60        )
61    }
62}
63
64/// Scroll panel widget is used to arrange its children widgets, so they can be offset by a certain amount of units
65/// from top-left corner. It is used to provide basic scrolling functionality.
66///
67/// ## Examples
68///
69/// ```rust
70/// # use fyrox_ui::{
71/// #     button::ButtonBuilder,
72/// #     core::{algebra::Vector2, pool::Handle},
73/// #     grid::{Column, GridBuilder, Row},
74/// #     scroll_panel::ScrollPanelBuilder,
75/// #     widget::WidgetBuilder,
76/// #     BuildContext, UiNode,
77/// # };
78/// # use fyrox_ui::scroll_panel::ScrollPanel;
79/// #
80/// fn create_scroll_panel(ctx: &mut BuildContext) -> Handle<ScrollPanel> {
81///     ScrollPanelBuilder::new(
82///         WidgetBuilder::new().with_child(
83///             GridBuilder::new(
84///                 WidgetBuilder::new()
85///                     .with_child(
86///                         ButtonBuilder::new(WidgetBuilder::new())
87///                             .with_text("Some Button")
88///                             .build(ctx),
89///                     )
90///                     .with_child(
91///                         ButtonBuilder::new(WidgetBuilder::new())
92///                             .with_text("Some Other Button")
93///                             .build(ctx),
94///                     ),
95///             )
96///             .add_row(Row::auto())
97///             .add_row(Row::auto())
98///             .add_column(Column::stretch())
99///             .build(ctx),
100///         ),
101///     )
102///     .with_scroll_value(Vector2::new(100.0, 200.0))
103///     .with_vertical_scroll_allowed(true)
104///     .with_horizontal_scroll_allowed(true)
105///     .build(ctx)
106/// }
107/// ```
108///
109/// ## Scrolling
110///
111/// Scrolling value for both axes can be set via [`ScrollPanelMessage::VerticalScroll`] and [`ScrollPanelMessage::HorizontalScroll`]:
112///
113/// ```rust
114/// use fyrox_ui::{
115///     core::pool::Handle, message::MessageDirection, scroll_panel::ScrollPanelMessage, UiNode,
116///     UserInterface,
117/// };
118/// fn set_scrolling_value(
119///     scroll_panel: Handle<UiNode>,
120///     horizontal: f32,
121///     vertical: f32,
122///     ui: &UserInterface,
123/// ) {
124///     ui.send(scroll_panel, ScrollPanelMessage::HorizontalScroll(horizontal));
125///     ui.send(scroll_panel, ScrollPanelMessage::VerticalScroll(vertical));
126/// }
127/// ```
128///
129/// ## Bringing child into view
130///
131/// Calculates the scroll values to bring a desired child into view, it can be used for automatic navigation:
132///
133/// ```rust
134/// # use fyrox_ui::{
135/// #     core::pool::Handle, message::MessageDirection, scroll_panel::ScrollPanelMessage, UiNode,
136/// #     UserInterface,
137/// # };
138/// fn bring_child_into_view(
139///     scroll_panel: Handle<UiNode>,
140///     child: Handle<UiNode>,
141///     ui: &UserInterface,
142/// ) {
143///     ui.send(scroll_panel, ScrollPanelMessage::BringIntoView(child))
144/// }
145/// ```
146#[derive(Default, Clone, Visit, Reflect, Debug, ComponentProvider)]
147#[reflect(derived_type = "UiNode")]
148pub struct ScrollPanel {
149    /// Base widget of the scroll panel.
150    pub widget: Widget,
151    /// Current scroll value of the scroll panel.
152    pub scroll: Vector2<f32>,
153    /// A flag, that defines whether the vertical scrolling is allowed or not.
154    pub vertical_scroll_allowed: bool,
155    /// A flag, that defines whether the horizontal scrolling is allowed or not.
156    pub horizontal_scroll_allowed: bool,
157}
158
159impl ConstructorProvider<UiNode, UserInterface> for ScrollPanel {
160    fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
161        GraphNodeConstructor::new::<Self>()
162            .with_variant("Scroll Panel", |ui| {
163                ScrollPanelBuilder::new(WidgetBuilder::new().with_name("Scroll Panel"))
164                    .build(&mut ui.build_ctx())
165                    .to_base()
166                    .into()
167            })
168            .with_group("Layout")
169    }
170}
171
172crate::define_widget_deref!(ScrollPanel);
173
174uuid_provider!(ScrollPanel = "1ab4936d-58c8-4cf7-b33c-4b56092f4826");
175
176impl ScrollPanel {
177    fn children_size(&self, ui: &UserInterface) -> Vector2<f32> {
178        let mut children_size = Vector2::<f32>::default();
179        for child_handle in self.widget.children() {
180            let desired_size = ui.node(*child_handle).desired_size();
181            children_size.x = children_size.x.max(desired_size.x);
182            children_size.y = children_size.y.max(desired_size.y);
183        }
184        children_size
185    }
186    fn bring_into_view(&self, ui: &UserInterface, handle: Handle<UiNode>) {
187        let Ok(node_to_focus_ref) = ui.try_get_node(handle) else {
188            return;
189        };
190        let mut parent = handle;
191        let mut relative_position = Vector2::default();
192        while parent.is_some() && parent != self.handle {
193            let node = ui.node(parent);
194            relative_position += node.actual_local_position();
195            parent = node.parent();
196        }
197        // This check is needed because it possible that given handle is not in
198        // subtree of the current scroll panel.
199        if parent != self.handle {
200            return;
201        }
202        let size = node_to_focus_ref.actual_local_size();
203        let children_size = self.children_size(ui);
204        let view_size = self.actual_local_size();
205        // Check if requested item already in "view box", this will prevent weird "jumping" effect
206        // when bring into view was requested on already visible element.
207        if self.vertical_scroll_allowed
208            && (relative_position.y < 0.0 || relative_position.y + size.y > view_size.y)
209        {
210            relative_position.y += self.scroll.y;
211            let scroll_max = (children_size.y - view_size.y).max(0.0);
212            relative_position.y = relative_position.y.clamp(0.0, scroll_max);
213            ui.send(
214                self.handle,
215                ScrollPanelMessage::VerticalScroll(relative_position.y),
216            );
217        }
218        if self.horizontal_scroll_allowed
219            && (relative_position.x < 0.0 || relative_position.x + size.x > view_size.x)
220        {
221            relative_position.x += self.scroll.x;
222            let scroll_max = (children_size.x - view_size.x).max(0.0);
223            relative_position.x = relative_position.x.clamp(0.0, scroll_max);
224            ui.send(
225                self.handle,
226                ScrollPanelMessage::HorizontalScroll(relative_position.x),
227            );
228        }
229    }
230}
231
232impl Control for ScrollPanel {
233    fn measure_override(&self, ui: &UserInterface, available_size: Vector2<f32>) -> Vector2<f32> {
234        let size_for_child = Vector2::new(
235            if self.horizontal_scroll_allowed {
236                f32::INFINITY
237            } else {
238                available_size.x
239            },
240            if self.vertical_scroll_allowed {
241                f32::INFINITY
242            } else {
243                available_size.y
244            },
245        );
246
247        let mut desired_size = Vector2::default();
248
249        for child_handle in self.widget.children() {
250            ui.measure_node(*child_handle, size_for_child);
251
252            let child = ui.nodes.borrow(*child_handle);
253            let child_desired_size = child.desired_size();
254            if child_desired_size.x > desired_size.x {
255                desired_size.x = child_desired_size.x;
256            }
257            if child_desired_size.y > desired_size.y {
258                desired_size.y = child_desired_size.y;
259            }
260        }
261
262        desired_size
263    }
264
265    fn arrange_override(&self, ui: &UserInterface, final_size: Vector2<f32>) -> Vector2<f32> {
266        let children_size = self.children_size(ui);
267
268        let child_rect = Rect::new(
269            -self.scroll.x,
270            -self.scroll.y,
271            if self.horizontal_scroll_allowed {
272                children_size.x.max(final_size.x)
273            } else {
274                final_size.x
275            },
276            if self.vertical_scroll_allowed {
277                children_size.y.max(final_size.y)
278            } else {
279                final_size.y
280            },
281        );
282
283        for child_handle in self.widget.children() {
284            ui.arrange_node(*child_handle, &child_rect);
285        }
286
287        final_size
288    }
289
290    fn draw(&self, drawing_context: &mut DrawingContext) {
291        // Emit transparent geometry so the panel will receive mouse events.
292        drawing_context.push_rect_filled(&self.widget.bounding_rect(), None);
293        drawing_context.commit(
294            self.clip_bounds(),
295            Brush::Solid(Color::TRANSPARENT),
296            CommandTexture::None,
297            &self.material,
298            None,
299        );
300    }
301
302    fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
303        self.widget.handle_routed_message(ui, message);
304
305        if message.destination() == self.handle() {
306            if let Some(msg) = message.data::<ScrollPanelMessage>() {
307                match *msg {
308                    ScrollPanelMessage::VerticalScroll(scroll) => {
309                        self.scroll.y = scroll;
310                        self.invalidate_arrange();
311                    }
312                    ScrollPanelMessage::HorizontalScroll(scroll) => {
313                        self.scroll.x = scroll;
314                        self.invalidate_arrange();
315                    }
316                    ScrollPanelMessage::BringIntoView(handle) => {
317                        self.bring_into_view(ui, handle);
318                    }
319                    ScrollPanelMessage::ScrollToEnd => {
320                        let max_size = self.children_size(ui);
321                        if self.vertical_scroll_allowed {
322                            ui.send(
323                                self.handle,
324                                ScrollPanelMessage::VerticalScroll(
325                                    (max_size.y - self.actual_local_size().y).max(0.0),
326                                ),
327                            );
328                        }
329                        if self.horizontal_scroll_allowed {
330                            ui.send(
331                                self.handle,
332                                ScrollPanelMessage::HorizontalScroll(
333                                    (max_size.x - self.actual_local_size().x).max(0.0),
334                                ),
335                            );
336                        }
337                    }
338                }
339            }
340        }
341    }
342}
343
344/// Scroll panel builder creates [`ScrollPanel`] widget instances and adds them to the user interface.
345pub struct ScrollPanelBuilder {
346    widget_builder: WidgetBuilder,
347    vertical_scroll_allowed: Option<bool>,
348    horizontal_scroll_allowed: Option<bool>,
349    scroll_value: Vector2<f32>,
350}
351
352impl ScrollPanelBuilder {
353    /// Creates new scroll panel builder.
354    pub fn new(widget_builder: WidgetBuilder) -> Self {
355        Self {
356            widget_builder,
357            vertical_scroll_allowed: None,
358            horizontal_scroll_allowed: None,
359            scroll_value: Default::default(),
360        }
361    }
362
363    /// Enables or disables vertical scrolling.
364    pub fn with_vertical_scroll_allowed(mut self, value: bool) -> Self {
365        self.vertical_scroll_allowed = Some(value);
366        self
367    }
368
369    /// Enables or disables horizontal scrolling.
370    pub fn with_horizontal_scroll_allowed(mut self, value: bool) -> Self {
371        self.horizontal_scroll_allowed = Some(value);
372        self
373    }
374
375    /// Sets the desired scrolling value for both axes at the same time.
376    pub fn with_scroll_value(mut self, scroll_value: Vector2<f32>) -> Self {
377        self.scroll_value = scroll_value;
378        self
379    }
380
381    /// Finishes scroll panel building and adds it to the user interface.
382    pub fn build(self, ctx: &mut BuildContext) -> Handle<ScrollPanel> {
383        ctx.add(ScrollPanel {
384            widget: self.widget_builder.build(ctx),
385            scroll: self.scroll_value,
386            vertical_scroll_allowed: self.vertical_scroll_allowed.unwrap_or(true),
387            horizontal_scroll_allowed: self.horizontal_scroll_allowed.unwrap_or(false),
388        })
389    }
390}
391
392#[cfg(test)]
393mod test {
394    use crate::scroll_panel::ScrollPanelBuilder;
395    use crate::{test::test_widget_deletion, widget::WidgetBuilder};
396
397    #[test]
398    fn test_deletion() {
399        test_widget_deletion(|ctx| ScrollPanelBuilder::new(WidgetBuilder::new()).build(ctx));
400    }
401}