use std::sync::Arc;
use derive_setters::Setters;
use tessera_ui::{Color, Dp, Modifier, provide_context, tessera, use_context};
use crate::{
alignment::{Alignment, CrossAxisAlignment, MainAxisAlignment},
modifier::{ModifierExt as _, Padding},
row::{RowArgs, RowScope, row},
spacer::spacer,
surface::{SurfaceArgs, surface},
text::{TextArgs, text},
theme::{ContentColor, MaterialColorScheme, MaterialTheme, provide_text_style},
};
pub struct AppBarDefaults;
impl AppBarDefaults {
pub const TOP_APP_BAR_HEIGHT: Dp = Dp(64.0);
pub const TOP_APP_BAR_ELEVATION: Dp = Dp(0.0);
pub const HORIZONTAL_PADDING: Dp = Dp(4.0);
pub const TITLE_INSET: Dp = Dp(16.0);
pub const ACTIONS_SPACING: Dp = Dp(0.0);
pub const CONTENT_PADDING: Padding = Padding::symmetric(Self::HORIZONTAL_PADDING, Dp(0.0));
pub fn container_color(scheme: &MaterialColorScheme) -> Color {
scheme.surface
}
pub fn title_color(scheme: &MaterialColorScheme) -> Color {
scheme.on_surface
}
pub fn navigation_icon_color(scheme: &MaterialColorScheme) -> Color {
scheme.on_surface
}
pub fn action_icon_color(scheme: &MaterialColorScheme) -> Color {
scheme.on_surface_variant
}
}
#[derive(Clone, Setters)]
pub struct AppBarArgs {
pub modifier: Modifier,
pub container_color: Color,
pub content_color: Color,
pub elevation: Dp,
pub content_padding: Padding,
pub main_axis_alignment: MainAxisAlignment,
pub cross_axis_alignment: CrossAxisAlignment,
}
impl Default for AppBarArgs {
fn default() -> Self {
let scheme = use_context::<MaterialTheme>()
.expect("MaterialTheme must be provided")
.get()
.color_scheme;
Self {
modifier: Modifier::new()
.fill_max_width()
.height(AppBarDefaults::TOP_APP_BAR_HEIGHT),
container_color: AppBarDefaults::container_color(&scheme),
content_color: AppBarDefaults::title_color(&scheme),
elevation: AppBarDefaults::TOP_APP_BAR_ELEVATION,
content_padding: AppBarDefaults::CONTENT_PADDING,
main_axis_alignment: MainAxisAlignment::Start,
cross_axis_alignment: CrossAxisAlignment::Center,
}
}
}
#[tessera]
pub fn app_bar<F>(args: impl Into<AppBarArgs>, content: F)
where
F: FnOnce(&mut RowScope) + Send + Sync + 'static,
{
let args: AppBarArgs = args.into();
surface(
SurfaceArgs::default()
.style(args.container_color.into())
.content_color(args.content_color)
.content_alignment(Alignment::CenterStart)
.elevation(args.elevation)
.tonal_elevation(args.elevation)
.modifier(args.modifier),
move || {
row(
RowArgs::default()
.modifier(
Modifier::new()
.fill_max_size()
.padding(args.content_padding),
)
.main_axis_alignment(args.main_axis_alignment)
.cross_axis_alignment(args.cross_axis_alignment),
content,
);
},
);
}
#[derive(Clone, Setters)]
pub struct TopAppBarArgs {
pub app_bar: AppBarArgs,
#[setters(into)]
pub title: String,
pub title_modifier: Modifier,
#[setters(skip)]
pub navigation_icon: Option<Arc<dyn Fn() + Send + Sync>>,
#[setters(skip)]
pub actions: Vec<Arc<dyn Fn() + Send + Sync>>,
pub navigation_icon_color: Color,
pub action_icon_color: Color,
pub title_inset: Dp,
pub actions_spacing: Dp,
}
impl Default for TopAppBarArgs {
fn default() -> Self {
let scheme = use_context::<MaterialTheme>()
.expect("MaterialTheme must be provided")
.get()
.color_scheme;
Self {
app_bar: AppBarArgs::default(),
title: String::new(),
title_modifier: Modifier::new(),
navigation_icon: None,
actions: Vec::new(),
navigation_icon_color: AppBarDefaults::navigation_icon_color(&scheme),
action_icon_color: AppBarDefaults::action_icon_color(&scheme),
title_inset: AppBarDefaults::TITLE_INSET,
actions_spacing: AppBarDefaults::ACTIONS_SPACING,
}
}
}
impl TopAppBarArgs {
pub fn new(title: impl Into<String>) -> Self {
Self {
title: title.into(),
..Self::default()
}
}
pub fn navigation_icon<F>(mut self, navigation_icon: F) -> Self
where
F: Fn() + Send + Sync + 'static,
{
self.navigation_icon = Some(Arc::new(navigation_icon));
self
}
pub fn navigation_icon_shared(mut self, navigation_icon: Arc<dyn Fn() + Send + Sync>) -> Self {
self.navigation_icon = Some(navigation_icon);
self
}
pub fn action<F>(mut self, action: F) -> Self
where
F: Fn() + Send + Sync + 'static,
{
self.actions.push(Arc::new(action));
self
}
pub fn action_shared(mut self, action: Arc<dyn Fn() + Send + Sync>) -> Self {
self.actions.push(action);
self
}
pub fn actions_shared(mut self, actions: Vec<Arc<dyn Fn() + Send + Sync>>) -> Self {
self.actions = actions;
self
}
}
#[tessera]
pub fn top_app_bar(args: impl Into<TopAppBarArgs>) {
let args: TopAppBarArgs = args.into();
let typography = use_context::<MaterialTheme>()
.expect("MaterialTheme must be provided")
.get()
.typography;
let TopAppBarArgs {
app_bar: app_bar_args,
title,
title_modifier,
navigation_icon,
actions,
navigation_icon_color,
action_icon_color,
title_inset,
actions_spacing,
} = args;
let start_padding = app_bar_args.content_padding.left;
let extra_inset = Dp((title_inset.0 - start_padding.0).max(0.0));
let title_style = typography.title_large;
app_bar(app_bar_args, move |scope| {
if let Some(navigation_icon) = navigation_icon {
let nav_color = navigation_icon_color;
scope.child(move || {
provide_context(
|| ContentColor { current: nav_color },
|| {
navigation_icon();
},
);
});
} else if extra_inset.0 > 0.0 {
let spacer_width = extra_inset;
scope.child(move || {
spacer(Modifier::new().width(spacer_width));
});
}
let title_text = title;
scope.child_weighted(
move || {
if title_text.is_empty() {
spacer(Modifier::new().fill_max_width());
} else {
provide_text_style(title_style, move || {
text(
TextArgs::default()
.text(title_text)
.modifier(title_modifier),
);
});
}
},
1.0,
);
if !actions.is_empty() {
let actions_len = actions.len();
let spacing = actions_spacing;
let action_color = action_icon_color;
scope.child(move || {
provide_context(
|| ContentColor {
current: action_color,
},
|| {
row(
RowArgs::default().cross_axis_alignment(CrossAxisAlignment::Center),
move |row_scope| {
for (index, action) in actions.into_iter().enumerate() {
row_scope.child(move || {
action();
});
if spacing.0 > 0.0 && index + 1 < actions_len {
let spacer_width = spacing;
row_scope.child(move || {
spacer(Modifier::new().width(spacer_width));
});
}
}
},
);
},
);
});
}
});
}