Skip to main content

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