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;
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>>,
35 open: bool,
36 hovered: bool,
37 content: Option<Box<dyn Widget>>,
39}
40
41impl CollapsingHeader {
42 pub fn new(text: impl Into<String>, font: Arc<Font>) -> Self {
45 let label = Label::new(text, Arc::clone(&font)).with_font_size(13.0);
46 Self {
47 bounds: Rect::default(),
48 children: vec![Box::new(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 {
70 "CollapsingHeader"
71 }
72 fn bounds(&self) -> Rect {
73 self.bounds
74 }
75 fn set_bounds(&mut self, b: Rect) {
76 self.bounds = b;
77 }
78 fn children(&self) -> &[Box<dyn Widget>] {
79 &self.children
80 }
81 fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
82 &mut self.children
83 }
84
85 fn layout(&mut self, available: Size) -> Size {
86 let w = available.width;
87
88 if self.open && self.children.len() == 1 {
91 if let Some(c) = self.content.take() {
92 self.children.push(c);
93 }
94 } else if !self.open && self.children.len() > 1 {
95 if let Some(c) = self.children.pop() {
96 self.content = Some(c);
97 }
98 }
99
100 let content_h = if self.open && self.children.len() > 1 {
102 let inset = INDENT * 0.5;
103 let avail_w = (w - inset).max(0.0);
104 let child = &mut self.children[1];
105 let cs = child.layout(Size::new(avail_w, available.height - HEADER_H));
106 child.set_bounds(Rect::new(inset, 0.0, cs.width, cs.height));
107 cs.height
108 } else {
109 0.0
110 };
111 let total_h = HEADER_H + content_h;
112
113 let label_avail = Size::new(w - INDENT - TRIANGLE_SIZE * 2.0, HEADER_H);
115 let ls = self.children[0].layout(label_avail);
116 let header_bottom = total_h - HEADER_H;
117 let label_y = header_bottom + (HEADER_H - ls.height) * 0.5;
118 self.children[0].set_bounds(Rect::new(
119 INDENT + TRIANGLE_SIZE * 2.0 + 4.0,
120 label_y,
121 ls.width,
122 ls.height,
123 ));
124
125 self.bounds = Rect::new(0.0, 0.0, w, total_h);
126 Size::new(w, total_h)
127 }
128
129 fn paint(&mut self, ctx: &mut dyn DrawCtx) {
130 let v = ctx.visuals();
131 let w = self.bounds.width;
132 let h = self.bounds.height;
133
134 let alpha = if self.hovered { 0.10 } else { 0.06 };
139 ctx.set_fill_color(Color::rgba(
140 v.text_color.r,
141 v.text_color.g,
142 v.text_color.b,
143 alpha,
144 ));
145 ctx.begin_path();
146 ctx.rect(0.0, h - HEADER_H, w, HEADER_H - 1.0);
147 ctx.fill();
148
149 ctx.set_fill_color(v.separator);
153 ctx.begin_path();
154 ctx.rect(0.0, h - 1.0, w, 1.0);
155 ctx.fill();
156
157 let center_y = h - HEADER_H * 0.5;
160 let tx = INDENT;
161 let ts = TRIANGLE_SIZE * 0.5;
162 ctx.set_fill_color(v.text_dim);
163 ctx.begin_path();
164 if self.open {
165 ctx.move_to(tx, center_y + ts * 0.5);
167 ctx.line_to(tx + ts * 2.0, center_y + ts * 0.5);
168 ctx.line_to(tx + ts, center_y - ts * 0.8);
169 } else {
170 ctx.move_to(tx, center_y + ts);
172 ctx.line_to(tx, center_y - ts);
173 ctx.line_to(tx + ts * 1.6, center_y);
174 }
175 ctx.fill();
176
177 self.children[0].set_label_color(v.text_color);
179
180 }
183
184 fn on_event(&mut self, event: &Event) -> EventResult {
185 let h = self.bounds.height;
186
187 match event {
188 Event::MouseMove { pos } => {
189 let in_header = pos.x >= 0.0
191 && pos.x <= self.bounds.width
192 && pos.y >= h - HEADER_H
193 && pos.y <= h;
194 let was = self.hovered;
195 self.hovered = in_header;
196 if self.hovered != was {
197 crate::animation::request_draw();
198 return EventResult::Consumed;
199 }
200 EventResult::Ignored
201 }
202 Event::MouseDown {
203 button: MouseButton::Left,
204 pos,
205 ..
206 } => {
207 let in_header = pos.x >= 0.0
208 && pos.x <= self.bounds.width
209 && pos.y >= h - HEADER_H
210 && pos.y <= h;
211 if in_header {
212 self.open = !self.open;
213 crate::animation::request_draw();
214 return EventResult::Consumed;
215 }
216 EventResult::Ignored
217 }
218 _ => EventResult::Ignored,
219 }
220 }
221
222 fn hit_test(&self, local_pos: Point) -> bool {
223 local_pos.x >= 0.0
224 && local_pos.x <= self.bounds.width
225 && local_pos.y >= 0.0
226 && local_pos.y <= self.bounds.height
227 }
228}