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(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 let id = Id::NULL.with(id_source);
71 Self {
72 id,
73 theme: None,
74 horizontal: false,
75 vertical: true,
76 auto_hide: true,
77 max_height: None,
78 max_width: None,
79 min_scrolled_height: None,
80 min_scrolled_width: None,
81 offset_override: None,
82 }
83 }
84
85 pub fn both(id_source: impl Hash + std::fmt::Debug) -> Self {
87 Self::new(id_source).horizontal(true).vertical(true)
88 }
89
90 pub fn theme(mut self, theme: &'a MacTheme) -> Self {
92 self.theme = Some(theme);
93 self
94 }
95
96 pub fn horizontal(mut self, enabled: bool) -> Self {
98 self.horizontal = enabled;
99 self
100 }
101
102 pub fn vertical(mut self, enabled: bool) -> Self {
104 self.vertical = enabled;
105 self
106 }
107
108 pub fn auto_hide(mut self, auto_hide: bool) -> Self {
110 self.auto_hide = auto_hide;
111 self
112 }
113
114 pub fn max_height(mut self, max_h: f32) -> Self {
116 self.max_height = Some(max_h);
117 self
118 }
119
120 pub fn max_width(mut self, max_w: f32) -> Self {
122 self.max_width = Some(max_w);
123 self
124 }
125
126 pub fn min_scrolled_height(mut self, min_h: f32) -> Self {
128 self.min_scrolled_height = Some(min_h);
129 self
130 }
131
132 pub fn min_scrolled_width(mut self, min_w: f32) -> Self {
134 self.min_scrolled_width = Some(min_w);
135 self
136 }
137
138 pub fn with_offset(mut self, offset: &'a mut Vec2) -> Self {
140 self.offset_override = Some(offset);
141 self
142 }
143
144 pub fn show<R>(
146 mut self,
147 ui: &mut Ui,
148 content: impl FnOnce(&mut Ui) -> R,
149 ) -> MacScrollViewOutput<R> {
150 let fallback_theme;
151 let theme = match self.theme {
152 Some(t) => t,
153 None => {
154 fallback_theme = MacTheme::from_ctx(ui.ctx());
155 &fallback_theme
156 }
157 };
158
159 let metrics = theme.scroll_view();
160 let scrollbar_thickness = metrics.scrollbar_thickness.points();
161 let scrollbar_hover_thickness = metrics.scrollbar_hover_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 theme.scrollbar_thumb_color(hovered || dragged, alpha)
277 };
278 let track_bg_color = theme.control().gamma_multiply(alpha * 0.5);
279
280 let has_v = self.vertical && max_scroll_y > 0.0;
281 let has_h = self.horizontal && max_scroll_x > 0.0;
282
283 if has_v {
285 let trail = if has_h {
286 edge_inset * 2.0 + scrollbar_thickness
287 } else {
288 edge_inset
289 };
290 if render_overlay_thumb(
291 ui,
292 &painter,
293 self.id.with("v_thumb"),
294 viewport_rect,
295 viewport_rect.height(),
296 content_size.y,
297 edge_inset,
298 trail,
299 edge_inset,
300 scrollbar_thickness,
301 scrollbar_hover_thickness,
302 corner_radius,
303 thumb_min_length,
304 min_visible_thumb_len,
305 &mut current_offset.y,
306 max_scroll_y,
307 true,
308 alpha,
309 track_bg_color,
310 &get_thumb_color,
311 last_active_id,
312 now,
313 ) && self.offset_override.is_none()
314 {
315 ui.data_mut(|d| d.insert_temp(self.id.with("drag_offset"), Some(current_offset)));
316 }
317 }
318
319 if has_h {
321 let trail = if has_v {
322 edge_inset * 2.0 + scrollbar_thickness
323 } else {
324 edge_inset
325 };
326 if render_overlay_thumb(
327 ui,
328 &painter,
329 self.id.with("h_thumb"),
330 viewport_rect,
331 viewport_rect.width(),
332 content_size.x,
333 edge_inset,
334 trail,
335 edge_inset,
336 scrollbar_thickness,
337 scrollbar_hover_thickness,
338 corner_radius,
339 thumb_min_length,
340 min_visible_thumb_len,
341 &mut current_offset.x,
342 max_scroll_x,
343 false,
344 alpha,
345 track_bg_color,
346 &get_thumb_color,
347 last_active_id,
348 now,
349 ) && self.offset_override.is_none()
350 {
351 ui.data_mut(|d| d.insert_temp(self.id.with("drag_offset"), Some(current_offset)));
352 }
353 }
354
355 if let Some(ref mut o) = self.offset_override {
357 **o = current_offset;
358 }
359
360 MacScrollViewOutput {
361 inner: output.inner,
362 inner_rect: viewport_rect,
363 content_size,
364 offset: current_offset,
365 }
366 }
367}