use crate::{
column, layout, mouse, overlay, Align, Clipboard, Column, Element, Event,
Hasher, Layout, Length, Point, Rectangle, Size, Vector, Widget,
};
use std::{f32, hash::Hash, u32};
#[allow(missing_debug_implementations)]
pub struct Scrollable<'a, Message, Renderer: self::Renderer> {
state: &'a mut State,
height: Length,
max_height: u32,
content: Column<'a, Message, Renderer>,
style: Renderer::Style,
}
impl<'a, Message, Renderer: self::Renderer> Scrollable<'a, Message, Renderer> {
pub fn new(state: &'a mut State) -> Self {
Scrollable {
state,
height: Length::Shrink,
max_height: u32::MAX,
content: Column::new(),
style: Renderer::Style::default(),
}
}
pub fn spacing(mut self, units: u16) -> Self {
self.content = self.content.spacing(units);
self
}
pub fn padding(mut self, units: u16) -> Self {
self.content = self.content.padding(units);
self
}
pub fn width(mut self, width: Length) -> Self {
self.content = self.content.width(width);
self
}
pub fn height(mut self, height: Length) -> Self {
self.height = height;
self
}
pub fn max_width(mut self, max_width: u32) -> Self {
self.content = self.content.max_width(max_width);
self
}
pub fn max_height(mut self, max_height: u32) -> Self {
self.max_height = max_height;
self
}
pub fn align_items(mut self, align_items: Align) -> Self {
self.content = self.content.align_items(align_items);
self
}
pub fn style(mut self, style: impl Into<Renderer::Style>) -> Self {
self.style = style.into();
self
}
pub fn push<E>(mut self, child: E) -> Self
where
E: Into<Element<'a, Message, Renderer>>,
{
self.content = self.content.push(child);
self
}
}
impl<'a, Message, Renderer> Widget<Message, Renderer>
for Scrollable<'a, Message, Renderer>
where
Renderer: self::Renderer,
{
fn width(&self) -> Length {
Widget::<Message, Renderer>::width(&self.content)
}
fn height(&self) -> Length {
self.height
}
fn layout(
&self,
renderer: &Renderer,
limits: &layout::Limits,
) -> layout::Node {
let limits = limits
.max_height(self.max_height)
.width(Widget::<Message, Renderer>::width(&self.content))
.height(self.height);
let child_limits = layout::Limits::new(
Size::new(limits.min().width, 0.0),
Size::new(limits.max().width, f32::INFINITY),
);
let content = self.content.layout(renderer, &child_limits);
let size = limits.resolve(content.size());
layout::Node::with_children(size, vec![content])
}
fn on_event(
&mut self,
event: Event,
layout: Layout<'_>,
cursor_position: Point,
messages: &mut Vec<Message>,
renderer: &Renderer,
clipboard: Option<&dyn Clipboard>,
) {
let bounds = layout.bounds();
let is_mouse_over = bounds.contains(cursor_position);
let content = layout.children().next().unwrap();
let content_bounds = content.bounds();
if is_mouse_over {
match event {
Event::Mouse(mouse::Event::WheelScrolled { delta }) => {
match delta {
mouse::ScrollDelta::Lines { y, .. } => {
self.state.scroll(y * 60.0, bounds, content_bounds);
}
mouse::ScrollDelta::Pixels { y, .. } => {
self.state.scroll(y, bounds, content_bounds);
}
}
}
_ => {}
}
}
let offset = self.state.offset(bounds, content_bounds);
let scrollbar = renderer.scrollbar(bounds, content_bounds, offset);
let is_mouse_over_scrollbar = scrollbar
.as_ref()
.map(|scrollbar| scrollbar.is_mouse_over(cursor_position))
.unwrap_or(false);
if self.state.is_scroller_grabbed() {
match event {
Event::Mouse(mouse::Event::ButtonReleased(
mouse::Button::Left,
)) => {
self.state.scroller_grabbed_at = None;
}
Event::Mouse(mouse::Event::CursorMoved { .. }) => {
if let (Some(scrollbar), Some(scroller_grabbed_at)) =
(scrollbar, self.state.scroller_grabbed_at)
{
self.state.scroll_to(
scrollbar.scroll_percentage(
scroller_grabbed_at,
cursor_position,
),
bounds,
content_bounds,
);
}
}
_ => {}
}
} else if is_mouse_over_scrollbar {
match event {
Event::Mouse(mouse::Event::ButtonPressed(
mouse::Button::Left,
)) => {
if let Some(scrollbar) = scrollbar {
if let Some(scroller_grabbed_at) =
scrollbar.grab_scroller(cursor_position)
{
self.state.scroll_to(
scrollbar.scroll_percentage(
scroller_grabbed_at,
cursor_position,
),
bounds,
content_bounds,
);
self.state.scroller_grabbed_at =
Some(scroller_grabbed_at);
}
}
}
_ => {}
}
}
let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar {
Point::new(
cursor_position.x,
cursor_position.y
+ self.state.offset(bounds, content_bounds) as f32,
)
} else {
Point::new(cursor_position.x, -1.0)
};
self.content.on_event(
event,
content,
cursor_position,
messages,
renderer,
clipboard,
)
}
fn draw(
&self,
renderer: &mut Renderer,
defaults: &Renderer::Defaults,
layout: Layout<'_>,
cursor_position: Point,
) -> Renderer::Output {
let bounds = layout.bounds();
let content_layout = layout.children().next().unwrap();
let content_bounds = content_layout.bounds();
let offset = self.state.offset(bounds, content_bounds);
let scrollbar = renderer.scrollbar(bounds, content_bounds, offset);
let is_mouse_over = bounds.contains(cursor_position);
let is_mouse_over_scrollbar = scrollbar
.as_ref()
.map(|scrollbar| scrollbar.is_mouse_over(cursor_position))
.unwrap_or(false);
let content = {
let cursor_position = if is_mouse_over && !is_mouse_over_scrollbar {
Point::new(cursor_position.x, cursor_position.y + offset as f32)
} else {
Point::new(cursor_position.x, -1.0)
};
self.content.draw(
renderer,
defaults,
content_layout,
cursor_position,
)
};
self::Renderer::draw(
renderer,
&self.state,
bounds,
content_layout.bounds(),
is_mouse_over,
is_mouse_over_scrollbar,
scrollbar,
offset,
&self.style,
content,
)
}
fn hash_layout(&self, state: &mut Hasher) {
struct Marker;
std::any::TypeId::of::<Marker>().hash(state);
self.height.hash(state);
self.max_height.hash(state);
self.content.hash_layout(state)
}
fn overlay(
&mut self,
layout: Layout<'_>,
) -> Option<overlay::Element<'_, Message, Renderer>> {
let Self { content, state, .. } = self;
content
.overlay(layout.children().next().unwrap())
.map(|overlay| {
let bounds = layout.bounds();
let content_layout = layout.children().next().unwrap();
let content_bounds = content_layout.bounds();
let offset = state.offset(bounds, content_bounds);
overlay.translate(Vector::new(0.0, -(offset as f32)))
})
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct State {
scroller_grabbed_at: Option<f32>,
offset: f32,
}
impl State {
pub fn new() -> Self {
State::default()
}
pub fn scroll(
&mut self,
delta_y: f32,
bounds: Rectangle,
content_bounds: Rectangle,
) {
if bounds.height >= content_bounds.height {
return;
}
self.offset = (self.offset - delta_y)
.max(0.0)
.min((content_bounds.height - bounds.height) as f32);
}
pub fn scroll_to(
&mut self,
percentage: f32,
bounds: Rectangle,
content_bounds: Rectangle,
) {
self.offset =
((content_bounds.height - bounds.height) * percentage).max(0.0);
}
pub fn offset(&self, bounds: Rectangle, content_bounds: Rectangle) -> u32 {
let hidden_content =
(content_bounds.height - bounds.height).max(0.0).round() as u32;
self.offset.min(hidden_content as f32) as u32
}
pub fn is_scroller_grabbed(&self) -> bool {
self.scroller_grabbed_at.is_some()
}
}
#[derive(Debug)]
pub struct Scrollbar {
pub bounds: Rectangle,
pub scroller: Scroller,
}
impl Scrollbar {
fn is_mouse_over(&self, cursor_position: Point) -> bool {
self.bounds.contains(cursor_position)
}
fn grab_scroller(&self, cursor_position: Point) -> Option<f32> {
if self.bounds.contains(cursor_position) {
Some(if self.scroller.bounds.contains(cursor_position) {
(cursor_position.y - self.scroller.bounds.y)
/ self.scroller.bounds.height
} else {
0.5
})
} else {
None
}
}
fn scroll_percentage(
&self,
grabbed_at: f32,
cursor_position: Point,
) -> f32 {
(cursor_position.y
- self.bounds.y
- self.scroller.bounds.height * grabbed_at)
/ (self.bounds.height - self.scroller.bounds.height)
}
}
#[derive(Debug, Clone, Copy)]
pub struct Scroller {
pub bounds: Rectangle,
}
pub trait Renderer: column::Renderer + Sized {
type Style: Default;
fn scrollbar(
&self,
bounds: Rectangle,
content_bounds: Rectangle,
offset: u32,
) -> Option<Scrollbar>;
fn draw(
&mut self,
scrollable: &State,
bounds: Rectangle,
content_bounds: Rectangle,
is_mouse_over: bool,
is_mouse_over_scrollbar: bool,
scrollbar: Option<Scrollbar>,
offset: u32,
style: &Self::Style,
content: Self::Output,
) -> Self::Output;
}
impl<'a, Message, Renderer> From<Scrollable<'a, Message, Renderer>>
for Element<'a, Message, Renderer>
where
Renderer: 'a + self::Renderer,
Message: 'a,
{
fn from(
scrollable: Scrollable<'a, Message, Renderer>,
) -> Element<'a, Message, Renderer> {
Element::new(scrollable)
}
}