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}