widgetry/widgets/
panel.rs

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    // (layout, root_dims)
24    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    /// Returns an empty panel. `event` and `draw` will have no effect.
50    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        // Unwrap the main widget from any scrollable containers if necessary.
79        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        // TODO Handle making room for a horizontal scrollbar on the bottom. The equivalent change
101        // to container_dims.height doesn't work as expected.
102        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        // Wrap the main widget in scrollable containers if necessary.
110        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            // We constantly destroy and recreate the scrollbar slider while dragging it. Preserve
120            // the dragging property, so we can keep dragging it.
121            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    // TODO: this method potentially gets called multiple times in a render pass as an
157    // optimization, we could replace all the current call sites with a "dirty" flag, e.g.
158    // `set_needs_layout()` and then call `layout_if_needed()` once at the last possible moment
159    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        // TODO Express more simply. Constraining this seems useless.
184        let container_size = Size {
185            width: AvailableSpace::MaxContent,
186            height: AvailableSpace::MaxContent,
187        };
188        taffy.compute_layout(root, container_size).unwrap();
189
190        // TODO I'm so confused why these 2 are acting differently. :(
191        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        // Remember this for the next event
315        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        // Debugging
332        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            // Draw the scrollbars after clipping is disabled, because they actually live just
352            // outside the rectangle.
353            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        // Since we just moved things around, let all widgets respond to the mouse being somewhere
374        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    /// Consumes the autocomplete widget. It's fine if the panel survives past this event; the
456    /// autocomplete just needs to be interacted with again to produce more values.
457    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    /// Grab a stashed value, with the ability to pass it around and modify it.
462    pub fn stash<T: 'static>(&self, name: &str) -> Rc<RefCell<T>> {
463        self.find::<Stash<T>>(name).get_value()
464    }
465
466    /// Grab a stashed value and clone it.
467    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    /// Swap the inner content of a `container` widget with `new_inner_content`.
507    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    // TODO Deprecate
527    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        // TODO Recompute layout and fire no_op_event?
544    }
545
546    /// All margins/padding/etc from the previous widget are retained. The ID is set on the new
547    /// widget; no need to do that yourself.
548    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        // TODO Since we just moved things around, let all widgets respond to the mouse being
561        // somewhere? Maybe always do this in recompute_layout?
562        //ctx.no_op_event(true, |ctx| assert!(matches!(self.event(ctx), Outcome::Nothing)));
563    }
564
565    /// Removes a widget from the panel. Does not recalculate layout!
566    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        // TODO No great way to populate OSD from here with "click to cancel"
572        !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                // Don't set size, because then scrolling breaks -- the actual size has to be based
623                // on the contents.
624                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        // There is a dependency cycle in our layout logic. As a consequence:
643        //   1. we have to call `recompute_layout` twice here
644        //   2. panels don't responsively change `contents_dims`
645        //
646        // - `panel.top_level.rect`, used here to set content_dims, is set by `recompute_layout`.
647        // - the output of `recompute_layout` depends on `container_dims`
648        // - `container_dims`, in the case of `MaxPercent`, depend on `content_dims`
649        //
650        // TODO: to support Panel's that can resize their `contents_dims`, we'll need to detangle
651        // this dependency. This might entail decomposing the flexbox calculation to layout first
652        // the inner content, and then potentially a second pass to layout any x/y scrollbars.
653        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        // Just trigger error if a button is double-defined
660        panel.get_all_click_actions();
661        // Let all widgets initially respond to the mouse being somewhere
662        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    // TODO Change all callers
701    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    /// When a panel is built, a fake, "no-op" mouseover event is immediately fired, to let all
707    /// widgets initially pick up the position of the mouse. Normally this event should only
708    /// produce `Outcome::Nothing`, since other outcomes will be lost -- there's no way for the
709    /// caller to see that first outcome.
710    ///
711    /// If a caller expects this first mouseover to possibly produce an outcome, they can call this
712    /// and avoid the assertion.
713    pub fn ignore_initial_events(mut self) -> PanelBuilder {
714        self.ignore_initial_events = true;
715        self
716    }
717}