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}