use crate::material_symbol::material_symbol_text;
use crate::theme::get_global_color;
use egui::{
ecolor::Color32,
epaint::{CornerRadius, Shadow},
Rect, Response, Sense, Ui, Vec2, Widget,
};
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum TopAppBarVariant {
Regular,
Medium,
Large,
CenterAligned,
}
#[must_use = "You should put this widget in a ui with `ui.add(widget);`"]
pub struct MaterialTopAppBar<'a> {
variant: TopAppBarVariant,
title: String,
navigation_icon: Option<(String, Box<dyn Fn() + Send + Sync + 'a>)>,
action_icons: Vec<(String, Box<dyn Fn() + Send + Sync + 'a>)>,
height: f32,
corner_radius: CornerRadius,
elevation: Option<Shadow>,
scrolled: bool,
id_salt: Option<String>,
background_color: Option<Color32>,
foreground_color: Option<Color32>,
title_spacing: f32,
leading_width: f32,
scrolled_under_elevation: f32,
surface_tint_color: Option<Color32>,
}
impl<'a> MaterialTopAppBar<'a> {
pub fn regular(title: impl Into<String>) -> Self {
Self::new(TopAppBarVariant::Regular, title)
}
pub fn medium(title: impl Into<String>) -> Self {
Self::new(TopAppBarVariant::Medium, title)
}
pub fn large(title: impl Into<String>) -> Self {
Self::new(TopAppBarVariant::Large, title)
}
pub fn center_aligned(title: impl Into<String>) -> Self {
Self::new(TopAppBarVariant::CenterAligned, title)
}
fn new(variant: TopAppBarVariant, title: impl Into<String>) -> Self {
let height = match variant {
TopAppBarVariant::Regular | TopAppBarVariant::CenterAligned => 64.0,
TopAppBarVariant::Medium => 112.0,
TopAppBarVariant::Large => 152.0,
};
Self {
variant,
title: title.into(),
navigation_icon: None,
action_icons: Vec::new(),
height,
corner_radius: CornerRadius::ZERO,
elevation: None,
scrolled: false,
id_salt: None,
background_color: None,
foreground_color: None,
title_spacing: 16.0,
leading_width: 56.0,
scrolled_under_elevation: 3.0,
surface_tint_color: None,
}
}
pub fn navigation_icon<F>(mut self, icon: impl Into<String>, callback: F) -> Self
where
F: Fn() + Send + Sync + 'a,
{
self.navigation_icon = Some((icon.into(), Box::new(callback)));
self
}
pub fn navigation_icon_char<F>(mut self, icon: char, callback: F) -> Self
where
F: Fn() + Send + Sync + 'a,
{
self.navigation_icon = Some((icon.to_string(), Box::new(callback)));
self
}
pub fn action_icon<F>(mut self, icon: impl Into<String>, callback: F) -> Self
where
F: Fn() + Send + Sync + 'a,
{
self.action_icons.push((icon.into(), Box::new(callback)));
self
}
pub fn action_icon_char<F>(mut self, icon: char, callback: F) -> Self
where
F: Fn() + Send + Sync + 'a,
{
self.action_icons.push((icon.to_string(), Box::new(callback)));
self
}
pub fn height(mut self, height: f32) -> Self {
self.height = height;
self
}
pub fn corner_radius(mut self, corner_radius: impl Into<CornerRadius>) -> Self {
self.corner_radius = corner_radius.into();
self
}
pub fn elevation(mut self, elevation: impl Into<Shadow>) -> Self {
self.elevation = Some(elevation.into());
self
}
pub fn scrolled(mut self, scrolled: bool) -> Self {
self.scrolled = scrolled;
self
}
pub fn id_salt(mut self, salt: impl Into<String>) -> Self {
self.id_salt = Some(salt.into());
self
}
pub fn background_color(mut self, color: Color32) -> Self {
self.background_color = Some(color);
self
}
pub fn foreground_color(mut self, color: Color32) -> Self {
self.foreground_color = Some(color);
self
}
pub fn title_spacing(mut self, spacing: f32) -> Self {
self.title_spacing = spacing;
self
}
pub fn leading_width(mut self, width: f32) -> Self {
self.leading_width = width;
self
}
pub fn scrolled_under_elevation(mut self, elevation: f32) -> Self {
self.scrolled_under_elevation = elevation;
self
}
pub fn surface_tint_color(mut self, color: Color32) -> Self {
self.surface_tint_color = Some(color);
self
}
fn get_background_color(&self) -> Color32 {
if let Some(color) = self.background_color {
return color;
}
if self.scrolled {
get_global_color("surfaceContainer")
} else {
get_global_color("surface")
}
}
fn get_foreground_color(&self) -> Color32 {
self.foreground_color
.unwrap_or_else(|| get_global_color("onSurface"))
}
fn get_leading_icon_color(&self) -> Color32 {
self.foreground_color
.unwrap_or_else(|| get_global_color("onSurface"))
}
fn get_action_icon_color(&self) -> Color32 {
get_global_color("onSurfaceVariant")
}
}
impl Widget for MaterialTopAppBar<'_> {
fn ui(self, ui: &mut Ui) -> Response {
let background_color = self.get_background_color();
let text_color = self.get_foreground_color();
let leading_icon_color = self.get_leading_icon_color();
let action_icon_color = self.get_action_icon_color();
let MaterialTopAppBar {
variant,
title,
navigation_icon,
action_icons,
height,
corner_radius,
elevation,
scrolled,
id_salt,
background_color: _,
foreground_color: _,
title_spacing,
leading_width,
scrolled_under_elevation,
surface_tint_color: _,
} = self;
let desired_size = Vec2::new(ui.available_width(), height);
let mut response = ui.allocate_response(desired_size, Sense::hover());
let rect = response.rect;
if ui.is_rect_visible(rect) {
if scrolled {
if let Some(_shadow) = elevation {
let shadow_rect = rect.translate(Vec2::new(0.0, 1.0));
ui.painter().rect_filled(
shadow_rect,
corner_radius,
Color32::from_rgba_unmultiplied(0, 0, 0, (scrolled_under_elevation * 7.0) as u8),
);
}
}
ui.painter()
.rect_filled(rect, corner_radius, background_color);
let icon_size = 24.0;
let icon_padding = 12.0;
let icon_total_size = icon_size + icon_padding * 2.0;
let mut left_x = rect.min.x + 4.0;
let toolbar_height = 64.0_f32;
let icon_y = rect.min.y + (toolbar_height - icon_total_size) / 2.0;
if let Some((nav_icon, nav_callback)) = navigation_icon {
let nav_rect =
Rect::from_min_size(egui::pos2(left_x, icon_y), Vec2::splat(icon_total_size));
let nav_id = if let Some(ref salt) = id_salt {
egui::Id::new((salt, "nav_icon"))
} else {
egui::Id::new(("top_app_bar_nav", &title))
};
let nav_response = ui.interact(nav_rect, nav_id, Sense::click());
if nav_response.hovered() {
let hover_color = Color32::from_rgba_unmultiplied(
leading_icon_color.r(),
leading_icon_color.g(),
leading_icon_color.b(),
20,
);
ui.painter()
.rect_filled(nav_rect, CornerRadius::from(20.0), hover_color);
}
let nav_icon_text = if nav_icon.chars().count() == 1 {
let ch = nav_icon.chars().next().unwrap();
if ('\u{e000}'..='\u{f8ff}').contains(&ch) || ('\u{ea00}'..='\u{eb8d}').contains(&ch) {
nav_icon.clone()
} else {
material_symbol_text(&nav_icon)
}
} else {
material_symbol_text(&nav_icon)
};
ui.painter().text(
nav_rect.center(),
egui::Align2::CENTER_CENTER,
&nav_icon_text,
egui::FontId::proportional(icon_size),
leading_icon_color,
);
if nav_response.clicked() {
nav_callback();
}
left_x += leading_width.max(icon_total_size);
response = response.union(nav_response);
}
let title_font_size = match variant {
TopAppBarVariant::Regular | TopAppBarVariant::CenterAligned => 22.0,
TopAppBarVariant::Medium => 24.0,
TopAppBarVariant::Large => 28.0,
};
let title_y = match variant {
TopAppBarVariant::Regular | TopAppBarVariant::CenterAligned => {
rect.min.y + (toolbar_height - title_font_size) / 2.0
}
TopAppBarVariant::Medium => rect.min.y + height - 20.0 - title_font_size,
TopAppBarVariant::Large => rect.min.y + height - 28.0 - title_font_size,
};
let title_x = match variant {
TopAppBarVariant::CenterAligned => {
let title_galley = ui.painter().layout_no_wrap(
title.clone(),
egui::FontId::proportional(title_font_size),
text_color,
);
rect.center().x - title_galley.size().x / 2.0
}
TopAppBarVariant::Medium | TopAppBarVariant::Large => {
rect.min.x + title_spacing
}
_ => left_x + title_spacing,
};
ui.painter().text(
egui::pos2(title_x, title_y),
egui::Align2::LEFT_TOP,
&title,
egui::FontId::proportional(title_font_size),
text_color,
);
let mut right_x = rect.max.x - 4.0;
for (action_index, (action_icon, action_callback)) in
action_icons.iter().enumerate().rev()
{
right_x -= icon_total_size;
let action_rect =
Rect::from_min_size(egui::pos2(right_x, icon_y), Vec2::splat(icon_total_size));
let action_id = if let Some(ref salt) = id_salt {
egui::Id::new((salt, "action_icon", action_index))
} else {
egui::Id::new(("top_app_bar_action", &title, action_index))
};
let action_response = ui.interact(action_rect, action_id, Sense::click());
if action_response.hovered() {
let hover_color = Color32::from_rgba_unmultiplied(
action_icon_color.r(),
action_icon_color.g(),
action_icon_color.b(),
20,
);
ui.painter()
.rect_filled(action_rect, CornerRadius::from(20.0), hover_color);
}
let action_icon_text = if action_icon.chars().count() == 1 {
let ch = action_icon.chars().next().unwrap();
if ('\u{e000}'..='\u{f8ff}').contains(&ch) || ('\u{ea00}'..='\u{eb8d}').contains(&ch) {
action_icon.clone()
} else {
material_symbol_text(action_icon.as_str())
}
} else {
material_symbol_text(action_icon.as_str())
};
ui.painter().text(
action_rect.center(),
egui::Align2::CENTER_CENTER,
&action_icon_text,
egui::FontId::proportional(icon_size),
action_icon_color,
);
if action_response.clicked() {
action_callback();
}
response = response.union(action_response);
}
}
response
}
}
pub fn top_app_bar(title: impl Into<String>) -> MaterialTopAppBar<'static> {
MaterialTopAppBar::regular(title)
}
pub fn center_aligned_top_app_bar(title: impl Into<String>) -> MaterialTopAppBar<'static> {
MaterialTopAppBar::center_aligned(title)
}
pub fn medium_top_app_bar(title: impl Into<String>) -> MaterialTopAppBar<'static> {
MaterialTopAppBar::medium(title)
}
pub fn large_top_app_bar(title: impl Into<String>) -> MaterialTopAppBar<'static> {
MaterialTopAppBar::large(title)
}