use eframe::egui;
pub const INDENT: f32 = 16.0;
const BOX: f32 = 13.0;
const GAP: f32 = 4.0;
const RIGHT_PAD: f32 = 10.0;
pub struct TreeRow<'a> {
pub guides: &'a [bool],
pub is_last: bool,
pub expandable: bool,
pub expanded: bool,
pub root: bool,
pub glyph: Option<&'a str>,
pub label: &'a str,
pub selected: bool,
pub draggable: bool,
}
impl<'a> TreeRow<'a> {
pub fn branch(guides: &'a [bool], is_last: bool, expanded: bool, label: &'a str) -> Self {
Self {
guides,
is_last,
expandable: true,
expanded,
root: false,
glyph: None,
label,
selected: false,
draggable: false,
}
}
pub fn leaf(guides: &'a [bool], is_last: bool, label: &'a str) -> Self {
Self {
guides,
is_last,
expandable: false,
expanded: false,
root: false,
glyph: None,
label,
selected: false,
draggable: false,
}
}
pub fn glyph(mut self, glyph: Option<&'a str>) -> Self {
self.glyph = glyph;
self
}
pub fn selected(mut self, selected: bool) -> Self {
self.selected = selected;
self
}
pub fn draggable(mut self, draggable: bool) -> Self {
self.draggable = draggable;
self
}
}
pub struct NodeResponse {
pub row_rect: egui::Rect,
pub box_rect: egui::Rect,
pub toggled: bool,
pub label: egui::Response,
}
pub fn child_guides(guides: &[bool], is_last: bool) -> Vec<bool> {
let mut next = guides.to_vec();
next.push(!is_last);
next
}
pub fn node(
ui: &mut egui::Ui,
row: TreeRow,
add_right: impl FnOnce(&mut egui::Ui),
) -> NodeResponse {
let connector_col = if row.root { 0 } else { row.guides.len() };
let indent = if row.root {
0.0
} else {
(connector_col as f32 + 1.0) * INDENT
};
let mut box_rect = egui::Rect::NOTHING;
let mut toggled = false;
let mut label_resp: Option<egui::Response> = None;
let inner = ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 0.0;
if indent > 0.0 {
ui.add_space(indent);
}
let (brect, bresp) = ui.allocate_exact_size(
egui::vec2(BOX, BOX),
if row.expandable {
egui::Sense::click()
} else {
egui::Sense::hover()
},
);
box_rect = brect;
if row.expandable {
paint_box(ui, brect, row.expanded);
if bresp.clicked() {
toggled = true;
}
}
ui.add_space(GAP);
if let Some(glyph) = row.glyph {
ui.label(egui::RichText::new(glyph).color(ui.visuals().text_color()));
ui.add_space(GAP);
}
let mut text = egui::RichText::new(row.label);
if row.selected {
text = text.strong();
}
let sense = if row.draggable {
egui::Sense::click_and_drag()
} else {
egui::Sense::click()
};
label_resp = Some(ui.add(egui::Label::new(text).sense(sense)));
ui.with_layout(egui::Layout::right_to_left(egui::Align::Center), |ui| {
ui.add_space(RIGHT_PAD);
add_right(ui);
});
});
let row_rect = inner.response.rect;
if !row.root {
paint_connectors(ui, row_rect, row.guides, connector_col, row.is_last);
}
NodeResponse {
row_rect,
box_rect,
toggled,
label: label_resp.expect("label always drawn"),
}
}
pub fn message_leaf(
ui: &mut egui::Ui,
guides: &[bool],
is_last: bool,
text: &str,
color: egui::Color32,
) -> egui::Rect {
let connector_col = guides.len();
let indent = (connector_col as f32 + 1.0) * INDENT + BOX + GAP;
let line_h = ui.text_style_height(&egui::TextStyle::Body);
let inner = ui.horizontal(|ui| {
ui.spacing_mut().item_spacing.x = 0.0;
ui.add_space(indent);
ui.vertical(|ui| {
ui.set_max_width((ui.available_width() - RIGHT_PAD).max(40.0));
ui.add(
egui::Label::new(egui::RichText::new(text).color(color))
.wrap_mode(egui::TextWrapMode::Wrap),
);
});
});
let row_rect = inner.response.rect;
let tick_y = row_rect.top() + line_h * 0.5;
paint_connectors_at(ui, row_rect, guides, connector_col, is_last, tick_y);
row_rect
}
fn paint_box(ui: &egui::Ui, rect: egui::Rect, expanded: bool) {
let painter = ui.painter();
let stroke = egui::Stroke::new(1.0, line_color(ui));
painter.rect(
rect,
egui::CornerRadius::same(2),
egui::Color32::TRANSPARENT,
stroke,
egui::StrokeKind::Inside,
);
let c = rect.center();
let arm = rect.width() * 0.28;
let sign = egui::Stroke::new(1.4, ui.visuals().text_color());
painter.line_segment(
[egui::pos2(c.x - arm, c.y), egui::pos2(c.x + arm, c.y)],
sign,
);
if !expanded {
painter.line_segment(
[egui::pos2(c.x, c.y - arm), egui::pos2(c.x, c.y + arm)],
sign,
);
}
}
fn paint_connectors(
ui: &egui::Ui,
row: egui::Rect,
guides: &[bool],
connector_col: usize,
is_last: bool,
) {
paint_connectors_at(ui, row, guides, connector_col, is_last, row.center().y);
}
fn paint_connectors_at(
ui: &egui::Ui,
row: egui::Rect,
guides: &[bool],
connector_col: usize,
is_last: bool,
mid: f32,
) {
let painter = ui.painter();
let stroke = egui::Stroke::new(1.0, line_color(ui));
let sp = ui.spacing().item_spacing.y + 1.0;
let left = row.left();
let top = row.top() - sp;
let bottom = row.bottom() + sp;
let col_x = |c: usize| left + c as f32 * INDENT + INDENT * 0.5;
for (i, on) in guides.iter().enumerate() {
if *on {
let x = col_x(i);
painter.line_segment([egui::pos2(x, top), egui::pos2(x, bottom)], stroke);
}
}
let x = col_x(connector_col);
let v_bottom = if is_last { mid } else { bottom };
painter.line_segment([egui::pos2(x, top), egui::pos2(x, v_bottom)], stroke);
painter.line_segment(
[egui::pos2(x, mid), egui::pos2(x + INDENT * 0.5, mid)],
stroke,
);
}
fn line_color(ui: &egui::Ui) -> egui::Color32 {
ui.visuals()
.widgets
.noninteractive
.fg_stroke
.color
.gamma_multiply(0.7)
}