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