repose-material 0.14.0

Material components for Repose
Documentation
#![allow(non_snake_case)]

mod components;
pub use components::*;

use std::rc::Rc;

use repose_core::*;
use repose_ui::{
    Box, Column, Row, Spacer, Stack, Surface, Text, TextStyle, ViewExt, anim::animate_f32,
    overlay::SnackbarAction,
};

pub fn AlertDialog(
    visible: bool,
    on_dismiss: impl Fn() + 'static,
    title: View,
    text: View,
    confirm_button: View,
    dismiss_button: Option<View>,
) -> View {
    if !visible {
        return Box(Modifier::new());
    }

    Stack(Modifier::new().fill_max_size()).child((
        // Scrim
        Box(Modifier::new()
            .fill_max_size()
            .background(Color::from_hex("#000000AA"))
            .clickable()
            .on_pointer_down(move |_| on_dismiss())),
        // Dialog content
        Surface(
            Modifier::new()
                .size(280.0, 200.0)
                .background(theme().surface)
                .clip_rounded(28.0)
                .padding(24.0),
            Column(Modifier::new()).child((
                title,
                Box(Modifier::new().size(1.0, 16.0)),
                text,
                Spacer(),
                Row(Modifier::new()).child((
                    dismiss_button.unwrap_or(Box(Modifier::new())),
                    Spacer(),
                    confirm_button,
                )),
            )),
        ),
    ))
}

pub fn BottomSheet(
    visible: bool,
    on_dismiss: impl Fn() + 'static,
    modifier: Modifier,
    content: View,
) -> View {
    let offset = animate_f32(
        "sheet_offset",
        if visible { 0.0 } else { 800.0 },
        AnimationSpec::spring_gentle(),
    );

    Stack(Modifier::new().fill_max_size()).child((
        // Scrim
        if visible {
            Box(Modifier::new()
                .fill_max_size()
                .background(Color::from_hex("#00000055"))
                .on_pointer_down(move |_| on_dismiss()))
        } else {
            Box(Modifier::new())
        },
        // Sheet
        Box(modifier
            .absolute()
            .offset(None, Some(offset), Some(0.0), Some(0.0)))
        .child(content),
    ))
}

pub fn NavigationBar(selected_index: usize, items: Vec<NavItem>) -> View {
    Row(Modifier::new()
        .fill_max_size()
        .background(theme().surface)
        .padding(8.0))
    .child(
        items
            .into_iter()
            .enumerate()
            .map(|(i, item)| NavigationBarItem(item, i == selected_index))
            .collect::<Vec<_>>(),
    )
}

pub struct NavItem {
    pub icon: View,
    pub label: String,
    pub on_click: Rc<dyn Fn()>,
}

fn NavigationBarItem(item: NavItem, selected: bool) -> View {
    let color = if selected {
        theme().primary
    } else {
        theme().on_surface
    };

    Column(
        Modifier::new()
            .flex_grow(1.0)
            .clickable()
            .on_pointer_down(move |_| (item.on_click)()),
    )
    .child((
        item.icon, // Tint with color
        Text(item.label).color(color),
    ))
}

pub fn Card(modifier: Modifier, elevated: bool, content: View) -> View {
    let th = theme();
    let bg = th.surface_container_low;
    let modifier = if elevated {
        modifier
    } else {
        modifier.border(1.0, th.outline_variant, 12.0)
    };
    Surface(
        modifier.background(bg).clip_rounded(12.0).padding(16.0),
        content,
    )
}

pub fn Snackbar(
    message: impl Into<String>,
    action: Option<SnackbarAction>,
    base_modifier: Modifier,
) -> View {
    let msg = message.into();
    let th = theme();
    let bg = th.surface_variant;
    let fg = th.on_surface;
    let action_color = th.primary;

    // Base (positioning) first
    let modifier = Modifier::new()
        .background(bg)
        .clip_rounded(th.shapes.small)
        .border(1.0, th.outline_variant, th.shapes.small)
        .then(base_modifier)
        .padding_values(PaddingValues {
            left: 16.0,
            right: 16.0,
            top: 12.0,
            bottom: 12.0,
        })
        .min_height(48.0)
        .min_width(280.0);

    Surface(
        modifier,
        Row(Modifier::new().align_items(repose_core::AlignItems::Center)).child((
            Text(msg)
                .color(fg)
                .size(th.typography.body_medium)
                .max_lines(2)
                .overflow_ellipsize(),
            Spacer(),
            action
                .map(|a| {
                    let label = a.label.clone();
                    Box(Modifier::new()
                        .padding_values(PaddingValues {
                            left: 8.0,
                            right: 8.0,
                            top: 6.0,
                            bottom: 6.0,
                        })
                        .clip_rounded(th.shapes.extra_small)
                        .clickable()
                        .on_pointer_down(move |_| (a.on_click)()))
                    .child(
                        Text(label)
                            .color(action_color)
                            .size(th.typography.label_large)
                            .single_line(),
                    )
                })
                .unwrap_or(Box(Modifier::new())),
        )),
    )
}

pub fn OutlinedCard(modifier: Modifier, content: View) -> View {
    Surface(
        modifier
            .border(1.0, Color::from_hex("#444444"), 12.0)
            .clip_rounded(12.0)
            .padding(16.0),
        content,
    )
}

pub fn FilterChip(
    selected: bool,
    on_click: impl Fn() + 'static,
    label: View,
    leading_icon: Option<View>,
) -> View {
    let bg = if selected {
        theme().primary
    } else {
        theme().surface
    };
    let fg = if selected {
        theme().on_primary
    } else {
        theme().on_surface
    };

    Surface(
        Modifier::new()
            .background(bg)
            .border(1.0, Color::from_hex("#444444"), 8.0)
            .clip_rounded(8.0)
            .padding(12.0)
            .clickable()
            .on_pointer_down(move |_| on_click()),
        Row(Modifier::new()).child((leading_icon.unwrap_or(Box(Modifier::new())), label)),
    )
}

pub fn Scaffold(
    top_bar: Option<View>,
    bottom_bar: Option<View>,
    floating_action_button: Option<View>,
    content: impl Fn(PaddingValues) -> View,
) -> View {
    Stack(Modifier::new().fill_max_size()).child((
        // Main content with padding
        Box(Modifier::new()
            .fill_max_size()
            .padding_values(PaddingValues {
                top: if top_bar.is_some() { 64.0 } else { 0.0 },
                bottom: if bottom_bar.is_some() { 80.0 } else { 0.0 },
                ..Default::default()
            }))
        .child(content(PaddingValues::default())),
        // Top bar
        if let Some(bar) = top_bar {
            Box(Modifier::new()
                .absolute()
                .offset(Some(0.0), Some(0.0), Some(0.0), None))
            .child(bar)
        } else {
            Box(Modifier::new())
        },
        // Bottom bar
        if let Some(bar) = bottom_bar {
            Box(Modifier::new()
                .absolute()
                .offset(Some(0.0), None, Some(0.0), Some(0.0)))
            .child(bar)
        } else {
            Box(Modifier::new())
        },
        // FAB
        if let Some(fab) = floating_action_button {
            Box(Modifier::new()
                .absolute()
                .offset(None, None, Some(16.0), Some(16.0)))
            .child(fab)
        } else {
            Box(Modifier::new())
        },
    ))
}