use std::hash::Hash;
use egui::{
accesskit, emath, epaint::Shadow, Align, Area, Color32, Context, CornerRadius, Frame, Id, Key,
Layout, Margin, Order, Pos2, Rect, Response, Sense, Stroke, Ui, WidgetInfo, WidgetText,
WidgetType,
};
use crate::{theme::Theme, Button, ButtonSize};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum DrawerSide {
Left,
Right,
}
#[must_use = "Call `.show(ctx, |ui| { ... })` to render the drawer."]
pub struct Drawer<'a> {
id_salt: Id,
open: &'a mut bool,
side: DrawerSide,
width: f32,
title: Option<WidgetText>,
subtitle: Option<WidgetText>,
close_on_backdrop: bool,
close_on_escape: bool,
}
impl<'a> std::fmt::Debug for Drawer<'a> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Drawer")
.field("id_salt", &self.id_salt)
.field("open", &*self.open)
.field("side", &self.side)
.field("width", &self.width)
.field("title", &self.title.as_ref().map(|t| t.text()))
.field("subtitle", &self.subtitle.as_ref().map(|t| t.text()))
.field("close_on_backdrop", &self.close_on_backdrop)
.field("close_on_escape", &self.close_on_escape)
.finish()
}
}
impl<'a> Drawer<'a> {
pub fn new(id_salt: impl Hash, open: &'a mut bool) -> Self {
Self {
id_salt: Id::new(id_salt),
open,
side: DrawerSide::Right,
width: 420.0,
title: None,
subtitle: None,
close_on_backdrop: true,
close_on_escape: true,
}
}
#[inline]
pub fn side(mut self, side: DrawerSide) -> Self {
self.side = side;
self
}
#[inline]
pub fn width(mut self, width: f32) -> Self {
self.width = width.max(120.0);
self
}
pub fn title(mut self, title: impl Into<WidgetText>) -> Self {
self.title = Some(title.into());
self
}
pub fn subtitle(mut self, subtitle: impl Into<WidgetText>) -> Self {
self.subtitle = Some(subtitle.into());
self
}
#[inline]
pub fn close_on_backdrop(mut self, close: bool) -> Self {
self.close_on_backdrop = close;
self
}
#[inline]
pub fn close_on_escape(mut self, close: bool) -> Self {
self.close_on_escape = close;
self
}
pub fn show<R>(self, ctx: &Context, add_contents: impl FnOnce(&mut Ui) -> R) -> Option<R> {
let focus_storage = Id::new(("elegance_drawer_focus", self.id_salt));
let mut focus_state: DrawerFocusState =
ctx.data(|d| d.get_temp(focus_storage).unwrap_or_default());
let is_open = *self.open;
let was_open = focus_state.was_open;
let just_opened = is_open && !was_open;
let just_closed = !is_open && was_open;
let progress = ctx.animate_bool_with_time_and_easing(
Id::new(("elegance_drawer_progress", self.id_salt)),
is_open,
ANIMATION_DURATION,
emath::easing::cubic_in_out,
);
if just_opened {
focus_state.prev_focus = ctx.memory(|m| m.focused());
}
if just_closed {
if let Some(prev) = focus_state.prev_focus.take() {
ctx.memory_mut(|m| m.request_focus(prev));
}
}
focus_state.was_open = is_open;
ctx.data_mut(|d| d.insert_temp(focus_storage, focus_state));
if !is_open && progress < 0.001 {
return None;
}
let theme = Theme::current(ctx);
let p = &theme.palette;
let mut should_close = false;
let mut close_btn_id: Option<Id> = None;
let screen = ctx.content_rect();
let panel_w = self.width;
let slide = (1.0 - progress) * panel_w;
let panel_rect = match self.side {
DrawerSide::Right => Rect::from_min_max(
Pos2::new(screen.max.x - panel_w + slide, screen.min.y),
Pos2::new(screen.max.x + slide, screen.max.y),
),
DrawerSide::Left => Rect::from_min_max(
Pos2::new(screen.min.x - slide, screen.min.y),
Pos2::new(screen.min.x + panel_w - slide, screen.max.y),
),
};
let backdrop_id = Id::new("elegance_drawer_backdrop").with(self.id_salt);
let backdrop_alpha = (progress * 150.0).round() as u8;
let backdrop = Area::new(backdrop_id)
.fixed_pos(screen.min)
.order(Order::Middle)
.constrain(false)
.show(ctx, |ui| {
ui.painter().rect_filled(
screen,
CornerRadius::ZERO,
Color32::from_rgba_premultiplied(0, 0, 0, backdrop_alpha),
);
ui.allocate_rect(screen, Sense::click())
});
if self.close_on_backdrop && backdrop.inner.clicked() {
should_close = true;
}
let panel_id = Id::new("elegance_drawer_panel").with(self.id_salt);
let title_text = self.title.as_ref().map(|t| t.text().to_string());
let title = self.title;
let subtitle = self.subtitle;
let side = self.side;
let result = Area::new(panel_id)
.order(Order::Foreground)
.fixed_pos(panel_rect.min)
.constrain(false)
.show(ctx, |ui| {
ui.set_min_size(panel_rect.size());
ui.set_max_size(panel_rect.size());
ui.set_clip_rect(panel_rect);
ui.ctx().accesskit_node_builder(ui.unique_id(), |node| {
node.set_role(accesskit::Role::Dialog);
if let Some(label) = title_text {
node.set_label(label);
}
});
let shadow = Shadow {
offset: match side {
DrawerSide::Right => [-12, 0],
DrawerSide::Left => [12, 0],
},
blur: 28,
spread: 0,
color: Color32::from_black_alpha(110),
};
ui.painter()
.add(shadow.as_shape(panel_rect, CornerRadius::ZERO));
ui.painter()
.rect_filled(panel_rect, CornerRadius::ZERO, p.card);
let pad = theme.card_padding as i8;
let inner = Frame::new()
.inner_margin(Margin::same(pad))
.show(ui, |ui| {
if title.is_some() {
paint_header(
ui,
&theme,
title.as_ref(),
subtitle.as_ref(),
&mut should_close,
&mut close_btn_id,
);
ui.separator();
ui.add_space(8.0);
}
add_contents(ui)
})
.inner;
let inner_x = match side {
DrawerSide::Right => panel_rect.left(),
DrawerSide::Left => panel_rect.right(),
};
ui.painter().line_segment(
[
Pos2::new(inner_x, panel_rect.top()),
Pos2::new(inner_x, panel_rect.bottom()),
],
Stroke::new(1.0, p.border),
);
inner
});
if self.close_on_escape && ctx.input(|i| i.key_pressed(Key::Escape)) {
should_close = true;
}
if just_opened {
if let Some(id) = close_btn_id {
ctx.memory_mut(|m| m.request_focus(id));
}
}
if should_close {
*self.open = false;
}
Some(result.inner)
}
}
const ANIMATION_DURATION: f32 = 0.26;
#[derive(Clone, Copy, Default, Debug)]
struct DrawerFocusState {
was_open: bool,
prev_focus: Option<Id>,
}
fn paint_header(
ui: &mut Ui,
theme: &Theme,
title: Option<&WidgetText>,
subtitle: Option<&WidgetText>,
should_close: &mut bool,
close_btn_id: &mut Option<Id>,
) {
ui.horizontal(|ui| {
ui.vertical(|ui| {
if let Some(t) = title {
ui.add(egui::Label::new(theme.heading_text(t.text())));
}
if let Some(s) = subtitle {
ui.add(egui::Label::new(theme.muted_text(s.text())));
}
});
ui.with_layout(Layout::right_to_left(Align::Min), |ui| {
let resp = drawer_close_button(ui);
if resp.clicked() {
*should_close = true;
}
*close_btn_id = Some(resp.id);
});
});
}
fn drawer_close_button(ui: &mut Ui) -> Response {
let inner = ui
.push_id("elegance_drawer_close", |ui| {
ui.add(Button::new("×").outline().size(ButtonSize::Small))
})
.inner;
let enabled = inner.enabled();
inner.widget_info(|| WidgetInfo::labeled(WidgetType::Button, enabled, "Close"));
inner
}