kael_ui 0.2.0

Professional shadcn-inspired UI component library for Kael. 100+ accessible components for building beautiful, performant desktop applications.
//! macOS Dock-style magnification row.

use kael::{prelude::FluentBuilder as _, *};

use crate::theme::use_theme;

pub struct DockState {
    cursor_x: Option<Pixels>,
}

impl DockState {
    pub fn new(_cx: &mut Context<Self>) -> Self {
        Self { cursor_x: None }
    }
}

impl Render for DockState {
    fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
        Empty
    }
}

#[allow(dead_code)]
struct DockItemInfo {
    center_x: f32,
    scale: f32,
}

fn compute_scales(
    item_count: usize,
    item_size_f: f32,
    gap: f32,
    cursor_rel_x: Option<f32>,
    max_scale: f32,
    sigma: f32,
) -> Vec<DockItemInfo> {
    let mut items = Vec::with_capacity(item_count);
    let total_width = item_count as f32 * item_size_f + (item_count.saturating_sub(1)) as f32 * gap;
    let start_x = -total_width * 0.5;

    for i in 0..item_count {
        let center = start_x + i as f32 * (item_size_f + gap) + item_size_f * 0.5;
        let scale = if let Some(cx_pos) = cursor_rel_x {
            let dist = (center - cx_pos).abs();
            1.0 + max_scale * (-dist * dist / (2.0 * sigma * sigma)).exp()
        } else {
            1.0
        };
        items.push(DockItemInfo {
            center_x: center,
            scale,
        });
    }
    items
}

#[derive(IntoElement)]
pub struct Dock {
    id: ElementId,
    state: Entity<DockState>,
    max_scale: f32,
    item_size: Pixels,
    gap: Pixels,
    style: StyleRefinement,
    children: Vec<AnyElement>,
}

impl Dock {
    pub fn new(id: impl Into<ElementId>, state: Entity<DockState>) -> Self {
        Self {
            id: id.into(),
            state,
            max_scale: 0.5,
            item_size: px(48.0),
            gap: px(4.0),
            style: StyleRefinement::default(),
            children: Vec::new(),
        }
    }

    pub fn max_scale(mut self, scale: f32) -> Self {
        self.max_scale = scale.max(0.0);
        self
    }

    pub fn item_size(mut self, size: Pixels) -> Self {
        self.item_size = size;
        self
    }

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

impl Styled for Dock {
    fn style(&mut self) -> &mut StyleRefinement {
        &mut self.style
    }
}

impl ParentElement for Dock {
    fn extend(&mut self, elements: impl IntoIterator<Item = AnyElement>) {
        self.children.extend(elements);
    }
}

impl RenderOnce for Dock {
    fn render(self, _window: &mut Window, cx: &mut App) -> impl IntoElement {
        let theme = use_theme();
        let user_style = self.style;
        let state = self.state.read(cx);
        let item_count = self.children.len();
        let item_size_f = self.item_size / px(1.0);
        let gap_f = self.gap / px(1.0);
        let sigma = item_size_f * 1.5;

        let cursor_rel = state.cursor_x.map(|cx_pos| cx_pos / px(1.0));

        let scales = compute_scales(
            item_count,
            item_size_f,
            gap_f,
            cursor_rel,
            self.max_scale,
            sigma,
        );

        let state_move = self.state.clone();
        let state_hover = self.state.clone();

        let mut row = div()
            .id(self.id)
            .flex()
            .flex_row()
            .items_end()
            .gap(self.gap)
            .px(px(8.0))
            .py(px(4.0))
            .rounded(px(12.0))
            .bg(theme.tokens.card)
            .border_1()
            .border_color(theme.tokens.border)
            .on_mouse_move(move |event: &MouseMoveEvent, _window, cx| {
                state_move.update(cx, |s, cx| {
                    s.cursor_x = Some(event.position.x);
                    cx.notify();
                });
            })
            .on_hover(move |hovered: &bool, _window, cx| {
                if !*hovered {
                    state_hover.update(cx, |s, cx| {
                        s.cursor_x = None;
                        cx.notify();
                    });
                }
            })
            .map(|this: Stateful<Div>| {
                let mut el = this;
                el.style().refine(&user_style);
                el
            });

        for (child, info) in self.children.into_iter().zip(scales.iter()) {
            let scaled_size = px(item_size_f * info.scale);
            row = row.child(
                div()
                    .flex_shrink_0()
                    .w(scaled_size)
                    .h(scaled_size)
                    .flex()
                    .items_center()
                    .justify_center()
                    .child(child),
            );
        }

        row
    }
}