liora-components 0.1.0

Enterprise-style native GPUI component library for Liora applications.
Documentation
use crate::motion::pop_in;
use gpui::{
    AnyElement, App, Bounds, Context, ElementId, Entity, GlobalElementId, InspectorElementId,
    IntoElement, LayoutId, Pixels, Render, ScrollHandle, Window, div, point, prelude::*, px,
};
use liora_core::Config;
use liora_icons::Icon;
use liora_icons_lucide::IconName;

pub struct Backtop {
    id: gpui::SharedString,
    scroll_handle: ScrollHandle,
    visibility_height: Pixels,
    right: Pixels,
    bottom: Pixels,
    is_visible: bool,
    content: Option<Box<dyn Fn(&mut Window, &mut Context<Backtop>) -> AnyElement + 'static>>,
}

impl Backtop {
    pub fn new(scroll_handle: ScrollHandle) -> Self {
        Self {
            id: liora_core::unique_id("backtop"),
            scroll_handle,
            visibility_height: px(200.0),
            right: px(40.0),
            bottom: px(40.0),
            is_visible: false,
            content: None,
        }
    }

    pub fn id(mut self, id: impl Into<gpui::SharedString>) -> Self {
        self.id = id.into();
        self
    }

    pub fn visibility_height(mut self, h: impl Into<Pixels>) -> Self {
        self.visibility_height = h.into();
        self
    }

    pub fn visibility_height_sm(self) -> Self {
        self.visibility_height(px(100.0))
    }

    pub fn right(mut self, r: impl Into<Pixels>) -> Self {
        self.right = r.into();
        self
    }

    pub fn right_lg(self) -> Self {
        self.right(px(100.0))
    }

    pub fn bottom(mut self, b: impl Into<Pixels>) -> Self {
        self.bottom = b.into();
        self
    }

    pub fn content<F>(mut self, f: F) -> Self
    where
        F: Fn(&mut Window, &mut Context<Backtop>) -> AnyElement + 'static,
    {
        self.content = Some(Box::new(f));
        self
    }
}

impl Render for Backtop {
    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        let theme = cx.global::<Config>().theme.clone();
        let is_visible = self.is_visible;

        let scroll_handle = self.scroll_handle.clone();

        div()
            .absolute()
            .top_0()
            .left_0()
            .size_full()
            .child(BacktopVisibilityTracker {
                backtop: cx.entity().clone(),
                scroll_handle: self.scroll_handle.clone(),
                visibility_height: self.visibility_height,
            })
            .when(is_visible, |s| {
                s.child(pop_in(
                    format!("{}-btn-motion", self.id),
                    div()
                        .id(format!("{}-btn", self.id))
                        .absolute()
                        .bottom(self.bottom)
                        .right(self.right)
                        .cursor_pointer()
                        .flex()
                        .items_center()
                        .justify_center()
                        .w(px(40.0))
                        .h(px(40.0))
                        .rounded_full()
                        .bg(theme.neutral.card)
                        .border_1()
                        .border_color(theme.neutral.border)
                        .shadow_lg()
                        .hover(|s| s.cursor_pointer().bg(theme.neutral.hover))
                        .on_click(move |_, _, _| {
                            scroll_handle.set_offset(point(px(0.0), px(0.0)));
                        })
                        .child(match &self.content {
                            Some(content_fn) => (content_fn)(_window, cx),
                            None => Icon::new(IconName::ChevronUp)
                                .size(px(20.0))
                                .color(theme.primary.base)
                                .into_any_element(),
                        }),
                ))
            })
    }
}

struct BacktopVisibilityTracker {
    backtop: Entity<Backtop>,
    scroll_handle: ScrollHandle,
    visibility_height: Pixels,
}

impl IntoElement for BacktopVisibilityTracker {
    type Element = Self;
    fn into_element(self) -> Self::Element {
        self
    }
}

impl gpui::Element for BacktopVisibilityTracker {
    type RequestLayoutState = ();
    type PrepaintState = ();

    fn id(&self) -> Option<ElementId> {
        None
    }

    fn source_location(&self) -> Option<&'static std::panic::Location<'static>> {
        None
    }

    fn request_layout(
        &mut self,
        _id: Option<&GlobalElementId>,
        _id2: Option<&InspectorElementId>,
        window: &mut Window,
        cx: &mut App,
    ) -> (LayoutId, Self::RequestLayoutState) {
        let mut style = gpui::Style::default();
        style.size.width = px(0.0).into();
        style.size.height = px(0.0).into();
        (window.request_layout(style, [], cx), ())
    }

    fn prepaint(
        &mut self,
        _id: Option<&GlobalElementId>,
        _id2: Option<&InspectorElementId>,
        _bounds: Bounds<Pixels>,
        _request_layout: &mut Self::RequestLayoutState,
        _window: &mut Window,
        _cx: &mut App,
    ) -> Self::PrepaintState {
    }

    fn paint(
        &mut self,
        _id: Option<&GlobalElementId>,
        _id2: Option<&InspectorElementId>,
        _bounds: Bounds<Pixels>,
        _request_layout: &mut Self::RequestLayoutState,
        _prepaint: &mut Self::PrepaintState,
        _window: &mut Window,
        cx: &mut App,
    ) {
        let visible = -self.scroll_handle.offset().y >= self.visibility_height;
        self.backtop.update(cx, |this, cx| {
            if this.is_visible != visible {
                this.is_visible = visible;
                cx.notify();
            }
        });
    }
}