use core::fmt::{self, Debug};
use waterui::accessibility::AccessibilityRole;
use waterui::layout::padding::EdgeInsets;
use waterui::shape::{Capsule, ShapeExt as _};
use waterui::{Environment, View, ViewExt as _};
use waterui_core::view::TupleViews;
use crate::color::{OnPrimaryContainer, OnSurface, PrimaryContainer, SurfaceContainer};
use crate::elevation::{MaterialElevationLevel, material_elevation};
const CONTAINER_HEIGHT: f32 = 64.0;
const FLOATING_HORIZONTAL_SPACE: f32 = 8.0;
const FLOATING_BETWEEN_SPACE: f32 = 4.0;
const FLOATING_EXTERNAL_PADDING: f32 = 16.0;
const DOCKED_HORIZONTAL_SPACE: f32 = 16.0;
const DOCKED_MIN_SPACING: f32 = 4.0;
const DOCKED_MAX_SPACING: f32 = 32.0;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
pub enum FloatingToolbarStyle {
#[default]
Standard,
Vibrant,
}
impl FloatingToolbarStyle {
#[must_use]
fn container_color(self) -> waterui::color::Color {
match self {
Self::Standard => SurfaceContainer.into(),
Self::Vibrant => PrimaryContainer.into(),
}
}
#[must_use]
fn content_color(self) -> waterui::color::Color {
match self {
Self::Standard => OnSurface.into(),
Self::Vibrant => OnPrimaryContainer.into(),
}
}
}
pub struct FloatingToolbar<Actions> {
actions: Actions,
style: FloatingToolbarStyle,
}
impl<Actions> Debug for FloatingToolbar<Actions> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("FloatingToolbar")
.field("style", &self.style)
.finish_non_exhaustive()
}
}
impl<Actions> FloatingToolbar<Actions> {
#[must_use]
pub const fn new(actions: Actions) -> Self {
Self {
actions,
style: FloatingToolbarStyle::Standard,
}
}
#[must_use]
pub const fn vibrant(mut self) -> Self {
self.style = FloatingToolbarStyle::Vibrant;
self
}
}
impl<Actions> View for FloatingToolbar<Actions>
where
Actions: TupleViews + 'static,
{
fn body(self, _env: &Environment) -> impl View {
let style = self.style;
material_elevation(
MaterialElevationLevel::LEVEL3,
CONTAINER_HEIGHT / 2.0,
waterui::component::hstack(self.actions)
.spacing(FLOATING_BETWEEN_SPACE)
.padding_with(EdgeInsets::new(
0.0,
0.0,
FLOATING_HORIZONTAL_SPACE,
FLOATING_HORIZONTAL_SPACE,
))
.height(CONTAINER_HEIGHT)
.foreground(style.content_color())
.background(Capsule.fill(style.container_color())),
)
.padding_with(FLOATING_EXTERNAL_PADDING)
.a11y_role(AccessibilityRole::Group)
}
}
pub struct DockedToolbar<Actions> {
actions: Actions,
spacing: f32,
}
impl<Actions> Debug for DockedToolbar<Actions> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("DockedToolbar")
.field("spacing", &self.spacing)
.finish_non_exhaustive()
}
}
impl<Actions> DockedToolbar<Actions> {
#[must_use]
pub const fn new(actions: Actions) -> Self {
Self {
actions,
spacing: DOCKED_MIN_SPACING,
}
}
#[must_use]
pub const fn spacing(mut self, spacing: f32) -> Self {
self.spacing = spacing.clamp(DOCKED_MIN_SPACING, DOCKED_MAX_SPACING);
self
}
}
impl<Actions> View for DockedToolbar<Actions>
where
Actions: TupleViews + 'static,
{
fn body(self, _env: &Environment) -> impl View {
waterui::component::hstack(self.actions)
.spacing(self.spacing)
.padding_with(EdgeInsets::new(
0.0,
0.0,
DOCKED_HORIZONTAL_SPACE,
DOCKED_HORIZONTAL_SPACE,
))
.height(CONTAINER_HEIGHT)
.max_width(f32::INFINITY)
.foreground(OnSurface)
.background(SurfaceContainer)
.a11y_role(AccessibilityRole::Group)
}
}
#[must_use]
pub const fn floating_toolbar<Actions>(actions: Actions) -> FloatingToolbar<Actions> {
FloatingToolbar::new(actions)
}
#[must_use]
pub const fn docked_toolbar<Actions>(actions: Actions) -> DockedToolbar<Actions> {
DockedToolbar::new(actions)
}
#[cfg(test)]
mod tests {
use super::{
CONTAINER_HEIGHT, DOCKED_HORIZONTAL_SPACE, DOCKED_MAX_SPACING, DOCKED_MIN_SPACING,
DockedToolbar, FLOATING_BETWEEN_SPACE, FLOATING_EXTERNAL_PADDING,
FLOATING_HORIZONTAL_SPACE,
};
#[test]
fn toolbar_tokens_match_compose_toolbar_tokens() {
assert_eq!(CONTAINER_HEIGHT, 64.0);
assert_eq!(FLOATING_HORIZONTAL_SPACE, 8.0);
assert_eq!(FLOATING_BETWEEN_SPACE, 4.0);
assert_eq!(FLOATING_EXTERNAL_PADDING, 16.0);
assert_eq!(DOCKED_HORIZONTAL_SPACE, 16.0);
assert_eq!(DOCKED_MIN_SPACING, 4.0);
assert_eq!(DOCKED_MAX_SPACING, 32.0);
}
#[test]
fn docked_toolbar_spacing_is_clamped_to_its_token_range() {
assert_eq!(
DockedToolbar::new(()).spacing(0.0).spacing,
DOCKED_MIN_SPACING
);
assert_eq!(
DockedToolbar::new(()).spacing(1_000.0).spacing,
DOCKED_MAX_SPACING
);
assert_eq!(DockedToolbar::new(()).spacing(12.0).spacing, 12.0);
assert_eq!(DockedToolbar::new(()).spacing, DOCKED_MIN_SPACING);
}
}