liora-components 0.1.2

Enterprise-style native GPUI component library for Liora applications.
Documentation
use gpui::{
    AnyElement, App, Bounds, Context, ElementId, GlobalElementId, InspectorElementId, IntoElement,
    LayoutId, Pixels, Render, Window, div, prelude::*, px,
};
use liora_core::push_passive_portal;

#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub enum AffixPosition {
    #[default]
    Top,
    Bottom,
}

pub struct Affix {
    offset: Pixels,
    position: AffixPosition,
    is_fixed: bool,
    last_bounds: Option<Bounds<Pixels>>,
    on_change: Option<Box<dyn Fn(bool, &mut Window, &mut App) + 'static>>,
    content: Arc<dyn Fn(&mut Window, &mut Context<Affix>) -> AnyElement + 'static>,
}

use std::sync::Arc;

impl Affix {
    pub fn new() -> Self {
        Self {
            offset: px(0.0),
            position: AffixPosition::Top,
            is_fixed: false,
            last_bounds: None,
            on_change: None,
            content: Arc::new(|_, _| div().into_any_element()),
        }
    }

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

    pub fn offset_md(self) -> Self {
        self.offset(px(20.0))
    }

    pub fn offset_lg(self) -> Self {
        self.offset(px(80.0))
    }

    pub fn position(mut self, pos: AffixPosition) -> Self {
        self.position = pos;
        self
    }

    pub fn on_change(mut self, f: impl Fn(bool, &mut Window, &mut App) + 'static) -> Self {
        self.on_change = Some(Box::new(f));
        self
    }

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

impl Render for Affix {
    fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
        let is_fixed = self.is_fixed;
        let offset = self.offset;
        let content_fn = self.content.clone();
        let affix_handle = cx.entity().clone();
        let last_bounds = self.last_bounds;

        if is_fixed {
            if let Some(bounds) = last_bounds {
                let fixed_top = match self.position {
                    AffixPosition::Top => offset,
                    AffixPosition::Bottom => {
                        _window.viewport_size().height - offset - bounds.size.height
                    }
                };
                let fixed_left = bounds.left();
                let fixed_width = bounds.size.width;
                let fixed_content = content_fn(_window, cx);

                push_passive_portal(
                    move |_, _| {
                        div()
                            .absolute()
                            .top(fixed_top)
                            .left(fixed_left)
                            .w(fixed_width)
                            .child(fixed_content)
                            .into_any_element()
                    },
                    cx,
                );
            }
        }

        let flow_content = if is_fixed {
            match last_bounds {
                Some(bounds) => div()
                    .w(bounds.size.width)
                    .h(bounds.size.height)
                    .into_any_element(),
                None => div().h(px(40.0)).into_any_element(),
            }
        } else {
            content_fn(_window, cx)
        };

        div().relative().child(BoundsTracker {
            child: flow_content,
            on_bounds_change: Box::new(move |bounds, window, cx| {
                let (offset, position, current_fixed) =
                    affix_handle.update(cx, |this, _| (this.offset, this.position, this.is_fixed));

                let should_be_fixed = match position {
                    AffixPosition::Top => bounds.top() <= offset,
                    AffixPosition::Bottom => {
                        let viewport_h = window.viewport_size().height;
                        bounds.bottom() >= viewport_h - offset
                    }
                };

                affix_handle.update(cx, |this, _| {
                    this.last_bounds = Some(bounds);
                });

                if should_be_fixed != current_fixed {
                    affix_handle.update(cx, |this, cx| {
                        this.is_fixed = should_be_fixed;
                        if let Some(ref on_change) = this.on_change {
                            (on_change)(should_be_fixed, window, cx);
                        }
                        cx.notify();
                    });
                }
            }),
        })
    }
}

struct BoundsTracker {
    child: AnyElement,
    on_bounds_change: Box<dyn Fn(Bounds<Pixels>, &mut Window, &mut App)>,
}

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

impl gpui::Element for BoundsTracker {
    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.child.request_layout(window, cx), ())
    }

    fn prepaint(
        &mut self,
        _id: Option<&GlobalElementId>,
        _id2: Option<&InspectorElementId>,
        bounds: Bounds<Pixels>,
        _rl: &mut (),
        window: &mut Window,
        cx: &mut App,
    ) -> () {
        self.child.prepaint_at(bounds.origin, window, cx);
    }

    fn paint(
        &mut self,
        _id: Option<&GlobalElementId>,
        _id2: Option<&InspectorElementId>,
        bounds: Bounds<Pixels>,
        _rl: &mut (),
        _ps: &mut (),
        window: &mut Window,
        cx: &mut App,
    ) {
        (self.on_bounds_change)(bounds, window, cx);
        self.child.paint(window, cx);
    }
}