use crate::controls::thumbnail_cell::MacThumbnailCell;
use crate::theme::MacTheme;
use egui::{
Align2, CornerRadius, FontId, Id, Painter, Pos2, Rect, Response, 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 {
ui: self.ui,
theme: self.theme,
row_id,
row_rect,
cursor_x: content_left,
accessory_cursor_right: row_rect.right() - accessory_spacing,
selected,
accessory_clicked: false,
};
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,
}
}
}
pub struct MacOutlineRow<'a> {
pub ui: &'a mut Ui,
pub theme: &'a MacTheme,
row_id: Id,
row_rect: Rect,
cursor_x: f32,
accessory_cursor_right: f32,
selected: bool,
accessory_clicked: bool,
}
impl<'a> MacOutlineRow<'a> {
pub fn thumbnail(&mut self, drawer: impl FnOnce(&Painter, Rect)) {
let size = self.theme.thumbnail().size.points();
let rect = Rect::from_center_size(
Pos2::new(self.cursor_x + size * 0.5, self.row_rect.center().y),
Vec2::splat(size),
);
let painter = self.ui.painter_at(rect);
MacThumbnailCell::new()
.theme(self.theme)
.custom_draw(drawer)
.paint_in_rect(&painter, rect, false);
self.cursor_x += size + self.theme.outline_view().content_spacing.points();
}
pub fn label(&mut self, text: &str) {
let spacing = self.theme.outline_view().content_spacing.points();
let avail_w = (self.accessory_cursor_right - self.cursor_x - spacing).max(0.0);
let label_rect = Rect::from_min_size(
Pos2::new(self.cursor_x, self.row_rect.top()),
Vec2::new(avail_w, self.row_rect.height()),
);
let painter = self.ui.painter_at(label_rect);
let font_size = self.theme.outline_view().font_size.points();
let font_id = FontId::proportional(font_size);
let text_color = if self.selected {
let bg_style = self.theme.selected_control_background_style();
bg_style.primary_text_color(self.theme)
} else {
self.theme.label()
};
let format = egui::TextFormat {
font_id,
color: text_color,
..Default::default()
};
let mut job = egui::text::LayoutJob::single_section(text.to_string(), format);
job.wrap.max_width = avail_w;
job.wrap.max_rows = 1;
job.wrap.break_anywhere = false;
job.wrap.overflow_character = Some('…');
let galley = painter.layout_job(job);
let pos = Pos2::new(
label_rect.left(),
label_rect.center().y - galley.size().y * 0.5,
);
painter.galley(pos, galley, text_color);
}
pub fn header(&mut self, text: &str) {
let spacing = self.theme.outline_view().content_spacing.points();
let avail_w = (self.accessory_cursor_right - self.cursor_x - spacing).max(0.0);
let label_rect = Rect::from_min_size(
Pos2::new(self.cursor_x, self.row_rect.top()),
Vec2::new(avail_w, self.row_rect.height()),
);
let painter = self.ui.painter_at(label_rect);
let font_size = self.theme.outline_view().header_font_size.points();
let font_id = FontId::proportional(font_size);
let text_color = self.theme.secondary_label();
let upper = text.to_uppercase();
painter.text(
Pos2::new(label_rect.left(), label_rect.center().y),
Align2::LEFT_CENTER,
upper,
font_id,
text_color,
);
}
pub fn accessory_custom(
&mut self,
id_source: impl Hash + Debug,
active: &mut bool,
drawer: impl FnOnce(&Painter, Rect, bool),
) -> Response {
let metrics = self.theme.outline_view();
let size = metrics.accessory_size.points();
let spacing = metrics.accessory_spacing.points();
self.accessory_cursor_right -= size;
let rect = Rect::from_center_size(
Pos2::new(
self.accessory_cursor_right + size * 0.5,
self.row_rect.center().y,
),
Vec2::splat(size),
);
self.accessory_cursor_right -= spacing;
let resp = self
.ui
.interact(rect, self.row_id.with(id_source), Sense::click());
if resp.clicked() {
*active = !*active;
self.accessory_clicked = true;
} else if resp.is_pointer_button_down_on() {
self.accessory_clicked = true;
}
let painter = self.ui.painter_at(rect);
drawer(&painter, rect, *active);
resp
}
}