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