#![allow(dead_code)] use freya::prelude::*; use crate::theme::colors; const LAZY_OVERSCAN: i64 = 3; const SCROLLBAR_GUTTER: f32 = 14.; pub(crate) fn normalize_wheel_delta(delta: f64, scale_factor: f64) -> f32 { if delta == 0.0 { return 0.0; } const FREYA_LINE_SPEED: f64 = 53.0; const FREYA_PIXEL_SPEED: f64 = 2.0; const LINE_DELTA_TOLERANCE: f64 = 1e-6; const LINE_SCROLL: f64 = 53.0; const PIXEL_SCROLL: f64 = 1.0; let lines = (delta / FREYA_LINE_SPEED).round(); if lines != 0.0 && (delta - lines * FREYA_LINE_SPEED).abs() <= LINE_DELTA_TOLERANCE { return (lines * LINE_SCROLL) as f32; } let scale_factor = if scale_factor > 0.0 { scale_factor } else { 1.0 }; (delta / (FREYA_PIXEL_SPEED * scale_factor) * PIXEL_SCROLL) as f32 } pub(crate) fn scroll_pos_from_wheel(wheel: f32, inner: f32, viewport: f32, current: f32) -> i32 { if viewport >= inner { return 0; } let new_pos = current + wheel; if new_pos >= 0.0 && wheel > 0.0 { return 0; } if new_pos <= -(inner - viewport) && wheel < 0.0 { return -(inner - viewport) as i32; } new_pos as i32 } pub(crate) fn corrected_scroll(inner: f32, viewport: f32, pos: f32) -> f32 { if pos > 0.0 { return 0.0; } if (-pos + viewport) > inner { return if viewport < inner { -(inner - viewport) } else { 0.0 }; } pos } pub(crate) fn scrollbar_pos_and_size(inner: f32, viewport: f32, scroll: f32) -> (f32, f32) { if viewport >= inner { return (0.0, inner); } let ratio = viewport / inner; let thumb = (viewport * ratio).max(50.0); let scroll_range = inner - viewport; let thumb_range = viewport - thumb; let normalized = -scroll / scroll_range; (normalized * thumb_range, thumb) } #[derive(Clone, Copy)] pub struct ScrollAreaCtx { pub corrected_x: f32, pub corrected_y: f32, pub viewport_w: f32, pub viewport_h: f32, pub viewport_top: f32, pub viewport_left: f32, } #[derive(Clone, Copy)] pub struct LazySection { pub header: bool, pub count: usize, } enum LazyRow { Header(usize), Items { section: usize, row: usize, start: usize, end: usize, }, } pub struct ScrollArea { width: Size, height: Size, padding: Gaps, spacing: f32, show_scrollbar: bool, scrollbar_gutter: bool, horizontal: bool, content_width: f32, stick_bottom: bool, reset_key: Option, controller: Option, on_user_scroll: Option>, on_ctx: Option>, children: Vec, builder: Option Element>>, } impl Default for ScrollArea { fn default() -> Self { Self::new() } } impl ScrollArea { pub fn new() -> Self { Self { width: Size::fill(), height: Size::flex(1.0), padding: Gaps::default(), spacing: 0., show_scrollbar: true, scrollbar_gutter: false, horizontal: false, content_width: 0., stick_bottom: false, reset_key: None, controller: None, on_user_scroll: None, on_ctx: None, children: Vec::new(), builder: None, } } pub fn width(mut self, width: Size) -> Self { self.width = width; self } pub fn height(mut self, height: Size) -> Self { self.height = height; self } pub fn padding(mut self, padding: Gaps) -> Self { self.padding = padding; self } pub fn spacing(mut self, spacing: f32) -> Self { self.spacing = spacing; self } pub fn show_scrollbar(mut self, show: bool) -> Self { self.show_scrollbar = show; self } pub fn scrollbar_gutter(mut self, gutter: bool) -> Self { self.scrollbar_gutter = gutter; self } pub fn horizontal(mut self, content_width: f32) -> Self { self.horizontal = true; self.content_width = content_width; self } pub fn stick_bottom(mut self, stick: bool) -> Self { self.stick_bottom = stick; self } pub fn reset_key(mut self, key: u64) -> Self { self.reset_key = Some(key); self } pub fn scroll_controller(mut self, controller: ScrollController) -> Self { self.controller = Some(controller); self } pub fn on_user_scroll(mut self, handler: impl FnMut(()) + 'static) -> Self { self.on_user_scroll = Some(EventHandler::new(handler)); self } pub fn on_ctx(mut self, handler: impl FnMut(ScrollAreaCtx) + 'static) -> Self { self.on_ctx = Some(EventHandler::new(handler)); self } pub fn children(mut self, children: impl IntoIterator) -> Self { self.children = children.into_iter().collect(); self } pub fn child(mut self, child: impl IntoElement) -> Self { self.children.push(child.into_element()); self } pub fn content(mut self, builder: impl Fn(ScrollAreaCtx) -> Element + 'static) -> Self { self.builder = Some(Box::new(builder)); self } pub fn lazy( mut self, count: usize, item_height: f32, spacing: f32, render: impl Fn(usize) -> Element + 'static, ) -> Self { let slot = (item_height + spacing).max(1.); self.builder = Some(Box::new(move |ctx: ScrollAreaCtx| { let first = (((-ctx.corrected_y) / slot).floor() as i64 - LAZY_OVERSCAN).max(0) as usize; let span = ((ctx.viewport_h / slot).ceil() as i64 + 2 * LAZY_OVERSCAN).max(0) as usize; let last = (first + span).min(count); let top_pad = first as f32 * slot; let bottom_pad = count.saturating_sub(last) as f32 * slot; let mut container = rect().vertical().width(Size::fill()); if top_pad > 0. { container = container.child(rect().width(Size::fill()).height(Size::px(top_pad))); } for i in first..last { container = container.child( rect() .key(i) .width(Size::fill()) .height(Size::px(slot)) .child( rect() .width(Size::fill()) .height(Size::px(item_height)) .child(render(i)), ), ); } if bottom_pad > 0. { container = container.child(rect().width(Size::fill()).height(Size::px(bottom_pad))); } container.into_element() })); self } #[allow(clippy::too_many_arguments)] pub fn lazy_sections( mut self, sections: Vec, item_height: f32, gap: f32, min_width: f32, max_cols: usize, header_height: f32, render: impl Fn(usize) -> Element + 'static, render_header: impl Fn(usize) -> Element + 'static, ) -> Self { let slot = (item_height + gap).max(1.); let header_slot = header_height + gap; self.builder = Some(Box::new(move |ctx: ScrollAreaCtx| { let cols = (((ctx.viewport_w + gap) / (min_width + gap)).floor() as usize) .clamp(1, max_cols.max(1)); let mut rows = Vec::new(); let mut offset = 0; for (s, section) in sections.iter().enumerate() { if section.header { rows.push(LazyRow::Header(s)); } for r in 0..section.count.div_ceil(cols) { let start = offset + r * cols; rows.push(LazyRow::Items { section: s, row: r, start, end: (start + cols).min(offset + section.count), }); } offset += section.count; } let row_slot = |row: &LazyRow| match row { LazyRow::Header(_) => header_slot, LazyRow::Items { .. } => slot, }; let overscan = LAZY_OVERSCAN as f32 * slot; let visible_top = -ctx.corrected_y - overscan; let visible_bottom = -ctx.corrected_y + ctx.viewport_h + overscan; let mut top_pad = 0.; let mut bottom_pad = 0.; let mut visible = Vec::new(); let mut y = 0.; for row in &rows { let h = row_slot(row); if y + h <= visible_top { top_pad += h; } else if y >= visible_bottom { bottom_pad += h; } else { visible.push(row); } y += h; } let mut container = rect().vertical().width(Size::fill()); if top_pad > 0. { container = container.child(rect().width(Size::fill()).height(Size::px(top_pad))); } for row in visible { let el = match *row { LazyRow::Header(section) => rect() .key(("h", section)) .width(Size::fill()) .height(Size::px(header_slot)) .child( rect() .width(Size::fill()) .height(Size::px(header_height)) .child(render_header(section)), ), LazyRow::Items { section, row, start, end, } => { let mut items = rect() .key(("r", section, row)) .horizontal() .width(Size::fill()) .height(Size::px(slot)) .spacing(gap) .content(Content::Flex); for idx in start..start + cols { let cell = rect().width(Size::flex(1.0)).height(Size::px(item_height)); items = items.child(if idx < end { cell.child(render(idx)) } else { cell }); } items } }; container = container.child(el); } if bottom_pad > 0. { container = container.child(rect().width(Size::fill()).height(Size::px(bottom_pad))); } container.into_element() })); self } fn view(&self) -> impl IntoElement { let horizontal = self.horizontal; let content_w = self.content_width; let spacing = self.spacing; let padding = self.padding; let show_scrollbar = self.show_scrollbar; let scrollbar_gutter = self.scrollbar_gutter; let stick_bottom = self.stick_bottom; let on_user_scroll = self.on_user_scroll.clone(); let internal = use_scroll_controller(ScrollConfig::default); let mut controller = self.controller.unwrap_or(internal); let scale_factor = Platform::get().scale_factor; let mut viewport_h = use_state(|| 0f32); let mut viewport_w = use_state(|| 0f32); let mut viewport_top = use_state(|| 0f32); let mut viewport_left = use_state(|| 0f32); let mut content_h = use_state(|| 0f32); let mut scroll_x = use_state(|| 0f32); let mut drag_start = use_state::>(|| None); let mut drag_start_x = use_state::>(|| None); let mut shift_held = use_state(|| false); let reset_key = self.reset_key; let mut last_reset = use_state(|| reset_key); if reset_key.is_some() && *last_reset.peek() != reset_key { last_reset.set(reset_key); controller.scroll_to_y(0); scroll_x.set_if_modified(0.); } let (_, scrolled_y) = controller.into(); let vp_h = *viewport_h.read(); let vp_w = *viewport_w.read(); let vp_top = *viewport_top.read(); let vp_left = *viewport_left.read(); let ct_h = *content_h.read(); let corrected_y = corrected_scroll(ct_h, vp_h, scrolled_y as f32); let corrected_x = if horizontal { corrected_scroll(content_w, vp_w, *scroll_x.read()) } else { 0. }; let (scrollbar_y, scrollbar_height) = scrollbar_pos_and_size(ct_h, vp_h, corrected_y); let show_v = show_scrollbar && vp_h > 0. && vp_h < ct_h; let (scrollbar_x, scrollbar_width) = scrollbar_pos_and_size(content_w, vp_w, corrected_x); let show_h = horizontal && vp_w > 0. && vp_w < content_w; let pressing_v = use_memo(move || drag_start.read().is_some()); let pressing_h = use_memo(move || drag_start_x.read().is_some()); let gutter = if show_v && scrollbar_gutter { SCROLLBAR_GUTTER } else { 0. }; let padding = if gutter > 0. { Gaps::new( padding.top(), padding.right() + gutter, padding.bottom(), padding.left(), ) } else { padding }; let ctx = ScrollAreaCtx { corrected_x, corrected_y, viewport_w: (vp_w - gutter).max(0.), viewport_h: vp_h, viewport_top: vp_top, viewport_left: vp_left, }; if let Some(cb) = &self.on_ctx { cb.call(ctx); } let content_el = if let Some(builder) = &self.builder { let inner = builder(ctx); if padding != Gaps::default() { rect() .width(Size::fill()) .padding(padding) .child(inner) .into_element() } else { inner } } else { rect() .vertical() .width(Size::fill()) .spacing(spacing) .padding(padding) .children(self.children.clone()) .into_element() }; let us_wheel = on_user_scroll.clone(); let us_thumb = on_user_scroll.clone(); let us_h_thumb = on_user_scroll; let can_scroll_v = vp_h > 0. && vp_h < ct_h; let can_scroll_h = horizontal && vp_w > 0. && vp_w < content_w; let on_wheel = move |e: Event| { let shift = *shift_held.read(); let scale = *scale_factor.peek(); let delta_x = normalize_wheel_delta(e.delta_x, scale); let delta_y = normalize_wheel_delta(e.delta_y, scale); let h_delta = if horizontal { if shift && delta_x == 0.0 { delta_y } else { delta_x } } else { 0.0 }; let v_delta = if horizontal && shift { 0.0 } else { delta_y }; let do_h = h_delta != 0.0 && can_scroll_h; let do_v = v_delta != 0.0 && can_scroll_v; if !do_h && !do_v { return; } e.stop_propagation(); if let Some(cb) = &us_wheel { cb.call(()); } if do_h { let cur_x = corrected_scroll(content_w, vp_w, *scroll_x.read()); let new_x = scroll_pos_from_wheel(h_delta, content_w, vp_w, cur_x); scroll_x.set(new_x as f32); } if do_v { let (_, cur_y) = controller.into(); let current = corrected_scroll(ct_h, vp_h, cur_y as f32); let new_y = scroll_pos_from_wheel(v_delta, ct_h, vp_h, current); if new_y != cur_y { controller.scroll_to_y(new_y); } } }; let on_thumb_down = move |e: Event| { if let Some(cb) = &us_thumb { cb.call(()); } let (_, y) = controller.into(); drag_start.set(Some((e.global_location().y as f32, y))); }; let on_h_thumb_down = move |e: Event| { if let Some(cb) = &us_h_thumb { cb.call(()); } drag_start_x.set(Some((e.global_location().x as f32, *scroll_x.read()))); }; let on_global_move = move |e: Event| { if let Some((grab_y, scroll_at_grab)) = *drag_start.read() { e.stop_propagation(); let thumb_range = (vp_h - scrollbar_height).max(1.); let scroll_range = (ct_h - vp_h).max(0.); let cursor_dy = e.global_location().y as f32 - grab_y; let scroll_dy = -(cursor_dy / thumb_range) * scroll_range; let target = scroll_at_grab + scroll_dy as i32; let clamped = corrected_scroll(ct_h, vp_h, target as f32) as i32; let (_, cur_y) = controller.into(); if clamped != cur_y { controller.scroll_to_y(clamped); } return; } if let Some((grab_x, scroll_at_grab)) = *drag_start_x.read() { e.stop_propagation(); let thumb_range = (vp_w - scrollbar_width).max(1.); let scroll_range = (content_w - vp_w).max(0.); let cursor_dx = e.global_location().x as f32 - grab_x; let scroll_dx = -(cursor_dx / thumb_range) * scroll_range; let target = scroll_at_grab + scroll_dx; scroll_x.set_if_modified(corrected_scroll(content_w, vp_w, target)); } }; let on_global_release = move |_: Event| { if drag_start.read().is_some() { drag_start.set(None); } if drag_start_x.read().is_some() { drag_start_x.set(None); } }; rect() .width(self.width.clone()) .height(self.height.clone()) .scrollable(true) .a11y_builder(move |node| { node.set_scroll_x(corrected_x as f64); node.set_scroll_y(corrected_y as f64); }) .on_wheel(on_wheel) .on_capture_global_pointer_move(on_global_move) .on_capture_global_pointer_press(on_global_release) .on_global_key_down(move |e: Event| { let held = e.modifiers.contains(Modifiers::SHIFT) || matches!(e.code, Code::ShiftLeft | Code::ShiftRight); shift_held.set_if_modified(held); }) .on_global_key_up(move |e: Event| { let held = if matches!(e.code, Code::ShiftLeft | Code::ShiftRight) { false } else { e.modifiers.contains(Modifiers::SHIFT) }; shift_held.set_if_modified(held); }) .child( rect() .width(Size::fill()) .height(Size::flex(1.0)) .overflow(Overflow::Clip) .on_sized(move |e: Event| { let h = e.area.height(); let w = e.area.width(); let top = e.area.min_y(); let left = e.area.min_x(); if (*viewport_h.read() - h).abs() > 0.5 { viewport_h.set(h); } if (*viewport_w.read() - w).abs() > 0.5 { viewport_w.set(w); } if (*viewport_top.read() - top).abs() > 0.5 { viewport_top.set(top); } if (*viewport_left.read() - left).abs() > 0.5 { viewport_left.set(left); } }) .child( rect() .width(Size::fill()) .offset_x(corrected_x) .offset_y(corrected_y) .on_sized(move |e: Event| { let h = e.area.height(); if (*content_h.read() - h).abs() > 0.5 { content_h.set(h); if stick_bottom { let vp = *viewport_h.read(); let target = -((h - vp).max(0.)); controller.scroll_to_y(target as i32); } } }) .child(content_el), ) .maybe_child(show_h.then(|| { ScrollThumb { vertical: false, main_pos: scrollbar_x, length: scrollbar_width, pressing: pressing_h.into_readable(), on_down: on_h_thumb_down.into(), } .into_element() })) .maybe_child(show_v.then(|| { ScrollThumb { vertical: true, main_pos: scrollbar_y, length: scrollbar_height, pressing: pressing_v.into_readable(), on_down: on_thumb_down.into(), } .into_element() })), ) } } impl PartialEq for ScrollArea { fn eq(&self, other: &Self) -> bool { if self.builder.is_some() || other.builder.is_some() { return false; } self.width == other.width && self.height == other.height && self.padding == other.padding && self.spacing == other.spacing && self.show_scrollbar == other.show_scrollbar && self.scrollbar_gutter == other.scrollbar_gutter && self.horizontal == other.horizontal && self.content_width == other.content_width && self.stick_bottom == other.stick_bottom && self.reset_key == other.reset_key && self.children == other.children } } impl Component for ScrollArea { fn render(&self) -> impl IntoElement { self.view() } } #[derive(PartialEq)] struct ScrollThumb { vertical: bool, main_pos: f32, length: f32, pressing: Readable, on_down: EventHandler>, } impl Component for ScrollThumb { fn render(&self) -> impl IntoElement { let mut hovering = use_state(|| false); let on_down = self.on_down.clone(); let pressing = *self.pressing.read(); let hovered = *hovering.read(); let color = if pressing { colors::fg_secondary_pressed() } else if hovered { colors::fg_secondary_hover() } else { colors::fg_secondary() }; let thickness = if pressing || hovered { 8. } else { 6. }; let (position, width, height, layer) = if self.vertical { ( Position::new_absolute().right(0.).top(self.main_pos), Size::px(14.), Size::px(self.length), Layer::Relative(999), ) } else { ( Position::new_absolute().left(self.main_pos).bottom(2.), Size::px(self.length), Size::px(8.), Layer::Relative(2), ) }; let (bar_w, bar_h) = if self.vertical { (Size::px(thickness), Size::fill()) } else { (Size::fill(), Size::px(thickness)) }; rect() .position(position) .width(width) .height(height) .center() .layer(layer) .on_pointer_down(move |e| on_down.call(e)) .on_pointer_over(move |_| hovering.set(true)) .on_pointer_out(move |_| hovering.set(false)) .child( rect() .width(bar_w) .height(bar_h) .corner_radius(CornerRadius::new_all(12.)) .background(color.with_a(160)), ) } }