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