rg3d_ui/
scroll_panel.rs

1use crate::{
2    brush::Brush,
3    core::{algebra::Vector2, color::Color, math::Rect, pool::Handle, scope_profile},
4    define_constructor,
5    draw::{CommandTexture, Draw, DrawingContext},
6    message::{MessageDirection, UiMessage},
7    widget::{Widget, WidgetBuilder},
8    BuildContext, Control, UiNode, UserInterface,
9};
10use std::{
11    any::{Any, TypeId},
12    ops::{Deref, DerefMut},
13};
14
15#[derive(Debug, Clone, PartialEq)]
16pub enum ScrollPanelMessage {
17    VerticalScroll(f32),
18    HorizontalScroll(f32),
19    /// Adjusts vertical and horizontal scroll values so given node will be in "view box"
20    /// of scroll panel.
21    BringIntoView(Handle<UiNode>),
22}
23
24impl ScrollPanelMessage {
25    define_constructor!(ScrollPanelMessage:VerticalScroll => fn vertical_scroll(f32), layout: false);
26    define_constructor!(ScrollPanelMessage:HorizontalScroll => fn horizontal_scroll(f32), layout: false);
27    define_constructor!(ScrollPanelMessage:BringIntoView => fn bring_into_view(Handle<UiNode>), layout: true);
28}
29
30/// Allows user to scroll content
31#[derive(Clone)]
32pub struct ScrollPanel {
33    widget: Widget,
34    scroll: Vector2<f32>,
35    vertical_scroll_allowed: bool,
36    horizontal_scroll_allowed: bool,
37}
38
39crate::define_widget_deref!(ScrollPanel);
40
41impl Control for ScrollPanel {
42    fn query_component(&self, type_id: TypeId) -> Option<&dyn Any> {
43        if type_id == TypeId::of::<Self>() {
44            Some(self)
45        } else {
46            None
47        }
48    }
49
50    fn measure_override(&self, ui: &UserInterface, available_size: Vector2<f32>) -> Vector2<f32> {
51        scope_profile!();
52
53        let size_for_child = Vector2::new(
54            if self.horizontal_scroll_allowed {
55                f32::INFINITY
56            } else {
57                available_size.x
58            },
59            if self.vertical_scroll_allowed {
60                f32::INFINITY
61            } else {
62                available_size.y
63            },
64        );
65
66        let mut desired_size = Vector2::default();
67
68        for child_handle in self.widget.children() {
69            ui.measure_node(*child_handle, size_for_child);
70
71            let child = ui.nodes.borrow(*child_handle);
72            let child_desired_size = child.desired_size();
73            if child_desired_size.x > desired_size.x {
74                desired_size.x = child_desired_size.x;
75            }
76            if child_desired_size.y > desired_size.y {
77                desired_size.y = child_desired_size.y;
78            }
79        }
80
81        desired_size
82    }
83
84    fn arrange_override(&self, ui: &UserInterface, final_size: Vector2<f32>) -> Vector2<f32> {
85        scope_profile!();
86
87        let mut children_size = Vector2::<f32>::default();
88        for child_handle in self.widget.children() {
89            let desired_size = ui.node(*child_handle).desired_size();
90            children_size.x = children_size.x.max(desired_size.x);
91            children_size.y = children_size.y.max(desired_size.y);
92        }
93
94        let child_rect = Rect::new(
95            -self.scroll.x,
96            -self.scroll.y,
97            if self.horizontal_scroll_allowed {
98                children_size.x.max(final_size.x)
99            } else {
100                final_size.x
101            },
102            if self.vertical_scroll_allowed {
103                children_size.y.max(final_size.y)
104            } else {
105                final_size.y
106            },
107        );
108
109        for child_handle in self.widget.children() {
110            ui.arrange_node(*child_handle, &child_rect);
111        }
112
113        final_size
114    }
115
116    fn draw(&self, drawing_context: &mut DrawingContext) {
117        // Emit transparent geometry so panel will receive mouse events.
118        drawing_context.push_rect_filled(&self.widget.screen_bounds(), None);
119        drawing_context.commit(
120            self.clip_bounds(),
121            Brush::Solid(Color::TRANSPARENT),
122            CommandTexture::None,
123            None,
124        );
125    }
126
127    fn handle_routed_message(&mut self, ui: &mut UserInterface, message: &mut UiMessage) {
128        self.widget.handle_routed_message(ui, message);
129
130        if message.destination() == self.handle() {
131            if let Some(msg) = message.data::<ScrollPanelMessage>() {
132                match *msg {
133                    ScrollPanelMessage::VerticalScroll(scroll) => {
134                        self.scroll.y = scroll;
135                        self.invalidate_arrange();
136                    }
137                    ScrollPanelMessage::HorizontalScroll(scroll) => {
138                        self.scroll.x = scroll;
139                        self.invalidate_arrange();
140                    }
141                    ScrollPanelMessage::BringIntoView(handle) => {
142                        if let Some(node_to_focus_ref) = ui.try_get_node(handle) {
143                            let size = node_to_focus_ref.actual_size();
144                            let mut parent = handle;
145                            let mut relative_position = Vector2::default();
146                            while parent.is_some() && parent != self.handle {
147                                let node = ui.node(parent);
148                                relative_position += node.actual_local_position();
149                                parent = node.parent();
150                            }
151                            // Check if requested item already in "view box", this will prevent weird "jumping" effect
152                            // when bring into view was requested on already visible element.
153                            if relative_position.x < 0.0
154                                || relative_position.y < 0.0
155                                || relative_position.x > self.actual_size().x
156                                || relative_position.y > self.actual_size().y
157                                || (relative_position.x + size.x) > self.actual_size().x
158                                || (relative_position.y + size.y) > self.actual_size().y
159                            {
160                                relative_position += self.scroll;
161                                // This check is needed because it possible that given handle is not in
162                                // sub-tree of current scroll panel.
163                                if parent == self.handle {
164                                    if self.vertical_scroll_allowed {
165                                        ui.send_message(ScrollPanelMessage::vertical_scroll(
166                                            self.handle,
167                                            MessageDirection::ToWidget,
168                                            relative_position.y,
169                                        ));
170                                    }
171                                    if self.horizontal_scroll_allowed {
172                                        ui.send_message(ScrollPanelMessage::horizontal_scroll(
173                                            self.handle,
174                                            MessageDirection::ToWidget,
175                                            relative_position.x,
176                                        ));
177                                    }
178                                }
179                            }
180                        }
181                    }
182                }
183            }
184        }
185    }
186}
187
188impl ScrollPanel {
189    pub fn new(widget: Widget) -> Self {
190        Self {
191            widget,
192            scroll: Default::default(),
193            vertical_scroll_allowed: true,
194            horizontal_scroll_allowed: false,
195        }
196    }
197}
198
199pub struct ScrollPanelBuilder {
200    widget_builder: WidgetBuilder,
201    vertical_scroll_allowed: Option<bool>,
202    horizontal_scroll_allowed: Option<bool>,
203}
204
205impl ScrollPanelBuilder {
206    pub fn new(widget_builder: WidgetBuilder) -> Self {
207        Self {
208            widget_builder,
209            vertical_scroll_allowed: None,
210            horizontal_scroll_allowed: None,
211        }
212    }
213
214    pub fn with_vertical_scroll_allowed(mut self, value: bool) -> Self {
215        self.vertical_scroll_allowed = Some(value);
216        self
217    }
218
219    pub fn with_horizontal_scroll_allowed(mut self, value: bool) -> Self {
220        self.horizontal_scroll_allowed = Some(value);
221        self
222    }
223
224    pub fn build(self, ui: &mut BuildContext) -> Handle<UiNode> {
225        ui.add_node(UiNode::new(ScrollPanel {
226            widget: self.widget_builder.build(),
227            scroll: Vector2::default(),
228            vertical_scroll_allowed: self.vertical_scroll_allowed.unwrap_or(true),
229            horizontal_scroll_allowed: self.horizontal_scroll_allowed.unwrap_or(false),
230        }))
231    }
232}