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
68impl ScrollPanelProps {
69 fn total_height(&self) -> f64 {
72 self.children
73 .iter()
74 .map(|x| x.props.get_size())
75 .reduce(|x, y| x + y)
76 .unwrap_or_default()
77 }
78}
79
80impl PartialEq for ScrollPanelProps {
81 fn eq(&self, _rhs: &Self) -> bool {
82 false
83 }
84}
85
86#[doc(hidden)]
87pub enum ScrollPanelMsg {
88 CalculateWindowContent,
89 UpdateViewportDimensions,
90 ResetAutoWidth,
91 ChildrenChanged,
92}
93
94pub struct ScrollPanel {
95 viewport_ref: NodeRef,
96 viewport_height: f64,
97 viewport_width: f64,
98 content_window: Option<ContentWindow>,
99 needs_rerender: bool,
100 total_height: f64,
101 _dimensions_reset_sub: Option<Subscription>,
102 _resize_sub: Option<Subscription>,
103}
104
105impl Component for ScrollPanel {
106 type Message = ScrollPanelMsg;
107 type Properties = ScrollPanelProps;
108
109 fn create(ctx: &Context<Self>) -> Self {
110 let _dimensions_reset_sub = ctx.props().on_dimensions_reset.as_ref().map(|pubsub| {
111 let link = ctx.link().clone();
112 pubsub.add_listener(move |_| {
113 link.send_message_batch(vec![
114 ScrollPanelMsg::ResetAutoWidth,
115 ScrollPanelMsg::CalculateWindowContent,
116 ])
117 })
118 });
119
120 let _resize_sub = ctx.props().on_resize.as_ref().map(|pubsub| {
121 let link = ctx.link().clone();
122 pubsub.add_listener(move |_| {
123 link.send_message_batch(vec![
124 ScrollPanelMsg::UpdateViewportDimensions,
125 ScrollPanelMsg::CalculateWindowContent,
126 ])
127 })
128 });
129
130 let total_height = ctx.props().total_height();
131 Self {
132 viewport_ref: Default::default(),
133 viewport_height: 0f64,
134 viewport_width: ctx.props().initial_width.unwrap_or_default(),
135 content_window: None,
136 needs_rerender: true,
137 total_height,
138 _dimensions_reset_sub,
139 _resize_sub,
140 }
141 }
142
143 fn update(&mut self, ctx: &Context<Self>, msg: Self::Message) -> bool {
144 match msg {
145 ScrollPanelMsg::ResetAutoWidth => {
146 self.viewport_width = 0.0;
147 self.calculate_window_content(ctx)
148 },
149 ScrollPanelMsg::UpdateViewportDimensions => {
150 let viewport = self.viewport_elem(ctx);
151 let rect = viewport.get_bounding_client_rect();
152 let viewport_height = rect.height() - 8.0;
153 let viewport_width = self.viewport_width.max(rect.width() - 6.0);
154 let re_render = self.viewport_height != viewport_height
155 || self.viewport_width != viewport_width;
156
157 self.viewport_height = rect.height() - 8.0;
158 self.viewport_width = self.viewport_width.max(rect.width() - 6.0);
159 ctx.props().on_auto_width.emit(self.viewport_width);
160 re_render
161 },
162 ScrollPanelMsg::CalculateWindowContent => self.calculate_window_content(ctx),
163 ScrollPanelMsg::ChildrenChanged => true,
164 }
165 }
166
167 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! {
209 <div
210 ref={self.viewport(ctx)}
211 id={ctx.props().id}
212 {onscroll}
213 ondragover={&ctx.props().dragover}
214 ondragenter={&ctx.props().dragenter}
215 ondragleave={&ctx.props().dragleave}
216 ondrop={&ctx.props().drop}
217 class={ctx.props().class.clone()}
218 >
219 <div class="scroll-panel-container" style={window_style}>
220 { for windowed_items.iter().cloned().map(Html::from) }
221 <div
222 key="__scroll-panel-auto-width__"
223 class="scroll-panel-auto-width"
224 style={width_style}
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! { <><LocalStyle href={css!("containers/scroll-panel")} />{ 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);
308 let content_window = Some(ContentWindow {
309 scroll_top,
310 start_y,
311 visible_range,
312 });
313
314 let re_render = self.content_window != content_window;
315 self.content_window = content_window;
316 re_render
317 }
318}