kael_ui 0.2.0

Professional shadcn-inspired UI component library for Kael. 100+ accessible components for building beautiful, performant desktop applications.
use kael::{prelude::FluentBuilder as _, *};
use std::rc::Rc;

use crate::theme::use_theme;

#[derive(Clone)]
pub struct SortableItemDrag {
    index: usize,
    position: Point<Pixels>,
}

impl std::fmt::Debug for SortableItemDrag {
    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
        f.debug_struct("SortableItemDrag")
            .field("index", &self.index)
            .finish()
    }
}

impl Render for SortableItemDrag {
    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
        let theme = use_theme();
        div().pl(self.position.x).pt(self.position.y).child(
            div()
                .px(px(12.0))
                .py(px(8.0))
                .bg(theme.tokens.card.opacity(0.95))
                .border_1()
                .border_color(theme.tokens.primary)
                .rounded(theme.tokens.radius_md)
                .shadow(smallvec::smallvec![BoxShadow {
                    color: hsla(0.0, 0.0, 0.0, 0.2),
                    offset: point(px(0.0), px(4.0)),
                    blur_radius: px(8.0),
                    spread_radius: px(0.0),
                    inset: false,
                }])
                .text_size(px(14.0))
                .text_color(theme.tokens.foreground)
                .font_family(theme.tokens.font_family.clone())
                .child("Moving..."),
        )
    }
}

pub struct SortableListState<T: Clone + 'static> {
    items: Vec<T>,
    dragging_index: Option<usize>,
    hover_index: Option<usize>,
}

impl<T: Clone + 'static> SortableListState<T> {
    pub fn new(items: Vec<T>) -> Self {
        Self {
            items,
            dragging_index: None,
            hover_index: None,
        }
    }

    pub fn items(&self) -> &[T] {
        &self.items
    }

    pub fn set_items(&mut self, items: Vec<T>) {
        self.items = items;
    }

    pub fn dragging_index(&self) -> Option<usize> {
        self.dragging_index
    }

    pub fn hover_index(&self) -> Option<usize> {
        self.hover_index
    }
}

#[derive(IntoElement)]
pub struct SortableList<T: Clone + 'static> {
    state: Entity<SortableListState<T>>,
    item_renderer: Rc<dyn Fn(&T, usize, bool) -> AnyElement>,
    on_reorder: Option<Rc<dyn Fn(Vec<T>, &mut Window, &mut App)>>,
    direction: Axis,
    gap: Pixels,
    style: StyleRefinement,
}

impl<T: Clone + 'static> SortableList<T> {
    pub fn new(
        state: Entity<SortableListState<T>>,
        renderer: impl Fn(&T, usize, bool) -> AnyElement + 'static,
    ) -> Self {
        Self {
            state,
            item_renderer: Rc::new(renderer),
            on_reorder: None,
            direction: Axis::Vertical,
            gap: px(4.0),
            style: StyleRefinement::default(),
        }
    }

    pub fn on_reorder(
        mut self,
        callback: impl Fn(Vec<T>, &mut Window, &mut App) + 'static,
    ) -> Self {
        self.on_reorder = Some(Rc::new(callback));
        self
    }

    pub fn direction(mut self, direction: Axis) -> Self {
        self.direction = direction;
        self
    }

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

impl<T: Clone + 'static> Styled for SortableList<T> {
    fn style(&mut self) -> &mut StyleRefinement {
        &mut self.style
    }
}

impl<T: Clone + 'static> RenderOnce for SortableList<T> {
    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
        let theme = use_theme();
        let user_style = self.style;
        let items = self.state.read(cx).items.clone();
        let dragging_index = self.state.read(cx).dragging_index;
        let drag_over_bg = theme.tokens.primary.opacity(0.1);
        let indicator_color = theme.tokens.primary;

        let mut container = div()
            .flex()
            .when(self.direction == Axis::Vertical, |d| d.flex_col())
            .gap(self.gap);

        for (idx, item) in items.iter().enumerate() {
            let is_dragging = dragging_index == Some(idx);
            let rendered = (self.item_renderer)(item, idx, is_dragging);

            let state_drop = self.state.clone();
            let on_reorder = self.on_reorder.clone();
            let state_drag = self.state.clone();

            let item_el = div()
                .id(ElementId::Name(format!("sortable-item-{}", idx).into()))
                .child(rendered)
                .on_drag(
                    SortableItemDrag {
                        index: idx,
                        position: Point::default(),
                    },
                    move |data: &SortableItemDrag, pos, _window, cx| {
                        state_drag.update(cx, |s, _| {
                            s.dragging_index = Some(data.index);
                        });
                        cx.new(|_| SortableItemDrag {
                            index: data.index,
                            position: pos,
                        })
                    },
                )
                .drag_over::<SortableItemDrag>(move |style, _, _, _| {
                    style
                        .bg(drag_over_bg)
                        .border_t(px(2.0))
                        .border_color(indicator_color)
                })
                .on_drop(move |dragged: &SortableItemDrag, window, cx| {
                    let from = dragged.index;
                    let to = idx;
                    if from == to {
                        state_drop.update(cx, |s, ctx| {
                            s.dragging_index = None;
                            s.hover_index = None;
                            ctx.notify();
                        });
                        return;
                    }

                    state_drop.update(cx, |s, ctx| {
                        let mut reordered = s.items.clone();
                        if from < reordered.len() {
                            let moved = reordered.remove(from);
                            let insert_at = to.min(reordered.len());
                            reordered.insert(insert_at, moved);
                            s.items = reordered;
                        }
                        s.dragging_index = None;
                        s.hover_index = None;
                        ctx.notify();
                    });

                    if let Some(ref callback) = on_reorder {
                        let reordered_items = state_drop.read(cx).items.clone();
                        callback(reordered_items, window, cx);
                    }
                })
                .when(is_dragging, |d| d.opacity(0.5));

            container = container.child(item_el);
        }

        container.map(|mut this| {
            this.style().refine(&user_style);
            this
        })
    }
}