use crate::layout::scrollbar::render_overlay_thumb;
use crate::theme::{MacAppearance, MacTheme};
use egui::scroll_area::ScrollBarVisibility;
use egui::{Color32, Id, Pos2, Rect, ScrollArea, Ui, Vec2};
use std::hash::Hash;
#[derive(Debug, Clone)]
pub struct MacScrollViewOutput<R> {
pub inner: R,
pub inner_rect: Rect,
pub content_size: Vec2,
pub offset: Vec2,
}
pub struct MacScrollView<'a> {
id: Id,
theme: Option<&'a MacTheme>,
horizontal: bool,
vertical: bool,
auto_hide: bool,
max_height: Option<f32>,
max_width: Option<f32>,
min_scrolled_height: Option<f32>,
min_scrolled_width: Option<f32>,
offset_override: Option<&'a mut Vec2>,
}
impl<'a> MacScrollView<'a> {
pub fn new(id_source: impl Hash + std::fmt::Debug) -> Self {
#[allow(deprecated)]
let id = Id::new(id_source);
Self {
id,
theme: None,
horizontal: false,
vertical: true,
auto_hide: true,
max_height: None,
max_width: None,
min_scrolled_height: None,
min_scrolled_width: None,
offset_override: None,
}
}
pub fn both(id_source: impl Hash + std::fmt::Debug) -> Self {
Self::new(id_source).horizontal(true).vertical(true)
}
pub fn theme(mut self, theme: &'a MacTheme) -> Self {
self.theme = Some(theme);
self
}
pub fn horizontal(mut self, enabled: bool) -> Self {
self.horizontal = enabled;
self
}
pub fn vertical(mut self, enabled: bool) -> Self {
self.vertical = enabled;
self
}
pub fn auto_hide(mut self, auto_hide: bool) -> Self {
self.auto_hide = auto_hide;
self
}
pub fn max_height(mut self, max_h: f32) -> Self {
self.max_height = Some(max_h);
self
}
pub fn max_width(mut self, max_w: f32) -> Self {
self.max_width = Some(max_w);
self
}
pub fn min_scrolled_height(mut self, min_h: f32) -> Self {
self.min_scrolled_height = Some(min_h);
self
}
pub fn min_scrolled_width(mut self, min_w: f32) -> Self {
self.min_scrolled_width = Some(min_w);
self
}
pub fn with_offset(mut self, offset: &'a mut Vec2) -> Self {
self.offset_override = Some(offset);
self
}
pub fn show<R>(
mut self,
ui: &mut Ui,
content: impl FnOnce(&mut Ui) -> R,
) -> MacScrollViewOutput<R> {
let fallback_theme;
let theme = match self.theme {
Some(t) => t,
None => {
fallback_theme = MacTheme::default();
&fallback_theme
}
};
let metrics = theme.scroll_view();
let scrollbar_thickness = metrics.scrollbar_thickness.points();
let thumb_min_length = metrics.thumb_min_length.points();
let corner_radius = metrics.corner_radius.points();
let fade_timeout = metrics.fade_timeout.points();
let edge_inset = metrics.edge_inset.points();
let min_w = self.min_scrolled_width.unwrap_or(0.0);
let min_h = self.min_scrolled_height.unwrap_or(0.0);
let mut area = ScrollArea::new([self.horizontal, self.vertical])
.id_salt(self.id.with("scroll_area"))
.scroll_bar_visibility(ScrollBarVisibility::AlwaysHidden)
.auto_shrink([false, false])
.min_scrolled_width(min_w)
.min_scrolled_height(min_h);
if let Some(mh) = self.max_height {
area = area.max_height(mh);
}
if let Some(mw) = self.max_width {
area = area.max_width(mw);
}
if let Some(ref o) = self.offset_override {
area = area.scroll_offset(**o);
} else if let Some(drag_o) = ui.data_mut(|d| {
d.get_temp::<Option<Vec2>>(self.id.with("drag_offset"))
.flatten()
}) {
area = area.scroll_offset(drag_o);
ui.data_mut(|d| d.insert_temp(self.id.with("drag_offset"), None::<Vec2>));
}
let output = area.show(ui, content);
let viewport_rect = output.inner_rect;
let content_size = output.content_size;
let mut current_offset = output.state.clamped_offset();
let max_scroll_x = (content_size.x - viewport_rect.width()).max(0.0);
let max_scroll_y = (content_size.y - viewport_rect.height()).max(0.0);
current_offset.x = current_offset.x.clamp(0.0, max_scroll_x);
current_offset.y = current_offset.y.clamp(0.0, max_scroll_y);
let prev_offset_id = self.id.with("prev_offset");
let prev_offset: Vec2 =
ui.data_mut(|d| *d.get_temp_mut_or_insert_with(prev_offset_id, || current_offset));
let scrolled_this_frame = (current_offset - prev_offset).length_sq() > f32::EPSILON;
ui.data_mut(|d| d.insert_temp(prev_offset_id, current_offset));
let last_active_id = self.id.with("last_active_time");
let now = ui.input(|i| i.time);
let mut last_active: f64 =
ui.data_mut(|d| *d.get_temp_mut_or_insert_with(last_active_id, || now));
let is_pointer_in_viewport = ui.rect_contains_pointer(viewport_rect);
let has_scroll_delta = ui.input(|i| {
i.smooth_scroll_delta != Vec2::ZERO
|| i.raw
.events
.iter()
.any(|e| matches!(e, egui::Event::MouseWheel { .. }))
});
let pointer_moving = ui.input(|i| i.pointer.delta() != Vec2::ZERO);
let hit_zone_padding = metrics.hit_zone_padding.points();
let min_visible_thumb_len = metrics.min_visible_thumb_length.points();
let zone_span = scrollbar_thickness + hit_zone_padding;
let v_zone = Rect::from_min_max(
Pos2::new(viewport_rect.right() - zone_span, viewport_rect.top()),
viewport_rect.max,
);
let h_zone = Rect::from_min_max(
Pos2::new(viewport_rect.left(), viewport_rect.bottom() - zone_span),
viewport_rect.max,
);
let in_scrollbar_zone = (self.vertical && ui.rect_contains_pointer(v_zone))
|| (self.horizontal && ui.rect_contains_pointer(h_zone));
if scrolled_this_frame
|| (is_pointer_in_viewport && has_scroll_delta)
|| (in_scrollbar_zone && pointer_moving)
{
last_active = now;
ui.data_mut(|d| d.insert_temp(last_active_id, last_active));
}
let init_id = self.id.with("init_done");
let initialized: bool = ui.data_mut(|d| *d.get_temp_mut_or_insert_with(init_id, || false));
if !initialized {
ui.data_mut(|d| d.insert_temp(init_id, true));
}
let elapsed = (now - last_active).max(0.0);
let is_active = !self.auto_hide || !initialized || elapsed < (fade_timeout as f64);
let alpha = if self.auto_hide {
if !initialized {
1.0
} else {
let a = ui.ctx().animate_bool(self.id.with("fade_anim"), is_active);
if a > 0.0 && a < 1.0 {
ui.ctx().request_repaint();
}
a
}
} else {
1.0
};
let painter = ui.painter_at(viewport_rect);
let get_thumb_color = |hovered: bool, dragged: bool| -> Color32 {
let base_alpha: u8 = if dragged {
150
} else if hovered {
120
} else {
80
};
let effective_alpha = ((base_alpha as f32) * alpha) as u8;
match theme.appearance {
MacAppearance::Aqua => Color32::from_black_alpha(effective_alpha),
MacAppearance::DarkAqua => Color32::from_white_alpha(effective_alpha),
}
};
let has_v = self.vertical && max_scroll_y > 0.0;
let has_h = self.horizontal && max_scroll_x > 0.0;
if has_v {
let trail = if has_h {
edge_inset * 2.0 + scrollbar_thickness
} else {
edge_inset
};
if render_overlay_thumb(
ui,
&painter,
self.id.with("v_thumb"),
viewport_rect,
viewport_rect.height(),
content_size.y,
edge_inset,
trail,
edge_inset,
scrollbar_thickness,
corner_radius,
thumb_min_length,
min_visible_thumb_len,
&mut current_offset.y,
max_scroll_y,
true,
alpha,
&get_thumb_color,
last_active_id,
now,
) && self.offset_override.is_none()
{
ui.data_mut(|d| d.insert_temp(self.id.with("drag_offset"), Some(current_offset)));
}
}
if has_h {
let trail = if has_v {
edge_inset * 2.0 + scrollbar_thickness
} else {
edge_inset
};
if render_overlay_thumb(
ui,
&painter,
self.id.with("h_thumb"),
viewport_rect,
viewport_rect.width(),
content_size.x,
edge_inset,
trail,
edge_inset,
scrollbar_thickness,
corner_radius,
thumb_min_length,
min_visible_thumb_len,
&mut current_offset.x,
max_scroll_x,
false,
alpha,
&get_thumb_color,
last_active_id,
now,
) && self.offset_override.is_none()
{
ui.data_mut(|d| d.insert_temp(self.id.with("drag_offset"), Some(current_offset)));
}
}
if let Some(ref mut o) = self.offset_override {
**o = current_offset;
}
MacScrollViewOutput {
inner: output.inner,
inner_rect: viewport_rect,
content_size,
offset: current_offset,
}
}
}