fluidattacks-designs 0.11.0

Fluid Attacks design system, GPUI variant: desktop widgets over the shared tokens
//! The footer: the range label, the page-size chooser, and the arrows that
//! step the page — the three the web's `TableFooter` carries, in that order.

use fluidattacks_designs_domain::icon::Style as IconStyle;
use fluidattacks_designs_domain::table::{
    write_range, Page, ARROW_BG, ARROW_BORDER, ARROW_DISABLED_BG, ARROW_DISABLED_ICON, ARROW_GLYPH,
    ARROW_HOVER_BG, ARROW_ICON, ARROW_PADDING, ARROW_RADIUS, BORDER_PX, FOOTER_GAP, FOOTER_PADDING,
    NEXT_ICON, PREVIOUS_ICON, RANGE_TEXT_SIZE, RANGE_WEIGHT,
};
use fluidattacks_designs_domain::typography::FONT_PRIMARY;
use gpui::prelude::FluentBuilder as _;
use gpui::{
    div, px, rgb, App, Div, Entity, FontWeight, InteractiveElement as _, ParentElement as _,
    SharedString, Stateful, StatefulInteractiveElement as _, Styled as _,
};

use crate::icon::Icon;
use crate::table::state::{PageHandler, TableState};
use crate::theme::{ActiveTheme as _, Theme};

pub fn footer(state: &Entity<TableState>, on_page: &PageHandler, cx: &App) -> Div {
    let read = state.read(cx);
    let page = read.page();
    let counts = read.page_counts();
    div()
        .flex()
        .flex_none()
        .items_center()
        .gap(px(FOOTER_GAP))
        .py(px(FOOTER_PADDING.0))
        .px(px(FOOTER_PADDING.1))
        .child(range_label(page, cx, state))
        .child(crate::table::page_size::page_size(state, on_page, cx))
        .child(
            div()
                .flex()
                .items_center()
                .gap(px(FOOTER_GAP))
                .child(arrow(
                    state,
                    on_page,
                    PREVIOUS_ICON,
                    page.can_previous().then(|| page.previous()),
                    cx.theme(),
                ))
                .child(arrow(
                    state,
                    on_page,
                    NEXT_ICON,
                    page.can_next(counts).then(|| page.next()),
                    cx.theme(),
                )),
        )
}

fn range_label(page: Page, cx: &App, state: &Entity<TableState>) -> Div {
    let mut label = String::new();
    // a string sink cannot fail; an empty label beats a panic mid-frame if one
    // ever did
    if write_range(&mut label, page, state.read(cx).page_counts()).is_err() {
        label.clear();
    }
    div()
        .flex_none()
        .font_family(FONT_PRIMARY)
        .text_size(px(RANGE_TEXT_SIZE.px()))
        .line_height(px(RANGE_TEXT_SIZE.line_height_px()))
        .font_weight(FontWeight(f32::from(RANGE_WEIGHT.value())))
        .child(SharedString::from(label))
}

/// One arrow. `target` is the page it moves to, and `None` is what makes it
/// spent: the arrow paints flat and answers nothing.
fn arrow(
    state: &Entity<TableState>,
    on_page: &PageHandler,
    glyph: &'static str,
    target: Option<Page>,
    theme: &Theme,
) -> Stateful<Div> {
    let stepping = state.clone();
    let handler = on_page.clone();
    let enabled = target.is_some();
    let hover_bg = theme.shade(ARROW_HOVER_BG);
    div()
        .id(crate::table::part(state, format!("page-{glyph}")))
        .flex()
        .flex_none()
        .items_center()
        .justify_center()
        .py(px(ARROW_PADDING.0))
        .px(px(ARROW_PADDING.1))
        .rounded(px(ARROW_RADIUS))
        .border(px(BORDER_PX))
        .border_color(rgb(theme.shade(ARROW_BORDER)))
        .map(|button| {
            if enabled {
                button
                    .bg(rgb(theme.shade(ARROW_BG)))
                    .cursor_pointer()
                    .hover(move |hovered| hovered.bg(rgb(hover_bg)))
            } else {
                button.bg(rgb(theme.shade(ARROW_DISABLED_BG)))
            }
        })
        .child(
            Icon::new(glyph)
                .style(IconStyle::Light)
                .size(ARROW_GLYPH)
                .color(theme.shade(if enabled {
                    ARROW_ICON
                } else {
                    ARROW_DISABLED_ICON
                })),
        )
        .when_some(target, |button, page| {
            button.on_click(move |_, _win, app| {
                stepping.update(app, |table, inner| table.set_page(page, inner));
                handler(page, app);
            })
        })
}