use std::hash::Hash;
use egui::{
emath::RectAlign, Color32, CornerRadius, Frame, Id, InnerResponse, Margin, Pos2, Rect,
Response, Shape, Stroke, Ui, Vec2, WidgetText,
};
use crate::theme::Theme;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PopoverSide {
Top,
Bottom,
Left,
Right,
}
impl PopoverSide {
fn to_rect_align(self) -> RectAlign {
match self {
PopoverSide::Top => RectAlign::TOP,
PopoverSide::Bottom => RectAlign::BOTTOM,
PopoverSide::Left => RectAlign::LEFT,
PopoverSide::Right => RectAlign::RIGHT,
}
}
}
#[derive(Debug, Clone)]
#[must_use = "Call `.show(&trigger, |ui| ...)` to render the popover."]
pub struct Popover {
id_salt: Id,
side: PopoverSide,
title: Option<WidgetText>,
width: Option<f32>,
min_width: f32,
gap: f32,
arrow: bool,
}
impl Popover {
pub fn new(id_salt: impl Hash) -> Self {
Self {
id_salt: Self::popup_id(id_salt),
side: PopoverSide::Bottom,
title: None,
width: None,
min_width: 200.0,
gap: 8.0,
arrow: true,
}
}
pub fn popup_id(id_salt: impl Hash) -> Id {
Id::new(("elegance::popover", Id::new(id_salt)))
}
#[inline]
pub fn side(mut self, side: PopoverSide) -> Self {
self.side = side;
self
}
#[inline]
pub fn title(mut self, title: impl Into<WidgetText>) -> Self {
self.title = Some(title.into());
self
}
#[inline]
pub fn width(mut self, width: f32) -> Self {
self.width = Some(width);
self
}
#[inline]
pub fn min_width(mut self, min_width: f32) -> Self {
self.min_width = min_width;
self
}
#[inline]
pub fn gap(mut self, gap: f32) -> Self {
self.gap = gap;
self
}
#[inline]
pub fn arrow(mut self, arrow: bool) -> Self {
self.arrow = arrow;
self
}
pub fn show<R>(
self,
trigger: &Response,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> Option<InnerResponse<R>> {
let theme = Theme::current(&trigger.ctx);
let p = &theme.palette;
let popup_id = self.id_salt;
let side = self.side;
let title = self.title;
let arrow = self.arrow;
let width = self.width;
let min_width = self.min_width;
let frame = Frame::new()
.fill(p.card)
.stroke(Stroke::new(1.0, p.border))
.corner_radius(CornerRadius::same(theme.card_radius as u8))
.inner_margin(Margin::same(12));
let mut popup = egui::Popup::from_toggle_button_response(trigger)
.id(popup_id)
.align(side.to_rect_align())
.align_alternatives(&[])
.close_behavior(egui::PopupCloseBehavior::CloseOnClickOutside)
.gap(self.gap)
.frame(frame);
if let Some(w) = width {
popup = popup.width(w);
}
let trigger_rect = trigger.rect;
let trigger_ctx = trigger.ctx.clone();
let response = popup.show(move |ui| {
ui.set_min_width(min_width);
if let Some(h) = &title {
let t = Theme::current(ui.ctx());
let rt = egui::RichText::new(h.text())
.color(t.palette.text)
.size(t.typography.body)
.strong();
ui.add(egui::Label::new(rt).wrap_mode(egui::TextWrapMode::Extend));
ui.add_space(4.0);
}
add_contents(ui)
});
let inner = response?;
if arrow {
let actual_side = detect_side(trigger_rect, inner.response.rect, side);
paint_arrow(
&trigger_ctx,
inner.response.layer_id,
inner.response.rect,
trigger_rect,
actual_side,
p.card,
p.border,
);
}
Some(inner)
}
}
fn detect_side(trigger: Rect, popup: Rect, requested: PopoverSide) -> PopoverSide {
match requested {
PopoverSide::Top | PopoverSide::Bottom => {
if popup.center().y < trigger.center().y {
PopoverSide::Top
} else {
PopoverSide::Bottom
}
}
PopoverSide::Left | PopoverSide::Right => {
if popup.center().x < trigger.center().x {
PopoverSide::Left
} else {
PopoverSide::Right
}
}
}
}
fn paint_arrow(
ctx: &egui::Context,
layer: egui::LayerId,
popup: Rect,
trigger: Rect,
side: PopoverSide,
fill: Color32,
border: Color32,
) {
let painter = ctx.layer_painter(layer);
let half_base = 6.0;
let depth = 6.0;
let inset = 10.0;
let (base_center, perp, base_axis) = match side {
PopoverSide::Bottom => {
let cx = trigger
.center()
.x
.clamp(popup.min.x + inset, popup.max.x - inset);
(
Pos2::new(cx, popup.min.y),
Vec2::new(0.0, -1.0),
Vec2::new(1.0, 0.0),
)
}
PopoverSide::Top => {
let cx = trigger
.center()
.x
.clamp(popup.min.x + inset, popup.max.x - inset);
(
Pos2::new(cx, popup.max.y),
Vec2::new(0.0, 1.0),
Vec2::new(1.0, 0.0),
)
}
PopoverSide::Right => {
let cy = trigger
.center()
.y
.clamp(popup.min.y + inset, popup.max.y - inset);
(
Pos2::new(popup.min.x, cy),
Vec2::new(-1.0, 0.0),
Vec2::new(0.0, 1.0),
)
}
PopoverSide::Left => {
let cy = trigger
.center()
.y
.clamp(popup.min.y + inset, popup.max.y - inset);
(
Pos2::new(popup.max.x, cy),
Vec2::new(1.0, 0.0),
Vec2::new(0.0, 1.0),
)
}
};
let base_a = base_center + base_axis * half_base;
let base_b = base_center - base_axis * half_base;
let tip = base_center + perp * depth;
painter.add(Shape::convex_polygon(
vec![base_a, tip, base_b],
fill,
Stroke::NONE,
));
painter.line_segment([base_a, base_b], Stroke::new(1.5, fill));
let stroke = Stroke::new(1.0, border);
painter.line_segment([base_a, tip], stroke);
painter.line_segment([base_b, tip], stroke);
}