agg_gui/widgets/
splitter.rs1use crate::draw_ctx::DrawCtx;
8use crate::event::{Event, EventResult, MouseButton};
9use crate::geometry::{Point, Rect, Size};
10use crate::layout_props::{HAnchor, Insets, VAnchor, WidgetBase};
11use crate::widget::Widget;
12
13pub struct Splitter {
21 bounds: Rect,
22 children: Vec<Box<dyn Widget>>, base: WidgetBase,
24 pub ratio: f64,
27 pub divider_width: f64,
29 pub vertical: bool,
32
33 hovered: bool,
34 dragging: bool,
35}
36
37impl Splitter {
38 pub fn new(left: Box<dyn Widget>, right: Box<dyn Widget>) -> Self {
40 Self {
41 bounds: Rect::default(),
42 children: vec![left, right],
43 base: WidgetBase::new(),
44 ratio: 0.5,
45 divider_width: 6.0,
46 vertical: false,
47 hovered: false,
48 dragging: false,
49 }
50 }
51
52 pub fn vertical(top: Box<dyn Widget>, bottom: Box<dyn Widget>) -> Self {
56 Self {
57 bounds: Rect::default(),
58 children: vec![top, bottom],
59 base: WidgetBase::new(),
60 ratio: 0.5,
61 divider_width: 6.0,
62 vertical: true,
63 hovered: false,
64 dragging: false,
65 }
66 }
67
68 pub fn with_ratio(mut self, ratio: f64) -> Self {
69 self.ratio = ratio.clamp(0.05, 0.95);
70 self
71 }
72
73 pub fn with_divider_width(mut self, w: f64) -> Self {
74 self.divider_width = w;
75 self
76 }
77
78 pub fn with_margin(mut self, m: Insets) -> Self {
79 self.base.margin = m;
80 self
81 }
82 pub fn with_h_anchor(mut self, h: HAnchor) -> Self {
83 self.base.h_anchor = h;
84 self
85 }
86 pub fn with_v_anchor(mut self, v: VAnchor) -> Self {
87 self.base.v_anchor = v;
88 self
89 }
90 pub fn with_min_size(mut self, s: Size) -> Self {
91 self.base.min_size = s;
92 self
93 }
94 pub fn with_max_size(mut self, s: Size) -> Self {
95 self.base.max_size = s;
96 self
97 }
98
99 fn axis_length(&self) -> f64 {
101 if self.vertical {
102 self.bounds.height
103 } else {
104 self.bounds.width
105 }
106 }
107
108 fn divider_pos(&self) -> f64 {
115 (self.axis_length() - self.divider_width) * self.ratio
116 }
117}
118
119impl Widget for Splitter {
120 fn type_name(&self) -> &'static str {
121 "Splitter"
122 }
123 fn bounds(&self) -> Rect {
124 self.bounds
125 }
126 fn set_bounds(&mut self, b: Rect) {
127 self.bounds = b;
128 }
129 fn children(&self) -> &[Box<dyn Widget>] {
130 &self.children
131 }
132 fn children_mut(&mut self) -> &mut Vec<Box<dyn Widget>> {
133 &mut self.children
134 }
135
136 fn margin(&self) -> Insets {
137 self.base.margin
138 }
139 fn widget_base(&self) -> Option<&WidgetBase> {
140 Some(&self.base)
141 }
142 fn widget_base_mut(&mut self) -> Option<&mut WidgetBase> {
143 Some(&mut self.base)
144 }
145 fn h_anchor(&self) -> HAnchor {
146 self.base.h_anchor
147 }
148 fn v_anchor(&self) -> VAnchor {
149 self.base.v_anchor
150 }
151 fn min_size(&self) -> Size {
152 self.base.min_size
153 }
154 fn max_size(&self) -> Size {
155 self.base.max_size
156 }
157
158 fn hit_test(&self, local_pos: Point) -> bool {
159 if self.dragging {
161 return true;
162 }
163 let b = self.bounds();
164 local_pos.x >= 0.0
165 && local_pos.x <= b.width
166 && local_pos.y >= 0.0
167 && local_pos.y <= b.height
168 }
169
170 fn layout(&mut self, available: Size) -> Size {
171 let div = self.divider_width;
172
173 if self.children.len() < 2 {
174 return available;
175 }
176
177 if self.vertical {
178 let top_h = ((available.height - div) * self.ratio).max(0.0);
181 let bot_h = (available.height - div - top_h).max(0.0);
182 let w = available.width;
183
184 self.children[0].layout(Size::new(w, top_h));
187 self.children[0].set_bounds(Rect::new(0.0, bot_h + div, w, top_h));
188
189 self.children[1].layout(Size::new(w, bot_h));
191 self.children[1].set_bounds(Rect::new(0.0, 0.0, w, bot_h));
192 } else {
193 let left_w = ((available.width - div) * self.ratio).max(0.0);
194 let right_w = (available.width - div - left_w).max(0.0);
195 let h = available.height;
196
197 self.children[0].layout(Size::new(left_w, h));
198 self.children[0].set_bounds(Rect::new(0.0, 0.0, left_w, h));
199
200 let right_x = left_w + div;
201 self.children[1].layout(Size::new(right_w, h));
202 self.children[1].set_bounds(Rect::new(right_x, 0.0, right_w, h));
203 }
204
205 available
206 }
207
208 fn paint(&mut self, ctx: &mut dyn DrawCtx) {
209 let v = ctx.visuals();
210 let color = if self.dragging {
211 v.accent.with_alpha(0.6)
212 } else if self.hovered {
213 v.text_color.with_alpha(0.15)
214 } else {
215 v.text_color.with_alpha(0.08)
216 };
217
218 let grip_color = if self.hovered || self.dragging {
219 v.accent.with_alpha(0.7)
220 } else {
221 v.text_color.with_alpha(0.25)
222 };
223
224 ctx.set_fill_color(color);
225
226 if self.vertical {
227 let bot_h = ((self.bounds.height - self.divider_width) * (1.0 - self.ratio)).max(0.0);
229 let div_y = bot_h;
230 let w = self.bounds.width;
231 ctx.begin_path();
232 ctx.rect(0.0, div_y, w, self.divider_width);
233 ctx.fill();
234
235 if w > 30.0 {
237 ctx.set_fill_color(grip_color);
238 let cy = div_y + self.divider_width * 0.5;
239 let cx = w * 0.5;
240 for i in -1i32..=1 {
241 ctx.begin_path();
242 ctx.circle(cx + i as f64 * 5.0, cy, 1.5);
243 ctx.fill();
244 }
245 }
246 } else {
247 let div_x = self.divider_pos();
248 let h = self.bounds.height;
249 ctx.begin_path();
250 ctx.rect(div_x, 0.0, self.divider_width, h);
251 ctx.fill();
252
253 if h > 30.0 {
254 ctx.set_fill_color(grip_color);
255 let cx = div_x + self.divider_width * 0.5;
256 let cy = h * 0.5;
257 for i in -1i32..=1 {
258 ctx.begin_path();
259 ctx.circle(cx, cy + i as f64 * 5.0, 1.5);
260 ctx.fill();
261 }
262 }
263 }
264 }
265
266 fn on_event(&mut self, event: &Event) -> EventResult {
267 if self.vertical {
268 let div = self.divider_width;
269 let total = self.bounds.height;
270 let bot_h = ((total - div) * (1.0 - self.ratio)).max(0.0);
272 let div_y = bot_h;
273 let div_end = div_y + div;
274
275 match event {
276 Event::MouseMove { pos } => {
277 let over_div = pos.y >= div_y - 2.0 && pos.y <= div_end + 2.0;
278 let was = self.hovered;
279 self.hovered = over_div;
280 if self.dragging {
281 if total > div {
282 let div_mid = pos.y;
286 let top_h = (total - div_mid).max(0.0);
287 self.ratio = (top_h / total).clamp(0.05, 0.95);
288 }
289 crate::animation::request_draw();
290 EventResult::Consumed
291 } else {
292 if was != self.hovered {
293 crate::animation::request_draw();
294 return EventResult::Consumed;
295 }
296 EventResult::Ignored
297 }
298 }
299 Event::MouseDown {
300 pos,
301 button: MouseButton::Left,
302 ..
303 } => {
304 if pos.y >= div_y - 2.0 && pos.y <= div_end + 2.0 {
305 self.dragging = true;
306 EventResult::Consumed
307 } else {
308 EventResult::Ignored
309 }
310 }
311 Event::MouseUp {
312 button: MouseButton::Left,
313 ..
314 } => {
315 let was_dragging = self.dragging;
316 self.dragging = false;
317 if was_dragging {
318 crate::animation::request_draw();
319 EventResult::Consumed
320 } else {
321 EventResult::Ignored
322 }
323 }
324 _ => EventResult::Ignored,
325 }
326 } else {
327 let div_x = self.divider_pos();
328 let div_end = div_x + self.divider_width;
329
330 match event {
331 Event::MouseMove { pos } => {
332 let over_div = pos.x >= div_x - 2.0 && pos.x <= div_end + 2.0;
333 let was = self.hovered;
334 self.hovered = over_div;
335 if self.dragging {
336 let total = self.bounds.width;
337 if total > self.divider_width {
338 self.ratio = (pos.x / total).clamp(0.05, 0.95);
339 }
340 crate::animation::request_draw();
341 EventResult::Consumed
342 } else {
343 if was != self.hovered {
344 crate::animation::request_draw();
345 return EventResult::Consumed;
346 }
347 EventResult::Ignored
348 }
349 }
350 Event::MouseDown {
351 pos,
352 button: MouseButton::Left,
353 ..
354 } => {
355 if pos.x >= div_x - 2.0 && pos.x <= div_end + 2.0 {
356 self.dragging = true;
357 EventResult::Consumed
358 } else {
359 EventResult::Ignored
360 }
361 }
362 Event::MouseUp {
363 button: MouseButton::Left,
364 ..
365 } => {
366 let was_dragging = self.dragging;
367 self.dragging = false;
368 if was_dragging {
369 crate::animation::request_draw();
370 EventResult::Consumed
371 } else {
372 EventResult::Ignored
373 }
374 }
375 _ => EventResult::Ignored,
376 }
377 }
378 }
379}