1use egui::{CornerRadius, Key, Modifiers, Pos2, Response, Sense, Shape, Stroke, Ui, WidgetInfo, WidgetType, pos2, vec2};
4
5use crate::automation::{self, Details, Role};
6use crate::controls::{FOCUS_WIDTH, text_galley};
7use crate::tokens::{self, Tokens};
8
9pub(crate) fn chevron(c: Pos2, side: f32, open: bool) -> Vec<Pos2> {
12 let h = side / 2.0;
13 if open {
14 vec![pos2(c.x - h, c.y - h / 2.0), pos2(c.x + h, c.y - h / 2.0), pos2(c.x, c.y + h / 2.0 + h / 4.0)]
15 } else {
16 vec![pos2(c.x - h / 2.0, c.y - h), pos2(c.x + h / 2.0 + h / 4.0, c.y), pos2(c.x - h / 2.0, c.y + h)]
17 }
18}
19
20pub(crate) fn chevron_center(t: &Tokens, left: f32, y: f32) -> (Pos2, f32) {
22 let side = (t.type_scale.body * 0.6).round().max(4.0);
23 (pos2(left + t.space.sm + side / 2.0, y), side)
24}
25
26pub struct Collapsible<'a> {
30 id: &'a str,
31 title: &'a str,
32 open: &'a mut bool,
33}
34
35pub struct CollapsibleOutput<R> {
37 pub header: Response,
39 pub body: Option<R>,
41}
42
43impl<'a> Collapsible<'a> {
44 pub fn new(id: &'a str, title: &'a str, open: &'a mut bool) -> Self {
45 Collapsible { id, title, open }
46 }
47
48 pub fn show<R>(self, ui: &mut Ui, add: impl FnOnce(&mut Ui) -> R) -> CollapsibleOutput<R> {
49 let t = tokens::current(ui.ctx());
50 let galley = text_galley(ui, ("ashlar_ui::content::collapsible", self.id), self.title, t.type_scale.body);
51 let width = ui.available_width().clamp(0.0, 8000.0);
52 let (auto_id, rect) = ui.allocate_space(vec2(width, t.control_height.max(galley.size().y)));
53 let id = if self.id.is_empty() { auto_id } else { ui.make_persistent_id(("ashlar_ui::content::collapsible", self.id)) };
54 let mut header = ui.interact(rect, id, Sense::click());
55 let mut toggled = header.clicked();
56 if header.has_focus() {
57 let open = *self.open;
58 let (right, left) = ui.input_mut(|i| (i.consume_key(Modifiers::NONE, Key::ArrowRight), i.consume_key(Modifiers::NONE, Key::ArrowLeft)));
59 toggled |= (right && !open) || (left && open);
60 }
61 if toggled {
62 *self.open = !*self.open;
63 header.mark_changed();
64 }
65 let open = *self.open;
66 let enabled = ui.is_enabled();
67 header.widget_info(|| WidgetInfo::labeled(WidgetType::CollapsingHeader, enabled, self.title));
68 ui.ctx().accesskit_node_builder(header.id, |node| node.set_expanded(open));
69
70 if ui.is_rect_visible(rect) {
71 let painter = ui.painter();
72 let radius = CornerRadius::same(t.radius.sm);
73 if enabled && header.is_pointer_button_down_on() {
74 painter.rect_filled(rect, radius, t.surface.pressed);
75 } else if enabled && header.hovered() {
76 painter.rect_filled(rect, radius, t.surface.hover);
77 }
78 let (c, side) = chevron_center(&t, rect.left(), rect.center().y);
79 painter.add(Shape::convex_polygon(chevron(c, side, open), t.text.secondary, Stroke::NONE));
80 let x = c.x + side / 2.0 + t.space.sm;
81 painter.galley(pos2(x, rect.center().y - galley.size().y / 2.0), galley, t.text.primary);
82 if header.has_focus() {
83 painter.rect_stroke(rect, radius, Stroke::new(FOCUS_WIDTH, t.focus), egui::StrokeKind::Inside);
84 }
85 }
86 automation::register_with(ui, &header, self.id, self.title, Role::Disclosure, Details { value: None, selected: Some(open) });
87 let indent = t.space.xl;
89 let body = open.then(|| {
90 ui.horizontal_top(|ui| {
91 ui.add_space(indent);
92 ui.vertical(add).inner
93 })
94 .inner
95 });
96 CollapsibleOutput { header, body }
97 }
98}
99
100pub struct Accordion<'a> {
113 id: &'a str,
114 open: &'a mut Option<usize>,
115}
116
117impl<'a> Accordion<'a> {
118 pub fn new(id: &'a str, open: &'a mut Option<usize>) -> Self {
119 Accordion { id, open }
120 }
121
122 pub fn section<R>(&mut self, ui: &mut Ui, index: usize, title: &str, add: impl FnOnce(&mut Ui) -> R) -> CollapsibleOutput<R> {
124 let mut open = *self.open == Some(index);
125 let id = if self.id.is_empty() { String::new() } else { format!("{}.{index}", self.id) };
126 let out = Collapsible::new(&id, title, &mut open).show(ui, add);
127 if out.header.changed() {
128 if open {
129 if self.open.is_some_and(|o| o < index) {
131 crate::wake::wake(ui.ctx(), crate::wake::Reason::INPUT);
132 }
133 *self.open = Some(index);
134 } else if *self.open == Some(index) {
135 *self.open = None;
136 }
137 }
138 out
139 }
140}