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