Skip to main content

perspective_viewer/ui/containers/
scroll_panel.rs

1// ┏━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
2// ┃ ██████ ██████ ██████       █      █      █      █      █ █▄  ▀███ █       ┃
3// ┃ ▄▄▄▄▄█ █▄▄▄▄▄ ▄▄▄▄▄█  ▀▀▀▀▀█▀▀▀▀▀ █ ▀▀▀▀▀█ ████████▌▐███ ███▄  ▀█ █ ▀▀▀▀▀ ┃
4// ┃ █▀▀▀▀▀ █▀▀▀▀▀ █▀██▀▀ ▄▄▄▄▄ █ ▄▄▄▄▄█ ▄▄▄▄▄█ ████████▌▐███ █████▄   █ ▄▄▄▄▄ ┃
5// ┃ █      ██████ █  ▀█▄       █ ██████      █      ███▌▐███ ███████▄ █       ┃
6// ┣━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┫
7// ┃ Copyright (c) 2017, the Perspective Authors.                              ┃
8// ┃ ╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌╌ ┃
9// ┃ This file is part of the Perspective library, distributed under the terms ┃
10// ┃ of the [Apache License 2.0](https://www.apache.org/licenses/LICENSE-2.0). ┃
11// ┗━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┛
12
13use std::ops::Range;
14use std::rc::Rc;
15
16use itertools::Itertools;
17use web_sys::Element;
18use yew::prelude::*;
19use yew::virtual_dom::VChild;
20
21use super::scroll_panel_item::ScrollPanelItem;
22use crate::utils::*;
23
24#[derive(Properties)]
25pub struct ScrollPanelProps {
26    #[prop_or_default]
27    pub children: Vec<VChild<ScrollPanelItem>>,
28
29    #[prop_or_default]
30    pub viewport_ref: Option<NodeRef>,
31
32    #[prop_or_default]
33    pub initial_width: Option<f64>,
34
35    #[prop_or_default]
36    pub class: Classes,
37
38    #[prop_or_default]
39    pub id: &'static str,
40
41    #[prop_or_default]
42    pub dragenter: Callback<DragEvent>,
43
44    #[prop_or_default]
45    pub dragover: Callback<DragEvent>,
46
47    #[prop_or_default]
48    pub dragleave: Callback<DragEvent>,
49
50    #[prop_or_default]
51    pub on_resize: Option<Rc<PubSub<()>>>,
52
53    #[prop_or_default]
54    pub on_auto_width: Callback<f64>,
55
56    #[prop_or_default]
57    pub on_dimensions_reset: Option<Rc<PubSub<()>>>,
58
59    #[prop_or_default]
60    pub drop: Callback<DragEvent>,
61
62    #[prop_or_default]
63    pub omit_autosize_div: bool,
64}
65
66impl ScrollPanelProps {
67    fn total_height(&self) -> f64 {
68        self.children
69            .iter()
70            .map(|x| x.props.get_size())
71            .reduce(|x, y| x + y)
72            .unwrap_or_default()
73    }
74}
75
76impl PartialEq for ScrollPanelProps {
77    fn eq(&self, _rhs: &Self) -> bool {
78        false
79    }
80}
81
82#[doc(hidden)]
83pub enum ScrollPanelMsg {
84    CalculateWindowContent,
85    UpdateViewportDimensions,
86    ResetAutoWidth,
87    ChildrenChanged,
88}
89
90pub struct ScrollPanel {
91    viewport_ref: NodeRef,
92    viewport_height: f64,
93    viewport_width: f64,
94    content_window: Option<ContentWindow>,
95    needs_rerender: bool,
96    total_height: f64,
97    _dimensions_reset_sub: Option<Subscription>,
98    _resize_sub: Option<Subscription>,
99}
100
101impl Component for ScrollPanel {
102    type Message = ScrollPanelMsg;
103    type Properties = ScrollPanelProps;
104
105    fn create(ctx: &Context<Self>) -> Self {
106        let _dimensions_reset_sub = ctx.props().on_dimensions_reset.as_ref().map(|pubsub| {
107            let link = ctx.link().clone();
108            pubsub.add_listener(move |_| {
109                link.send_message_batch(vec![
110                    ScrollPanelMsg::ResetAutoWidth,
111                    ScrollPanelMsg::CalculateWindowContent,
112                ])
113            })
114        });
115
116        let _resize_sub = ctx.props().on_resize.as_ref().map(|pubsub| {
117            let link = ctx.link().clone();
118            pubsub.add_listener(move |_| {
119                link.send_message_batch(vec![
120                    ScrollPanelMsg::UpdateViewportDimensions,
121                    ScrollPanelMsg::CalculateWindowContent,
122                ])
123            })
124        });
125
126        let total_height = ctx.props().total_height();
127        Self {
128            viewport_ref: Default::default(),
129            viewport_height: 0f64,
130            viewport_width: ctx.props().initial_width.unwrap_or_default(),
131            content_window: None,
132            needs_rerender: true,
133            total_height,
134            _dimensions_reset_sub,
135            _resize_sub,
136        }
137    }
138
139    fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
140        match msg {
141            ScrollPanelMsg::ResetAutoWidth => {
142                self.viewport_width = 0.0;
143                self.calculate_window_content(ctx)
144            },
145
146            ScrollPanelMsg::UpdateViewportDimensions => {
147                let viewport = self.viewport_elem(ctx);
148                let rect = viewport.get_bounding_client_rect();
149                let viewport_height = rect.height() - 8.0;
150                let scroll_offset = if ctx.props().omit_autosize_div {
151                    0.0
152                } else {
153                    6.0
154                };
155
156                let viewport_width = self.viewport_width.max(rect.width() - scroll_offset);
157                let re_render = self.viewport_height != viewport_height
158                    || self.viewport_width != viewport_width;
159
160                self.viewport_height = rect.height() - 8.0;
161                self.viewport_width = self.viewport_width.max(rect.width() - scroll_offset);
162                ctx.props().on_auto_width.emit(self.viewport_width);
163                re_render
164            },
165            ScrollPanelMsg::CalculateWindowContent => self.calculate_window_content(ctx),
166            ScrollPanelMsg::ChildrenChanged => true,
167        }
168    }
169
170    fn changed(&mut self, ctx: &Context<Self>, _old: &Self::Properties) -> bool {
171        let total_height = ctx.props().total_height();
172        self.needs_rerender =
173            self.needs_rerender || (self.total_height - total_height).abs() > 0.1f64;
174        self.total_height = total_height;
175        ctx.link().send_message_batch(vec![
176            ScrollPanelMsg::UpdateViewportDimensions,
177            ScrollPanelMsg::CalculateWindowContent,
178            ScrollPanelMsg::ChildrenChanged,
179        ]);
180
181        false
182    }
183
184    fn view(&self, ctx: &Context<Self>) -> Html {
185        let content_style = format!("height:{}px", self.total_height);
186        let (window_style, windowed_items) = match &self.content_window {
187            None => ("".to_string(), &[][..]),
188            Some(cw) => (
189                format!(
190                    "position:sticky;top:0;transform:translateY({}px);",
191                    cw.start_y - cw.scroll_top
192                ),
193                (&ctx.props().children[cw.visible_range.clone()]),
194            ),
195        };
196
197        let width_style = format!("width:{}px", self.viewport_width.max(0.0));
198        let items = if !windowed_items.is_empty() {
199            let onscroll = ctx.link().batch_callback(|_| {
200                vec![
201                    ScrollPanelMsg::UpdateViewportDimensions,
202                    ScrollPanelMsg::CalculateWindowContent,
203                ]
204            });
205
206            html! {
207                <div
208                    ref={self.viewport(ctx)}
209                    id={ctx.props().id}
210                    {onscroll}
211                    ondragover={&ctx.props().dragover}
212                    ondragenter={&ctx.props().dragenter}
213                    ondragleave={&ctx.props().dragleave}
214                    ondrop={&ctx.props().drop}
215                    class={ctx.props().class.clone()}
216                >
217                    <div class="scroll-panel-container" style={window_style}>
218                        { for windowed_items.iter().cloned().map(Html::from) }
219                        if !ctx.props().omit_autosize_div {
220                            <div
221                                key="__scroll-panel-auto-width__"
222                                class="scroll-panel-auto-width"
223                                style={width_style}
224                            />
225                        }
226                    </div>
227                    <div class="scroll-panel-content" style={content_style} />
228                </div>
229            }
230        } else {
231            html! {
232                <div
233                    ref={self.viewport(ctx)}
234                    id={ctx.props().id}
235                    class={ctx.props().class.clone()}
236                >
237                    <div style={content_style} />
238                </div>
239            }
240        };
241
242        html! { <>{ items }</> }
243    }
244
245    fn rendered(&mut self, ctx: &Context<Self>, _first_render: bool) {
246        ctx.link().send_message_batch(vec![
247            ScrollPanelMsg::UpdateViewportDimensions,
248            ScrollPanelMsg::CalculateWindowContent,
249        ]);
250    }
251}
252
253impl ScrollPanel {
254    fn viewport<'a, 'b: 'a, 'c: 'a>(&'b self, ctx: &'c Context<Self>) -> &'a NodeRef {
255        ctx.props()
256            .viewport_ref
257            .as_ref()
258            .unwrap_or(&self.viewport_ref)
259    }
260
261    fn viewport_elem(&self, ctx: &Context<Self>) -> Element {
262        self.viewport(ctx).cast::<Element>().unwrap()
263    }
264}
265
266#[derive(PartialEq)]
267struct ContentWindow {
268    scroll_top: f64,
269    start_y: f64,
270    visible_range: Range<usize>,
271}
272
273impl ScrollPanel {
274    fn calculate_window_content(&mut self, ctx: &Context<Self>) -> bool {
275        let viewport = self.viewport_elem(ctx);
276        let scroll_top = viewport.scroll_top() as f64;
277        let mut start_node = 0;
278        let mut start_y = 0_f64;
279        let mut offset = 0_f64;
280        let end_node = ctx
281            .props()
282            .children
283            .iter()
284            .enumerate()
285            .find_or_last(|(i, x)| {
286                if offset + x.props.get_size() < scroll_top {
287                    start_node = *i + 1;
288                    start_y = offset + x.props.get_size();
289                }
290
291                offset += x.props.get_size();
292                offset > scroll_top + self.viewport_height
293            })
294            .map(|x| x.0)
295            .unwrap_or_default();
296
297        let visible_range = start_node..ctx.props().children.len().min(end_node + 2);
298        let content_window = Some(ContentWindow {
299            scroll_top,
300            start_y,
301            visible_range,
302        });
303
304        let re_render = self.content_window != content_window;
305        self.content_window = content_window;
306        re_render
307    }
308}