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