aetna-core 0.3.0

Aetna — backend-agnostic UI library core
Documentation
//! Sidebar anatomy — familiar navigation groups and menu rows.

use std::panic::Location;

use crate::cursor::Cursor;
use crate::metrics::MetricsRole;
use crate::style::StyleProfile;
use crate::tokens;
use crate::tree::*;
use crate::widgets::text::text;
use crate::{IntoIconSource, icon};

#[track_caller]
pub fn sidebar<I, E>(children: I) -> El
where
    I: IntoIterator<Item = E>,
    E: Into<El>,
{
    column(children)
        .at_loc(Location::caller())
        .style_profile(StyleProfile::Surface)
        .surface_role(SurfaceRole::Panel)
        .fill(tokens::CARD)
        .stroke(tokens::BORDER)
        .width(Size::Fixed(tokens::SIDEBAR_WIDTH))
        .height(Size::Fill(1.0))
        .default_padding(tokens::SPACE_4)
        .default_gap(tokens::SPACE_4)
}

#[track_caller]
pub fn sidebar_header<I, E>(children: I) -> El
where
    I: IntoIterator<Item = E>,
    E: Into<El>,
{
    column(children)
        .at_loc(Location::caller())
        .width(Size::Fill(1.0))
        .height(Size::Hug)
        .gap(tokens::SPACE_1)
}

#[track_caller]
pub fn sidebar_group<I, E>(children: I) -> El
where
    I: IntoIterator<Item = E>,
    E: Into<El>,
{
    column(children)
        .at_loc(Location::caller())
        .width(Size::Fill(1.0))
        .height(Size::Hug)
        .gap(tokens::SPACE_1)
}

#[track_caller]
pub fn sidebar_group_label(label: impl Into<String>) -> El {
    text(label)
        .at_loc(Location::caller())
        .caption()
        .semibold()
        .muted()
        .ellipsis()
        .padding(Sides::xy(tokens::SPACE_2, tokens::SPACE_1))
        .width(Size::Fill(1.0))
}

#[track_caller]
pub fn sidebar_menu<I, E>(children: I) -> El
where
    I: IntoIterator<Item = E>,
    E: Into<El>,
{
    column(children)
        .at_loc(Location::caller())
        .width(Size::Fill(1.0))
        .height(Size::Hug)
        .gap(tokens::SPACE_1)
}

#[track_caller]
pub fn sidebar_menu_item(child: impl Into<El>) -> El {
    row([child.into()])
        .at_loc(Location::caller())
        .width(Size::Fill(1.0))
        .height(Size::Hug)
        .align(Align::Center)
}

#[track_caller]
pub fn sidebar_menu_button(label: impl Into<String>, current: bool) -> El {
    let button = row([sidebar_menu_label(label)])
        .at_loc(Location::caller())
        .style_profile(StyleProfile::Solid)
        .metrics_role(MetricsRole::ListItem)
        .focusable()
        .cursor(Cursor::Pointer)
        .fill(tokens::CARD)
        .default_radius(tokens::RADIUS_SM)
        .default_gap(tokens::SPACE_2)
        .width(Size::Fill(1.0))
        .align(Align::Center);
    if current {
        button.current()
    } else {
        button.ghost()
    }
}

#[track_caller]
pub fn sidebar_menu_button_with_icon(
    source: impl IntoIconSource,
    label: impl Into<String>,
    current: bool,
) -> El {
    let button = row([
        icon(source)
            .icon_size(tokens::ICON_SM)
            .color(tokens::MUTED_FOREGROUND),
        sidebar_menu_label(label),
    ])
    .at_loc(Location::caller())
    .style_profile(StyleProfile::Solid)
    .metrics_role(MetricsRole::ListItem)
    .focusable()
    .cursor(Cursor::Pointer)
    .fill(tokens::CARD)
    .default_radius(tokens::RADIUS_SM)
    .default_gap(tokens::SPACE_2)
    .width(Size::Fill(1.0))
    .align(Align::Center);
    if current {
        button.current()
    } else {
        button.ghost()
    }
}

#[track_caller]
pub fn sidebar_menu_label(label: impl Into<String>) -> El {
    text(label)
        .at_loc(Location::caller())
        .label()
        .font_weight(FontWeight::Medium)
        .ellipsis()
        .width(Size::Fill(1.0))
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn sidebar_uses_standard_width_and_panel_surface() {
        let s = sidebar([sidebar_header([text("Aetna")])]);

        assert_eq!(s.width, Size::Fixed(tokens::SIDEBAR_WIDTH));
        assert_eq!(s.height, Size::Fill(1.0));
        assert_eq!(s.surface_role, SurfaceRole::Panel);
        assert_eq!(s.fill, Some(tokens::CARD));
    }

    #[test]
    fn sidebar_menu_button_uses_list_density_and_current_treatment() {
        let current = sidebar_menu_button_with_icon("layout-dashboard", "Overview", true);
        let inactive = sidebar_menu_button("Settings", false);

        assert_eq!(current.metrics_role, Some(MetricsRole::ListItem));
        assert_eq!(current.align, Align::Center);
        assert_eq!(current.surface_role, SurfaceRole::Current);
        assert!(current.focusable);
        assert!(inactive.fill.is_none());
    }
}