1use std::cell::RefCell;
2use std::collections::HashSet;
3use std::rc::Rc;
4
5use taffy::geometry::Size;
6use taffy::layout::AvailableSpace;
7use taffy::node::{Node, Taffy};
8use taffy::style::{Dimension, Style};
9
10use geom::Polygon;
11
12use crate::widgets::slider;
13use crate::widgets::spinner::SpinnerValue;
14use crate::widgets::Container;
15use crate::{
16 Autocomplete, Button, Color, Dropdown, EventCtx, GfxCtx, HorizontalAlignment, Menu, Outcome,
17 PersistentSplit, ScreenDims, ScreenPt, ScreenRectangle, Slider, Spinner, Stash, TextBox,
18 Toggle, VerticalAlignment, Widget, WidgetImpl, WidgetOutput,
19};
20
21pub struct Panel {
22 top_level: Widget,
23 cached_flexbox: Option<(Taffy, Vec<Node>, ScreenDims)>,
25 horiz: HorizontalAlignment,
26 vert: VerticalAlignment,
27 dims_x: PanelDims,
28 dims_y: PanelDims,
29
30 scrollable_x: bool,
31 scrollable_y: bool,
32 contents_dims: ScreenDims,
33 container_dims: ScreenDims,
34 clip_rect: Option<ScreenRectangle>,
35}
36
37impl Panel {
38 pub fn new_builder(top_level: Widget) -> PanelBuilder {
39 PanelBuilder {
40 top_level,
41 horiz: HorizontalAlignment::Center,
42 vert: VerticalAlignment::Center,
43 dims_x: PanelDims::MaxPercent(1.0),
44 dims_y: PanelDims::MaxPercent(1.0),
45 ignore_initial_events: false,
46 }
47 }
48
49 pub fn empty(ctx: &mut EventCtx) -> Panel {
51 Panel::new_builder(Widget::col(vec![])).build_custom(ctx)
52 }
53
54 fn update_container_dims_for_canvas_dims(&mut self, canvas_dims: ScreenDims) {
55 let width = match self.dims_x {
56 PanelDims::MaxPercent(pct) => self.contents_dims.width.min(pct * canvas_dims.width),
57 PanelDims::ExactPercent(pct) => pct * canvas_dims.width,
58 PanelDims::ExactPixels(x) => x,
59 };
60 let height = match self.dims_y {
61 PanelDims::MaxPercent(pct) => self.contents_dims.height.min(pct * canvas_dims.height),
62 PanelDims::ExactPercent(pct) => pct * canvas_dims.height,
63 PanelDims::ExactPixels(x) => x,
64 };
65 self.container_dims = ScreenDims::new(width, height);
66 }
67
68 fn recompute_scrollbar_layout(&mut self, ctx: &EventCtx) {
69 let old_scrollable_x = self.scrollable_x;
70 let old_scrollable_y = self.scrollable_y;
71 let old_scroll_offset = self.scroll_offset();
72 let mut was_dragging_x = false;
73 let mut was_dragging_y = false;
74
75 self.scrollable_x = self.contents_dims.width > self.container_dims.width;
76 self.scrollable_y = self.contents_dims.height > self.container_dims.height;
77
78 if old_scrollable_y {
80 let container = self.top_level.widget.downcast_mut::<Container>().unwrap();
81 was_dragging_y = container.members[1]
82 .widget
83 .downcast_ref::<Slider>()
84 .unwrap()
85 .dragging;
86 self.top_level = container.members.remove(0);
87 }
88
89 if old_scrollable_x {
90 let container = self.top_level.widget.downcast_mut::<Container>().unwrap();
91 was_dragging_x = container.members[1]
92 .widget
93 .downcast_ref::<Slider>()
94 .unwrap()
95 .dragging;
96 self.top_level = container.members.remove(0);
97 }
98
99 let mut container_dims = self.container_dims;
100 if self.scrollable_y {
103 container_dims.width += slider::SCROLLBAR_BG_WIDTH;
104 }
105 let top_left = ctx
106 .canvas
107 .align_window(container_dims, self.horiz, self.vert);
108
109 if self.scrollable_x {
111 let mut slider = Slider::horizontal_scrollbar(
112 ctx,
113 self.container_dims.width,
114 self.container_dims.width * (self.container_dims.width / self.contents_dims.width),
115 0.0,
116 )
117 .named("horiz scrollbar")
118 .abs(top_left.x, top_left.y + self.container_dims.height);
119 if was_dragging_x {
122 slider.widget.downcast_mut::<Slider>().unwrap().dragging = true;
123 }
124
125 let old_top_level = std::mem::replace(&mut self.top_level, Widget::nothing());
126 self.top_level = Widget::custom_col(vec![old_top_level, slider]);
127 }
128
129 if self.scrollable_y {
130 let mut slider = Slider::vertical_scrollbar(
131 ctx,
132 self.container_dims.height,
133 self.container_dims.height
134 * (self.container_dims.height / self.contents_dims.height),
135 0.0,
136 )
137 .named("vert scrollbar")
138 .abs(top_left.x + self.container_dims.width, top_left.y);
139 if was_dragging_y {
140 slider.widget.downcast_mut::<Slider>().unwrap().dragging = true;
141 }
142
143 let old_top_level = std::mem::replace(&mut self.top_level, Widget::nothing());
144 self.top_level = Widget::custom_row(vec![old_top_level, slider]);
145 }
146
147 self.update_scroll_sliders(ctx, old_scroll_offset);
148
149 self.clip_rect = if self.scrollable_x || self.scrollable_y {
150 Some(ScreenRectangle::top_left(top_left, self.container_dims))
151 } else {
152 None
153 };
154 }
155
156 fn recompute_layout(&mut self, ctx: &EventCtx, recompute_bg: bool) {
160 self.invalidate_flexbox();
161 self.recompute_layout_if_needed(ctx, recompute_bg)
162 }
163
164 fn invalidate_flexbox(&mut self) {
165 self.cached_flexbox = None;
166 }
167
168 fn compute_flexbox(&self) -> (Taffy, Vec<Node>, ScreenDims) {
169 let mut taffy = Taffy::new();
170 let root = taffy
171 .new_with_children(
172 Style {
173 ..Default::default()
174 },
175 &[],
176 )
177 .unwrap();
178
179 let mut nodes = vec![];
180 self.top_level.get_flexbox(root, &mut taffy, &mut nodes);
181 nodes.reverse();
182
183 let container_size = Size {
185 width: AvailableSpace::MaxContent,
186 height: AvailableSpace::MaxContent,
187 };
188 taffy.compute_layout(root, container_size).unwrap();
189
190 let effective_dims = if self.scrollable_x || self.scrollable_y {
192 self.container_dims
193 } else {
194 let result = taffy.layout(root).unwrap();
195 ScreenDims::new(result.size.width.into(), result.size.height.into())
196 };
197
198 (taffy, nodes, effective_dims)
199 }
200
201 fn recompute_layout_if_needed(&mut self, ctx: &EventCtx, recompute_bg: bool) {
202 self.recompute_scrollbar_layout(ctx);
203 let (taffy, nodes, effective_dims) = self
204 .cached_flexbox
205 .take()
206 .unwrap_or_else(|| self.compute_flexbox());
207
208 {
209 let top_left = ctx
210 .canvas
211 .align_window(effective_dims, self.horiz, self.vert);
212 let offset = self.scroll_offset();
213 let mut nodes = nodes.clone();
214 self.top_level.apply_flexbox(
215 &taffy,
216 &mut nodes,
217 top_left.x,
218 top_left.y,
219 offset,
220 ctx,
221 recompute_bg,
222 false,
223 );
224 assert!(nodes.is_empty());
225 }
226 self.cached_flexbox = Some((taffy, nodes, effective_dims));
227 }
228
229 fn scroll_offset(&self) -> (f64, f64) {
230 let x = if self.scrollable_x {
231 self.slider("horiz scrollbar").get_percent()
232 * (self.contents_dims.width - self.container_dims.width).max(0.0)
233 } else {
234 0.0
235 };
236 let y = if self.scrollable_y {
237 self.slider("vert scrollbar").get_percent()
238 * (self.contents_dims.height - self.container_dims.height).max(0.0)
239 } else {
240 0.0
241 };
242 (x, y)
243 }
244
245 fn update_scroll_sliders(&mut self, ctx: &EventCtx, offset: (f64, f64)) -> bool {
246 let mut changed = false;
247 if self.scrollable_x {
248 changed = true;
249 let max = (self.contents_dims.width - self.container_dims.width).max(0.0);
250 if max == 0.0 {
251 self.slider_mut("horiz scrollbar").set_percent(ctx, 0.0);
252 } else {
253 self.slider_mut("horiz scrollbar")
254 .set_percent(ctx, offset.0.clamp(0.0, max) / max);
255 }
256 }
257 if self.scrollable_y {
258 changed = true;
259 let max = (self.contents_dims.height - self.container_dims.height).max(0.0);
260 if max == 0.0 {
261 self.slider_mut("vert scrollbar").set_percent(ctx, 0.0);
262 } else {
263 self.slider_mut("vert scrollbar")
264 .set_percent(ctx, offset.1.clamp(0.0, max) / max);
265 }
266 }
267 changed
268 }
269
270 fn set_scroll_offset(&mut self, ctx: &EventCtx, offset: (f64, f64)) {
271 if self.update_scroll_sliders(ctx, offset) {
272 self.recompute_layout_if_needed(ctx, false);
273 }
274 }
275
276 pub fn event(&mut self, ctx: &mut EventCtx) -> Outcome {
277 if (self.scrollable_x || self.scrollable_y)
278 && ctx
279 .canvas
280 .get_cursor_in_screen_space()
281 .map(|pt| self.top_level.rect.contains(pt))
282 .unwrap_or(false)
283 {
284 if let Some((dx, dy)) = ctx.input.get_mouse_scroll() {
285 let x_offset = if self.scrollable_x {
286 self.scroll_offset().0 - dx * (ctx.canvas.settings.gui_scroll_speed as f64)
287 } else {
288 0.0
289 };
290 let y_offset = if self.scrollable_y {
291 self.scroll_offset().1 - dy * (ctx.canvas.settings.gui_scroll_speed as f64)
292 } else {
293 0.0
294 };
295 self.set_scroll_offset(ctx, (x_offset, y_offset));
296 }
297 }
298
299 if ctx.input.is_window_resized() {
300 self.update_container_dims_for_canvas_dims(ctx.canvas.get_window_dims());
301 self.recompute_layout(ctx, false);
302 }
303
304 let before = self.scroll_offset();
305 let mut output = WidgetOutput::new();
306 self.top_level.widget.event(ctx, &mut output);
307
308 if output.redo_layout {
309 self.recompute_layout(ctx, true);
310 } else if self.scroll_offset() != before {
311 self.recompute_layout_if_needed(ctx, true);
312 }
313
314 if let Outcome::Focused(ref id) = output.outcome {
316 assert!(ctx.next_focus_owned_by.is_none());
317 ctx.next_focus_owned_by = Some(id.clone());
318 }
319
320 output.outcome
321 }
322
323 pub fn draw(&self, g: &mut GfxCtx) {
324 if let Some(ref rect) = self.clip_rect {
325 g.enable_clipping(rect.clone());
326 g.canvas.mark_covered_area(rect.clone());
327 } else {
328 g.canvas.mark_covered_area(self.top_level.rect.clone());
329 }
330
331 if false {
333 g.fork_screenspace();
334 g.draw_polygon(Color::RED.alpha(0.5), self.top_level.rect.to_polygon());
335
336 let top_left = g
337 .canvas
338 .align_window(self.container_dims, self.horiz, self.vert);
339 g.draw_polygon(
340 Color::BLUE.alpha(0.5),
341 Polygon::rectangle(self.container_dims.width, self.container_dims.height)
342 .translate(top_left.x, top_left.y),
343 );
344 g.unfork();
345 }
346
347 self.top_level.draw(g);
348 if self.scrollable_x || self.scrollable_y {
349 g.disable_clipping();
350
351 if self.scrollable_x {
354 self.slider("horiz scrollbar").draw(g);
355 }
356 if self.scrollable_y {
357 self.slider("vert scrollbar").draw(g);
358 }
359 }
360 }
361
362 pub fn get_all_click_actions(&self) -> HashSet<String> {
363 let mut actions = HashSet::new();
364 self.top_level.get_all_click_actions(&mut actions);
365 actions
366 }
367
368 pub fn restore(&mut self, ctx: &mut EventCtx, prev: &Panel) {
369 self.set_scroll_offset(ctx, prev.scroll_offset());
370
371 self.top_level.restore(ctx, prev);
372
373 ctx.no_op_event(true, |ctx| {
375 assert!(matches!(self.event(ctx), Outcome::Nothing))
376 });
377 }
378
379 pub fn restore_scroll(&mut self, ctx: &mut EventCtx, prev: &Panel) {
380 self.set_scroll_offset(ctx, prev.scroll_offset());
381 }
382
383 pub fn scroll_to_member(&mut self, ctx: &EventCtx, name: String) {
384 if let Some(w) = self.top_level.find(&name) {
385 let y1 = w.rect.y1;
386 self.set_scroll_offset(ctx, (0.0, y1));
387 } else {
388 panic!("Can't scroll_to_member of unknown {}", name);
389 }
390 }
391
392 pub fn has_widget(&self, name: &str) -> bool {
393 self.top_level.find(name).is_some()
394 }
395
396 pub fn slider(&self, name: &str) -> &Slider {
397 self.find(name)
398 }
399 pub fn slider_mut(&mut self, name: &str) -> &mut Slider {
400 self.find_mut(name)
401 }
402
403 pub fn take_menu_choice<T: 'static>(&mut self, name: &str) -> T {
404 self.find_mut::<Menu<T>>(name).take_current_choice()
405 }
406
407 pub fn is_checked(&self, name: &str) -> bool {
408 self.find::<Toggle>(name).enabled
409 }
410 pub fn maybe_is_checked(&self, name: &str) -> Option<bool> {
411 if self.has_widget(name) {
412 Some(self.find::<Toggle>(name).enabled)
413 } else {
414 None
415 }
416 }
417 pub fn set_checked(&mut self, name: &str, on_off: bool) {
418 self.find_mut::<Toggle>(name).enabled = on_off
419 }
420
421 pub fn text_box(&self, name: &str) -> String {
422 self.find::<TextBox>(name).get_line()
423 }
424
425 pub fn spinner<T: 'static + SpinnerValue>(&self, name: &str) -> T {
426 self.find::<Spinner<T>>(name).current
427 }
428 pub fn modify_spinner<T: 'static + SpinnerValue>(
429 &mut self,
430 ctx: &EventCtx,
431 name: &str,
432 delta: T,
433 ) {
434 self.find_mut::<Spinner<T>>(name).modify(ctx, delta)
435 }
436
437 pub fn dropdown_value<T: 'static + PartialEq + Clone, I: AsRef<str>>(&self, name: I) -> T {
438 self.find::<Dropdown<T>>(name.as_ref()).current_value()
439 }
440 pub fn maybe_dropdown_value<T: 'static + PartialEq + Clone, I: AsRef<str>>(
441 &self,
442 name: I,
443 ) -> Option<T> {
444 let name = name.as_ref();
445 if self.has_widget(name) {
446 Some(self.find::<Dropdown<T>>(name).current_value())
447 } else {
448 None
449 }
450 }
451 pub fn persistent_split_value<T: 'static + PartialEq + Clone>(&self, name: &str) -> T {
452 self.find::<PersistentSplit<T>>(name).current_value()
453 }
454
455 pub fn autocomplete_done<T: 'static + Clone>(&mut self, name: &str) -> Option<Vec<T>> {
458 self.find_mut::<Autocomplete<T>>(name).take_final_value()
459 }
460
461 pub fn stash<T: 'static>(&self, name: &str) -> Rc<RefCell<T>> {
463 self.find::<Stash<T>>(name).get_value()
464 }
465
466 pub fn clone_stashed<T: 'static + Clone>(&self, name: &str) -> T {
468 self.find::<Stash<T>>(name).get_value().borrow().clone()
469 }
470
471 pub fn is_button_enabled(&self, name: &str) -> bool {
472 self.find::<Button>(name).is_enabled()
473 }
474
475 pub fn maybe_find_widget(&self, name: &str) -> Option<&Widget> {
476 self.top_level.find(name)
477 }
478
479 pub fn maybe_find<T: WidgetImpl>(&self, name: &str) -> Option<&T> {
480 self.maybe_find_widget(name).map(|w| {
481 if let Some(x) = w.widget.downcast_ref::<T>() {
482 x
483 } else {
484 panic!("Found widget {}, but wrong type", name);
485 }
486 })
487 }
488
489 pub fn find<T: WidgetImpl>(&self, name: &str) -> &T {
490 self.maybe_find(name)
491 .unwrap_or_else(|| panic!("Can't find widget {}", name))
492 }
493
494 pub fn find_mut<T: WidgetImpl>(&mut self, name: &str) -> &mut T {
495 if let Some(w) = self.top_level.find_mut(name) {
496 if let Some(x) = w.widget.downcast_mut::<T>() {
497 x
498 } else {
499 panic!("Found widget {}, but wrong type", name);
500 }
501 } else {
502 panic!("Can't find widget {}", name);
503 }
504 }
505
506 pub(crate) fn swap_inner_content(
508 &mut self,
509 ctx: &EventCtx,
510 container_name: &str,
511 new_inner_content: &mut Widget,
512 ) {
513 let old_container: &mut Container = self.find_mut(container_name);
514 assert_eq!(
515 old_container.members.len(),
516 1,
517 "method only intended to be used for containers created with `Widget::container`"
518 );
519 std::mem::swap(&mut old_container.members[0], new_inner_content);
520 self.recompute_layout(ctx, true);
521 }
522
523 pub fn rect_of(&self, name: &str) -> &ScreenRectangle {
524 &self.top_level.find(name).unwrap().rect
525 }
526 pub fn center_of(&self, name: &str) -> ScreenPt {
528 self.rect_of(name).center()
529 }
530 pub fn center_of_panel(&self) -> ScreenPt {
531 self.top_level.rect.center()
532 }
533 pub fn panel_rect(&self) -> &ScreenRectangle {
534 &self.top_level.rect
535 }
536 pub fn panel_dims(&self) -> ScreenDims {
537 self.top_level.rect.dims()
538 }
539
540 pub fn align(&mut self, horiz: HorizontalAlignment, vert: VerticalAlignment) {
541 self.horiz = horiz;
542 self.vert = vert;
543 }
545
546 pub fn replace(&mut self, ctx: &mut EventCtx, id: &str, mut new: Widget) {
549 if let Some(ref new_id) = new.id {
550 assert_eq!(id, new_id);
551 }
552 new = new.named(id);
553 let old = self
554 .top_level
555 .find_mut(id)
556 .unwrap_or_else(|| panic!("Panel doesn't have {}", id));
557 new.layout.style = old.layout.style;
558 *old = new;
559 self.recompute_layout(ctx, true);
560 }
564
565 pub fn take(&mut self, id: &str) -> Widget {
567 self.top_level.take(id).unwrap()
568 }
569
570 pub fn clicked_outside(&self, ctx: &mut EventCtx) -> bool {
571 !self.top_level.rect.contains(ctx.canvas.get_cursor()) && ctx.normal_left_click()
573 }
574
575 pub fn currently_hovering(&self) -> Option<&String> {
576 self.top_level.currently_hovering()
577 }
578}
579
580pub struct PanelBuilder {
581 top_level: Widget,
582 horiz: HorizontalAlignment,
583 vert: VerticalAlignment,
584 dims_x: PanelDims,
585 dims_y: PanelDims,
586 ignore_initial_events: bool,
587}
588
589#[derive(Clone, Copy)]
590pub enum PanelDims {
591 MaxPercent(f64),
592 ExactPercent(f64),
593 ExactPixels(f64),
594}
595
596impl PanelBuilder {
597 pub fn build(mut self, ctx: &mut EventCtx) -> Panel {
598 self.top_level = self.top_level.padding(16).bg(ctx.style.panel_bg);
599 self.build_custom(ctx)
600 }
601
602 pub fn build_custom(self, ctx: &mut EventCtx) -> Panel {
603 let ignore_initial_events = self.ignore_initial_events;
604 let mut panel = Panel {
605 top_level: self.top_level,
606
607 horiz: self.horiz,
608 vert: self.vert,
609 dims_x: self.dims_x,
610 dims_y: self.dims_y,
611
612 scrollable_x: false,
613 scrollable_y: false,
614 contents_dims: ScreenDims::new(0.0, 0.0),
615 container_dims: ScreenDims::new(0.0, 0.0),
616 clip_rect: None,
617 cached_flexbox: None,
618 };
619 match self.dims_x {
620 PanelDims::MaxPercent(_) => {}
621 PanelDims::ExactPercent(pct) => {
622 panel.top_level.layout.style.min_size.width =
625 Dimension::Points((pct * ctx.canvas.window_width) as f32);
626 }
627 PanelDims::ExactPixels(x) => {
628 panel.top_level.layout.style.min_size.width = Dimension::Points(x as f32);
629 }
630 }
631 match self.dims_y {
632 PanelDims::MaxPercent(_) => {}
633 PanelDims::ExactPercent(pct) => {
634 panel.top_level.layout.style.min_size.height =
635 Dimension::Points((pct * ctx.canvas.window_height) as f32);
636 }
637 PanelDims::ExactPixels(x) => {
638 panel.top_level.layout.style.min_size.height = Dimension::Points(x as f32);
639 }
640 }
641
642 panel.recompute_layout(ctx, false);
654 panel.contents_dims =
655 ScreenDims::new(panel.top_level.rect.width(), panel.top_level.rect.height());
656 panel.update_container_dims_for_canvas_dims(ctx.canvas.get_window_dims());
657 panel.recompute_layout(ctx, false);
658
659 panel.get_all_click_actions();
661 ctx.no_op_event(true, |ctx| {
663 if ignore_initial_events {
664 panel.event(ctx);
665 } else {
666 let outcome = panel.event(ctx);
667 if !matches!(outcome, Outcome::Nothing) {
668 panic!(
669 "Initial panel outcome is {}. Consider calling ignore_initial_events",
670 outcome.describe()
671 );
672 }
673 }
674 });
675 panel
676 }
677
678 pub fn aligned(mut self, horiz: HorizontalAlignment, vert: VerticalAlignment) -> PanelBuilder {
679 self.horiz = horiz;
680 self.vert = vert;
681 self
682 }
683
684 pub fn aligned_pair(mut self, pair: (HorizontalAlignment, VerticalAlignment)) -> PanelBuilder {
685 self.horiz = pair.0;
686 self.vert = pair.1;
687 self
688 }
689
690 pub fn dims_width(mut self, dims: PanelDims) -> PanelBuilder {
691 self.dims_x = dims;
692 self
693 }
694
695 pub fn dims_height(mut self, dims: PanelDims) -> PanelBuilder {
696 self.dims_y = dims;
697 self
698 }
699
700 pub fn exact_size_percent(self, x: usize, y: usize) -> PanelBuilder {
702 self.dims_width(PanelDims::ExactPercent((x as f64) / 100.0))
703 .dims_height(PanelDims::ExactPercent((y as f64) / 100.0))
704 }
705
706 pub fn ignore_initial_events(mut self) -> PanelBuilder {
714 self.ignore_initial_events = true;
715 self
716 }
717}