agg_gui/widgets/
collapsing_header.rs1use std::sync::Arc;
15
16use crate::color::Color;
17use crate::draw_ctx::DrawCtx;
18use crate::event::{Event, EventResult, MouseButton};
19use crate::geometry::{Point, Rect, Size};
20use crate::text::Font;
21use crate::widget::{Widget, paint_subtree};
22use crate::widgets::label::Label;
23
24const HEADER_H: f64 = 22.0;
25const TRIANGLE_SIZE: f64 = 6.0;
26const INDENT: f64 = 12.0;
27
28pub struct CollapsingHeader {
31 bounds: Rect,
32 children: Vec<Box<dyn Widget>>,
33 label: Label,
34 open: bool,
35 hovered: bool,
36 content: Option<Box<dyn Widget>>,
38}
39
40impl CollapsingHeader {
41 pub fn new(text: impl Into<String>, font: Arc<Font>) -> Self {
44 let label = Label::new(text, Arc::clone(&font)).with_font_size(13.0);
45 Self {
46 bounds: Rect::default(),
47 children: Vec::new(),
48 label,
49 open: true,
50 hovered: false,
51 content: None,
52 }
53 }
54
55 pub fn default_open(mut self, open: bool) -> Self {
57 self.open = open;
58 self
59 }
60
61 pub fn with_content(mut self, content: Box<dyn Widget>) -> Self {
63 self.content = Some(content);
64 self
65 }
66}
67
68impl Widget for CollapsingHeader {
69 fn type_name(&self) -> &'static str { "CollapsingHeader" }
70 fn bounds(&self) -> Rect { self.bounds }
71 fn set_bounds(&mut self, b: Rect) { self.bounds = b; }
72 fn children(&self) -> &[Box<dyn Widget>] { &self.children }
73 fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> { &mut self.children }
74
75 fn layout(&mut self, available: Size) -> Size {
76 let w = available.width;
77
78 if self.open && self.children.is_empty() {
82 if let Some(c) = self.content.take() {
83 self.children.push(c);
84 }
85 } else if !self.open && !self.children.is_empty() {
86 if let Some(c) = self.children.pop() {
87 self.content = Some(c);
88 }
89 }
90
91 let label_available = Size::new(w - INDENT - TRIANGLE_SIZE * 2.0, HEADER_H);
93 let ls = self.label.layout(label_available);
94 let ly = (HEADER_H - ls.height) * 0.5;
95 self.label.set_bounds(Rect::new(INDENT + TRIANGLE_SIZE * 2.0 + 4.0, ly, ls.width, ls.height));
96
97 let content_h = if self.open && !self.children.is_empty() {
101 let inset = INDENT * 0.5;
102 let avail_w = (w - inset).max(0.0);
103 let child = &mut self.children[0];
104 let cs = child.layout(Size::new(avail_w, available.height - HEADER_H));
105 child.set_bounds(Rect::new(inset, 0.0, cs.width, cs.height));
107 cs.height
108 } else {
109 0.0
110 };
111
112 let total_h = HEADER_H + content_h;
113 self.bounds = Rect::new(0.0, 0.0, w, total_h);
114 Size::new(w, total_h)
115 }
116
117 fn paint(&mut self, ctx: &mut dyn DrawCtx) {
118 let v = ctx.visuals();
119 let w = self.bounds.width;
120 let h = self.bounds.height;
121
122 let alpha = if self.hovered { 0.10 } else { 0.06 };
127 ctx.set_fill_color(Color::rgba(
128 v.text_color.r, v.text_color.g, v.text_color.b, alpha,
129 ));
130 ctx.begin_path();
131 ctx.rect(0.0, h - HEADER_H, w, HEADER_H - 1.0);
132 ctx.fill();
133
134 ctx.set_fill_color(v.separator);
138 ctx.begin_path();
139 ctx.rect(0.0, h - 1.0, w, 1.0);
140 ctx.fill();
141
142 let center_y = h - HEADER_H * 0.5;
145 let tx = INDENT;
146 let ts = TRIANGLE_SIZE * 0.5;
147 ctx.set_fill_color(v.text_dim);
148 ctx.begin_path();
149 if self.open {
150 ctx.move_to(tx, center_y + ts * 0.5);
152 ctx.line_to(tx + ts * 2.0, center_y + ts * 0.5);
153 ctx.line_to(tx + ts, center_y - ts * 0.8);
154 } else {
155 ctx.move_to(tx, center_y + ts);
157 ctx.line_to(tx, center_y - ts);
158 ctx.line_to(tx + ts * 1.6, center_y);
159 }
160 ctx.fill();
161
162 self.label.set_color(v.text_color);
164 let lb = self.label.bounds();
165 let label_offset_y = h - HEADER_H + lb.y;
167 ctx.save();
168 ctx.translate(lb.x, label_offset_y);
169 paint_subtree(&mut self.label, ctx);
170 ctx.restore();
171
172 }
176
177 fn on_event(&mut self, event: &Event) -> EventResult {
178 let h = self.bounds.height;
179
180 match event {
181 Event::MouseMove { pos } => {
182 let in_header = pos.x >= 0.0 && pos.x <= self.bounds.width
184 && pos.y >= h - HEADER_H && pos.y <= h;
185 let was = self.hovered;
186 self.hovered = in_header;
187 if self.hovered != was {
188 crate::animation::request_tick();
189 return EventResult::Consumed;
190 }
191 EventResult::Ignored
192 }
193 Event::MouseDown { button: MouseButton::Left, pos, .. } => {
194 let in_header = pos.x >= 0.0 && pos.x <= self.bounds.width
195 && pos.y >= h - HEADER_H && pos.y <= h;
196 if in_header {
197 self.open = !self.open;
198 crate::animation::request_tick();
199 return EventResult::Consumed;
200 }
201 EventResult::Ignored
202 }
203 _ => EventResult::Ignored,
204 }
205 }
206
207 fn hit_test(&self, local_pos: Point) -> bool {
208 local_pos.x >= 0.0 && local_pos.x <= self.bounds.width
209 && local_pos.y >= 0.0 && local_pos.y <= self.bounds.height
210 }
211}