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
25use crate::{
26 brush::Brush,
27 core::{
28 algebra::Vector2, color::Color, math::Rect, pool::Handle, reflect::prelude::*,
29 type_traits::prelude::*, visitor::prelude::*,
30 },
31 define_constructor,
32 draw::{CommandTexture, Draw, DrawingContext},
33 message::{MessageDirection, UiMessage},
34 widget::{Widget, WidgetBuilder},
35 BuildContext, Control, UiNode, UserInterface,
36};
37
38use fyrox_core::uuid_provider;
39use fyrox_graph::constructor::{ConstructorProvider, GraphNodeConstructor};
40use fyrox_graph::BaseSceneGraph;
41use std::ops::{Deref, DerefMut};
42
43/// A set of messages, that is used to modify the state of a scroll panel.
44#[derive(Debug, Clone, PartialEq)]
45pub enum ScrollPanelMessage {
46 /// Sets the desired scrolling value for the vertical axis.
47 VerticalScroll(f32),
48 /// Sets the desired scrolling value for the horizontal axis.
49 HorizontalScroll(f32),
50 /// Adjusts vertical and horizontal scroll values so given node will be in "view box" of scroll panel.
51 BringIntoView(Handle<UiNode>),
52 /// Scrolls to end of the content.
53 ScrollToEnd,
54}
55
56impl ScrollPanelMessage {
57 define_constructor!(
58 /// Creates [`ScrollPanelMessage::VerticalScroll`] message.
59 ScrollPanelMessage:VerticalScroll => fn vertical_scroll(f32), layout: false
60 );
61 define_constructor!(
62 /// Creates [`ScrollPanelMessage::HorizontalScroll`] message.
63 ScrollPanelMessage:HorizontalScroll => fn horizontal_scroll(f32), layout: false
64 );
65 define_constructor!(
66 /// Creates [`ScrollPanelMessage::BringIntoView`] message.
67 ScrollPanelMessage:BringIntoView => fn bring_into_view(Handle<UiNode>), layout: true
68 );
69 define_constructor!(
70 /// Creates [`ScrollPanelMessage::ScrollToEnd`] message.
71 ScrollPanelMessage:ScrollToEnd => fn scroll_to_end(), layout: true
72 );
73}
74
75/// Scroll panel widget is used to arrange its children widgets, so they can be offset by a certain amount of units
76/// from top-left corner. It is used to provide basic scrolling functionality.
77///
78/// ## Examples
79///
80/// ```rust
81/// # use fyrox_ui::{
82/// # button::ButtonBuilder,
83/// # core::{algebra::Vector2, pool::Handle},
84/// # grid::{Column, GridBuilder, Row},
85/// # scroll_panel::ScrollPanelBuilder,
86/// # widget::WidgetBuilder,
87/// # BuildContext, UiNode,
88/// # };
89/// #
90/// fn create_scroll_panel(ctx: &mut BuildContext) -> Handle<UiNode> {
91/// ScrollPanelBuilder::new(
92/// WidgetBuilder::new().with_child(
93/// GridBuilder::new(
94/// WidgetBuilder::new()
95/// .with_child(
96/// ButtonBuilder::new(WidgetBuilder::new())
97/// .with_text("Some Button")
98/// .build(ctx),
99/// )
100/// .with_child(
101/// ButtonBuilder::new(WidgetBuilder::new())
102/// .with_text("Some Other Button")
103/// .build(ctx),
104/// ),
105/// )
106/// .add_row(Row::auto())
107/// .add_row(Row::auto())
108/// .add_column(Column::stretch())
109/// .build(ctx),
110/// ),
111/// )
112/// .with_scroll_value(Vector2::new(100.0, 200.0))
113/// .with_vertical_scroll_allowed(true)
114/// .with_horizontal_scroll_allowed(true)
115/// .build(ctx)
116/// }
117/// ```
118///
119/// ## Scrolling
120///
121/// Scrolling value for both axes can be set via [`ScrollPanelMessage::VerticalScroll`] and [`ScrollPanelMessage::HorizontalScroll`]:
122///
123/// ```rust
124/// use fyrox_ui::{
125/// core::pool::Handle, message::MessageDirection, scroll_panel::ScrollPanelMessage, UiNode,
126/// UserInterface,
127/// };
128/// fn set_scrolling_value(
129/// scroll_panel: Handle<UiNode>,
130/// horizontal: f32,
131/// vertical: f32,
132/// ui: &UserInterface,
133/// ) {
134/// ui.send_message(ScrollPanelMessage::horizontal_scroll(
135/// scroll_panel,
136/// MessageDirection::ToWidget,
137/// horizontal,
138/// ));
139/// ui.send_message(ScrollPanelMessage::vertical_scroll(
140/// scroll_panel,
141/// MessageDirection::ToWidget,
142/// vertical,
143/// ));
144/// }
145/// ```
146///
147/// ## Bringing child into view
148///
149/// Calculates the scroll values to bring a desired child into view, it can be used for automatic navigation:
150///
151/// ```rust
152/// # use fyrox_ui::{
153/// # core::pool::Handle, message::MessageDirection, scroll_panel::ScrollPanelMessage, UiNode,
154/// # UserInterface,
155/// # };
156/// fn bring_child_into_view(
157/// scroll_panel: Handle<UiNode>,
158/// child: Handle<UiNode>,
159/// ui: &UserInterface,
160/// ) {
161/// ui.send_message(ScrollPanelMessage::bring_into_view(
162/// scroll_panel,
163/// MessageDirection::ToWidget,
164/// child,
165/// ))
166/// }
167/// ```
168#[derive(Default, Clone, Visit, Reflect, Debug, ComponentProvider)]
169#[reflect(derived_type = "UiNode")]
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_node(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 &self.material,
321 None,
322 );
323 }
324
325 fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
326 self.widget.handle_routed_message(ui, message);
327
328 if message.destination() == self.handle() {
329 if let Some(msg) = message.data::<ScrollPanelMessage>() {
330 match *msg {
331 ScrollPanelMessage::VerticalScroll(scroll) => {
332 self.scroll.y = scroll;
333 self.invalidate_arrange();
334 }
335 ScrollPanelMessage::HorizontalScroll(scroll) => {
336 self.scroll.x = scroll;
337 self.invalidate_arrange();
338 }
339 ScrollPanelMessage::BringIntoView(handle) => {
340 self.bring_into_view(ui, handle);
341 }
342 ScrollPanelMessage::ScrollToEnd => {
343 let max_size = self.children_size(ui);
344 if self.vertical_scroll_allowed {
345 ui.send_message(ScrollPanelMessage::vertical_scroll(
346 self.handle,
347 MessageDirection::ToWidget,
348 (max_size.y - self.actual_local_size().y).max(0.0),
349 ));
350 }
351 if self.horizontal_scroll_allowed {
352 ui.send_message(ScrollPanelMessage::horizontal_scroll(
353 self.handle,
354 MessageDirection::ToWidget,
355 (max_size.x - self.actual_local_size().x).max(0.0),
356 ));
357 }
358 }
359 }
360 }
361 }
362 }
363}
364
365/// Scroll panel builder creates [`ScrollPanel`] widget instances and adds them to the user interface.
366pub struct ScrollPanelBuilder {
367 widget_builder: WidgetBuilder,
368 vertical_scroll_allowed: Option<bool>,
369 horizontal_scroll_allowed: Option<bool>,
370 scroll_value: Vector2<f32>,
371}
372
373impl ScrollPanelBuilder {
374 /// Creates new scroll panel builder.
375 pub fn new(widget_builder: WidgetBuilder) -> Self {
376 Self {
377 widget_builder,
378 vertical_scroll_allowed: None,
379 horizontal_scroll_allowed: None,
380 scroll_value: Default::default(),
381 }
382 }
383
384 /// Enables or disables vertical scrolling.
385 pub fn with_vertical_scroll_allowed(mut self, value: bool) -> Self {
386 self.vertical_scroll_allowed = Some(value);
387 self
388 }
389
390 /// Enables or disables horizontal scrolling.
391 pub fn with_horizontal_scroll_allowed(mut self, value: bool) -> Self {
392 self.horizontal_scroll_allowed = Some(value);
393 self
394 }
395
396 /// Sets the desired scrolling value for both axes at the same time.
397 pub fn with_scroll_value(mut self, scroll_value: Vector2<f32>) -> Self {
398 self.scroll_value = scroll_value;
399 self
400 }
401
402 /// Finishes scroll panel building and adds it to the user interface.
403 pub fn build(self, ctx: &mut BuildContext) -> Handle<UiNode> {
404 ctx.add_node(UiNode::new(ScrollPanel {
405 widget: self.widget_builder.build(ctx),
406 scroll: self.scroll_value,
407 vertical_scroll_allowed: self.vertical_scroll_allowed.unwrap_or(true),
408 horizontal_scroll_allowed: self.horizontal_scroll_allowed.unwrap_or(false),
409 }))
410 }
411}
412
413#[cfg(test)]
414mod test {
415 use crate::scroll_panel::ScrollPanelBuilder;
416 use crate::{test::test_widget_deletion, widget::WidgetBuilder};
417
418 #[test]
419 fn test_deletion() {
420 test_widget_deletion(|ctx| ScrollPanelBuilder::new(WidgetBuilder::new()).build(ctx));
421 }
422}