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