1use crate::layout::scrollbar::render_overlay_thumb;
10use crate::theme::{MacAppearance, MacTheme};
11use egui::scroll_area::ScrollBarVisibility;
12use egui::{Color32, Id, Pos2, Rect, ScrollArea, Ui, Vec2};
13use std::hash::Hash;
14
15#[derive(Debug, Clone)]
17pub struct MacScrollViewOutput<R> {
18 pub inner: R,
20 pub inner_rect: Rect,
22 pub content_size: Vec2,
24 pub offset: Vec2,
26}
27
28pub struct MacScrollView<'a> {
55 id: Id,
56 theme: Option<&'a MacTheme>,
57 horizontal: bool,
58 vertical: bool,
59 auto_hide: bool,
60 max_height: Option<f32>,
61 max_width: Option<f32>,
62 min_scrolled_height: Option<f32>,
63 min_scrolled_width: Option<f32>,
64 offset_override: Option<&'a mut Vec2>,
65}
66
67impl<'a> MacScrollView<'a> {
68 pub fn new(id_source: impl Hash + std::fmt::Debug) -> Self {
70 #[allow(deprecated)]
71 let id = Id::new(id_source);
72 Self {
73 id,
74 theme: None,
75 horizontal: false,
76 vertical: true,
77 auto_hide: true,
78 max_height: None,
79 max_width: None,
80 min_scrolled_height: None,
81 min_scrolled_width: None,
82 offset_override: None,
83 }
84 }
85
86 pub fn both(id_source: impl Hash + std::fmt::Debug) -> Self {
88 Self::new(id_source).horizontal(true).vertical(true)
89 }
90
91 pub fn theme(mut self, theme: &'a MacTheme) -> Self {
93 self.theme = Some(theme);
94 self
95 }
96
97 pub fn horizontal(mut self, enabled: bool) -> Self {
99 self.horizontal = enabled;
100 self
101 }
102
103 pub fn vertical(mut self, enabled: bool) -> Self {
105 self.vertical = enabled;
106 self
107 }
108
109 pub fn auto_hide(mut self, auto_hide: bool) -> Self {
111 self.auto_hide = auto_hide;
112 self
113 }
114
115 pub fn max_height(mut self, max_h: f32) -> Self {
117 self.max_height = Some(max_h);
118 self
119 }
120
121 pub fn max_width(mut self, max_w: f32) -> Self {
123 self.max_width = Some(max_w);
124 self
125 }
126
127 pub fn min_scrolled_height(mut self, min_h: f32) -> Self {
129 self.min_scrolled_height = Some(min_h);
130 self
131 }
132
133 pub fn min_scrolled_width(mut self, min_w: f32) -> Self {
135 self.min_scrolled_width = Some(min_w);
136 self
137 }
138
139 pub fn with_offset(mut self, offset: &'a mut Vec2) -> Self {
141 self.offset_override = Some(offset);
142 self
143 }
144
145 pub fn show<R>(
147 mut self,
148 ui: &mut Ui,
149 content: impl FnOnce(&mut Ui) -> R,
150 ) -> MacScrollViewOutput<R> {
151 let fallback_theme;
152 let theme = match self.theme {
153 Some(t) => t,
154 None => {
155 fallback_theme = MacTheme::default();
156 &fallback_theme
157 }
158 };
159
160 let metrics = theme.scroll_view();
161 let scrollbar_thickness = metrics.scrollbar_thickness.points();
162 let thumb_min_length = metrics.thumb_min_length.points();
163 let corner_radius = metrics.corner_radius.points();
164 let fade_timeout = metrics.fade_timeout.points();
165 let edge_inset = metrics.edge_inset.points();
166
167 let min_w = self.min_scrolled_width.unwrap_or(0.0);
168 let min_h = self.min_scrolled_height.unwrap_or(0.0);
169
170 let mut area = ScrollArea::new([self.horizontal, self.vertical])
172 .id_salt(self.id.with("scroll_area"))
173 .scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
174 .auto_shrink([false, false])
175 .min_scrolled_width(min_w)
176 .min_scrolled_height(min_h);
177
178 if let Some(mh) = self.max_height {
179 area = area.max_height(mh);
180 }
181 if let Some(mw) = self.max_width {
182 area = area.max_width(mw);
183 }
184
185 if let Some(ref o) = self.offset_override {
186 area = area.scroll_offset(**o);
187 } else if let Some(drag_o) = ui.data_mut(|d| {
188 d.get_temp::<Option<Vec2>>(self.id.with("drag_offset"))
189 .flatten()
190 }) {
191 area = area.scroll_offset(drag_o);
192 ui.data_mut(|d| d.insert_temp(self.id.with("drag_offset"), None::<Vec2>));
193 }
194
195 let output = area.show(ui, content);
196 let viewport_rect = output.inner_rect;
197 let content_size = output.content_size;
198 let mut current_offset = output.state.clamped_offset();
199
200 let max_scroll_x = (content_size.x - viewport_rect.width()).max(0.0);
202 let max_scroll_y = (content_size.y - viewport_rect.height()).max(0.0);
203 current_offset.x = current_offset.x.clamp(0.0, max_scroll_x);
204 current_offset.y = current_offset.y.clamp(0.0, max_scroll_y);
205
206 let prev_offset_id = self.id.with("prev_offset");
208 let prev_offset: Vec2 =
209 ui.data_mut(|d| *d.get_temp_mut_or_insert_with(prev_offset_id, || current_offset));
210 let scrolled_this_frame = (current_offset - prev_offset).length_sq() > f32::EPSILON;
211 ui.data_mut(|d| d.insert_temp(prev_offset_id, current_offset));
212
213 let last_active_id = self.id.with("last_active_time");
214 let now = ui.input(|i| i.time);
215 let mut last_active: f64 =
216 ui.data_mut(|d| *d.get_temp_mut_or_insert_with(last_active_id, || now));
217
218 let is_pointer_in_viewport = ui.rect_contains_pointer(viewport_rect);
219 let has_scroll_delta = ui.input(|i| {
220 i.smooth_scroll_delta != Vec2::ZERO
221 || i.raw
222 .events
223 .iter()
224 .any(|e| matches!(e, egui::Event::MouseWheel { .. }))
225 });
226 let pointer_moving = ui.input(|i| i.pointer.delta() != Vec2::ZERO);
227
228 let hit_zone_padding = metrics.hit_zone_padding.points();
229 let min_visible_thumb_len = metrics.min_visible_thumb_length.points();
230 let zone_span = scrollbar_thickness + hit_zone_padding;
231 let v_zone = Rect::from_min_max(
232 Pos2::new(viewport_rect.right() - zone_span, viewport_rect.top()),
233 viewport_rect.max,
234 );
235 let h_zone = Rect::from_min_max(
236 Pos2::new(viewport_rect.left(), viewport_rect.bottom() - zone_span),
237 viewport_rect.max,
238 );
239 let in_scrollbar_zone = (self.vertical && ui.rect_contains_pointer(v_zone))
240 || (self.horizontal && ui.rect_contains_pointer(h_zone));
241
242 if scrolled_this_frame
243 || (is_pointer_in_viewport && has_scroll_delta)
244 || (in_scrollbar_zone && pointer_moving)
245 {
246 last_active = now;
247 ui.data_mut(|d| d.insert_temp(last_active_id, last_active));
248 }
249
250 let init_id = self.id.with("init_done");
251 let initialized: bool = ui.data_mut(|d| *d.get_temp_mut_or_insert_with(init_id, || false));
252 if !initialized {
253 ui.data_mut(|d| d.insert_temp(init_id, true));
254 }
255
256 let elapsed = (now - last_active).max(0.0);
257 let is_active = !self.auto_hide || !initialized || elapsed < (fade_timeout as f64);
258 let alpha = if self.auto_hide {
259 if !initialized {
260 1.0
261 } else {
262 let a = ui.ctx().animate_bool(self.id.with("fade_anim"), is_active);
263 if a > 0.0 && a < 1.0 {
264 ui.ctx().request_repaint();
265 }
266 a
267 }
268 } else {
269 1.0
270 };
271
272 let painter = ui.painter_at(viewport_rect);
273
274 let get_thumb_color = |hovered: bool, dragged: bool| -> Color32 {
276 let base_alpha: u8 = if dragged {
277 150
278 } else if hovered {
279 120
280 } else {
281 80
282 };
283 let effective_alpha = ((base_alpha as f32) * alpha) as u8;
284 match theme.appearance {
285 MacAppearance::Aqua => Color32::from_black_alpha(effective_alpha),
286 MacAppearance::DarkAqua => Color32::from_white_alpha(effective_alpha),
287 }
288 };
289
290 let has_v = self.vertical && max_scroll_y > 0.0;
291 let has_h = self.horizontal && max_scroll_x > 0.0;
292
293 if has_v {
295 let trail = if has_h {
296 edge_inset * 2.0 + scrollbar_thickness
297 } else {
298 edge_inset
299 };
300 if render_overlay_thumb(
301 ui,
302 &painter,
303 self.id.with("v_thumb"),
304 viewport_rect,
305 viewport_rect.height(),
306 content_size.y,
307 edge_inset,
308 trail,
309 edge_inset,
310 scrollbar_thickness,
311 corner_radius,
312 thumb_min_length,
313 min_visible_thumb_len,
314 &mut current_offset.y,
315 max_scroll_y,
316 true,
317 alpha,
318 &get_thumb_color,
319 last_active_id,
320 now,
321 ) && self.offset_override.is_none()
322 {
323 ui.data_mut(|d| d.insert_temp(self.id.with("drag_offset"), Some(current_offset)));
324 }
325 }
326
327 if has_h {
329 let trail = if has_v {
330 edge_inset * 2.0 + scrollbar_thickness
331 } else {
332 edge_inset
333 };
334 if render_overlay_thumb(
335 ui,
336 &painter,
337 self.id.with("h_thumb"),
338 viewport_rect,
339 viewport_rect.width(),
340 content_size.x,
341 edge_inset,
342 trail,
343 edge_inset,
344 scrollbar_thickness,
345 corner_radius,
346 thumb_min_length,
347 min_visible_thumb_len,
348 &mut current_offset.x,
349 max_scroll_x,
350 false,
351 alpha,
352 &get_thumb_color,
353 last_active_id,
354 now,
355 ) && self.offset_override.is_none()
356 {
357 ui.data_mut(|d| d.insert_temp(self.id.with("drag_offset"), Some(current_offset)));
358 }
359 }
360
361 if let Some(ref mut o) = self.offset_override {
363 **o = current_offset;
364 }
365
366 MacScrollViewOutput {
367 inner: output.inner,
368 inner_rect: viewport_rect,
369 content_size,
370 offset: current_offset,
371 }
372 }
373}