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 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}