use alloc::rc::Rc;
use alloc::vec::Vec;
use core::cell::RefCell;
use rlvgl_core::draw::draw_widget_bg;
use rlvgl_core::event::Event;
use rlvgl_core::renderer::{ClipRenderer, Renderer};
use rlvgl_core::style::Style;
use rlvgl_core::widget::{Rect, Widget};
const SCROLLBAR_WIDTH: i32 = 4;
const SCROLLBAR_MARGIN: i32 = 2;
const SCROLLBAR_MIN_THUMB: i32 = 8;
pub struct ScrollView {
bounds: Rect,
content_height: i32,
scroll_y: i32,
children: Vec<Rc<RefCell<dyn Widget>>>,
pub style: Style,
pub show_scrollbar: bool,
dirty: bool,
}
impl ScrollView {
pub fn new(bounds: Rect, content_height: i32) -> Self {
Self {
bounds,
content_height: content_height.max(0),
scroll_y: 0,
children: Vec::new(),
style: Style::default(),
show_scrollbar: false,
dirty: false,
}
}
pub fn add_child(&mut self, child: Rc<RefCell<dyn Widget>>) {
self.children.push(child);
}
pub fn set_content_height(&mut self, content_height: i32) {
self.content_height = content_height.max(0);
let clamped = self.scroll_y.clamp(0, self.max_scroll());
if clamped != self.scroll_y {
self.scroll_y = clamped;
self.dirty = true;
}
}
pub fn viewport(&self) -> Rect {
self.bounds
}
pub fn content_height(&self) -> i32 {
self.content_height
}
pub fn scroll_y(&self) -> i32 {
self.scroll_y
}
pub fn max_scroll(&self) -> i32 {
(self.content_height - self.bounds.height).max(0)
}
pub fn scroll_to(&mut self, y: i32) {
let clamped = y.clamp(0, self.max_scroll());
if clamped != self.scroll_y {
self.scroll_y = clamped;
self.dirty = true;
}
}
pub fn scroll_by(&mut self, dy: i32) {
self.scroll_to(self.scroll_y + dy);
}
pub fn take_dirty(&mut self) -> Option<Rect> {
if self.dirty {
self.dirty = false;
Some(self.bounds)
} else {
None
}
}
fn scrollbar_thumb(&self) -> Option<Rect> {
if !self.show_scrollbar || self.content_height <= self.bounds.height {
return None;
}
let track_h = self.bounds.height;
let thumb_h = ((track_h as i64 * track_h as i64) / self.content_height as i64) as i32;
let thumb_h = thumb_h.clamp(SCROLLBAR_MIN_THUMB, track_h);
let travel = track_h - thumb_h;
let max = self.max_scroll();
let offset = if max > 0 {
((self.scroll_y as i64 * travel as i64) / max as i64) as i32
} else {
0
};
Some(Rect {
x: self.bounds.x + self.bounds.width - SCROLLBAR_WIDTH - SCROLLBAR_MARGIN,
y: self.bounds.y + offset,
width: SCROLLBAR_WIDTH,
height: thumb_h,
})
}
fn to_content(&self, x: i32, y: i32) -> Option<(i32, i32)> {
let b = self.bounds;
if x < b.x || x >= b.x + b.width || y < b.y || y >= b.y + b.height {
return None;
}
Some((x - b.x, y - b.y + self.scroll_y))
}
fn translate_pointer(&self, event: &Event) -> Option<Option<Event>> {
let remap = |x: i32, y: i32, build: fn(i32, i32) -> Event| -> Option<Event> {
self.to_content(x, y).map(|(cx, cy)| build(cx, cy))
};
let translated = match *event {
Event::PointerDown { x, y } => remap(x, y, |x, y| Event::PointerDown { x, y }),
Event::PointerUp { x, y } => remap(x, y, |x, y| Event::PointerUp { x, y }),
Event::PointerMove { x, y } => remap(x, y, |x, y| Event::PointerMove { x, y }),
Event::PressDown { x, y } => remap(x, y, |x, y| Event::PressDown { x, y }),
Event::PressRelease { x, y } => remap(x, y, |x, y| Event::PressRelease { x, y }),
Event::DoubleTap { x, y } => remap(x, y, |x, y| Event::DoubleTap { x, y }),
_ => return None,
};
Some(translated)
}
}
impl Widget for ScrollView {
fn bounds(&self) -> Rect {
self.bounds
}
fn draw(&self, renderer: &mut dyn Renderer) {
draw_widget_bg(renderer, self.bounds, &self.style);
{
let mut clipped = ClipRenderer::with_offset(
renderer,
self.bounds,
self.bounds.x,
self.bounds.y - self.scroll_y,
);
for child in &self.children {
child.borrow().draw(&mut clipped);
}
}
if let Some(thumb) = self.scrollbar_thumb() {
renderer.fill_rect(thumb, self.style.border_color);
}
}
fn handle_event(&mut self, event: &Event) -> bool {
match self.translate_pointer(event) {
Some(Some(translated)) => {
for child in &self.children {
if child.borrow_mut().handle_event(&translated) {
return true;
}
}
false
}
Some(None) => false,
None => {
for child in &self.children {
if child.borrow_mut().handle_event(event) {
return true;
}
}
false
}
}
}
}