use crate::layout::scrollbar::render_overlay_thumb;
use crate::theme::MacTheme;
use egui::scroll_area::ScrollBarVisibility;
use egui::{Color32, Id, Pos2, Rect, ScrollArea, Ui, Vec2};
use std::hash::Hash;
#[derive(Clone, Copy, Debug)]
struct ScrollViewTempState {
drag_offset: Option<Vec2>,
prev_offset: Vec2,
last_active: f64,
initialized: bool,
}
#[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 {
let id = Id::NULL.with(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 theme_cow = MacTheme::resolve(self.theme, ui.ctx());
let theme: &MacTheme = &theme_cow;
let metrics = theme.scroll_view();
let scrollbar_thickness = metrics.scrollbar_thickness.points();
let scrollbar_hover_thickness = metrics.scrollbar_hover_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)
.scroll_source(egui::containers::scroll_area::ScrollSource::MOUSE_WHEEL)
.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);
}
let state_id = self.id.with("sv_state");
let now = ui.input(|i| i.time);
let sv_state = ui.data_mut(|d| {
if let Some(st) = d.get_temp_mut_or_default::<Option<ScrollViewTempState>>(state_id) {
let taken_drag = if self.offset_override.is_none() {
st.drag_offset.take()
} else {
None
};
Some((*st, taken_drag))
} else {
None
}
});
if let Some(ref o) = self.offset_override {
area = area.scroll_offset(**o);
} else if let Some((_, Some(drag_o))) = sv_state {
area = area.scroll_offset(drag_o);
}
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 mut st = sv_state.map(|(s, _)| s).unwrap_or(ScrollViewTempState {
drag_offset: None,
prev_offset: current_offset,
last_active: now,
initialized: false,
});
let scrolled_this_frame = (current_offset - st.prev_offset).length_sq() > f32::EPSILON;
st.prev_offset = current_offset;
let mut last_active = st.last_active;
let initialized = st.initialized;
st.initialized = true;
let is_pointer_in_viewport = ui.rect_contains_pointer(viewport_rect);
let (has_scroll_delta, pointer_moving) = ui.input(|i| {
let wheel = i.smooth_scroll_delta != Vec2::ZERO
|| i.raw
.events
.iter()
.any(|e| matches!(e, egui::Event::MouseWheel { .. }));
(wheel, 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;
st.last_active = now;
}
let has_v = self.vertical && max_scroll_y > 0.0;
let has_h = self.horizontal && max_scroll_x > 0.0;
let elapsed = (now - last_active).max(0.0);
let timeout_secs = fade_timeout as f64;
let fade_duration = timeout_secs * 0.5 * 0.5;
let alpha = if !self.auto_hide {
1.0
} else if !(has_v || has_h) {
0.0
} else if !initialized || elapsed < timeout_secs {
let remaining = (timeout_secs - elapsed) as f32;
if remaining > 0.0 {
ui.ctx().request_repaint_after_secs(remaining);
} else {
ui.ctx()
.request_repaint_after(std::time::Duration::from_millis(16));
}
1.0
} else if elapsed < timeout_secs + fade_duration {
let t = ((elapsed - timeout_secs) / fade_duration).clamp(0.0, 1.0) as f32;
ui.ctx()
.request_repaint_after(std::time::Duration::from_millis(16));
1.0 - t
} else {
0.0
};
let painter = ui.painter_at(viewport_rect);
let get_thumb_color = |hovered: bool, dragged: bool| -> Color32 {
theme.scrollbar_thumb_color(hovered || dragged, alpha)
};
let track_bg_color = theme.control().gamma_multiply(alpha * 0.5);
let mut thumb_interacted = false;
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,
scrollbar_hover_thickness,
corner_radius,
thumb_min_length,
min_visible_thumb_len,
&mut current_offset.y,
max_scroll_y,
true,
alpha,
track_bg_color,
&get_thumb_color,
&mut thumb_interacted,
) && self.offset_override.is_none()
{
st.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,
scrollbar_hover_thickness,
corner_radius,
thumb_min_length,
min_visible_thumb_len,
&mut current_offset.x,
max_scroll_x,
false,
alpha,
track_bg_color,
&get_thumb_color,
&mut thumb_interacted,
) && self.offset_override.is_none()
{
st.drag_offset = Some(current_offset);
}
}
if thumb_interacted {
st.last_active = now;
if self.auto_hide && (has_v || has_h) {
if alpha < 1.0 {
ui.ctx()
.request_repaint_after(std::time::Duration::from_millis(16));
} else {
ui.ctx().request_repaint_after_secs(fade_timeout);
}
}
}
ui.data_mut(|d| d.insert_temp(state_id, Some(st)));
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,
}
}
}