use core::fmt::{self, Debug};
use core::marker::PhantomData;
use waterui::accessibility::{AccessibilityChildren, AccessibilityRole, AccessibilityState};
use waterui::border::Border;
use waterui::color::Color;
use waterui::layout::frame::Frame;
use waterui::layout::padding::EdgeInsets;
use waterui::reactive::SignalExt as _;
use waterui::shape::{Rectangle, RoundedRectangle, ShapeExt as _};
use waterui::{Binding, Environment, Str, View, ViewExt as _};
use waterui_controls::label::{IntoLabel, Label};
use waterui_core::handler::{Handler, SharedAction, boxed_action};
use crate::color::{
OnSecondaryContainer, OnSurface, OnSurfaceVariant, Outline, SecondaryContainer, Surface,
};
use crate::icons::CheckmarkIcon;
use crate::semantics::{conditional_color, interaction_style, label_plain_text};
use crate::theme::typography;
const ASSIST_CHIP_CONTAINER_HEIGHT: f32 = 32.0;
const ASSIST_CHIP_CONTAINER_SHAPE: f32 = 8.0;
const ASSIST_CHIP_CONTAINER_CLIP_RADIUS: f32 = 0.25;
const ASSIST_CHIP_OUTLINE_WIDTH: f32 = 1.0;
const ASSIST_CHIP_LEADING_SPACE: f32 = 16.0;
const ASSIST_CHIP_TRAILING_SPACE: f32 = 16.0;
const FILTER_CHIP_CONTAINER_HEIGHT: f32 = 32.0;
const FILTER_CHIP_CONTAINER_SHAPE: f32 = 8.0;
const FILTER_CHIP_CONTAINER_CLIP_RADIUS: f32 = 0.25;
const FILTER_CHIP_UNSELECTED_OUTLINE_WIDTH: f32 = 1.0;
const FILTER_CHIP_SELECTED_OUTLINE_WIDTH: f32 = 0.0;
const FILTER_CHIP_LEADING_SPACE: f32 = 16.0;
const FILTER_CHIP_TRAILING_SPACE: f32 = 16.0;
const FILTER_CHIP_WITH_ICON_LEADING_SPACE: f32 = 8.0;
const FILTER_CHIP_ICON_LABEL_SPACE: f32 =
FILTER_CHIP_LEADING_SPACE - FILTER_CHIP_WITH_ICON_LEADING_SPACE;
const FILTER_CHIP_ICON_SIZE: f32 = 18.0;
const FILTER_CHIP_CHECKMARK_LINE_WIDTH: f32 = 2.0;
const INPUT_CHIP_CONTAINER_HEIGHT: f32 = 32.0;
const INPUT_CHIP_CONTAINER_SHAPE: f32 = 8.0;
const INPUT_CHIP_CONTAINER_CLIP_RADIUS: f32 = 0.25;
const INPUT_CHIP_UNSELECTED_OUTLINE_WIDTH: f32 = 1.0;
const INPUT_CHIP_LEADING_SPACE: f32 = 16.0;
const INPUT_CHIP_WITH_TRAILING_ICON_TRAILING_SPACE: f32 = 8.0;
const INPUT_CHIP_ICON_LABEL_SPACE: f32 = 8.0;
const INPUT_CHIP_TRAILING_ICON_SIZE: f32 = 18.0;
const INPUT_CHIP_REMOVE_ICON_LINE_WIDTH: f32 = 2.0;
pub struct OutlinedChip<Action = fn(&Environment), LabelColor = OnSurface> {
label: Label,
accessibility_label: Str,
action: Action,
label_color: PhantomData<LabelColor>,
}
impl<Action, LabelColor> Debug for OutlinedChip<Action, LabelColor> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("OutlinedChip")
.field("label", &self.label)
.finish_non_exhaustive()
}
}
impl<LabelColor> OutlinedChip<fn(&Environment), LabelColor> {
#[must_use]
pub fn new(label: impl IntoLabel) -> Self {
let label = label.into_label();
let accessibility_label = label_plain_text(&label);
Self {
label,
accessibility_label,
action: noop,
label_color: PhantomData,
}
}
}
impl<Action, LabelColor> OutlinedChip<Action, LabelColor> {
#[must_use]
pub fn action<F, Args>(self, action: F) -> OutlinedChip<impl FnMut(&Environment), LabelColor>
where
F: Handler<Args, ()> + 'static,
{
OutlinedChip {
label: self.label,
accessibility_label: self.accessibility_label,
action: boxed_action(action),
label_color: PhantomData,
}
}
}
impl<Action, LabelColor> View for OutlinedChip<Action, LabelColor>
where
Action: FnMut(&Environment) + 'static,
LabelColor: Default + Into<Color> + 'static,
{
fn body(self, _env: &Environment) -> impl View {
let mut action = self.action;
let accessibility_label = self.accessibility_label.clone();
let label = self
.label
.font(typography::label_large())
.foreground(LabelColor::default());
label
.height(ASSIST_CHIP_CONTAINER_HEIGHT)
.padding_with(EdgeInsets::new(
0.0,
0.0,
ASSIST_CHIP_LEADING_SPACE,
ASSIST_CHIP_TRAILING_SPACE,
))
.background(RoundedRectangle::new(ASSIST_CHIP_CONTAINER_CLIP_RADIUS).fill(Surface))
.border_with(
Border::new(Outline, ASSIST_CHIP_OUTLINE_WIDTH)
.corner_radius(ASSIST_CHIP_CONTAINER_SHAPE),
)
.on_tap(move |env: Environment| action(&env))
.a11y_label(accessibility_label)
.a11y_role(AccessibilityRole::Button)
.a11y_children(AccessibilityChildren::ExcludeDescendants)
.install(interaction_style(
LabelColor::default(),
f64::from(ASSIST_CHIP_CONTAINER_SHAPE),
))
}
}
const fn noop(_env: &Environment) {}
pub type AssistChip<Action = fn(&Environment)> = OutlinedChip<Action, OnSurface>;
pub type SuggestionChip<Action = fn(&Environment)> = OutlinedChip<Action, OnSurfaceVariant>;
pub struct FilterChip<Action = fn(&Environment)> {
label: Label,
accessibility_label: Str,
selected: Binding<bool>,
action: Action,
}
pub struct InputChip<Action = fn(&Environment), RemoveAction = fn(&Environment)> {
label: Label,
accessibility_label: Str,
remove_accessibility_label: Str,
action: Action,
remove_action: RemoveAction,
}
impl<Action, RemoveAction> Debug for InputChip<Action, RemoveAction> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("InputChip")
.field("label", &self.label)
.finish_non_exhaustive()
}
}
impl InputChip<fn(&Environment), fn(&Environment)> {
#[must_use]
pub fn new(label: impl IntoLabel) -> Self {
let label = label.into_label();
let accessibility_label = label_plain_text(&label);
let remove_accessibility_label = Str::from(format!("Remove {accessibility_label}"));
Self {
label,
accessibility_label,
remove_accessibility_label,
action: noop,
remove_action: noop,
}
}
}
impl<Action, RemoveAction> InputChip<Action, RemoveAction> {
#[must_use]
pub fn action<F, Args>(self, action: F) -> InputChip<impl FnMut(&Environment), RemoveAction>
where
F: Handler<Args, ()> + 'static,
{
InputChip {
label: self.label,
accessibility_label: self.accessibility_label,
remove_accessibility_label: self.remove_accessibility_label,
action: boxed_action(action),
remove_action: self.remove_action,
}
}
#[must_use]
pub fn remove_action<F, Args>(self, action: F) -> InputChip<Action, impl FnMut(&Environment)>
where
F: Handler<Args, ()> + 'static,
{
InputChip {
label: self.label,
accessibility_label: self.accessibility_label,
remove_accessibility_label: self.remove_accessibility_label,
action: self.action,
remove_action: boxed_action(action),
}
}
}
impl<Action> Debug for FilterChip<Action> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("FilterChip")
.field("label", &self.label)
.finish_non_exhaustive()
}
}
impl FilterChip<fn(&Environment)> {
#[must_use]
pub fn new(label: impl IntoLabel, selected: &Binding<bool>) -> Self {
let label = label.into_label();
let accessibility_label = label_plain_text(&label);
Self {
label,
accessibility_label,
selected: selected.clone(),
action: noop,
}
}
}
impl<Action> FilterChip<Action> {
#[must_use]
pub fn action<F, Args>(self, action: F) -> FilterChip<impl FnMut(&Environment)>
where
F: Handler<Args, ()> + 'static,
{
FilterChip {
label: self.label,
accessibility_label: self.accessibility_label,
selected: self.selected,
action: boxed_action(action),
}
}
}
impl<Action> View for FilterChip<Action>
where
Action: FnMut(&Environment) + 'static,
{
fn body(self, _env: &Environment) -> impl View {
let mut action = self.action;
let action = SharedAction::new(move |env: Environment| action(&env));
let selected_for_tap = self.selected.clone();
let accessibility_state = self
.selected
.map(|selected| AccessibilityState::new().selected(selected));
let foreground = conditional_color(
self.selected.clone(),
OnSecondaryContainer,
OnSurfaceVariant,
);
let background = conditional_color(self.selected.clone(), SecondaryContainer, Surface);
let outline = conditional_color(
self.selected.clone(),
Outline.with_opacity(FILTER_CHIP_SELECTED_OUTLINE_WIDTH),
Outline,
);
let state_layer_color = conditional_color(
self.selected.clone(),
OnSecondaryContainer,
OnSurfaceVariant,
);
let checkmark_opacity = self
.selected
.map(|selected| if selected { 1.0 } else { 0.0 });
let checkmark_width = self.selected.map(
|selected| {
if selected { FILTER_CHIP_ICON_SIZE } else { 0.0 }
},
);
let checkmark = Frame::new(
CheckmarkIcon::new(
OnSecondaryContainer,
FILTER_CHIP_ICON_SIZE,
FILTER_CHIP_CHECKMARK_LINE_WIDTH,
)
.container_height(FILTER_CHIP_CONTAINER_HEIGHT)
.opacity(checkmark_opacity),
)
.width(checkmark_width);
waterui::component::hstack((
checkmark,
self.label
.font(typography::label_large())
.foreground(foreground),
))
.spacing(FILTER_CHIP_ICON_LABEL_SPACE)
.height(FILTER_CHIP_CONTAINER_HEIGHT)
.padding_with(EdgeInsets::new(
0.0,
0.0,
FILTER_CHIP_WITH_ICON_LEADING_SPACE,
FILTER_CHIP_TRAILING_SPACE,
))
.background(RoundedRectangle::new(FILTER_CHIP_CONTAINER_CLIP_RADIUS).fill(background))
.border_with(
Border::new(outline, FILTER_CHIP_UNSELECTED_OUTLINE_WIDTH)
.corner_radius(FILTER_CHIP_CONTAINER_SHAPE),
)
.on_tap(move |env: Environment| {
selected_for_tap.set(!selected_for_tap.get());
action.call(&env);
})
.a11y_label(self.accessibility_label)
.a11y_role(AccessibilityRole::Button)
.a11y_children(AccessibilityChildren::ExcludeDescendants)
.a11y_state_signal(accessibility_state)
.install(interaction_style(
state_layer_color,
f64::from(FILTER_CHIP_CONTAINER_SHAPE),
))
}
}
impl<Action, RemoveAction> View for InputChip<Action, RemoveAction>
where
Action: FnMut(&Environment) + 'static,
RemoveAction: FnMut(&Environment) + 'static,
{
fn body(self, _env: &Environment) -> impl View {
let mut action = self.action;
let action = SharedAction::new(move |env: Environment| action(&env));
let mut remove_action = self.remove_action;
let remove_action = SharedAction::new(move |env: Environment| remove_action(&env));
let accessibility_label = self.accessibility_label;
let remove_accessibility_label = self.remove_accessibility_label;
waterui::component::hstack((
self.label
.font(typography::label_large())
.foreground(OnSurfaceVariant)
.on_tap(move |env: Environment| action.call(&env))
.a11y_label(accessibility_label)
.a11y_role(AccessibilityRole::Button)
.a11y_children(AccessibilityChildren::ExcludeDescendants)
.install(interaction_style(
OnSurfaceVariant,
f64::from(INPUT_CHIP_CONTAINER_SHAPE),
)),
RemoveButton {
accessibility_label: remove_accessibility_label,
action: remove_action,
},
))
.spacing(INPUT_CHIP_ICON_LABEL_SPACE)
.height(INPUT_CHIP_CONTAINER_HEIGHT)
.padding_with(EdgeInsets::new(
0.0,
0.0,
INPUT_CHIP_LEADING_SPACE,
INPUT_CHIP_WITH_TRAILING_ICON_TRAILING_SPACE,
))
.background(RoundedRectangle::new(INPUT_CHIP_CONTAINER_CLIP_RADIUS).fill(Surface))
.border_with(
Border::new(Outline, INPUT_CHIP_UNSELECTED_OUTLINE_WIDTH)
.corner_radius(INPUT_CHIP_CONTAINER_SHAPE),
)
}
}
#[must_use]
pub fn assist_chip(label: impl IntoLabel) -> AssistChip {
AssistChip::new(label)
}
#[must_use]
pub fn suggestion_chip(label: impl IntoLabel) -> SuggestionChip {
SuggestionChip::new(label)
}
#[must_use]
pub fn filter_chip(label: impl IntoLabel, selected: &Binding<bool>) -> FilterChip {
FilterChip::new(label, selected)
}
#[must_use]
pub fn input_chip(label: impl IntoLabel) -> InputChip {
InputChip::new(label)
}
struct RemoveButton {
accessibility_label: Str,
action: SharedAction,
}
impl View for RemoveButton {
fn body(self, _env: &Environment) -> impl View {
let accessibility_label = self.accessibility_label;
let action = self.action;
waterui::component::hstack((RemoveIcon,))
.size(INPUT_CHIP_TRAILING_ICON_SIZE, INPUT_CHIP_TRAILING_ICON_SIZE)
.on_tap(move |env: Environment| action.call(&env))
.a11y_label(accessibility_label)
.a11y_role(AccessibilityRole::Button)
.a11y_children(AccessibilityChildren::ExcludeDescendants)
.install(interaction_style(
OnSurfaceVariant,
f64::from(INPUT_CHIP_TRAILING_ICON_SIZE * 0.5),
))
}
}
struct RemoveIcon;
impl View for RemoveIcon {
fn body(self, _env: &Environment) -> impl View {
let line = || {
Rectangle
.fill(OnSurfaceVariant)
.size(10.5, INPUT_CHIP_REMOVE_ICON_LINE_WIDTH)
};
waterui::component::zstack((line().rotation(45.0), line().rotation(-45.0)))
.size(INPUT_CHIP_TRAILING_ICON_SIZE, INPUT_CHIP_TRAILING_ICON_SIZE)
}
}
#[cfg(test)]
mod tests {
use super::{
ASSIST_CHIP_CONTAINER_HEIGHT, ASSIST_CHIP_CONTAINER_SHAPE, ASSIST_CHIP_LEADING_SPACE,
ASSIST_CHIP_OUTLINE_WIDTH, ASSIST_CHIP_TRAILING_SPACE, FILTER_CHIP_CONTAINER_HEIGHT,
FILTER_CHIP_CONTAINER_SHAPE, FILTER_CHIP_ICON_LABEL_SPACE, FILTER_CHIP_ICON_SIZE,
FILTER_CHIP_LEADING_SPACE, FILTER_CHIP_SELECTED_OUTLINE_WIDTH, FILTER_CHIP_TRAILING_SPACE,
FILTER_CHIP_UNSELECTED_OUTLINE_WIDTH, FILTER_CHIP_WITH_ICON_LEADING_SPACE,
INPUT_CHIP_CONTAINER_HEIGHT, INPUT_CHIP_CONTAINER_SHAPE, INPUT_CHIP_ICON_LABEL_SPACE,
INPUT_CHIP_LEADING_SPACE, INPUT_CHIP_TRAILING_ICON_SIZE,
INPUT_CHIP_UNSELECTED_OUTLINE_WIDTH, INPUT_CHIP_WITH_TRAILING_ICON_TRAILING_SPACE,
};
#[test]
fn assist_chip_tokens_match_compose_assist_chip_tokens() {
assert_eq!(ASSIST_CHIP_CONTAINER_HEIGHT, 32.0);
assert_eq!(ASSIST_CHIP_CONTAINER_SHAPE, 8.0);
assert_eq!(ASSIST_CHIP_OUTLINE_WIDTH, 1.0);
assert_eq!(ASSIST_CHIP_LEADING_SPACE, 16.0);
assert_eq!(ASSIST_CHIP_TRAILING_SPACE, 16.0);
}
#[test]
fn filter_chip_tokens_match_compose_filter_chip_tokens() {
assert_eq!(FILTER_CHIP_CONTAINER_HEIGHT, 32.0);
assert_eq!(FILTER_CHIP_CONTAINER_SHAPE, 8.0);
assert_eq!(FILTER_CHIP_UNSELECTED_OUTLINE_WIDTH, 1.0);
assert_eq!(FILTER_CHIP_SELECTED_OUTLINE_WIDTH, 0.0);
assert_eq!(FILTER_CHIP_LEADING_SPACE, 16.0);
assert_eq!(FILTER_CHIP_WITH_ICON_LEADING_SPACE, 8.0);
assert_eq!(FILTER_CHIP_ICON_LABEL_SPACE, 8.0);
assert_eq!(FILTER_CHIP_TRAILING_SPACE, 16.0);
assert_eq!(FILTER_CHIP_ICON_SIZE, 18.0);
}
#[test]
fn input_chip_tokens_match_compose_input_chip_tokens() {
assert_eq!(INPUT_CHIP_CONTAINER_HEIGHT, 32.0);
assert_eq!(INPUT_CHIP_CONTAINER_SHAPE, 8.0);
assert_eq!(INPUT_CHIP_UNSELECTED_OUTLINE_WIDTH, 1.0);
assert_eq!(INPUT_CHIP_LEADING_SPACE, 16.0);
assert_eq!(INPUT_CHIP_ICON_LABEL_SPACE, 8.0);
assert_eq!(INPUT_CHIP_WITH_TRAILING_ICON_TRAILING_SPACE, 8.0);
assert_eq!(INPUT_CHIP_TRAILING_ICON_SIZE, 18.0);
}
}