fyrox_ui/
scroll_viewer.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 viewer is a scrollable region with two scroll bars for each axis. It is used to wrap a content of unknown
22//! size to ensure that all of it will be accessible in a parent widget bounds. See [`ScrollViewer`] docs for more
23//! info and usage examples.
24
25#![warn(missing_docs)]
26
27use crate::{
28    core::{
29        algebra::Vector2, pool::Handle, reflect::prelude::*, type_traits::prelude::*,
30        uuid_provider, visitor::prelude::*,
31    },
32    define_constructor,
33    grid::{Column, GridBuilder, Row},
34    message::{MessageDirection, UiMessage},
35    scroll_bar::{ScrollBar, ScrollBarBuilder, ScrollBarMessage},
36    scroll_panel::{ScrollPanelBuilder, ScrollPanelMessage},
37    widget::{Widget, WidgetBuilder, WidgetMessage},
38    BuildContext, Control, Orientation, UiNode, UserInterface,
39};
40use fyrox_graph::{
41    constructor::{ConstructorProvider, GraphNodeConstructor},
42    BaseSceneGraph,
43};
44use std::ops::{Deref, DerefMut};
45
46/// A set of messages that could be used to alternate the state of a [`ScrollViewer`] widget.
47#[derive(Debug, Clone, PartialEq)]
48pub enum ScrollViewerMessage {
49    /// Sets the new content of the scroll viewer.
50    Content(Handle<UiNode>),
51    /// Adjusts vertical and horizontal scroll values so given node will be in "view box" of the scroll viewer.
52    BringIntoView(Handle<UiNode>),
53    /// Sets the new vertical scrolling speed.
54    VScrollSpeed(f32),
55    /// Sets the new horizontal scrolling speed.
56    HScrollSpeed(f32),
57    /// Scrolls to end of the content.
58    ScrollToEnd,
59    /// Sets the vertical scrolling value.
60    VerticalScroll(f32),
61    /// Sets the horizontal scrolling value.
62    HorizontalScroll(f32),
63}
64
65impl ScrollViewerMessage {
66    define_constructor!(
67        /// Creates [`ScrollViewerMessage::Content`] message.
68        ScrollViewerMessage:Content => fn content(Handle<UiNode>), layout: false
69    );
70    define_constructor!(
71        /// Creates [`ScrollViewerMessage::BringIntoView`] message.
72        ScrollViewerMessage:BringIntoView => fn bring_into_view(Handle<UiNode>), layout: true
73    );
74    define_constructor!(
75        /// Creates [`ScrollViewerMessage::VScrollSpeed`] message.
76        ScrollViewerMessage:VScrollSpeed => fn v_scroll_speed(f32), layout: true
77    );
78    define_constructor!(
79        /// Creates [`ScrollViewerMessage::HScrollSpeed`] message.
80        ScrollViewerMessage:HScrollSpeed => fn h_scroll_speed(f32), layout: true
81    );
82    define_constructor!(
83        /// Creates [`ScrollViewerMessage::ScrollToEnd`] message.
84        ScrollViewerMessage:ScrollToEnd => fn scroll_to_end(), layout: true
85    );
86    define_constructor!(
87        /// Creates [`ScrollViewerMessage::HorizontalScroll`] message.
88        ScrollViewerMessage:HorizontalScroll => fn horizontal_scroll(f32), layout: false
89    );
90    define_constructor!(
91        /// Creates [`ScrollViewerMessage::VerticalScroll`] message.
92        ScrollViewerMessage:VerticalScroll => fn vertical_scroll(f32), layout: false
93    );
94}
95
96/// Scroll viewer is a scrollable region with two scroll bars for each axis. It is used to wrap a content of unknown
97/// size to ensure that all of it will be accessible in a parent widget bounds. For example, it could be used in a
98/// Window widget to allow a content of the window to be accessible, even if the window is smaller than the content.
99///
100/// ## Example
101///
102/// A scroll viewer widget could be created using [`ScrollViewerBuilder`]:
103///
104/// ```rust
105/// # use fyrox_ui::{
106/// #     button::ButtonBuilder, core::pool::Handle, scroll_viewer::ScrollViewerBuilder,
107/// #     stack_panel::StackPanelBuilder, text::TextBuilder, widget::WidgetBuilder, BuildContext,
108/// #     UiNode,
109/// # };
110/// #
111/// fn create_scroll_viewer(ctx: &mut BuildContext) -> Handle<UiNode> {
112///     ScrollViewerBuilder::new(WidgetBuilder::new())
113///         .with_content(
114///             StackPanelBuilder::new(
115///                 WidgetBuilder::new()
116///                     .with_child(
117///                         ButtonBuilder::new(WidgetBuilder::new())
118///                             .with_text("Click Me!")
119///                             .build(ctx),
120///                     )
121///                     .with_child(
122///                         TextBuilder::new(WidgetBuilder::new())
123///                             .with_text("Some\nlong\ntext")
124///                             .build(ctx),
125///                     ),
126///             )
127///             .build(ctx),
128///         )
129///         .build(ctx)
130/// }
131/// ```
132///
133/// Keep in mind, that you can change the content of a scroll viewer at runtime using [`ScrollViewerMessage::Content`] message.
134///
135/// ## Scrolling Speed and Controls
136///
137/// Scroll viewer can have an arbitrary scrolling speed for each axis. Scrolling is performed via mouse wheel and by default it
138/// scrolls vertical axis, which can be changed by holding `Shift` key. Scrolling speed can be set during the build phase:
139///
140/// ```rust
141/// # use fyrox_ui::{
142/// #     core::pool::Handle, scroll_viewer::ScrollViewerBuilder, widget::WidgetBuilder,
143/// #     BuildContext, UiNode,
144/// # };
145/// #
146/// fn create_scroll_viewer(ctx: &mut BuildContext) -> Handle<UiNode> {
147///     ScrollViewerBuilder::new(WidgetBuilder::new())
148///         // Set vertical scrolling speed twice as fast as default scrolling speed.
149///         .with_v_scroll_speed(60.0)
150///         // Set horizontal scrolling speed slightly lower than the default value (30.0).
151///         .with_h_scroll_speed(20.0)
152///         .build(ctx)
153/// }
154/// ```
155///
156/// Also it could be set using [`ScrollViewerMessage::HScrollSpeed`] or [`ScrollViewerMessage::VScrollSpeed`] messages.
157///
158/// ## Bringing a child into view
159///
160/// Calculates the scroll values to bring a desired child into view, it can be used for automatic navigation:
161///
162/// ```rust
163/// # use fyrox_ui::{
164/// #     core::pool::Handle, message::MessageDirection, scroll_viewer::ScrollViewerMessage, UiNode,
165/// #     UserInterface,
166/// # };
167/// fn bring_child_into_view(
168///     scroll_viewer: Handle<UiNode>,
169///     child: Handle<UiNode>,
170///     ui: &UserInterface,
171/// ) {
172///     ui.send_message(ScrollViewerMessage::bring_into_view(
173///         scroll_viewer,
174///         MessageDirection::ToWidget,
175///         child,
176///     ))
177/// }
178/// ```
179#[derive(Default, Clone, Debug, Visit, Reflect, ComponentProvider)]
180pub struct ScrollViewer {
181    /// Base widget of the scroll viewer.
182    pub widget: Widget,
183    /// A handle of a content.
184    pub content: Handle<UiNode>,
185    /// A handle of [`crate::scroll_panel::ScrollPanel`] widget instance that does the actual layouting.
186    pub scroll_panel: Handle<UiNode>,
187    /// A handle of scroll bar widget for vertical axis.
188    pub v_scroll_bar: Handle<UiNode>,
189    /// A handle of scroll bar widget for horizontal axis.
190    pub h_scroll_bar: Handle<UiNode>,
191    /// Current vertical scrolling speed.
192    pub v_scroll_speed: f32,
193    /// Current horizontal scrolling speed.
194    pub h_scroll_speed: f32,
195}
196
197impl ConstructorProvider<UiNode, UserInterface> for ScrollViewer {
198    fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
199        GraphNodeConstructor::new::<Self>()
200            .with_variant("Scroll Viewer", |ui| {
201                ScrollViewerBuilder::new(WidgetBuilder::new().with_name("Scroll Viewer"))
202                    .build(&mut ui.build_ctx())
203                    .into()
204            })
205            .with_group("Layout")
206    }
207}
208
209crate::define_widget_deref!(ScrollViewer);
210
211uuid_provider!(ScrollViewer = "173e869f-7da0-4ae2-915a-5d545d8150cc");
212
213impl Control for ScrollViewer {
214    fn arrange_override(&self, ui: &UserInterface, final_size: Vector2<f32>) -> Vector2<f32> {
215        let size = self.widget.arrange_override(ui, final_size);
216
217        if self.content.is_some() {
218            let content_size = ui.node(self.content).desired_size();
219            let available_size_for_content = ui.node(self.scroll_panel).desired_size();
220
221            let x_max = (content_size.x - available_size_for_content.x).max(0.0);
222            let x_size_ratio = if content_size.x > f32::EPSILON {
223                (available_size_for_content.x / content_size.x).min(1.0)
224            } else {
225                1.0
226            };
227            ui.send_message(ScrollBarMessage::max_value(
228                self.h_scroll_bar,
229                MessageDirection::ToWidget,
230                x_max,
231            ));
232            ui.send_message(ScrollBarMessage::size_ratio(
233                self.h_scroll_bar,
234                MessageDirection::ToWidget,
235                x_size_ratio,
236            ));
237
238            let y_max = (content_size.y - available_size_for_content.y).max(0.0);
239            let y_size_ratio = if content_size.y > f32::EPSILON {
240                (available_size_for_content.y / content_size.y).min(1.0)
241            } else {
242                1.0
243            };
244            ui.send_message(ScrollBarMessage::max_value(
245                self.v_scroll_bar,
246                MessageDirection::ToWidget,
247                y_max,
248            ));
249            ui.send_message(ScrollBarMessage::size_ratio(
250                self.v_scroll_bar,
251                MessageDirection::ToWidget,
252                y_size_ratio,
253            ));
254        }
255
256        size
257    }
258
259    fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
260        self.widget.handle_routed_message(ui, message);
261
262        if let Some(WidgetMessage::MouseWheel { amount, .. }) = message.data::<WidgetMessage>() {
263            if !message.handled() {
264                let (scroll_bar, scroll_speed) = if ui.keyboard_modifiers().shift {
265                    (self.h_scroll_bar, self.h_scroll_speed)
266                } else {
267                    (self.v_scroll_bar, self.v_scroll_speed)
268                };
269
270                if let Some(scroll_bar) = ui.node(scroll_bar).cast::<ScrollBar>() {
271                    let old_value = *scroll_bar.value;
272                    let new_value = old_value - amount * scroll_speed;
273                    if (old_value - new_value).abs() > f32::EPSILON {
274                        message.set_handled(true);
275                    }
276                    ui.send_message(ScrollBarMessage::value(
277                        scroll_bar.handle,
278                        MessageDirection::ToWidget,
279                        new_value,
280                    ));
281                }
282            }
283        } else if let Some(msg) = message.data::<ScrollPanelMessage>() {
284            if message.destination() == self.scroll_panel {
285                let msg = match *msg {
286                    ScrollPanelMessage::VerticalScroll(value) => ScrollBarMessage::value(
287                        self.v_scroll_bar,
288                        MessageDirection::ToWidget,
289                        value,
290                    ),
291                    ScrollPanelMessage::HorizontalScroll(value) => ScrollBarMessage::value(
292                        self.h_scroll_bar,
293                        MessageDirection::ToWidget,
294                        value,
295                    ),
296                    _ => return,
297                };
298                // handle flag here is raised to prevent infinite message loop with the branch down below (ScrollBar::value).
299                msg.set_handled(true);
300                ui.send_message(msg);
301            }
302        } else if let Some(msg) = message.data::<ScrollBarMessage>() {
303            if message.direction() == MessageDirection::FromWidget {
304                match msg {
305                    ScrollBarMessage::Value(new_value) => {
306                        if !message.handled() {
307                            if message.destination() == self.v_scroll_bar
308                                && self.v_scroll_bar.is_some()
309                            {
310                                ui.send_message(ScrollPanelMessage::vertical_scroll(
311                                    self.scroll_panel,
312                                    MessageDirection::ToWidget,
313                                    *new_value,
314                                ));
315                            } else if message.destination() == self.h_scroll_bar
316                                && self.h_scroll_bar.is_some()
317                            {
318                                ui.send_message(ScrollPanelMessage::horizontal_scroll(
319                                    self.scroll_panel,
320                                    MessageDirection::ToWidget,
321                                    *new_value,
322                                ));
323                            }
324                        }
325                    }
326                    &ScrollBarMessage::MaxValue(_) => {
327                        if message.destination() == self.v_scroll_bar && self.v_scroll_bar.is_some()
328                        {
329                            if let Some(scroll_bar) = ui.node(self.v_scroll_bar).cast::<ScrollBar>()
330                            {
331                                let visibility =
332                                    (*scroll_bar.max - *scroll_bar.min).abs() >= f32::EPSILON;
333                                ui.send_message(WidgetMessage::visibility(
334                                    self.v_scroll_bar,
335                                    MessageDirection::ToWidget,
336                                    visibility,
337                                ));
338                            }
339                        } else if message.destination() == self.h_scroll_bar
340                            && self.h_scroll_bar.is_some()
341                        {
342                            if let Some(scroll_bar) = ui.node(self.h_scroll_bar).cast::<ScrollBar>()
343                            {
344                                let visibility =
345                                    (*scroll_bar.max - *scroll_bar.min).abs() >= f32::EPSILON;
346                                ui.send_message(WidgetMessage::visibility(
347                                    self.h_scroll_bar,
348                                    MessageDirection::ToWidget,
349                                    visibility,
350                                ));
351                            }
352                        }
353                    }
354                    _ => (),
355                }
356            }
357        } else if let Some(msg) = message.data::<ScrollViewerMessage>() {
358            if message.destination() == self.handle() {
359                match msg {
360                    ScrollViewerMessage::Content(content) => {
361                        for child in ui.node(self.scroll_panel).children() {
362                            ui.send_message(WidgetMessage::remove(
363                                *child,
364                                MessageDirection::ToWidget,
365                            ));
366                        }
367                        ui.send_message(WidgetMessage::link(
368                            *content,
369                            MessageDirection::ToWidget,
370                            self.scroll_panel,
371                        ));
372                    }
373                    &ScrollViewerMessage::BringIntoView(handle) => {
374                        // Re-cast message to inner panel.
375                        ui.send_message(ScrollPanelMessage::bring_into_view(
376                            self.scroll_panel,
377                            MessageDirection::ToWidget,
378                            handle,
379                        ));
380                    }
381                    &ScrollViewerMessage::HScrollSpeed(speed) => {
382                        if self.h_scroll_speed != speed
383                            && message.direction() == MessageDirection::ToWidget
384                        {
385                            self.h_scroll_speed = speed;
386
387                            ui.send_message(message.reverse());
388                        }
389                    }
390                    &ScrollViewerMessage::VScrollSpeed(speed) => {
391                        if self.v_scroll_speed != speed
392                            && message.direction() == MessageDirection::ToWidget
393                        {
394                            self.v_scroll_speed = speed;
395
396                            ui.send_message(message.reverse());
397                        }
398                    }
399                    ScrollViewerMessage::ScrollToEnd => {
400                        // Re-cast message to inner panel.
401                        ui.send_message(ScrollPanelMessage::scroll_to_end(
402                            self.scroll_panel,
403                            MessageDirection::ToWidget,
404                        ));
405                    }
406                    ScrollViewerMessage::HorizontalScroll(value) => {
407                        ui.send_message(ScrollBarMessage::value(
408                            self.h_scroll_bar,
409                            MessageDirection::ToWidget,
410                            *value,
411                        ));
412                    }
413                    ScrollViewerMessage::VerticalScroll(value) => {
414                        ui.send_message(ScrollBarMessage::value(
415                            self.v_scroll_bar,
416                            MessageDirection::ToWidget,
417                            *value,
418                        ));
419                    }
420                }
421            }
422        }
423    }
424}
425
426/// Scroll viewer builder creates [`ScrollViewer`] widget instances and adds them to the user interface.
427pub struct ScrollViewerBuilder {
428    widget_builder: WidgetBuilder,
429    content: Handle<UiNode>,
430    h_scroll_bar: Option<Handle<UiNode>>,
431    v_scroll_bar: Option<Handle<UiNode>>,
432    horizontal_scroll_allowed: bool,
433    vertical_scroll_allowed: bool,
434    v_scroll_speed: f32,
435    h_scroll_speed: f32,
436}
437
438impl ScrollViewerBuilder {
439    /// Creates new builder instance.
440    pub fn new(widget_builder: WidgetBuilder) -> Self {
441        Self {
442            widget_builder,
443            content: Handle::NONE,
444            h_scroll_bar: None,
445            v_scroll_bar: None,
446            horizontal_scroll_allowed: false,
447            vertical_scroll_allowed: true,
448            v_scroll_speed: 30.0,
449            h_scroll_speed: 30.0,
450        }
451    }
452
453    /// Sets the desired content of the scroll viewer.
454    pub fn with_content(mut self, content: Handle<UiNode>) -> Self {
455        self.content = content;
456        self
457    }
458
459    /// Sets the desired vertical scroll bar widget.
460    pub fn with_vertical_scroll_bar(mut self, v_scroll_bar: Handle<UiNode>) -> Self {
461        self.v_scroll_bar = Some(v_scroll_bar);
462        self
463    }
464
465    /// Sets the desired horizontal scroll bar widget.
466    pub fn with_horizontal_scroll_bar(mut self, h_scroll_bar: Handle<UiNode>) -> Self {
467        self.h_scroll_bar = Some(h_scroll_bar);
468        self
469    }
470
471    /// Enables or disables vertical scrolling.
472    pub fn with_vertical_scroll_allowed(mut self, value: bool) -> Self {
473        self.vertical_scroll_allowed = value;
474        self
475    }
476
477    /// Enables or disables horizontal scrolling.
478    pub fn with_horizontal_scroll_allowed(mut self, value: bool) -> Self {
479        self.horizontal_scroll_allowed = value;
480        self
481    }
482
483    /// Sets the desired vertical scrolling speed.
484    pub fn with_v_scroll_speed(mut self, speed: f32) -> Self {
485        self.v_scroll_speed = speed;
486        self
487    }
488
489    /// Sets the desired horizontal scrolling speed.
490    pub fn with_h_scroll_speed(mut self, speed: f32) -> Self {
491        self.h_scroll_speed = speed;
492        self
493    }
494
495    /// Finishes widget building and adds it to the user interface.
496    pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode> {
497        let content_presenter = ScrollPanelBuilder::new(
498            WidgetBuilder::new()
499                .with_child(self.content)
500                .on_row(0)
501                .on_column(0),
502        )
503        .with_horizontal_scroll_allowed(self.horizontal_scroll_allowed)
504        .with_vertical_scroll_allowed(self.vertical_scroll_allowed)
505        .build(ctx);
506
507        let v_scroll_bar = self.v_scroll_bar.unwrap_or_else(|| {
508            ScrollBarBuilder::new(WidgetBuilder::new().with_width(16.0))
509                .with_step(30.0)
510                .with_orientation(Orientation::Vertical)
511                .build(ctx)
512        });
513        ctx[v_scroll_bar].set_row(0).set_column(1);
514
515        let h_scroll_bar = self.h_scroll_bar.unwrap_or_else(|| {
516            ScrollBarBuilder::new(WidgetBuilder::new().with_height(16.0))
517                .with_step(30.0)
518                .with_orientation(Orientation::Horizontal)
519                .build(ctx)
520        });
521        ctx[h_scroll_bar].set_row(1).set_column(0);
522
523        let sv = ScrollViewer {
524            widget: self
525                .widget_builder
526                .with_child(
527                    GridBuilder::new(
528                        WidgetBuilder::new()
529                            .with_child(content_presenter)
530                            .with_child(h_scroll_bar)
531                            .with_child(v_scroll_bar),
532                    )
533                    .add_row(Row::stretch())
534                    .add_row(Row::auto())
535                    .add_column(Column::stretch())
536                    .add_column(Column::auto())
537                    .build(ctx),
538                )
539                .build(ctx),
540            content: self.content,
541            v_scroll_bar,
542            h_scroll_bar,
543            scroll_panel: content_presenter,
544            v_scroll_speed: self.v_scroll_speed,
545            h_scroll_speed: self.h_scroll_speed,
546        };
547        ctx.add_node(UiNode::new(sv))
548    }
549}
550
551#[cfg(test)]
552mod test {
553    use crate::scroll_viewer::ScrollViewerBuilder;
554    use crate::{test::test_widget_deletion, widget::WidgetBuilder};
555
556    #[test]
557    fn test_deletion() {
558        test_widget_deletion(|ctx| ScrollViewerBuilder::new(WidgetBuilder::new()).build(ctx));
559    }
560}