use blinc_core::reactive::{Computed, ReactiveGraph, Signal, computed};
use blinc_layout::div::ElementTypeId;
use blinc_layout::element::RenderProps;
use blinc_layout::prelude::*;
use blinc_layout::tree::{LayoutNodeId, LayoutTree};
use blinc_layout::visual_animation::VisualAnimationConfig;
use blinc_theme::ThemeState;
const CHEVRON_DOWN_SVG: &str = r#"<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="m6 9 6 6 6-6"/></svg>"#;
pub struct Collapsible {
inner: Div,
}
impl Collapsible {
pub fn class(mut self, name: impl AsRef<str>) -> 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 Collapsible {
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 element_type_id(&self) -> ElementTypeId {
self.inner.element_type_id()
}
fn layout_style(&self) -> Option<&taffy::Style> {
self.inner.layout_style()
}
fn element_classes(&self) -> &[std::sync::Arc<str>] {
self.inner.element_classes()
}
}
type ContentBuilderFn = std::sync::Arc<dyn Fn() -> Div + Send + Sync>;
fn open_and_closed(is_open: Signal<bool>) -> (Computed<bool>, Computed<bool>) {
(
computed(move |g: &ReactiveGraph| g.get(is_open).unwrap_or(false)),
computed(move |g: &ReactiveGraph| !g.get(is_open).unwrap_or(false)),
)
}
fn fold_body(content: impl ElementBuilder + 'static, is_open: Signal<bool>) -> Div {
let (open, closed) = open_and_closed(is_open);
div()
.class("cn-collapsible-content")
.attr_when("data-state", "open", &open)
.attr_when("data-state", "closed", &closed)
.w_full()
.flex_col()
.overflow_clip()
.animate_bounds(
VisualAnimationConfig::height()
.with_key(format!("cn-collapsible-{}", is_open.id().to_raw()))
.clip_to_animated()
.theme_timed(),
)
.child(content)
}
pub struct CollapsibleBuilder {
is_open: Signal<bool>,
content: Option<ContentBuilderFn>,
built: std::cell::OnceCell<Collapsible>,
}
impl CollapsibleBuilder {
pub fn new(is_open: Signal<bool>) -> Self {
Self {
is_open,
content: None,
built: std::cell::OnceCell::new(),
}
}
fn get_or_build(&self) -> &Collapsible {
::blinc_layout::build_once::build_once(&self.built, || {
let body = self.content.as_ref().map_or_else(div, |f| f());
Collapsible {
inner: div()
.class("cn-collapsible")
.w_full()
.flex_col()
.child(fold_body(body, self.is_open)),
}
})
}
pub fn content<F>(mut self, content: F) -> Self
where
F: Fn() -> Div + Send + Sync + 'static,
{
self.content = Some(std::sync::Arc::new(content));
self
}
pub fn toggle(&self) {
let current = self.is_open.try_get().unwrap_or(false);
self.is_open.set(!current);
}
pub fn set_open(&self, open: bool) {
self.is_open.set(open);
}
}
impl ElementBuilder for CollapsibleBuilder {
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 element_type_id(&self) -> ElementTypeId {
self.get_or_build().element_type_id()
}
fn layout_style(&self) -> Option<&taffy::Style> {
self.get_or_build().layout_style()
}
fn element_classes(&self) -> &[std::sync::Arc<str>] {
self.get_or_build().element_classes()
}
}
pub struct CollapsibleTrigger {
inner: Div,
}
impl CollapsibleTrigger {
pub fn new(label: impl Into<String>, is_open: Signal<bool>) -> Self {
let (open, closed) = open_and_closed(is_open);
let inner = div()
.class("cn-collapsible-trigger")
.class("tabindex=0")
.attr_when("data-state", "open", &open)
.attr_when("data-state", "closed", &closed)
.flex_row()
.w_full()
.justify_between()
.items_center()
.p(3.0)
.child(text(label.into()).size(ThemeState::get().typography().text_sm))
.child(
div()
.class("cn-chevron")
.child(svg(CHEVRON_DOWN_SVG).size(16.0, 16.0)),
)
.on_click(move |_| is_open.set(!is_open.try_get().unwrap_or(false)));
Self { inner }
}
}
impl ElementBuilder for CollapsibleTrigger {
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 element_type_id(&self) -> ElementTypeId {
self.inner.element_type_id()
}
fn layout_style(&self) -> Option<&taffy::Style> {
self.inner.layout_style()
}
fn element_classes(&self) -> &[std::sync::Arc<str>] {
self.inner.element_classes()
}
fn element_id(&self) -> Option<&str> {
ElementBuilder::element_id(&self.inner)
}
fn event_handlers(&self) -> Option<&blinc_layout::event_handler::EventHandlers> {
ElementBuilder::event_handlers(&self.inner)
}
}
pub fn collapsible(is_open: Signal<bool>) -> CollapsibleBuilder {
CollapsibleBuilder::new(is_open)
}
pub fn collapsible_section<F, E>(
trigger_label: impl Into<String>,
is_open: Signal<bool>,
content: F,
) -> Div
where
F: FnOnce() -> E,
E: ElementBuilder + 'static,
{
div()
.class("cn-collapsible-section")
.flex_col()
.w_full()
.overflow_clip()
.child(CollapsibleTrigger::new(trigger_label, is_open))
.child(fold_body(content(), is_open))
}