widgetry/widgets/
mod.rs

1use std::collections::HashSet;
2
3use taffy::geometry::{Rect, Size};
4use taffy::layout::AvailableSpace;
5use taffy::node::{Node, Taffy};
6use taffy::style::{
7    AlignItems, Dimension, FlexDirection, FlexWrap, JustifyContent, PositionType, Style,
8};
9
10use abstutil::CloneableAny;
11use geom::{CornerRadii, Distance, Percent, Polygon};
12
13use crate::widgets::containers::{Container, Nothing};
14pub use crate::widgets::panel::{Panel, PanelBuilder, PanelDims};
15use crate::{
16    Button, Choice, Color, DeferDraw, Drawable, Dropdown, EventCtx, GeomBatch, GfxCtx, JustDraw,
17    OutlineStyle, ScreenDims, ScreenPt, ScreenRectangle, Text, Toggle,
18};
19
20pub mod autocomplete;
21pub mod button;
22pub mod compare_times;
23pub mod containers;
24pub mod drag_drop;
25pub mod dropdown;
26pub mod fan_chart;
27pub mod filler;
28pub mod image;
29pub mod just_draw;
30pub mod line_plot;
31pub mod menu;
32mod panel;
33pub mod persistent_split;
34pub mod plots;
35pub mod scatter_plot;
36pub mod slider;
37pub mod spinner;
38pub mod stash;
39pub mod table;
40pub mod tabs;
41pub mod text_box;
42pub mod toggle;
43
44pub const DEFAULT_CORNER_RADIUS: f64 = 5.0;
45
46/// Create a new widget by implementing this trait. You can instantiate your widget by calling
47/// `Widget::new(Box::new(instance of your new widget))`, which gives you the usual style options.
48pub trait WidgetImpl: downcast_rs::Downcast {
49    /// What width and height does the widget occupy? If this changes, be sure to set
50    /// `redo_layout` to true in `event`.
51    fn get_dims(&self) -> ScreenDims;
52    /// Your widget's top left corner should be here. Handle mouse events and draw appropriately.
53    fn set_pos(&mut self, top_left: ScreenPt);
54    /// Your chance to react to an event. Any side effects outside of this widget are communicated
55    /// through the output.
56    fn event(&mut self, ctx: &mut EventCtx, output: &mut WidgetOutput);
57    /// Draw the widget. Be sure to draw relative to the top-left specified by `set_pos`.
58    fn draw(&self, g: &mut GfxCtx);
59    /// If a new Panel is being created to replace an older one, all widgets have the chance to
60    /// preserve state from the previous version.
61    fn can_restore(&self) -> bool {
62        false
63    }
64    /// Restore state from the previous version of this widget, with the same ID. Implementors must
65    /// downcast.
66    fn restore(&mut self, _: &mut EventCtx, _prev: &dyn WidgetImpl) {
67        unreachable!()
68    }
69}
70
71/// The result of a Panel handling an event
72pub enum Outcome {
73    /// An action was done
74    Clicked(String),
75    /// An action was done, with custom data. The caller must cast to the proper type.
76    ClickCustom(Box<dyn CloneableAny>),
77    /// A dropdown, checkbox, spinner, etc changed values. The name of the changed widget is
78    /// returned, but not the value, since its type is generic.
79    Changed(String),
80    /// On a DragDrop widget, a member was clicked on and released. Its position may have changed.
81    /// (name, orig_idx, new_idx)
82    DragDropReleased(String, usize, usize),
83    /// Some named widget currently holds focus
84    Focused(String),
85    /// Nothing happened
86    Nothing,
87}
88
89impl Outcome {
90    pub(crate) fn describe(&self) -> String {
91        match self {
92            Outcome::Clicked(x) => format!("Outcome::Clicked({x})"),
93            Outcome::ClickCustom(_) => format!("Outcome::ClickCustom(???)"),
94            Outcome::Changed(x) => format!("Outcome::Changed({x})"),
95            Outcome::DragDropReleased(x, _, _) => format!("Outcome::DragDropReleased({x}, ...)"),
96            Outcome::Focused(x) => format!("Outcome::Focused({x})"),
97            Outcome::Nothing => format!("Outcome::Nothing"),
98        }
99    }
100}
101
102/// When an action happens through a button-like widget, what data is plumbed back?
103pub enum ClickOutcome {
104    Label(String),
105    Custom(Box<dyn CloneableAny>),
106}
107
108pub struct WidgetOutput {
109    /// This widget changed dimensions, so recalculate layout.
110    pub redo_layout: bool,
111    /// This widget produced an Outcome, and event handling should immediately stop. Most widgets
112    /// shouldn't set this.
113    pub outcome: Outcome,
114}
115
116impl WidgetOutput {
117    pub fn new() -> WidgetOutput {
118        WidgetOutput {
119            redo_layout: false,
120            outcome: Outcome::Nothing,
121        }
122    }
123}
124
125downcast_rs::impl_downcast!(WidgetImpl);
126
127pub struct Widget {
128    // TODO pub just for Container. Just move that here?
129    pub(crate) widget: Box<dyn WidgetImpl>,
130    layout: LayoutStyle,
131    pub(crate) rect: ScreenRectangle,
132    bg: Option<Drawable>,
133    // to_geom forces this one to happen
134    bg_batch: Option<GeomBatch>,
135    id: Option<String>,
136}
137
138#[derive(Debug, Clone, Copy)]
139pub enum CornerRounding {
140    CornerRadii(CornerRadii),
141    FullyRounded,
142    NoRounding,
143}
144
145impl std::convert::From<f64> for CornerRounding {
146    fn from(uniform: f64) -> Self {
147        CornerRounding::CornerRadii(uniform.into())
148    }
149}
150
151impl std::convert::From<CornerRadii> for CornerRounding {
152    fn from(radii: CornerRadii) -> Self {
153        CornerRounding::CornerRadii(radii)
154    }
155}
156
157impl Default for CornerRounding {
158    fn default() -> Self {
159        CornerRounding::CornerRadii(CornerRadii::default())
160    }
161}
162
163struct LayoutStyle {
164    bg_color: Option<Color>,
165    // (thickness, color)
166    outline: Option<(f64, Color)>,
167    corner_rounding: CornerRounding,
168    style: Style,
169}
170
171// Layouting
172// TODO Maybe I just want margin, not padding. And maybe more granular controls per side. And to
173// apply margin to everything in a row or column.
174// TODO Row and columns feel backwards when using them.
175impl Widget {
176    pub fn centered(mut self) -> Widget {
177        self.layout.style.align_items = AlignItems::Center;
178        self.layout.style.justify_content = JustifyContent::SpaceAround;
179        self
180    }
181
182    pub fn centered_horiz(self) -> Widget {
183        Widget::row(vec![self]).centered()
184    }
185
186    pub fn centered_vert(self) -> Widget {
187        Widget::col(vec![self]).centered()
188    }
189
190    pub fn centered_cross(mut self) -> Widget {
191        self.layout.style.align_items = AlignItems::Center;
192        self
193    }
194
195    pub fn evenly_spaced(mut self) -> Widget {
196        self.layout.style.justify_content = JustifyContent::SpaceBetween;
197        self
198    }
199
200    pub fn fill_width(mut self) -> Widget {
201        self.layout.style.size.width = Dimension::Percent(1.0);
202        self
203    }
204    pub fn fill_height(mut self) -> Widget {
205        self.layout.style.size.height = Dimension::Percent(1.0);
206        self
207    }
208
209    /// This one is really weird. percent_width should be LESS than the max_size_percent given to
210    /// the overall Panel, otherwise weird things happen.
211    /// Only makes sense for rows/columns.
212    pub fn flex_wrap(mut self, ctx: &EventCtx, width: Percent) -> Widget {
213        self.layout.style.size = Size {
214            width: Dimension::Points((ctx.canvas.window_width * width.inner()) as f32),
215            height: Dimension::Undefined,
216        };
217        self.layout.style.flex_wrap = FlexWrap::Wrap;
218        self.layout.style.justify_content = JustifyContent::SpaceAround;
219        self
220    }
221    /// Like flex_wrap, but doesn't horizontally space out widgets on the same row.
222    pub fn flex_wrap_no_inner_spacing(mut self, ctx: &EventCtx, width: Percent) -> Widget {
223        self.layout.style.size = Size {
224            width: Dimension::Points((ctx.canvas.window_width * width.inner()) as f32),
225            height: Dimension::Undefined,
226        };
227        self.layout.style.flex_wrap = FlexWrap::Wrap;
228        self
229    }
230    /// Only for rows/columns. Used to force table columns to line up.
231    pub fn force_width(mut self, width: f64) -> Widget {
232        self.layout.style.size.width = Dimension::Points(width as f32);
233        self
234    }
235    pub fn force_width_window_pct(mut self, ctx: &EventCtx, width: Percent) -> Widget {
236        self.layout.style.size.width =
237            Dimension::Points((ctx.canvas.window_width * width.inner()) as f32);
238        self
239    }
240    pub fn force_width_parent_pct(mut self, width: f64) -> Widget {
241        self.layout.style.size.width = Dimension::Percent(width as f32);
242        self
243    }
244
245    /// Needed for force_width.
246    pub fn get_width_for_forcing(&self) -> f64 {
247        self.widget.get_dims().width
248    }
249
250    pub fn bg(mut self, color: Color) -> Widget {
251        self.layout.bg_color = Some(color);
252        self
253    }
254
255    /// Callers have to adjust padding too, probably
256    pub fn outline(mut self, style: OutlineStyle) -> Widget {
257        self.layout.outline = Some(style);
258        self
259    }
260
261    pub fn corner_rounding<R: Into<CornerRounding>>(mut self, value: R) -> Widget {
262        self.layout.corner_rounding = value.into();
263        self
264    }
265
266    /// Things like padding don't work on many widgets, so just make a convenient way to wrap in a
267    /// row/column first
268    pub fn container(self) -> Widget {
269        Widget::row(vec![self])
270    }
271
272    pub fn section(self, ctx: &EventCtx) -> Widget {
273        self.bg(ctx.style().section_bg)
274            .padding(16)
275            .outline(ctx.style().section_outline)
276    }
277
278    pub fn tab_body(self, ctx: &EventCtx) -> Widget {
279        let mut tab_body =
280            self.bg(ctx.style().section_bg)
281                .padding(16)
282                .corner_rounding(CornerRadii {
283                    top_left: 0.0,
284                    top_right: DEFAULT_CORNER_RADIUS,
285                    bottom_left: DEFAULT_CORNER_RADIUS,
286                    bottom_right: DEFAULT_CORNER_RADIUS,
287                });
288
289        // really short tab bodies look out of place in the panels
290        tab_body.layout.style.min_size.height = Dimension::Points(200.0);
291        tab_body
292    }
293
294    // TODO Maybe panic if we call this on a non-container
295    pub fn padding<I: Into<EdgeInsets>>(mut self, insets: I) -> Widget {
296        let insets = insets.into();
297        self.layout.style.padding = Rect::from(insets);
298        self
299    }
300
301    pub fn padding_top(mut self, pixels: usize) -> Widget {
302        self.layout.style.padding.top = Dimension::Points(pixels as f32);
303        self
304    }
305
306    pub fn padding_left(mut self, pixels: usize) -> Widget {
307        self.layout.style.padding.left = Dimension::Points(pixels as f32);
308        self
309    }
310
311    pub fn padding_bottom(mut self, pixels: usize) -> Widget {
312        self.layout.style.padding.bottom = Dimension::Points(pixels as f32);
313        self
314    }
315
316    pub fn padding_right(mut self, pixels: usize) -> Widget {
317        self.layout.style.padding.right = Dimension::Points(pixels as f32);
318        self
319    }
320
321    pub fn margin<I: Into<EdgeInsets>>(mut self, insets: I) -> Widget {
322        let insets = insets.into();
323        self.layout.style.margin = Rect::from(insets);
324        self
325    }
326
327    pub fn margin_above(mut self, pixels: usize) -> Widget {
328        self.layout.style.margin.top = Dimension::Points(pixels as f32);
329        self
330    }
331    pub fn margin_below(mut self, pixels: usize) -> Widget {
332        self.layout.style.margin.bottom = Dimension::Points(pixels as f32);
333        self
334    }
335    pub fn margin_left(mut self, pixels: usize) -> Widget {
336        self.layout.style.margin.left = Dimension::Points(pixels as f32);
337        self
338    }
339    pub fn margin_right(mut self, pixels: usize) -> Widget {
340        self.layout.style.margin.right = Dimension::Points(pixels as f32);
341        self
342    }
343    pub fn margin_horiz(mut self, pixels: usize) -> Widget {
344        self.layout.style.margin.left = Dimension::Points(pixels as f32);
345        self.layout.style.margin.right = Dimension::Points(pixels as f32);
346        self
347    }
348    pub fn margin_vert(mut self, pixels: usize) -> Widget {
349        self.layout.style.margin.top = Dimension::Points(pixels as f32);
350        self.layout.style.margin.bottom = Dimension::Points(pixels as f32);
351        self
352    }
353
354    pub fn align_left(mut self) -> Widget {
355        self.layout.style.margin.right = Dimension::Auto;
356        self
357    }
358    pub fn align_right(mut self) -> Widget {
359        self.layout.style.margin = Rect {
360            left: Dimension::Auto,
361            right: Dimension::Undefined,
362            top: Dimension::Undefined,
363            bottom: Dimension::Undefined,
364        };
365        self
366    }
367    pub fn align_bottom(mut self) -> Widget {
368        self.layout.style.margin = Rect {
369            left: Dimension::Undefined,
370            right: Dimension::Undefined,
371            top: Dimension::Auto,
372            bottom: Dimension::Undefined,
373        };
374        self
375    }
376    /// This doesn't count against the entire container
377    pub fn align_vert_center(mut self) -> Widget {
378        self.layout.style.margin = Rect {
379            left: Dimension::Undefined,
380            right: Dimension::Undefined,
381            top: Dimension::Auto,
382            bottom: Dimension::Auto,
383        };
384        self
385    }
386
387    fn abs(mut self, x: f64, y: f64) -> Widget {
388        self.layout.style.position_type = PositionType::Absolute;
389        self.layout.style.position = Rect {
390            left: Dimension::Points(x as f32),
391            right: Dimension::Undefined,
392            top: Dimension::Points(y as f32),
393            bottom: Dimension::Undefined,
394        };
395        self
396    }
397
398    pub fn named<I: Into<String>>(mut self, id: I) -> Widget {
399        self.id = Some(id.into());
400        self
401    }
402
403    /// If the argument is true, don't actually create this widget. May be more readable than an
404    /// if/else block.
405    pub fn hide(self, x: bool) -> Widget {
406        if x {
407            Widget::nothing()
408        } else {
409            self
410        }
411    }
412}
413
414// Convenient?? constructors
415impl Widget {
416    pub(crate) fn new(widget: Box<dyn WidgetImpl>) -> Widget {
417        Widget {
418            widget,
419            layout: LayoutStyle {
420                bg_color: None,
421                outline: None,
422                corner_rounding: CornerRounding::from(DEFAULT_CORNER_RADIUS),
423                style: Style {
424                    ..Default::default()
425                },
426            },
427            rect: ScreenRectangle::placeholder(),
428            bg: None,
429            bg_batch: None,
430            id: None,
431        }
432    }
433
434    // TODO Change this to Dropdown::widget
435    pub fn dropdown<T: 'static + PartialEq + Clone + std::fmt::Debug, I: AsRef<str>>(
436        ctx: &EventCtx,
437        label: I,
438        default_value: T,
439        choices: Vec<Choice<T>>,
440    ) -> Widget {
441        let label = label.as_ref();
442        Widget::new(Box::new(Dropdown::new(
443            ctx,
444            label,
445            default_value,
446            choices,
447            false,
448        )))
449        .named(label)
450    }
451
452    /// Creates a row with the specified widgets. No margins or other layouting is applied.
453    pub fn custom_row(widgets: Vec<Widget>) -> Widget {
454        Widget::new(Box::new(Container::new(true, widgets)))
455    }
456
457    /// Creates a row with the specified widgets. Every member gets a default horizontal margin.
458    pub fn row(widgets: Vec<Widget>) -> Widget {
459        Widget::evenly_spaced_row(10, widgets)
460    }
461
462    /// Creates a row with the specified widgets, with a `spacing` sized margin between members
463    pub fn evenly_spaced_row(spacing: usize, widgets: Vec<Widget>) -> Widget {
464        let mut new = Vec::new();
465        let len = widgets.len();
466        // TODO Time for that is_last iterator?
467        for (idx, w) in widgets.into_iter().enumerate() {
468            if idx == len - 1 {
469                new.push(w);
470            } else {
471                new.push(w.margin_right(spacing));
472            }
473        }
474        Widget::new(Box::new(Container::new(true, new)))
475    }
476
477    /// Creates a column with the specified widgets. No margins or other layouting is applied.
478    pub fn custom_col(widgets: Vec<Widget>) -> Widget {
479        Widget::new(Box::new(Container::new(false, widgets)))
480    }
481
482    /// Creates a column with the specified widgets, with a `spacing` sized margin between members
483    pub fn evenly_spaced_col(spacing: usize, widgets: Vec<Widget>) -> Widget {
484        let mut new = Vec::new();
485        let len = widgets.len();
486        // TODO Time for that is_last iterator?
487        for (idx, w) in widgets.into_iter().enumerate() {
488            if idx == len - 1 {
489                new.push(w);
490            } else {
491                new.push(w.margin_below(spacing));
492            }
493        }
494        Widget::new(Box::new(Container::new(false, new)))
495    }
496
497    /// Creates a column with the specified widgets. Every member gets a default vertical margin.
498    pub fn col(widgets: Vec<Widget>) -> Widget {
499        Self::evenly_spaced_col(10, widgets)
500    }
501
502    pub fn nothing() -> Widget {
503        Widget::new(Box::new(Nothing {}))
504    }
505
506    // Also returns the hitbox of the entire widget
507    pub fn into_geom(
508        mut self,
509        ctx: &EventCtx,
510        exact_pct_width: Option<f64>,
511    ) -> (GeomBatch, Polygon) {
512        if let Some(w) = exact_pct_width {
513            // TODO 35 is a sad magic number. By default, Panels have padding of 16, so assuming
514            // this geometry is going in one of those, it makes sense to subtract 32. But that still
515            // caused some scrolling in a test, so snip away a few more pixels.
516            self.layout.style.min_size.width =
517                Dimension::Points((w * ctx.canvas.window_width) as f32 - 35.0);
518        }
519
520        // Pretend we're in a Panel and basically copy recompute_layout
521        {
522            let mut taffy = Taffy::new();
523            let root = taffy
524                .new_with_children(
525                    Style {
526                        ..Default::default()
527                    },
528                    &[],
529                )
530                .unwrap();
531
532            let mut nodes = vec![];
533            self.get_flexbox(root, &mut taffy, &mut nodes);
534            nodes.reverse();
535
536            let container_size = Size {
537                width: AvailableSpace::MaxContent,
538                height: AvailableSpace::MaxContent,
539            };
540            taffy.compute_layout(root, container_size).unwrap();
541
542            self.apply_flexbox(&taffy, &mut nodes, 0.0, 0.0, (0.0, 0.0), ctx, true, true);
543            assert!(nodes.is_empty());
544        }
545
546        // Now build one big batch from all of the geometry, which now has the correct top left
547        // position.
548        let hitbox = self.rect.to_polygon();
549        let mut batch = GeomBatch::new();
550        self.consume_geometry(&mut batch);
551        batch.autocrop_dims = false;
552        (batch, hitbox)
553    }
554
555    pub fn horiz_separator(ctx: &EventCtx, pct_container_width: f64) -> Widget {
556        GeomBatch::from(vec![(Color::CLEAR, Polygon::rectangle(0.1, 2.0))])
557            .into_widget(ctx)
558            .container()
559            .bg(ctx.style().section_outline.1)
560            .force_width_parent_pct(pct_container_width)
561            .centered_horiz()
562    }
563
564    pub fn vert_separator(ctx: &EventCtx, height_px: f64) -> Widget {
565        GeomBatch::from(vec![(
566            ctx.style().section_outline.1,
567            Polygon::rectangle(2.0, height_px),
568        )])
569        .into_widget(ctx)
570    }
571
572    // TODO Clean up other uses
573    pub fn vertical_separator(ctx: &EventCtx) -> Widget {
574        let thickness = 3.0;
575        GeomBatch::from(vec![(Color::CLEAR, Polygon::rectangle(thickness, 0.1))])
576            .into_widget(ctx)
577            .container()
578            .bg(ctx.style().section_outline.1)
579    }
580
581    pub fn placeholder(ctx: &EventCtx, label: &str) -> Widget {
582        Text::new().into_widget(ctx).named(label)
583    }
584}
585
586// Internals
587impl Widget {
588    pub(crate) fn draw(&self, g: &mut GfxCtx) {
589        // Don't draw these yet; clipping is still in effect.
590        if self.id == Some("horiz scrollbar".to_string())
591            || self.id == Some("vert scrollbar".to_string())
592        {
593            return;
594        }
595
596        if let Some(ref bg) = self.bg {
597            g.redraw_at(ScreenPt::new(self.rect.x1, self.rect.y1), bg);
598        }
599
600        self.widget.draw(g);
601    }
602
603    // Populate a flattened list of Nodes, matching the traversal order
604    fn get_flexbox(&self, parent: Node, taffy: &mut Taffy, nodes: &mut Vec<Node>) {
605        let mut style = self.layout.style;
606        if let Some(container) = self.widget.downcast_ref::<Container>() {
607            style.flex_direction = if container.is_row {
608                FlexDirection::Row
609            } else {
610                FlexDirection::Column
611            };
612            let node = taffy.new_with_children(style, &[]).unwrap();
613            nodes.push(node);
614            for widget in &container.members {
615                widget.get_flexbox(node, taffy, nodes);
616            }
617            taffy.add_child(parent, node).unwrap();
618        } else {
619            style.size = Size {
620                width: Dimension::Points(self.widget.get_dims().width as f32),
621                height: Dimension::Points(self.widget.get_dims().height as f32),
622            };
623            let node = taffy.new_with_children(style, &[]).unwrap();
624            taffy.add_child(parent, node).unwrap();
625            nodes.push(node);
626        }
627    }
628
629    // TODO Clean up argument passing
630    fn apply_flexbox(
631        &mut self,
632        taffy: &Taffy,
633        nodes: &mut Vec<Node>,
634        dx: f64,
635        dy: f64,
636        scroll_offset: (f64, f64),
637        ctx: &EventCtx,
638        recompute_layout: bool,
639        defer_draw: bool,
640    ) {
641        let result = taffy.layout(nodes.pop().unwrap()).unwrap();
642        let x: f64 = result.location.x.into();
643        let y: f64 = result.location.y.into();
644        let width: f64 = result.size.width.into();
645        let height: f64 = result.size.height.into();
646        // Don't scroll the scrollbars
647        let top_left = if self.id == Some("horiz scrollbar".to_string())
648            || self.id == Some("vert scrollbar".to_string())
649        {
650            ScreenPt::new(x, y)
651        } else {
652            ScreenPt::new(x + dx - scroll_offset.0, y + dy - scroll_offset.1)
653        };
654        self.rect = ScreenRectangle::top_left(top_left, ScreenDims::new(width, height));
655
656        // Assume widgets don't dynamically change, so we just upload the background once.
657        if (self.bg.is_none() || recompute_layout)
658            && (self.layout.bg_color.is_some() || self.layout.outline.is_some())
659        {
660            let mut batch = GeomBatch::new();
661            if let Some(color) = self.layout.bg_color {
662                batch.push(
663                    color,
664                    match self.layout.corner_rounding {
665                        CornerRounding::CornerRadii(corner_radii) => {
666                            Polygon::rounded_rectangle(width, height, corner_radii)
667                        }
668                        CornerRounding::FullyRounded => Polygon::pill(width, height),
669                        CornerRounding::NoRounding => Polygon::rectangle(width, height),
670                    },
671                );
672            }
673
674            if let Some((thickness, color)) = self.layout.outline {
675                batch.push(
676                    color,
677                    match self.layout.corner_rounding {
678                        CornerRounding::CornerRadii(corner_radii) => {
679                            Polygon::rounded_rectangle(width, height, corner_radii)
680                        }
681                        CornerRounding::FullyRounded => Polygon::pill(width, height),
682                        CornerRounding::NoRounding => Polygon::rectangle(width, height),
683                    }
684                    .to_outline(Distance::meters(thickness)),
685                );
686            }
687            if defer_draw {
688                self.bg_batch = Some(batch);
689            } else {
690                self.bg = Some(ctx.upload(batch));
691            }
692        }
693
694        if let Some(container) = self.widget.downcast_mut::<Container>() {
695            // layout() doesn't return absolute position; it's relative to the container.
696            for widget in &mut container.members {
697                widget.apply_flexbox(
698                    taffy,
699                    nodes,
700                    x + dx,
701                    y + dy,
702                    scroll_offset,
703                    ctx,
704                    recompute_layout,
705                    defer_draw,
706                );
707            }
708        } else {
709            self.widget.set_pos(top_left);
710        }
711    }
712
713    fn get_all_click_actions(&self, actions: &mut HashSet<String>) {
714        if let Some(btn) = self.widget.downcast_ref::<Button>() {
715            if btn.is_enabled() {
716                if actions.contains(&btn.action) {
717                    panic!("Two buttons in one Panel both use action {}", btn.action);
718                }
719                actions.insert(btn.action.clone());
720            }
721        } else if let Some(container) = self.widget.downcast_ref::<Container>() {
722            for w in &container.members {
723                w.get_all_click_actions(actions);
724            }
725        }
726    }
727
728    fn currently_hovering(&self) -> Option<&String> {
729        if let Some(btn) = self.widget.downcast_ref::<Button>() {
730            if btn.hovering {
731                return Some(&btn.action);
732            }
733        } else if let Some(checkbox) = self.widget.downcast_ref::<Toggle>() {
734            if checkbox.btn.hovering {
735                return Some(&checkbox.btn.action);
736            }
737        } else if let Some(container) = self.widget.downcast_ref::<Container>() {
738            for w in &container.members {
739                if let Some(a) = w.currently_hovering() {
740                    return Some(a);
741                }
742            }
743        }
744        None
745    }
746
747    fn restore(&mut self, ctx: &mut EventCtx, prev: &Panel) {
748        if let Some(container) = self.widget.downcast_mut::<Container>() {
749            for w in &mut container.members {
750                w.restore(ctx, prev);
751            }
752        } else if self.widget.can_restore() {
753            if let Some(other) = prev.maybe_find_widget(self.id.as_ref().unwrap()) {
754                self.widget.restore(ctx, other.widget.as_ref());
755            }
756        }
757    }
758
759    fn consume_geometry(mut self, batch: &mut GeomBatch) {
760        if let Some(bg) = self.bg_batch.take() {
761            batch.append(bg.translate(self.rect.x1, self.rect.y1));
762        }
763
764        if self.widget.is::<Container>() {
765            // downcast() consumes, so we have to do the is() check first
766            if let Ok(container) = self.widget.downcast::<Container>() {
767                for w in container.members {
768                    w.consume_geometry(batch);
769                }
770            }
771        } else if let Ok(defer) = self.widget.downcast::<DeferDraw>() {
772            batch.append(defer.batch.translate(defer.top_left.x, defer.top_left.y));
773        } else {
774            panic!("to_geom called on a widget tree that has something interactive");
775        }
776    }
777
778    fn find(&self, name: &str) -> Option<&Widget> {
779        if self.id == Some(name.to_string()) {
780            return Some(self);
781        }
782
783        if let Some(container) = self.widget.downcast_ref::<Container>() {
784            for widget in &container.members {
785                if let Some(w) = widget.find(name) {
786                    return Some(w);
787                }
788            }
789        }
790
791        None
792    }
793    fn find_mut(&mut self, name: &str) -> Option<&mut Widget> {
794        if self.id == Some(name.to_string()) {
795            return Some(self);
796        }
797
798        if let Some(container) = self.widget.downcast_mut::<Container>() {
799            for widget in &mut container.members {
800                if let Some(w) = widget.find_mut(name) {
801                    return Some(w);
802                }
803            }
804        }
805
806        None
807    }
808
809    fn take(&mut self, name: &str) -> Option<Widget> {
810        if self.id == Some(name.to_string()) {
811            panic!("Can't take({}), it's a top-level widget", name);
812        }
813
814        if let Some(container) = self.widget.downcast_mut::<Container>() {
815            let mut members = Vec::new();
816            let mut found = None;
817            for mut widget in container.members.drain(..) {
818                if widget.id == Some(name.to_string()) {
819                    found = Some(widget);
820                } else if let Some(w) = widget.take(name) {
821                    found = Some(w);
822                    members.push(widget);
823                } else {
824                    members.push(widget);
825                }
826            }
827            found
828        } else {
829            None
830        }
831    }
832
833    pub(crate) fn take_just_draw(self) -> JustDraw {
834        *self.widget.downcast::<JustDraw>().ok().unwrap()
835    }
836}
837
838#[derive(Copy, Clone, Debug, Default, PartialEq)]
839pub struct EdgeInsets {
840    pub top: f64,
841    pub left: f64,
842    pub bottom: f64,
843    pub right: f64,
844}
845
846impl EdgeInsets {
847    pub fn zero() -> Self {
848        EdgeInsets {
849            top: 0.0,
850            left: 0.0,
851            bottom: 0.0,
852            right: 0.0,
853        }
854    }
855
856    pub fn uniform(inset: f64) -> Self {
857        EdgeInsets {
858            top: inset,
859            left: inset,
860            bottom: inset,
861            right: inset,
862        }
863    }
864}
865
866impl From<usize> for EdgeInsets {
867    fn from(uniform_size: usize) -> EdgeInsets {
868        EdgeInsets::uniform(uniform_size as f64)
869    }
870}
871
872impl From<f64> for EdgeInsets {
873    fn from(uniform_size: f64) -> EdgeInsets {
874        EdgeInsets::uniform(uniform_size)
875    }
876}
877
878impl From<EdgeInsets> for Rect<Dimension> {
879    fn from(insets: EdgeInsets) -> Rect<Dimension> {
880        Rect {
881            left: Dimension::Points(insets.left as f32),
882            right: Dimension::Points(insets.right as f32),
883            top: Dimension::Points(insets.top as f32),
884            bottom: Dimension::Points(insets.bottom as f32),
885        }
886    }
887}