1use crate::layout::scrollbar::render_overlay_thumb;
10use crate::theme::MacTheme;
11use egui::scroll_area::ScrollBarVisibility;
12use egui::{Color32, Id, Pos2, Rect, ScrollArea, Ui, Vec2};
13use std::hash::Hash;
14
15#[derive(Clone, Copy, Debug)]
16struct ScrollViewTempState {
17 drag_offset: Option<Vec2>,
18 prev_offset: Vec2,
19 last_active: f64,
20 initialized: bool,
21}
22
23#[derive(Debug, Clone)]
25pub struct MacScrollViewOutput<R> {
26 pub inner: R,
28 pub inner_rect: Rect,
30 pub content_size: Vec2,
32 pub offset: Vec2,
34}
35
36pub struct MacScrollView<'a> {
63 id: Id,
64 theme: Option<&'a MacTheme>,
65 horizontal: bool,
66 vertical: bool,
67 auto_hide: bool,
68 max_height: Option<f32>,
69 max_width: Option<f32>,
70 min_scrolled_height: Option<f32>,
71 min_scrolled_width: Option<f32>,
72 offset_override: Option<&'a mut Vec2>,
73}
74
75impl<'a> MacScrollView<'a> {
76 pub fn new(id_source: impl Hash + std::fmt::Debug) -> Self {
78 let id = Id::NULL.with(id_source);
79 Self {
80 id,
81 theme: None,
82 horizontal: false,
83 vertical: true,
84 auto_hide: true,
85 max_height: None,
86 max_width: None,
87 min_scrolled_height: None,
88 min_scrolled_width: None,
89 offset_override: None,
90 }
91 }
92
93 pub fn both(id_source: impl Hash + std::fmt::Debug) -> Self {
95 Self::new(id_source).horizontal(true).vertical(true)
96 }
97
98 pub fn theme(mut self, theme: &'a MacTheme) -> Self {
100 self.theme = Some(theme);
101 self
102 }
103
104 pub fn horizontal(mut self, enabled: bool) -> Self {
106 self.horizontal = enabled;
107 self
108 }
109
110 pub fn vertical(mut self, enabled: bool) -> Self {
112 self.vertical = enabled;
113 self
114 }
115
116 pub fn auto_hide(mut self, auto_hide: bool) -> Self {
118 self.auto_hide = auto_hide;
119 self
120 }
121
122 pub fn max_height(mut self, max_h: f32) -> Self {
124 self.max_height = Some(max_h);
125 self
126 }
127
128 pub fn max_width(mut self, max_w: f32) -> Self {
130 self.max_width = Some(max_w);
131 self
132 }
133
134 pub fn min_scrolled_height(mut self, min_h: f32) -> Self {
136 self.min_scrolled_height = Some(min_h);
137 self
138 }
139
140 pub fn min_scrolled_width(mut self, min_w: f32) -> Self {
142 self.min_scrolled_width = Some(min_w);
143 self
144 }
145
146 pub fn with_offset(mut self, offset: &'a mut Vec2) -> Self {
148 self.offset_override = Some(offset);
149 self
150 }
151
152 pub fn show<R>(
154 mut self,
155 ui: &mut Ui,
156 content: impl FnOnce(&mut Ui) -> R,
157 ) -> MacScrollViewOutput<R> {
158 let theme_cow = MacTheme::resolve(self.theme, ui.ctx());
159 let theme: &MacTheme = &theme_cow;
160
161 let metrics = theme.scroll_view();
162 let scrollbar_thickness = metrics.scrollbar_thickness.points();
163 let scrollbar_hover_thickness = metrics.scrollbar_hover_thickness.points();
164 let thumb_min_length = metrics.thumb_min_length.points();
165 let corner_radius = metrics.corner_radius.points();
166 let fade_timeout = metrics.fade_timeout.points();
167 let edge_inset = metrics.edge_inset.points();
168
169 let min_w = self.min_scrolled_width.unwrap_or(0.0);
170 let min_h = self.min_scrolled_height.unwrap_or(0.0);
171
172 let mut area = ScrollArea::new([self.horizontal, self.vertical])
174 .id_salt(self.id.with("scroll_area"))
175 .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
176 .scroll_source(egui::containers::scroll_area::ScrollSource::MOUSE_WHEEL)
177 .auto_shrink([false, false])
178 .min_scrolled_width(min_w)
179 .min_scrolled_height(min_h);
180
181 if let Some(mh) = self.max_height {
182 area = area.max_height(mh);
183 }
184 if let Some(mw) = self.max_width {
185 area = area.max_width(mw);
186 }
187
188 let state_id = self.id.with("sv_state");
189 let now = ui.input(|i| i.time);
190 let sv_state = ui.data_mut(|d| {
191 if let Some(st) = d.get_temp_mut_or_default::<Option<ScrollViewTempState>>(state_id) {
192 let taken_drag = if self.offset_override.is_none() {
193 st.drag_offset.take()
194 } else {
195 None
196 };
197 Some((*st, taken_drag))
198 } else {
199 None
200 }
201 });
202
203 if let Some(ref o) = self.offset_override {
204 area = area.scroll_offset(**o);
205 } else if let Some((_, Some(drag_o))) = sv_state {
206 area = area.scroll_offset(drag_o);
207 }
208
209 let output = area.show(ui, content);
210 let viewport_rect = output.inner_rect;
211 let content_size = output.content_size;
212 let mut current_offset = output.state.clamped_offset();
213
214 let max_scroll_x = (content_size.x - viewport_rect.width()).max(0.0);
216 let max_scroll_y = (content_size.y - viewport_rect.height()).max(0.0);
217 current_offset.x = current_offset.x.clamp(0.0, max_scroll_x);
218 current_offset.y = current_offset.y.clamp(0.0, max_scroll_y);
219
220 let mut st = sv_state.map(|(s, _)| s).unwrap_or(ScrollViewTempState {
222 drag_offset: None,
223 prev_offset: current_offset,
224 last_active: now,
225 initialized: false,
226 });
227 let scrolled_this_frame = (current_offset - st.prev_offset).length_sq() > f32::EPSILON;
228 st.prev_offset = current_offset;
229 let mut last_active = st.last_active;
230 let initialized = st.initialized;
231 st.initialized = true;
232
233 let is_pointer_in_viewport = ui.rect_contains_pointer(viewport_rect);
234 let (has_scroll_delta, pointer_moving) = ui.input(|i| {
235 let wheel = i.smooth_scroll_delta != Vec2::ZERO
236 || i.raw
237 .events
238 .iter()
239 .any(|e| matches!(e, egui::Event::MouseWheel { .. }));
240 (wheel, i.pointer.delta() != Vec2::ZERO)
241 });
242
243 let hit_zone_padding = metrics.hit_zone_padding.points();
244 let min_visible_thumb_len = metrics.min_visible_thumb_length.points();
245 let zone_span = scrollbar_thickness + hit_zone_padding;
246 let v_zone = Rect::from_min_max(
247 Pos2::new(viewport_rect.right() - zone_span, viewport_rect.top()),
248 viewport_rect.max,
249 );
250 let h_zone = Rect::from_min_max(
251 Pos2::new(viewport_rect.left(), viewport_rect.bottom() - zone_span),
252 viewport_rect.max,
253 );
254 let in_scrollbar_zone = (self.vertical && ui.rect_contains_pointer(v_zone))
255 || (self.horizontal && ui.rect_contains_pointer(h_zone));
256
257 if scrolled_this_frame
258 || (is_pointer_in_viewport && has_scroll_delta)
259 || (in_scrollbar_zone && pointer_moving)
260 {
261 last_active = now;
262 st.last_active = now;
263 }
264
265 let has_v = self.vertical && max_scroll_y > 0.0;
266 let has_h = self.horizontal && max_scroll_x > 0.0;
267 let elapsed = (now - last_active).max(0.0);
268 let timeout_secs = fade_timeout as f64;
269 let fade_duration = timeout_secs * 0.5 * 0.5;
270 let alpha = if !self.auto_hide {
271 1.0
272 } else if !(has_v || has_h) {
273 0.0
274 } else if !initialized || elapsed < timeout_secs {
275 let remaining = (timeout_secs - elapsed) as f32;
276 if remaining > 0.0 {
277 ui.ctx().request_repaint_after_secs(remaining);
278 } else {
279 ui.ctx()
280 .request_repaint_after(std::time::Duration::from_millis(16));
281 }
282 1.0
283 } else if elapsed < timeout_secs + fade_duration {
284 let t = ((elapsed - timeout_secs) / fade_duration).clamp(0.0, 1.0) as f32;
285 ui.ctx()
286 .request_repaint_after(std::time::Duration::from_millis(16));
287 1.0 - t
288 } else {
289 0.0
290 };
291
292 let painter = ui.painter_at(viewport_rect);
293
294 let get_thumb_color = |hovered: bool, dragged: bool| -> Color32 {
296 theme.scrollbar_thumb_color(hovered || dragged, alpha)
297 };
298 let track_bg_color = theme.control().gamma_multiply(alpha * 0.5);
299 let mut thumb_interacted = false;
300
301 if has_v {
303 let trail = if has_h {
304 edge_inset * 2.0 + scrollbar_thickness
305 } else {
306 edge_inset
307 };
308 if render_overlay_thumb(
309 ui,
310 &painter,
311 self.id.with("v_thumb"),
312 viewport_rect,
313 viewport_rect.height(),
314 content_size.y,
315 edge_inset,
316 trail,
317 edge_inset,
318 scrollbar_thickness,
319 scrollbar_hover_thickness,
320 corner_radius,
321 thumb_min_length,
322 min_visible_thumb_len,
323 &mut current_offset.y,
324 max_scroll_y,
325 true,
326 alpha,
327 track_bg_color,
328 &get_thumb_color,
329 &mut thumb_interacted,
330 ) && self.offset_override.is_none()
331 {
332 st.drag_offset = Some(current_offset);
333 }
334 }
335
336 if has_h {
338 let trail = if has_v {
339 edge_inset * 2.0 + scrollbar_thickness
340 } else {
341 edge_inset
342 };
343 if render_overlay_thumb(
344 ui,
345 &painter,
346 self.id.with("h_thumb"),
347 viewport_rect,
348 viewport_rect.width(),
349 content_size.x,
350 edge_inset,
351 trail,
352 edge_inset,
353 scrollbar_thickness,
354 scrollbar_hover_thickness,
355 corner_radius,
356 thumb_min_length,
357 min_visible_thumb_len,
358 &mut current_offset.x,
359 max_scroll_x,
360 false,
361 alpha,
362 track_bg_color,
363 &get_thumb_color,
364 &mut thumb_interacted,
365 ) && self.offset_override.is_none()
366 {
367 st.drag_offset = Some(current_offset);
368 }
369 }
370
371 if thumb_interacted {
372 st.last_active = now;
373 if self.auto_hide && (has_v || has_h) {
374 if alpha < 1.0 {
375 ui.ctx()
376 .request_repaint_after(std::time::Duration::from_millis(16));
377 } else {
378 ui.ctx().request_repaint_after_secs(fade_timeout);
379 }
380 }
381 }
382
383 ui.data_mut(|d| d.insert_temp(state_id, Some(st)));
384
385 if let Some(ref mut o) = self.offset_override {
387 **o = current_offset;
388 }
389
390 MacScrollViewOutput {
391 inner: output.inner,
392 inner_rect: viewport_rect,
393 content_size,
394 offset: current_offset,
395 }
396 }
397}