use crate::controls::MacControlSize;
use crate::motion::CASpring;
use crate::theme::MacTheme;
use egui::{
CornerRadius, Id, InnerResponse, Pos2, Rect, Response, Sense, Shape, Stroke, StrokeKind, Ui,
UiBuilder, Vec2, Widget, WidgetText,
};
use std::hash::Hash;
pub struct MacDisclosureGroup<'a> {
id_salt: Id,
title: WidgetText,
is_expanded: &'a mut bool,
theme: Option<&'a MacTheme>,
}
impl<'a> MacDisclosureGroup<'a> {
pub fn new(
id_salt: impl Hash + std::fmt::Debug,
title: impl Into<WidgetText>,
is_expanded: &'a mut bool,
) -> Self {
Self {
id_salt: Id::NULL.with(id_salt),
title: title.into(),
is_expanded,
theme: None,
}
}
pub fn theme(mut self, theme: &'a MacTheme) -> Self {
self.theme = Some(theme);
self
}
pub fn chevron_size(&self) -> Vec2 {
let theme_cow = MacTheme::resolve(self.theme, &egui::Context::default());
let metrics = theme_cow.disclosure_group();
let sz = metrics.chevron_size.points();
Vec2::new(sz, sz)
}
pub fn label_gap(&self) -> f32 {
let theme_cow = MacTheme::resolve(self.theme, &egui::Context::default());
theme_cow.disclosure_group().label_gap.points()
}
pub fn rotation_angle(expanded: bool) -> f32 {
if expanded {
std::f32::consts::FRAC_PI_2
} else {
0.0
}
}
pub fn show<R>(
self,
ui: &mut Ui,
add_contents: impl FnOnce(&mut Ui) -> R,
) -> InnerResponse<Option<R>> {
let theme_cow = MacTheme::resolve(self.theme, ui.ctx());
let theme: &MacTheme = &theme_cow;
let metrics = theme.disclosure_group();
let chevron_sz = metrics.chevron_size.points();
let gap = metrics.label_gap.points();
let spring = CASpring::from_physical(
metrics.spring_mass.points(),
metrics.spring_stiffness.points(),
metrics.spring_damping.points(),
);
let spring_key = self.id_salt.with("chevron_rot");
let target_angle = Self::rotation_angle(*self.is_expanded);
let anim_angle = spring.animate_f32(ui.ctx(), spring_key, target_angle);
let galley = self.title.into_galley(
ui,
Some(egui::TextWrapMode::Extend),
f32::INFINITY,
egui::TextStyle::Button,
);
let header_w = chevron_sz + gap + galley.size().x;
let header_h = chevron_sz.max(galley.size().y);
let header_desired = Vec2::new(header_w, header_h);
let (header_rect, response) = ui.allocate_exact_size(header_desired, Sense::click());
if response.clicked() {
*self.is_expanded = !*self.is_expanded;
}
if ui.is_rect_visible(header_rect) {
let painter = ui.painter_at(header_rect);
let chevron_center = Pos2::new(
header_rect.min.x + chevron_sz * 0.5,
header_rect.min.y + header_h * 0.5,
);
let cos = anim_angle.cos();
let sin = anim_angle.sin();
let half_w = chevron_sz * 0.5;
let half_h = chevron_sz * 0.5;
let rotate = |ox: f32, oy: f32| -> Pos2 {
let rx = ox * cos - oy * sin;
let ry = ox * sin + oy * cos;
Pos2::new(chevron_center.x + rx, chevron_center.y + ry)
};
let p1 = rotate(-half_w * 0.5, -half_h);
let p2 = rotate(-half_w * 0.5, half_h);
let p3 = rotate(half_w, 0.0);
let glyph_color = if response.hovered() {
theme.label()
} else {
theme.secondary_label()
};
painter.add(Shape::convex_polygon(
vec![p1, p2, p3],
glyph_color,
Stroke::NONE,
));
let label_pos = Pos2::new(
header_rect.min.x + chevron_sz + gap,
header_rect.min.y + (header_h - galley.size().y) * 0.5,
);
painter.galley(label_pos, galley, theme.label());
}
let inner = if *self.is_expanded {
let indent = chevron_sz + gap;
let content_rect = Rect::from_min_max(
Pos2::new(ui.min_rect().min.x + indent, ui.cursor().min.y),
Pos2::new(ui.max_rect().max.x, ui.max_rect().max.y),
);
let mut child_ui = ui.new_child(
UiBuilder::new()
.id_salt(self.id_salt.with("content"))
.max_rect(content_rect),
);
let result = add_contents(&mut child_ui);
ui.advance_cursor_after_rect(child_ui.min_rect());
Some(result)
} else {
None
};
InnerResponse::new(inner, response)
}
}
pub struct MacDisclosureButton<'a> {
is_expanded: &'a mut bool,
size: MacControlSize,
theme: Option<&'a MacTheme>,
}
impl<'a> MacDisclosureButton<'a> {
pub fn new(is_expanded: &'a mut bool) -> Self {
Self {
is_expanded,
size: MacControlSize::Regular,
theme: None,
}
}
pub fn control_size(mut self, size: MacControlSize) -> Self {
self.size = size;
self
}
pub fn theme(mut self, theme: &'a MacTheme) -> Self {
self.theme = Some(theme);
self
}
pub fn frame_size(&self) -> Vec2 {
let theme_cow = MacTheme::resolve(self.theme, &egui::Context::default());
let m = theme_cow.disclosure_group();
match self.size {
MacControlSize::Regular | MacControlSize::Large => {
Vec2::new(m.button_regular_width.points(), m.button_regular_height.points())
}
MacControlSize::Small | MacControlSize::CompactInfobox => {
Vec2::new(m.button_small_width.points(), m.button_small_height.points())
}
MacControlSize::Mini => {
Vec2::new(m.button_mini_width.points(), m.button_mini_height.points())
}
}
}
pub fn intrinsic_size(&self) -> Vec2 {
let theme_cow = MacTheme::resolve(self.theme, &egui::Context::default());
let m = theme_cow.disclosure_group();
match self.size {
MacControlSize::Regular | MacControlSize::Large => {
let sz = m.button_regular_intrinsic.points();
Vec2::new(sz, sz)
}
MacControlSize::Small | MacControlSize::CompactInfobox => {
let sz = m.button_small_intrinsic.points();
Vec2::new(sz, sz)
}
MacControlSize::Mini => {
let sz = m.button_mini_intrinsic.points();
Vec2::new(sz, sz)
}
}
}
}
impl<'a> Widget for MacDisclosureButton<'a> {
fn ui(self, ui: &mut Ui) -> Response {
let theme_cow = MacTheme::resolve(self.theme, ui.ctx());
let theme: &MacTheme = &theme_cow;
let metrics = theme.disclosure_group();
let desired_size = self.frame_size();
let (rect, mut response) = ui.allocate_exact_size(desired_size, Sense::click());
if response.clicked() {
*self.is_expanded = !*self.is_expanded;
response.mark_changed();
}
if ui.is_rect_visible(rect) {
let painter = ui.painter_at(rect);
let corner_r = rect.height() * 0.5;
let fill = if response.is_pointer_button_down_on() {
theme.selected_control()
} else if response.hovered() {
theme.control()
} else {
theme.control_background()
};
painter.rect(
rect,
CornerRadius::same(corner_r as u8),
fill,
Stroke::new(1.0, theme.separator()),
StrokeKind::Inside,
);
let spring = CASpring::from_physical(
metrics.spring_mass.points(),
metrics.spring_stiffness.points(),
metrics.spring_damping.points(),
);
let target_angle = MacDisclosureGroup::rotation_angle(*self.is_expanded);
let anim_angle = spring.animate_f32(ui.ctx(), response.id.with("pill_rot"), target_angle);
let center = rect.center();
let chevron_sz = metrics.chevron_size.points() * 0.5;
let cos = anim_angle.cos();
let sin = anim_angle.sin();
let rotate = |ox: f32, oy: f32| -> Pos2 {
let rx = ox * cos - oy * sin;
let ry = ox * sin + oy * cos;
Pos2::new(center.x + rx, center.y + ry)
};
let p1 = rotate(-chevron_sz * 0.5, -chevron_sz);
let p2 = rotate(-chevron_sz * 0.5, chevron_sz);
let p3 = rotate(chevron_sz, 0.0);
let glyph_color = if response.hovered() {
theme.label()
} else {
theme.secondary_label()
};
painter.add(Shape::convex_polygon(
vec![p1, p2, p3],
glyph_color,
Stroke::NONE,
));
}
response
}
}