perspective_viewer/ui/containers/
scroll_panel.rs1use 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 mut rows = windowed_items
199 .iter()
200 .cloned()
201 .map(Html::from)
202 .collect::<Vec<_>>();
203
204 if !ctx.props().omit_autosize_div {
205 rows.push(html! {
206 <div
207 key="__scroll-panel-auto-width__"
208 class="scroll-panel-auto-width"
209 style={width_style}
210 />
211 });
212 }
213
214 let items = if !windowed_items.is_empty() {
215 let onscroll = ctx.link().batch_callback(|_| {
216 vec![
217 ScrollPanelMsg::UpdateViewportDimensions,
218 ScrollPanelMsg::CalculateWindowContent,
219 ]
220 });
221
222 html! {
223 <div
224 ref={self.viewport(ctx)}
225 id={ctx.props().id}
226 {onscroll}
227 ondragover={&ctx.props().dragover}
228 ondragenter={&ctx.props().dragenter}
229 ondragleave={&ctx.props().dragleave}
230 ondrop={&ctx.props().drop}
231 class={ctx.props().class.clone()}
232 >
233 <div class="scroll-panel-container" style={window_style}>{ for rows }</div>
234 <div class="scroll-panel-content" style={content_style} />
235 </div>
236 }
237 } else {
238 html! {
239 <div
240 ref={self.viewport(ctx)}
241 id={ctx.props().id}
242 class={ctx.props().class.clone()}
243 >
244 <div style={content_style} />
245 </div>
246 }
247 };
248
249 html! { <>{ items }</> }
250 }
251
252 fn rendered(&mut self, ctx: &Context<Self>, _first_render: bool) {
253 ctx.link().send_message_batch(vec![
254 ScrollPanelMsg::UpdateViewportDimensions,
255 ScrollPanelMsg::CalculateWindowContent,
256 ]);
257 }
258}
259
260impl ScrollPanel {
261 fn viewport<'a, 'b: 'a, 'c: 'a>(&'b self, ctx: &'c Context<Self>) -> &'a NodeRef {
262 ctx.props()
263 .viewport_ref
264 .as_ref()
265 .unwrap_or(&self.viewport_ref)
266 }
267
268 fn viewport_elem(&self, ctx: &Context<Self>) -> Element {
269 self.viewport(ctx).cast::<Element>().unwrap()
270 }
271}
272
273#[derive(PartialEq)]
274struct ContentWindow {
275 scroll_top: f64,
276 start_y: f64,
277 visible_range: Range<usize>,
278}
279
280impl ScrollPanel {
281 fn calculate_window_content(&mut self, ctx: &Context<Self>) -> bool {
282 let viewport = self.viewport_elem(ctx);
283 let scroll_top = viewport.scroll_top() as f64;
284 let mut start_node = 0;
285 let mut start_y = 0_f64;
286 let mut offset = 0_f64;
287 let end_node = ctx
288 .props()
289 .children
290 .iter()
291 .enumerate()
292 .find_or_last(|(i, x)| {
293 if offset + x.props.get_size() < scroll_top {
294 start_node = *i + 1;
295 start_y = offset + x.props.get_size();
296 }
297
298 offset += x.props.get_size();
299 offset > scroll_top + self.viewport_height
300 })
301 .map(|x| x.0)
302 .unwrap_or_default();
303
304 let visible_range = start_node..ctx.props().children.len().min(end_node + 2);
305 let content_window = Some(ContentWindow {
306 scroll_top,
307 start_y,
308 visible_range,
309 });
310
311 let re_render = self.content_window != content_window;
312 self.content_window = content_window;
313 re_render
314 }
315}