use egui::{
Align, Area, Color32, Context, CornerRadius, Frame, Id, Layout, Margin, Pos2, Rect, Sense,
Stroke, Ui, UiBuilder, WidgetText, accesskit, emath, epaint::Shadow,
};
use crate::{overlay, theme::Theme};
#[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 crate::IdSalt, open: &'a mut bool) -> Self {
Self {
id_salt: Id::unique(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::unique(("elegance_drawer_focus", self.id_salt));
let mut focus_state = overlay::FocusState::load(ctx, focus_storage);
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::unique(("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 && let Some(prev) = focus_state.prev_focus.take() {
ctx.memory_mut(|m| m.request_focus(prev));
focus_state.pending_restore = Some(prev);
} else if let Some(prev) = focus_state.pending_restore.take() {
ctx.memory_mut(|m| m.request_focus(prev));
}
focus_state.was_open = is_open;
focus_state.store(ctx, focus_storage);
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 popup_was_open = egui::Popup::is_any_open(ctx);
let area_id = Id::unique("elegance_drawer").with(self.id_salt);
let backdrop_alpha = (progress * 150.0).round() as u8;
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 outer = Area::new(area_id)
.order(overlay::ORDER)
.fixed_pos(screen.min)
.constrain(false)
.show(ctx, |ui| {
ui.painter().rect_filled(
screen,
CornerRadius::ZERO,
Color32::from_rgba_premultiplied(0, 0, 0, backdrop_alpha),
);
let backdrop = ui.interact(screen, ui.scope_id().with("backdrop"), Sense::click());
let panel = ui.scope_builder(
UiBuilder::new().sense(Sense::click()).max_rect(panel_rect),
|ui| {
ui.set_min_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
},
);
(panel.inner, backdrop)
});
let (result, backdrop) = outer.inner;
if self.close_on_backdrop && backdrop.clicked() {
should_close = true;
}
let layer = overlay::layer_id(area_id);
let is_topmost = is_open && overlay::register_open(ctx, layer);
if self.close_on_escape && overlay::escape_dismisses(ctx, is_topmost, popup_was_open) {
should_close = true;
}
if is_open && !should_close {
overlay::claim_modal_layer(ctx, layer);
}
if just_opened && let Some(id) = close_btn_id {
ctx.memory_mut(|m| m.request_focus(id));
}
if should_close {
*self.open = false;
}
Some(result)
}
}
const ANIMATION_DURATION: f32 = 0.26;
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 = overlay::close_button(ui, "elegance_drawer_close");
if resp.clicked() {
*should_close = true;
}
*close_btn_id = Some(resp.id);
});
});
}