use std::cell::{OnceCell, RefCell};
use blinc_core::Color;
use blinc_layout::element::RenderProps;
use blinc_layout::prelude::*;
use blinc_layout::tree::{LayoutNodeId, LayoutTree};
use blinc_layout::widgets::scroll::{
scroll, Scroll, ScrollDirection, ScrollbarSize,
ScrollbarVisibility as LayoutScrollbarVisibility,
};
use blinc_theme::{ColorToken, ThemeState};
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum ScrollbarVisibility {
Always,
Hover,
#[default]
Auto,
Never,
}
impl ScrollbarVisibility {
fn to_layout(self) -> LayoutScrollbarVisibility {
match self {
ScrollbarVisibility::Always => LayoutScrollbarVisibility::Always,
ScrollbarVisibility::Hover => LayoutScrollbarVisibility::Hover,
ScrollbarVisibility::Auto => LayoutScrollbarVisibility::Auto,
ScrollbarVisibility::Never => LayoutScrollbarVisibility::Never,
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub enum ScrollAreaSize {
Small,
#[default]
Medium,
Large,
}
impl ScrollAreaSize {
fn scrollbar_width(&self) -> f32 {
match self {
ScrollAreaSize::Small => 4.0,
ScrollAreaSize::Medium => 6.0,
ScrollAreaSize::Large => 10.0,
}
}
fn to_layout(self) -> ScrollbarSize {
match self {
ScrollAreaSize::Small => ScrollbarSize::Thin,
ScrollAreaSize::Medium => ScrollbarSize::Normal,
ScrollAreaSize::Large => ScrollbarSize::Wide,
}
}
}
struct ScrollAreaConfig {
visibility: ScrollbarVisibility,
direction: ScrollDirection,
scrollbar_width: Option<f32>,
size: ScrollAreaSize,
thumb_color: Option<Color>,
track_color: Option<Color>,
width: Option<f32>,
height: Option<f32>,
bounce: bool,
content: Option<Div>,
rounded: Option<f32>,
bg: Option<Color>,
}
impl Default for ScrollAreaConfig {
fn default() -> Self {
Self {
visibility: ScrollbarVisibility::default(),
direction: ScrollDirection::Vertical,
scrollbar_width: None,
size: ScrollAreaSize::default(),
thumb_color: None,
track_color: None,
width: None,
height: None,
bounce: true,
content: None,
rounded: None,
bg: None,
}
}
}
struct BuiltScrollArea {
inner: Scroll,
}
impl BuiltScrollArea {
fn from_config(config: ScrollAreaConfig) -> Self {
let theme = ThemeState::get();
let thumb_color = config
.thumb_color
.unwrap_or_else(|| theme.color(ColorToken::Border).with_alpha(0.5));
let track_color = config
.track_color
.unwrap_or_else(|| theme.color(ColorToken::Surface).with_alpha(0.1));
let viewport_width = config.width.unwrap_or(300.0);
let viewport_height = config.height.unwrap_or(400.0);
let mut scroll_widget = scroll()
.w(viewport_width)
.h(viewport_height)
.direction(config.direction)
.bounce(config.bounce)
.scrollbar_visibility(config.visibility.to_layout())
.scrollbar_thumb_color(thumb_color.r, thumb_color.g, thumb_color.b, thumb_color.a)
.scrollbar_track_color(track_color.r, track_color.g, track_color.b, track_color.a);
if let Some(width) = config.scrollbar_width {
scroll_widget = scroll_widget.scrollbar_width(width);
} else {
scroll_widget = scroll_widget.scrollbar_size(config.size.to_layout());
}
if let Some(radius) = config.rounded {
scroll_widget = scroll_widget.rounded(radius);
}
if let Some(bg) = config.bg {
scroll_widget = scroll_widget.bg(bg);
}
if let Some(content) = config.content {
scroll_widget = scroll_widget.child(content);
}
Self {
inner: scroll_widget,
}
}
}
pub struct ScrollArea {
inner: Div,
}
impl ScrollArea {
pub fn class(mut self, name: impl Into<String>) -> Self {
self.inner = self.inner.class(name);
self
}
pub fn id(mut self, id: &str) -> Self {
self.inner = self.inner.id(id);
self
}
}
impl ElementBuilder for ScrollArea {
fn build(&self, tree: &mut LayoutTree) -> LayoutNodeId {
self.inner.build(tree)
}
fn render_props(&self) -> RenderProps {
self.inner.render_props()
}
fn children_builders(&self) -> &[Box<dyn ElementBuilder>] {
self.inner.children_builders()
}
fn event_handlers(&self) -> Option<&blinc_layout::event_handler::EventHandlers> {
ElementBuilder::event_handlers(&self.inner)
}
fn element_classes(&self) -> &[String] {
self.inner.element_classes()
}
}
pub struct ScrollAreaBuilder {
config: RefCell<ScrollAreaConfig>,
built: OnceCell<ScrollArea>,
}
impl ScrollAreaBuilder {
#[track_caller]
pub fn new() -> Self {
Self {
config: RefCell::new(ScrollAreaConfig::default()),
built: OnceCell::new(),
}
}
fn get_or_build(&self) -> &ScrollArea {
self.built.get_or_init(|| {
let config = self.config.take();
let built = BuiltScrollArea::from_config(config);
ScrollArea {
inner: div().class("cn-scroll-area").child(built.inner),
}
})
}
pub fn scrollbar(self, visibility: ScrollbarVisibility) -> Self {
self.config.borrow_mut().visibility = visibility;
self
}
pub fn direction(self, direction: ScrollDirection) -> Self {
self.config.borrow_mut().direction = direction;
self
}
pub fn vertical(self) -> Self {
self.config.borrow_mut().direction = ScrollDirection::Vertical;
self
}
pub fn horizontal(self) -> Self {
self.config.borrow_mut().direction = ScrollDirection::Horizontal;
self
}
pub fn both_directions(self) -> Self {
self.config.borrow_mut().direction = ScrollDirection::Both;
self
}
pub fn size(self, size: ScrollAreaSize) -> Self {
self.config.borrow_mut().size = size;
self
}
pub fn scrollbar_width(self, width: f32) -> Self {
self.config.borrow_mut().scrollbar_width = Some(width);
self
}
pub fn thumb_color(self, color: impl Into<Color>) -> Self {
self.config.borrow_mut().thumb_color = Some(color.into());
self
}
pub fn track_color(self, color: impl Into<Color>) -> Self {
self.config.borrow_mut().track_color = Some(color.into());
self
}
pub fn w(self, width: f32) -> Self {
self.config.borrow_mut().width = Some(width);
self
}
pub fn h(self, height: f32) -> Self {
self.config.borrow_mut().height = Some(height);
self
}
pub fn bounce(self, enabled: bool) -> Self {
self.config.borrow_mut().bounce = enabled;
self
}
pub fn no_bounce(self) -> Self {
self.config.borrow_mut().bounce = false;
self
}
pub fn rounded(self, radius: f32) -> Self {
self.config.borrow_mut().rounded = Some(radius);
self
}
pub fn bg(self, color: impl Into<Color>) -> Self {
self.config.borrow_mut().bg = Some(color.into());
self
}
pub fn child(self, content: Div) -> Self {
self.config.borrow_mut().content = Some(content);
self
}
pub fn build_final(self) -> ScrollArea {
let config = self.config.into_inner();
let built = BuiltScrollArea::from_config(config);
ScrollArea {
inner: div().class("cn-scroll-area").child(built.inner),
}
}
}
impl Default for ScrollAreaBuilder {
fn default() -> Self {
Self::new()
}
}
impl ElementBuilder for ScrollAreaBuilder {
fn build(&self, tree: &mut LayoutTree) -> LayoutNodeId {
self.get_or_build().build(tree)
}
fn render_props(&self) -> RenderProps {
self.get_or_build().render_props()
}
fn children_builders(&self) -> &[Box<dyn ElementBuilder>] {
self.get_or_build().children_builders()
}
fn event_handlers(&self) -> Option<&blinc_layout::event_handler::EventHandlers> {
self.get_or_build().event_handlers()
}
fn element_classes(&self) -> &[String] {
self.get_or_build().element_classes()
}
}
#[track_caller]
pub fn scroll_area() -> ScrollAreaBuilder {
ScrollAreaBuilder::new()
}
#[cfg(test)]
mod tests {
use super::*;
use blinc_theme::ThemeState;
fn init_theme() {
let _ = ThemeState::try_get().unwrap_or_else(|| {
ThemeState::init_default();
ThemeState::get()
});
}
#[test]
fn test_scrollbar_width_presets() {
assert_eq!(ScrollAreaSize::Small.scrollbar_width(), 4.0);
assert_eq!(ScrollAreaSize::Medium.scrollbar_width(), 6.0);
assert_eq!(ScrollAreaSize::Large.scrollbar_width(), 10.0);
}
#[test]
fn test_scroll_area_builder_config() {
init_theme();
let builder = scroll_area()
.scrollbar(ScrollbarVisibility::Always)
.size(ScrollAreaSize::Large)
.h(500.0)
.w(300.0);
let config = builder.config.borrow();
assert_eq!(config.visibility, ScrollbarVisibility::Always);
assert_eq!(config.size, ScrollAreaSize::Large);
assert_eq!(config.height, Some(500.0));
assert_eq!(config.width, Some(300.0));
}
}