use bevy::ecs::relationship::Relationship;
use bevy::prelude::*;
use crate::{
i18n::LocalizedText,
icons::{IconStyle, MaterialIcon},
ripple::RippleHost,
theme::MaterialTheme,
tokens::{CornerRadius, Spacing},
};
const MAX_ANCESTOR_DEPTH: usize = 32;
pub struct AppBarPlugin;
impl Plugin for AppBarPlugin {
fn build(&self, app: &mut App) {
if !app.is_plugin_added::<crate::MaterialUiCorePlugin>() {
app.add_plugins(crate::MaterialUiCorePlugin);
}
app.add_message::<AppBarNavigationEvent>()
.add_message::<AppBarActionEvent>()
.add_systems(
Update,
(top_app_bar_scroll_system, app_bar_interaction_system),
);
}
}
#[derive(Event, Clone, bevy::prelude::Message)]
pub struct AppBarNavigationEvent {
pub app_bar: Entity,
}
#[derive(Event, Clone, bevy::prelude::Message)]
pub struct AppBarActionEvent {
pub app_bar: Entity,
pub action: String,
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum TopAppBarVariant {
#[default]
Small,
CenterAligned,
Medium,
Large,
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum TopAppBarScrollBehavior {
#[default]
Fixed,
Scroll,
EnterExit,
Collapse,
}
#[derive(Debug, Clone, Copy, PartialEq, Default)]
pub enum BottomAppBarLayout {
#[default]
Standard,
WithFab,
}
#[derive(Component)]
pub struct TopAppBar {
pub variant: TopAppBarVariant,
pub title: String,
pub navigation_icon: Option<String>,
pub actions: Vec<AppBarAction>,
pub scroll_behavior: TopAppBarScrollBehavior,
pub scroll_offset: f32,
pub elevated: bool,
}
#[derive(Debug, Clone)]
pub struct AppBarAction {
pub icon: String,
pub id: String,
pub disabled: bool,
}
impl AppBarAction {
pub fn new(icon: impl Into<String>, id: impl Into<String>) -> Self {
Self {
icon: icon.into(),
id: id.into(),
disabled: false,
}
}
pub fn disabled(mut self, disabled: bool) -> Self {
self.disabled = disabled;
self
}
}
impl TopAppBar {
pub fn new(title: impl Into<String>) -> Self {
Self {
variant: TopAppBarVariant::default(),
title: title.into(),
navigation_icon: None,
actions: Vec::new(),
scroll_behavior: TopAppBarScrollBehavior::default(),
scroll_offset: 0.0,
elevated: false,
}
}
pub fn with_variant(mut self, variant: TopAppBarVariant) -> Self {
self.variant = variant;
self
}
pub fn with_navigation(mut self, icon: impl Into<String>) -> Self {
self.navigation_icon = Some(icon.into());
self
}
pub fn with_back_button(self) -> Self {
self.with_navigation("arrow_back")
}
pub fn with_menu_button(self) -> Self {
self.with_navigation("menu")
}
pub fn add_action(mut self, action: AppBarAction) -> Self {
self.actions.push(action);
self
}
pub fn with_scroll_behavior(mut self, behavior: TopAppBarScrollBehavior) -> Self {
self.scroll_behavior = behavior;
self
}
pub fn elevated(mut self) -> Self {
self.elevated = true;
self
}
pub fn height(&self) -> f32 {
match self.variant {
TopAppBarVariant::Small | TopAppBarVariant::CenterAligned => TOP_APP_BAR_HEIGHT_SMALL,
TopAppBarVariant::Medium => {
let collapsed = (self.scroll_offset / 100.0).clamp(0.0, 1.0);
TOP_APP_BAR_HEIGHT_MEDIUM
- collapsed * (TOP_APP_BAR_HEIGHT_MEDIUM - TOP_APP_BAR_HEIGHT_SMALL)
}
TopAppBarVariant::Large => {
let collapsed = (self.scroll_offset / 100.0).clamp(0.0, 1.0);
TOP_APP_BAR_HEIGHT_LARGE
- collapsed * (TOP_APP_BAR_HEIGHT_LARGE - TOP_APP_BAR_HEIGHT_SMALL)
}
}
}
pub fn background_color(&self, theme: &MaterialTheme) -> Color {
if self.elevated {
theme.surface_container
} else {
theme.surface
}
}
pub fn title_color(&self, theme: &MaterialTheme) -> Color {
theme.on_surface
}
}
impl Default for TopAppBar {
fn default() -> Self {
Self::new("")
}
}
#[derive(Component)]
pub struct BottomAppBar {
pub layout: BottomAppBarLayout,
pub actions: Vec<AppBarAction>,
pub has_fab: bool,
pub fab_icon: Option<String>,
pub elevated: bool,
}
impl BottomAppBar {
pub fn new() -> Self {
Self {
layout: BottomAppBarLayout::default(),
actions: Vec::new(),
has_fab: false,
fab_icon: None,
elevated: false,
}
}
pub fn add_action(mut self, action: AppBarAction) -> Self {
self.actions.push(action);
self
}
pub fn with_fab(mut self, icon: impl Into<String>) -> Self {
self.has_fab = true;
self.fab_icon = Some(icon.into());
self.layout = BottomAppBarLayout::WithFab;
self
}
pub fn elevated(mut self) -> Self {
self.elevated = true;
self
}
pub fn background_color(&self, theme: &MaterialTheme) -> Color {
if self.elevated {
theme.surface_container
} else {
theme.surface_container_low
}
}
}
impl Default for BottomAppBar {
fn default() -> Self {
Self::new()
}
}
#[derive(Component)]
pub struct AppBarNavigation;
#[derive(Component)]
pub struct AppBarActionButton {
pub id: String,
}
#[derive(Component)]
pub struct AppBarTitle;
pub const TOP_APP_BAR_HEIGHT_SMALL: f32 = 64.0;
pub const TOP_APP_BAR_HEIGHT_MEDIUM: f32 = 112.0;
pub const TOP_APP_BAR_HEIGHT_LARGE: f32 = 152.0;
pub const BOTTOM_APP_BAR_HEIGHT: f32 = 80.0;
pub struct TopAppBarBuilder {
app_bar: TopAppBar,
title_key: Option<String>,
}
impl TopAppBarBuilder {
pub fn new(title: impl Into<String>) -> Self {
Self {
app_bar: TopAppBar::new(title),
title_key: None,
}
}
pub fn title_key(mut self, key: impl Into<String>) -> Self {
self.app_bar.title = String::new();
self.title_key = Some(key.into());
self
}
pub fn small(mut self) -> Self {
self.app_bar.variant = TopAppBarVariant::Small;
self
}
pub fn center_aligned(mut self) -> Self {
self.app_bar.variant = TopAppBarVariant::CenterAligned;
self
}
pub fn medium(mut self) -> Self {
self.app_bar.variant = TopAppBarVariant::Medium;
self
}
pub fn large(mut self) -> Self {
self.app_bar.variant = TopAppBarVariant::Large;
self
}
pub fn with_back(mut self) -> Self {
self.app_bar.navigation_icon = Some("←".to_string());
self
}
pub fn with_menu(mut self) -> Self {
self.app_bar.navigation_icon = Some("☰".to_string());
self
}
pub fn with_navigation(mut self, icon: impl Into<String>) -> Self {
self.app_bar.navigation_icon = Some(icon.into());
self
}
pub fn add_action(mut self, icon: impl Into<String>, id: impl Into<String>) -> Self {
self.app_bar.actions.push(AppBarAction::new(icon, id));
self
}
pub fn elevated(mut self) -> Self {
self.app_bar.elevated = true;
self
}
pub fn build(self, theme: &MaterialTheme) -> impl Bundle {
let height = self.app_bar.height();
let bg_color = self.app_bar.background_color(theme);
(
self.app_bar,
Node {
width: Val::Percent(100.0),
height: Val::Px(height),
padding: UiRect::axes(Val::Px(Spacing::EXTRA_SMALL), Val::Px(0.0)),
flex_direction: FlexDirection::Row,
align_items: AlignItems::Center,
justify_content: JustifyContent::SpaceBetween,
..default()
},
BackgroundColor(bg_color),
)
}
}
pub struct BottomAppBarBuilder {
app_bar: BottomAppBar,
}
impl BottomAppBarBuilder {
pub fn new() -> Self {
Self {
app_bar: BottomAppBar::new(),
}
}
pub fn add_action(mut self, icon: impl Into<String>, id: impl Into<String>) -> Self {
self.app_bar.actions.push(AppBarAction::new(icon, id));
self
}
pub fn with_fab(mut self, icon: impl Into<String>) -> Self {
self.app_bar.has_fab = true;
self.app_bar.fab_icon = Some(icon.into());
self
}
pub fn elevated(mut self) -> Self {
self.app_bar.elevated = true;
self
}
pub fn build(self, theme: &MaterialTheme) -> impl Bundle {
let bg_color = self.app_bar.background_color(theme);
(
self.app_bar,
Node {
width: Val::Percent(100.0),
height: Val::Px(BOTTOM_APP_BAR_HEIGHT),
padding: UiRect::axes(Val::Px(Spacing::LARGE), Val::Px(Spacing::MEDIUM)),
flex_direction: FlexDirection::Row,
align_items: AlignItems::Center,
justify_content: JustifyContent::SpaceBetween,
position_type: PositionType::Absolute,
bottom: Val::Px(0.0),
left: Val::Px(0.0),
right: Val::Px(0.0),
..default()
},
BackgroundColor(bg_color),
)
}
}
impl Default for BottomAppBarBuilder {
fn default() -> Self {
Self::new()
}
}
pub trait SpawnAppBarChild {
fn spawn_top_app_bar(
&mut self,
theme: &MaterialTheme,
title: impl Into<String>,
with_content: impl FnOnce(&mut ChildSpawnerCommands),
);
fn spawn_top_app_bar_with(
&mut self,
theme: &MaterialTheme,
builder: TopAppBarBuilder,
with_content: impl FnOnce(&mut ChildSpawnerCommands),
);
fn spawn_bottom_app_bar(
&mut self,
theme: &MaterialTheme,
with_content: impl FnOnce(&mut ChildSpawnerCommands),
);
fn spawn_bottom_app_bar_with(
&mut self,
theme: &MaterialTheme,
builder: BottomAppBarBuilder,
with_content: impl FnOnce(&mut ChildSpawnerCommands),
);
}
pub trait SpawnTopAppBarWithRightContentChild {
fn spawn_top_app_bar_with_right_content(
&mut self,
theme: &MaterialTheme,
builder: TopAppBarBuilder,
with_right_content: impl FnOnce(&mut ChildSpawnerCommands),
) -> Entity;
}
impl SpawnAppBarChild for ChildSpawnerCommands<'_> {
fn spawn_top_app_bar(
&mut self,
theme: &MaterialTheme,
title: impl Into<String>,
with_content: impl FnOnce(&mut ChildSpawnerCommands),
) {
self.spawn_top_app_bar_with(theme, TopAppBarBuilder::new(title), with_content);
}
fn spawn_top_app_bar_with(
&mut self,
theme: &MaterialTheme,
builder: TopAppBarBuilder,
with_content: impl FnOnce(&mut ChildSpawnerCommands),
) {
let title_text = builder.app_bar.title.clone();
let title_key = builder.title_key.clone();
let title_color = builder.app_bar.title_color(theme);
self.spawn(builder.build(theme)).with_children(|bar| {
if let Some(key) = title_key.as_deref() {
bar.spawn((
Text::new(""),
LocalizedText::new(key),
TextFont {
font_size: 22.0,
..default()
},
TextColor(title_color),
Node {
flex_grow: 1.0,
..default()
},
));
} else {
bar.spawn((
Text::new(&title_text),
TextFont {
font_size: 22.0,
..default()
},
TextColor(title_color),
Node {
flex_grow: 1.0,
..default()
},
));
}
with_content(bar);
});
}
fn spawn_bottom_app_bar(
&mut self,
theme: &MaterialTheme,
with_content: impl FnOnce(&mut ChildSpawnerCommands),
) {
self.spawn_bottom_app_bar_with(theme, BottomAppBarBuilder::new(), with_content);
}
fn spawn_bottom_app_bar_with(
&mut self,
theme: &MaterialTheme,
builder: BottomAppBarBuilder,
with_content: impl FnOnce(&mut ChildSpawnerCommands),
) {
self.spawn(builder.build(theme)).with_children(with_content);
}
}
impl SpawnTopAppBarWithRightContentChild for ChildSpawnerCommands<'_> {
fn spawn_top_app_bar_with_right_content(
&mut self,
theme: &MaterialTheme,
builder: TopAppBarBuilder,
with_right_content: impl FnOnce(&mut ChildSpawnerCommands),
) -> Entity {
let title = builder.app_bar.title.clone();
let title_key = builder.title_key.clone();
let title_color = builder.app_bar.title_color(theme);
let nav_icon = builder.app_bar.navigation_icon.clone();
let actions = builder.app_bar.actions.clone();
let variant = builder.app_bar.variant;
self.spawn(builder.build(theme))
.with_children(|parent| {
parent
.spawn(Node {
flex_direction: FlexDirection::Row,
align_items: AlignItems::Center,
column_gap: Val::Px(Spacing::EXTRA_SMALL),
..default()
})
.with_children(|left| {
if let Some(icon) = &nav_icon {
left.spawn((
AppBarNavigation,
Button,
Interaction::None,
RippleHost::new(),
Node {
width: Val::Px(48.0),
height: Val::Px(48.0),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
border_radius: BorderRadius::all(Val::Px(CornerRadius::FULL)),
..default()
},
BackgroundColor(Color::NONE),
))
.with_children(|btn| {
if let Some(icon) = MaterialIcon::from_name(icon) {
btn.spawn((
icon,
IconStyle::outlined()
.with_color(theme.on_surface)
.with_size(24.0),
));
}
});
}
if variant == TopAppBarVariant::Small {
if let Some(key) = title_key.as_deref() {
left.spawn((
AppBarTitle,
Text::new(""),
LocalizedText::new(key),
TextFont {
font_size: 22.0,
..default()
},
TextColor(title_color),
));
} else {
left.spawn((
AppBarTitle,
Text::new(&title),
TextFont {
font_size: 22.0,
..default()
},
TextColor(title_color),
));
}
}
});
if variant == TopAppBarVariant::CenterAligned {
if let Some(key) = title_key.as_deref() {
parent.spawn((
AppBarTitle,
Text::new(""),
LocalizedText::new(key),
TextFont {
font_size: 22.0,
..default()
},
TextColor(title_color),
Node {
flex_grow: 1.0,
justify_content: JustifyContent::Center,
..default()
},
));
} else {
parent.spawn((
AppBarTitle,
Text::new(&title),
TextFont {
font_size: 22.0,
..default()
},
TextColor(title_color),
Node {
flex_grow: 1.0,
justify_content: JustifyContent::Center,
..default()
},
));
}
}
parent
.spawn(Node {
flex_direction: FlexDirection::Row,
align_items: AlignItems::Center,
column_gap: Val::Px(Spacing::EXTRA_SMALL),
..default()
})
.with_children(|right| {
with_right_content(right);
for action in &actions {
right
.spawn((
AppBarActionButton {
id: action.id.clone(),
},
Button,
Interaction::None,
RippleHost::new(),
Node {
width: Val::Px(48.0),
height: Val::Px(48.0),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
border_radius: BorderRadius::all(Val::Px(CornerRadius::FULL)),
..default()
},
BackgroundColor(Color::NONE),
))
.with_children(|btn| {
if let Some(icon) = MaterialIcon::from_name(&action.icon) {
btn.spawn((
icon,
IconStyle::outlined()
.with_color(theme.on_surface_variant)
.with_size(24.0),
));
}
});
}
});
})
.id()
}
}
pub fn spawn_top_app_bar(
commands: &mut Commands,
theme: &MaterialTheme,
builder: TopAppBarBuilder,
) -> Entity {
let title = builder.app_bar.title.clone();
let title_color = builder.app_bar.title_color(theme);
let nav_icon = builder.app_bar.navigation_icon.clone();
let actions = builder.app_bar.actions.clone();
let variant = builder.app_bar.variant;
commands
.spawn(builder.build(theme))
.with_children(|parent| {
parent
.spawn(Node {
flex_direction: FlexDirection::Row,
align_items: AlignItems::Center,
column_gap: Val::Px(Spacing::EXTRA_SMALL),
..default()
})
.with_children(|left| {
if let Some(icon) = &nav_icon {
left.spawn((
AppBarNavigation,
Button,
RippleHost::new(),
Node {
width: Val::Px(48.0),
height: Val::Px(48.0),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
border_radius: BorderRadius::all(Val::Px(CornerRadius::FULL)),
..default()
},
BackgroundColor(Color::NONE),
))
.with_children(|btn| {
if let Some(icon) = MaterialIcon::from_name(icon) {
btn.spawn((
icon,
IconStyle::outlined()
.with_color(theme.on_surface)
.with_size(24.0),
));
}
});
}
if variant == TopAppBarVariant::Small {
left.spawn((
AppBarTitle,
Text::new(&title),
TextFont {
font_size: 22.0,
..default()
},
TextColor(title_color),
));
}
});
if variant == TopAppBarVariant::CenterAligned {
parent.spawn((
AppBarTitle,
Text::new(&title),
TextFont {
font_size: 22.0,
..default()
},
TextColor(title_color),
Node {
flex_grow: 1.0,
justify_content: JustifyContent::Center,
..default()
},
));
}
parent
.spawn(Node {
flex_direction: FlexDirection::Row,
align_items: AlignItems::Center,
column_gap: Val::Px(Spacing::EXTRA_SMALL),
..default()
})
.with_children(|right| {
for action in &actions {
right
.spawn((
AppBarActionButton {
id: action.id.clone(),
},
Button,
RippleHost::new(),
Node {
width: Val::Px(48.0),
height: Val::Px(48.0),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
border_radius: BorderRadius::all(Val::Px(CornerRadius::FULL)),
..default()
},
BackgroundColor(Color::NONE),
))
.with_children(|btn| {
if let Some(icon) = MaterialIcon::from_name(&action.icon) {
btn.spawn((
icon,
IconStyle::outlined()
.with_color(theme.on_surface_variant)
.with_size(24.0),
));
}
});
}
});
})
.id()
}
pub fn spawn_top_app_bar_with_right_content(
commands: &mut Commands,
theme: &MaterialTheme,
builder: TopAppBarBuilder,
with_right_content: impl FnOnce(&mut ChildSpawnerCommands),
) -> Entity {
let title = builder.app_bar.title.clone();
let title_color = builder.app_bar.title_color(theme);
let nav_icon = builder.app_bar.navigation_icon.clone();
let actions = builder.app_bar.actions.clone();
let variant = builder.app_bar.variant;
commands
.spawn(builder.build(theme))
.with_children(|parent| {
parent
.spawn(Node {
flex_direction: FlexDirection::Row,
align_items: AlignItems::Center,
column_gap: Val::Px(Spacing::EXTRA_SMALL),
..default()
})
.with_children(|left| {
if nav_icon.is_some() {
left.spawn((
AppBarNavigation,
Button,
Interaction::None,
RippleHost::new(),
GlobalZIndex(1002),
Node {
height: Val::Px(48.0),
flex_direction: FlexDirection::Row,
align_items: AlignItems::Center,
padding: UiRect::horizontal(Val::Px(8.0)),
column_gap: Val::Px(Spacing::EXTRA_SMALL),
border_radius: BorderRadius::all(Val::Px(CornerRadius::FULL)),
..default()
},
BackgroundColor(Color::NONE),
))
.with_children(|btn| {
if let Some(icon_name) = &nav_icon {
if let Some(icon) = MaterialIcon::from_name(icon_name) {
btn.spawn((
icon,
IconStyle::outlined()
.with_color(theme.on_surface)
.with_size(24.0),
));
}
}
if variant == TopAppBarVariant::Small {
btn.spawn((
AppBarTitle,
Text::new(&title),
TextFont {
font_size: 22.0,
..default()
},
TextColor(title_color),
));
}
});
} else if variant == TopAppBarVariant::Small {
left.spawn((
AppBarTitle,
Text::new(&title),
TextFont {
font_size: 22.0,
..default()
},
TextColor(title_color),
));
}
});
if variant == TopAppBarVariant::CenterAligned {
parent.spawn((
AppBarTitle,
Text::new(&title),
TextFont {
font_size: 22.0,
..default()
},
TextColor(title_color),
Node {
flex_grow: 1.0,
justify_content: JustifyContent::Center,
..default()
},
));
}
parent
.spawn(Node {
flex_direction: FlexDirection::Row,
align_items: AlignItems::Center,
column_gap: Val::Px(Spacing::EXTRA_SMALL),
..default()
})
.with_children(|right| {
with_right_content(right);
for action in &actions {
right
.spawn((
AppBarActionButton {
id: action.id.clone(),
},
Button,
Interaction::None,
RippleHost::new(),
GlobalZIndex(1002),
Node {
width: Val::Px(48.0),
height: Val::Px(48.0),
justify_content: JustifyContent::Center,
align_items: AlignItems::Center,
border_radius: BorderRadius::all(Val::Px(CornerRadius::FULL)),
..default()
},
BackgroundColor(Color::NONE),
))
.with_children(|btn| {
if let Some(icon) = MaterialIcon::from_name(&action.icon) {
btn.spawn((
icon,
IconStyle::outlined()
.with_color(theme.on_surface_variant)
.with_size(24.0),
));
}
});
}
});
})
.id()
}
fn top_app_bar_scroll_system(
mut _app_bars: Query<(&mut TopAppBar, &mut Node)>,
) {
}
fn app_bar_interaction_system(
theme: Res<MaterialTheme>,
nav_buttons: Query<(Entity, &Interaction), (Changed<Interaction>, With<AppBarNavigation>)>,
action_buttons: Query<(Entity, &Interaction, &AppBarActionButton), Changed<Interaction>>,
parents: Query<&ChildOf>,
app_bars: Query<Entity, With<TopAppBar>>,
mut bgs: Query<&mut BackgroundColor>,
mut nav_events: MessageWriter<AppBarNavigationEvent>,
mut action_events: MessageWriter<AppBarActionEvent>,
) {
let find_app_bar_ancestor = |mut cursor: Entity| {
for _ in 0..MAX_ANCESTOR_DEPTH {
if app_bars.get(cursor).is_ok() {
return Some(cursor);
}
if let Ok(parent) = parents.get(cursor) {
cursor = parent.get();
} else {
break;
}
}
None
};
for (entity, interaction) in nav_buttons.iter() {
if let Ok(mut bg) = bgs.get_mut(entity) {
*bg = match interaction {
Interaction::Hovered | Interaction::Pressed => {
BackgroundColor(theme.surface_container_highest)
}
Interaction::None => BackgroundColor(Color::NONE),
};
}
if *interaction == Interaction::Pressed {
if let Some(app_bar) = find_app_bar_ancestor(entity) {
nav_events.write(AppBarNavigationEvent { app_bar });
}
}
}
for (entity, interaction, action) in action_buttons.iter() {
if let Ok(mut bg) = bgs.get_mut(entity) {
*bg = match interaction {
Interaction::Hovered | Interaction::Pressed => {
BackgroundColor(theme.surface_container_highest)
}
Interaction::None => BackgroundColor(Color::NONE),
};
}
if *interaction == Interaction::Pressed {
if let Some(app_bar) = find_app_bar_ancestor(entity) {
action_events.write(AppBarActionEvent {
app_bar,
action: action.id.clone(),
});
}
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_top_app_bar_creation() {
let app_bar = TopAppBar::new("Title")
.with_back_button()
.add_action(AppBarAction::new("search", "search"));
assert_eq!(app_bar.title, "Title");
assert!(app_bar.navigation_icon.is_some());
assert_eq!(app_bar.actions.len(), 1);
}
#[test]
fn test_top_app_bar_variants() {
let small = TopAppBar::new("Small").with_variant(TopAppBarVariant::Small);
assert_eq!(small.height(), TOP_APP_BAR_HEIGHT_SMALL);
let medium = TopAppBar::new("Medium").with_variant(TopAppBarVariant::Medium);
assert_eq!(medium.height(), TOP_APP_BAR_HEIGHT_MEDIUM);
let large = TopAppBar::new("Large").with_variant(TopAppBarVariant::Large);
assert_eq!(large.height(), TOP_APP_BAR_HEIGHT_LARGE);
}
#[test]
fn test_bottom_app_bar_creation() {
let app_bar = BottomAppBar::new()
.add_action(AppBarAction::new("home", "home"))
.with_fab("add");
assert_eq!(app_bar.actions.len(), 1);
assert!(app_bar.has_fab);
assert_eq!(app_bar.fab_icon, Some("add".to_string()));
}
#[test]
fn test_app_bar_builder() {
let builder = TopAppBarBuilder::new("My App")
.small()
.with_back()
.add_action("⋮", "more");
assert_eq!(builder.app_bar.title, "My App");
assert_eq!(builder.app_bar.variant, TopAppBarVariant::Small);
}
}