pub mod row;
pub use row::MacOutlineRow;
use crate::theme::MacTheme;
use egui::{CornerRadius, Id, Pos2, Rect, Sense, Stroke, StrokeKind, Ui, Vec2};
use std::fmt::Debug;
use std::hash::Hash;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct MacOutlineRowResponse {
pub clicked: bool,
pub double_clicked: bool,
pub disclosure_clicked: bool,
pub is_expanded: bool,
pub is_selected: bool,
}
pub struct MacOutlineView<'a> {
id: Id,
theme: Option<&'a MacTheme>,
row_height: Option<f32>,
indent_step: Option<f32>,
}
impl<'a> MacOutlineView<'a> {
pub fn new(id_source: impl Hash + Debug) -> Self {
Self {
id: Id::NULL.with(id_source),
theme: None,
row_height: None,
indent_step: None,
}
}
pub fn id(&self) -> Id {
self.id
}
pub fn theme(mut self, theme: &'a MacTheme) -> Self {
self.theme = Some(theme);
self
}
pub fn row_height(mut self, height: f32) -> Self {
self.row_height = Some(height);
self
}
pub fn indent_step(mut self, step: f32) -> Self {
self.indent_step = Some(step);
self
}
pub fn show<R>(self, ui: &mut Ui, content: impl FnOnce(&mut MacOutlineTree<'_>) -> R) -> R {
let theme_cow = MacTheme::resolve(self.theme, ui.ctx());
let theme: &MacTheme = &theme_cow;
ui.with_layout(egui::Layout::top_down(egui::Align::Min), |child_ui| {
let mut tree = MacOutlineTree {
id: self.id,
ui: child_ui,
theme,
row_height: self
.row_height
.unwrap_or_else(|| theme.outline_view().row_height.points()),
indent_step: self
.indent_step
.unwrap_or_else(|| theme.outline_view().indent_step.points()),
};
content(&mut tree)
})
.inner
}
}
pub struct MacOutlineTree<'t> {
pub id: Id,
pub ui: &'t mut Ui,
pub theme: &'t MacTheme,
row_height: f32,
indent_step: f32,
}
impl<'t> MacOutlineTree<'t> {
pub fn row(
&mut self,
id_source: impl Hash + Debug,
depth: usize,
expandable: bool,
expanded: &mut bool,
selected: bool,
content: impl FnOnce(&mut MacOutlineRow<'_>),
) -> MacOutlineRowResponse {
let row_id = self.id.with(id_source);
let avail_w = self.ui.available_width();
let (row_rect, row_resp) = self
.ui
.allocate_exact_size(Vec2::new(avail_w, self.row_height), Sense::click());
let metrics = self.theme.outline_view();
let corner_radius = metrics.corner_radius.points();
let painter = self.ui.painter_at(row_rect);
if selected {
painter.rect(
row_rect,
CornerRadius::same(corner_radius as u8),
self.theme.selected_control(),
Stroke::NONE,
StrokeKind::Inside,
);
} else if row_resp.hovered() {
painter.rect(
row_rect,
CornerRadius::same(corner_radius as u8),
self.theme.control(),
Stroke::NONE,
StrokeKind::Inside,
);
}
let indent_offset = depth as f32 * self.indent_step;
let disc_size = metrics.disclosure_size.points();
let disc_margin = metrics.disclosure_margin.points();
let disc_x = row_rect.left() + indent_offset + disc_margin;
let disc_rect = Rect::from_center_size(
Pos2::new(disc_x + disc_size * 0.5, row_rect.center().y),
Vec2::splat(disc_size + disc_margin),
);
let mut disclosure_clicked = false;
if expandable {
let disc_resp = self
.ui
.interact(disc_rect, row_id.with("disclosure"), Sense::click());
if disc_resp.clicked() {
*expanded = !*expanded;
disclosure_clicked = true;
}
let spring = self.theme.motion().swiftui_snappy.to_spring();
let state_key = row_id.with("disc_spring_state");
let target_frac = if *expanded { 1.0f32 } else { 0.0f32 };
let current_frac = spring.animate_f32(self.ui.ctx(), state_key, target_frac);
let angle = current_frac * std::f32::consts::FRAC_PI_2;
let (sin_a, cos_a) = angle.sin_cos();
let rotate = |dx: f32, dy: f32, center: Pos2| -> Pos2 {
Pos2::new(
center.x + dx * cos_a - dy * sin_a,
center.y + dx * sin_a + dy * cos_a,
)
};
let center = disc_rect.center();
let half = metrics.chevron_size.points() * 0.5;
let chev_depth = metrics.chevron_depth.points();
let chevron_stroke = Stroke::new(
metrics.chevron_stroke_width.points(),
if selected {
let bg_style = self.theme.selected_control_background_style();
bg_style.primary_text_color(self.theme)
} else {
self.theme.placeholder_text()
},
);
let p_top = rotate(-chev_depth, -half, center);
let p_tip = rotate(chev_depth, 0.0, center);
let p_bot = rotate(-chev_depth, half, center);
painter.line_segment([p_top, p_tip], chevron_stroke);
painter.line_segment([p_tip, p_bot], chevron_stroke);
}
let content_spacing = metrics.content_spacing.points();
let accessory_spacing = metrics.accessory_spacing.points();
let content_left = disc_x + disc_size + content_spacing;
let mut row_builder = MacOutlineRow::new(
self.ui,
self.theme,
row_id,
row_rect,
content_left,
row_rect.right() - accessory_spacing,
selected,
);
content(&mut row_builder);
MacOutlineRowResponse {
clicked: row_resp.clicked() && !disclosure_clicked && !row_builder.accessory_clicked,
double_clicked: row_resp.double_clicked(),
disclosure_clicked,
is_expanded: *expanded,
is_selected: selected,
}
}
}