fyrox_ui/scroll_panel.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 panel widget is used to arrange its children widgets, so they can be offset by a certain amount of units
22//! from top-left corner. It is used to provide basic scrolling functionality. See [`ScrollPanel`] docs for more
23//! info and usage examples.
24
25#![allow(missing_docs)]
26
27use crate::{
28 brush::Brush,
29 core::{
30 algebra::Vector2, color::Color, math::Rect, pool::Handle, reflect::prelude::*,
31 type_traits::prelude::*, visitor::prelude::*,
32 },
33 define_constructor,
34 draw::{CommandTexture, Draw, DrawingContext},
35 message::{MessageDirection, UiMessage},
36 widget::{Widget, WidgetBuilder},
37 BuildContext, Control, UiNode, UserInterface,
38};
39use fyrox_core::uuid_provider;
40use fyrox_graph::constructor::{ConstructorProvider, GraphNodeConstructor};
41use fyrox_graph::BaseSceneGraph;
42use std::ops::{Deref, DerefMut};
43
44/// A set of messages, that is used to modify the state of a scroll panel.
45#[derive(Debug, Clone, PartialEq)]
46pub enum ScrollPanelMessage {
47 /// Sets the desired scrolling value for the vertical axis.
48 VerticalScroll(f32),
49 /// Sets the desired scrolling value for the horizontal axis.
50 HorizontalScroll(f32),
51 /// Adjusts vertical and horizontal scroll values so given node will be in "view box" of scroll panel.
52 BringIntoView(Handle<UiNode>),
53 /// Scrolls to end of the content.
54 ScrollToEnd,
55}
56
57impl ScrollPanelMessage {
58 define_constructor!(
59 /// Creates [`ScrollPanelMessage::VerticalScroll`] message.
60 ScrollPanelMessage:VerticalScroll => fn vertical_scroll(f32), layout: false
61 );
62 define_constructor!(
63 /// Creates [`ScrollPanelMessage::HorizontalScroll`] message.
64 ScrollPanelMessage:HorizontalScroll => fn horizontal_scroll(f32), layout: false
65 );
66 define_constructor!(
67 /// Creates [`ScrollPanelMessage::BringIntoView`] message.
68 ScrollPanelMessage:BringIntoView => fn bring_into_view(Handle<UiNode>), layout: true
69 );
70 define_constructor!(
71 /// Creates [`ScrollPanelMessage::ScrollToEnd`] message.
72 ScrollPanelMessage:ScrollToEnd => fn scroll_to_end(), layout: true
73 );
74}
75
76/// Scroll panel widget is used to arrange its children widgets, so they can be offset by a certain amount of units
77/// from top-left corner. It is used to provide basic scrolling functionality.
78///
79/// ## Examples
80///
81/// ```rust
82/// # use fyrox_ui::{
83/// # button::ButtonBuilder,
84/// # core::{algebra::Vector2, pool::Handle},
85/// # grid::{Column, GridBuilder, Row},
86/// # scroll_panel::ScrollPanelBuilder,
87/// # widget::WidgetBuilder,
88/// # BuildContext, UiNode,
89/// # };
90/// #
91/// fn create_scroll_panel(ctx: &mut BuildContext) -> Handle<UiNode> {
92/// ScrollPanelBuilder::new(
93/// WidgetBuilder::new().with_child(
94/// GridBuilder::new(
95/// WidgetBuilder::new()
96/// .with_child(
97/// ButtonBuilder::new(WidgetBuilder::new())
98/// .with_text("Some Button")
99/// .build(ctx),
100/// )
101/// .with_child(
102/// ButtonBuilder::new(WidgetBuilder::new())
103/// .with_text("Some Other Button")
104/// .build(ctx),
105/// ),
106/// )
107/// .add_row(Row::auto())
108/// .add_row(Row::auto())
109/// .add_column(Column::stretch())
110/// .build(ctx),
111/// ),
112/// )
113/// .with_scroll_value(Vector2::new(100.0, 200.0))
114/// .with_vertical_scroll_allowed(true)
115/// .with_horizontal_scroll_allowed(true)
116/// .build(ctx)
117/// }
118/// ```
119///
120/// ## Scrolling
121///
122/// Scrolling value for both axes can be set via [`ScrollPanelMessage::VerticalScroll`] and [`ScrollPanelMessage::HorizontalScroll`]:
123///
124/// ```rust
125/// use fyrox_ui::{
126/// core::pool::Handle, message::MessageDirection, scroll_panel::ScrollPanelMessage, UiNode,
127/// UserInterface,
128/// };
129/// fn set_scrolling_value(
130/// scroll_panel: Handle<UiNode>,
131/// horizontal: f32,
132/// vertical: f32,
133/// ui: &UserInterface,
134/// ) {
135/// ui.send_message(ScrollPanelMessage::horizontal_scroll(
136/// scroll_panel,
137/// MessageDirection::ToWidget,
138/// horizontal,
139/// ));
140/// ui.send_message(ScrollPanelMessage::vertical_scroll(
141/// scroll_panel,
142/// MessageDirection::ToWidget,
143/// vertical,
144/// ));
145/// }
146/// ```
147///
148/// ## Bringing child into view
149///
150/// Calculates the scroll values to bring a desired child into view, it can be used for automatic navigation:
151///
152/// ```rust
153/// # use fyrox_ui::{
154/// # core::pool::Handle, message::MessageDirection, scroll_panel::ScrollPanelMessage, UiNode,
155/// # UserInterface,
156/// # };
157/// fn bring_child_into_view(
158/// scroll_panel: Handle<UiNode>,
159/// child: Handle<UiNode>,
160/// ui: &UserInterface,
161/// ) {
162/// ui.send_message(ScrollPanelMessage::bring_into_view(
163/// scroll_panel,
164/// MessageDirection::ToWidget,
165/// child,
166/// ))
167/// }
168/// ```
169#[derive(Default, Clone, Visit, Reflect, Debug, ComponentProvider)]
170pub struct ScrollPanel {
171 /// Base widget of the scroll panel.
172 pub widget: Widget,
173 /// Current scroll value of the scroll panel.
174 pub scroll: Vector2<f32>,
175 /// A flag, that defines whether the vertical scrolling is allowed or not.
176 pub vertical_scroll_allowed: bool,
177 /// A flag, that defines whether the horizontal scrolling is allowed or not.
178 pub horizontal_scroll_allowed: bool,
179}
180
181impl ConstructorProvider<UiNode, UserInterface> for ScrollPanel {
182 fn constructor() -> GraphNodeConstructor<UiNode, UserInterface> {
183 GraphNodeConstructor::new::<Self>()
184 .with_variant("Scroll Panel", |ui| {
185 ScrollPanelBuilder::new(WidgetBuilder::new().with_name("Scroll Panel"))
186 .build(&mut ui.build_ctx())
187 .into()
188 })
189 .with_group("Layout")
190 }
191}
192
193crate::define_widget_deref!(ScrollPanel);
194
195uuid_provider!(ScrollPanel = "1ab4936d-58c8-4cf7-b33c-4b56092f4826");
196
197impl ScrollPanel {
198 fn children_size(&self, ui: &UserInterface) -> Vector2<f32> {
199 let mut children_size = Vector2::<f32>::default();
200 for child_handle in self.widget.children() {
201 let desired_size = ui.node(*child_handle).desired_size();
202 children_size.x = children_size.x.max(desired_size.x);
203 children_size.y = children_size.y.max(desired_size.y);
204 }
205 children_size
206 }
207 fn bring_into_view(&self, ui: &UserInterface, handle: Handle<UiNode>) {
208 let Some(node_to_focus_ref) = ui.try_get(handle) else {
209 return;
210 };
211 let mut parent = handle;
212 let mut relative_position = Vector2::default();
213 while parent.is_some() && parent != self.handle {
214 let node = ui.node(parent);
215 relative_position += node.actual_local_position();
216 parent = node.parent();
217 }
218 // This check is needed because it possible that given handle is not in
219 // sub-tree of current scroll panel.
220 if parent != self.handle {
221 return;
222 }
223 let size = node_to_focus_ref.actual_local_size();
224 let children_size = self.children_size(ui);
225 let view_size = self.actual_local_size();
226 // Check if requested item already in "view box", this will prevent weird "jumping" effect
227 // when bring into view was requested on already visible element.
228 if self.vertical_scroll_allowed
229 && (relative_position.y < 0.0 || relative_position.y + size.y > view_size.y)
230 {
231 relative_position.y += self.scroll.y;
232 let scroll_max = (children_size.y - view_size.y).max(0.0);
233 relative_position.y = relative_position.y.clamp(0.0, scroll_max);
234 ui.send_message(ScrollPanelMessage::vertical_scroll(
235 self.handle,
236 MessageDirection::ToWidget,
237 relative_position.y,
238 ));
239 }
240 if self.horizontal_scroll_allowed
241 && (relative_position.x < 0.0 || relative_position.x + size.x > view_size.x)
242 {
243 relative_position.x += self.scroll.x;
244 let scroll_max = (children_size.x - view_size.x).max(0.0);
245 relative_position.x = relative_position.x.clamp(0.0, scroll_max);
246 ui.send_message(ScrollPanelMessage::horizontal_scroll(
247 self.handle,
248 MessageDirection::ToWidget,
249 relative_position.x,
250 ));
251 }
252 }
253}
254
255impl Control for ScrollPanel {
256 fn measure_override(&self, ui: &UserInterface, available_size: Vector2<f32>) -> Vector2<f32> {
257 let size_for_child = Vector2::new(
258 if self.horizontal_scroll_allowed {
259 f32::INFINITY
260 } else {
261 available_size.x
262 },
263 if self.vertical_scroll_allowed {
264 f32::INFINITY
265 } else {
266 available_size.y
267 },
268 );
269
270 let mut desired_size = Vector2::default();
271
272 for child_handle in self.widget.children() {
273 ui.measure_node(*child_handle, size_for_child);
274
275 let child = ui.nodes.borrow(*child_handle);
276 let child_desired_size = child.desired_size();
277 if child_desired_size.x > desired_size.x {
278 desired_size.x = child_desired_size.x;
279 }
280 if child_desired_size.y > desired_size.y {
281 desired_size.y = child_desired_size.y;
282 }
283 }
284
285 desired_size
286 }
287
288 fn arrange_override(&self, ui: &UserInterface, final_size: Vector2<f32>) -> Vector2<f32> {
289 let children_size = self.children_size(ui);
290
291 let child_rect = Rect::new(
292 -self.scroll.x,
293 -self.scroll.y,
294 if self.horizontal_scroll_allowed {
295 children_size.x.max(final_size.x)
296 } else {
297 final_size.x
298 },
299 if self.vertical_scroll_allowed {
300 children_size.y.max(final_size.y)
301 } else {
302 final_size.y
303 },
304 );
305
306 for child_handle in self.widget.children() {
307 ui.arrange_node(*child_handle, &child_rect);
308 }
309
310 final_size
311 }
312
313 fn draw(&self, drawing_context: &mut DrawingContext) {
314 // Emit transparent geometry so panel will receive mouse events.
315 drawing_context.push_rect_filled(&self.widget.bounding_rect(), None);
316 drawing_context.commit(
317 self.clip_bounds(),
318 Brush::Solid(Color::TRANSPARENT),
319 CommandTexture::None,
320 None,
321 );
322 }
323
324 fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
325 self.widget.handle_routed_message(ui, message);
326
327 if message.destination() == self.handle() {
328 if let Some(msg) = message.data::<ScrollPanelMessage>() {
329 match *msg {
330 ScrollPanelMessage::VerticalScroll(scroll) => {
331 self.scroll.y = scroll;
332 self.invalidate_arrange();
333 }
334 ScrollPanelMessage::HorizontalScroll(scroll) => {
335 self.scroll.x = scroll;
336 self.invalidate_arrange();
337 }
338 ScrollPanelMessage::BringIntoView(handle) => {
339 self.bring_into_view(ui, handle);
340 }
341 ScrollPanelMessage::ScrollToEnd => {
342 let max_size = self.children_size(ui);
343 if self.vertical_scroll_allowed {
344 ui.send_message(ScrollPanelMessage::vertical_scroll(
345 self.handle,
346 MessageDirection::ToWidget,
347 (max_size.y - self.actual_local_size().y).max(0.0),
348 ));
349 }
350 if self.horizontal_scroll_allowed {
351 ui.send_message(ScrollPanelMessage::horizontal_scroll(
352 self.handle,
353 MessageDirection::ToWidget,
354 (max_size.x - self.actual_local_size().x).max(0.0),
355 ));
356 }
357 }
358 }
359 }
360 }
361 }
362}
363
364/// Scroll panel builder creates [`ScrollPanel`] widget instances and adds them to the user interface.
365pub struct ScrollPanelBuilder {
366 widget_builder: WidgetBuilder,
367 vertical_scroll_allowed: Option<bool>,
368 horizontal_scroll_allowed: Option<bool>,
369 scroll_value: Vector2<f32>,
370}
371
372impl ScrollPanelBuilder {
373 /// Creates new scroll panel builder.
374 pub fn new(widget_builder: WidgetBuilder) -> Self {
375 Self {
376 widget_builder,
377 vertical_scroll_allowed: None,
378 horizontal_scroll_allowed: None,
379 scroll_value: Default::default(),
380 }
381 }
382
383 /// Enables or disables vertical scrolling.
384 pub fn with_vertical_scroll_allowed(mut self, value: bool) -> Self {
385 self.vertical_scroll_allowed = Some(value);
386 self
387 }
388
389 /// Enables or disables horizontal scrolling.
390 pub fn with_horizontal_scroll_allowed(mut self, value: bool) -> Self {
391 self.horizontal_scroll_allowed = Some(value);
392 self
393 }
394
395 /// Sets the desired scrolling value for both axes at the same time.
396 pub fn with_scroll_value(mut self, scroll_value: Vector2<f32>) -> Self {
397 self.scroll_value = scroll_value;
398 self
399 }
400
401 /// Finishes scroll panel building and adds it to the user interface.
402 pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode> {
403 ctx.add_node(UiNode::new(ScrollPanel {
404 widget: self.widget_builder.build(ctx),
405 scroll: self.scroll_value,
406 vertical_scroll_allowed: self.vertical_scroll_allowed.unwrap_or(true),
407 horizontal_scroll_allowed: self.horizontal_scroll_allowed.unwrap_or(false),
408 }))
409 }
410}
411
412#[cfg(test)]
413mod test {
414 use crate::scroll_panel::ScrollPanelBuilder;
415 use crate::{test::test_widget_deletion, widget::WidgetBuilder};
416
417 #[test]
418 fn test_deletion() {
419 test_widget_deletion(|ctx| ScrollPanelBuilder::new(WidgetBuilder::new()).build(ctx));
420 }
421}