1 use crate::{
2     AreaSlider, Autocomplete, Checkbox, Color, Dropdown, EventCtx, GfxCtx, HorizontalAlignment,
3     Menu, Outcome, PersistentSplit, ScreenDims, ScreenPt, ScreenRectangle, Slider, Spinner,
4     TextBox, VerticalAlignment, Widget, WidgetImpl, WidgetOutput,
5 };
6 use geom::{Percent, Polygon};
7 use std::collections::HashSet;
8 use stretch::geometry::Size;
9 use stretch::node::Stretch;
10 use stretch::number::Number;
11 use stretch::style::{Dimension, Style};
12 
13 pub struct Panel {
14     pub(crate) top_level: Widget,
15     horiz: HorizontalAlignment,
16     vert: VerticalAlignment,
17     dims: Dims,
18 
19     scrollable_x: bool,
20     scrollable_y: bool,
21     contents_dims: ScreenDims,
22     container_dims: ScreenDims,
23     clip_rect: Option<ScreenRectangle>,
24 }
25 
26 impl Panel {
new(top_level: Widget) -> PanelBuilder27     pub fn new(top_level: Widget) -> PanelBuilder {
28         PanelBuilder {
29             top_level,
30             horiz: HorizontalAlignment::Center,
31             vert: VerticalAlignment::Center,
32             dims: Dims::MaxPercent(Percent::int(100), Percent::int(100)),
33         }
34     }
35 
recompute_layout(&mut self, ctx: &EventCtx, recompute_bg: bool)36     fn recompute_layout(&mut self, ctx: &EventCtx, recompute_bg: bool) {
37         let mut stretch = Stretch::new();
38         let root = stretch
39             .new_node(
40                 Style {
41                     ..Default::default()
42                 },
43                 Vec::new(),
44             )
45             .unwrap();
46 
47         let mut nodes = vec![];
48         self.top_level.get_flexbox(root, &mut stretch, &mut nodes);
49         nodes.reverse();
50 
51         // TODO Express more simply. Constraining this seems useless.
52         let container_size = Size {
53             width: Number::Undefined,
54             height: Number::Undefined,
55         };
56         stretch.compute_layout(root, container_size).unwrap();
57 
58         // TODO I'm so confused why these 2 are acting differently. :(
59         let effective_dims = if self.scrollable_x || self.scrollable_y {
60             self.container_dims
61         } else {
62             let result = stretch.layout(root).unwrap();
63             ScreenDims::new(result.size.width.into(), result.size.height.into())
64         };
65         let top_left = ctx
66             .canvas
67             .align_window(effective_dims, self.horiz, self.vert);
68         let offset = self.scroll_offset();
69         self.top_level.apply_flexbox(
70             &stretch,
71             &mut nodes,
72             top_left.x,
73             top_left.y,
74             offset,
75             ctx,
76             recompute_bg,
77             false,
78         );
79         assert!(nodes.is_empty());
80     }
81 
scroll_offset(&self) -> (f64, f64)82     fn scroll_offset(&self) -> (f64, f64) {
83         let x = if self.scrollable_x {
84             self.slider("horiz scrollbar").get_percent()
85                 * (self.contents_dims.width - self.container_dims.width).max(0.0)
86         } else {
87             0.0
88         };
89         let y = if self.scrollable_y {
90             self.slider("vert scrollbar").get_percent()
91                 * (self.contents_dims.height - self.container_dims.height).max(0.0)
92         } else {
93             0.0
94         };
95         (x, y)
96     }
97 
set_scroll_offset(&mut self, ctx: &EventCtx, offset: (f64, f64))98     fn set_scroll_offset(&mut self, ctx: &EventCtx, offset: (f64, f64)) {
99         let mut changed = false;
100         if self.scrollable_x {
101             changed = true;
102             let max = (self.contents_dims.width - self.container_dims.width).max(0.0);
103             if max == 0.0 {
104                 self.slider_mut("horiz scrollbar").set_percent(ctx, 0.0);
105             } else {
106                 self.slider_mut("horiz scrollbar")
107                     .set_percent(ctx, abstutil::clamp(offset.0, 0.0, max) / max);
108             }
109         }
110         if self.scrollable_y {
111             changed = true;
112             let max = (self.contents_dims.height - self.container_dims.height).max(0.0);
113             if max == 0.0 {
114                 self.slider_mut("vert scrollbar").set_percent(ctx, 0.0);
115             } else {
116                 self.slider_mut("vert scrollbar")
117                     .set_percent(ctx, abstutil::clamp(offset.1, 0.0, max) / max);
118             }
119         }
120         if changed {
121             self.recompute_layout(ctx, false);
122         }
123     }
124 
event(&mut self, ctx: &mut EventCtx) -> Outcome125     pub fn event(&mut self, ctx: &mut EventCtx) -> Outcome {
126         if (self.scrollable_x || self.scrollable_y)
127             && ctx
128                 .canvas
129                 .get_cursor_in_screen_space()
130                 .map(|pt| self.top_level.rect.contains(pt))
131                 .unwrap_or(false)
132         {
133             if let Some((dx, dy)) = ctx.input.get_mouse_scroll() {
134                 let x_offset = if self.scrollable_x {
135                     self.scroll_offset().0 + dx * (ctx.canvas.gui_scroll_speed as f64)
136                 } else {
137                     0.0
138                 };
139                 let y_offset = if self.scrollable_y {
140                     self.scroll_offset().1 - dy * (ctx.canvas.gui_scroll_speed as f64)
141                 } else {
142                     0.0
143                 };
144                 self.set_scroll_offset(ctx, (x_offset, y_offset));
145             }
146         }
147 
148         if ctx.input.is_window_resized() {
149             self.recompute_layout(ctx, false);
150         }
151 
152         let before = self.scroll_offset();
153         let mut output = WidgetOutput::new();
154         self.top_level.widget.event(ctx, &mut output);
155         if self.scroll_offset() != before || output.redo_layout {
156             self.recompute_layout(ctx, true);
157         }
158 
159         output.outcome
160     }
161 
draw(&self, g: &mut GfxCtx)162     pub fn draw(&self, g: &mut GfxCtx) {
163         if let Some(ref rect) = self.clip_rect {
164             g.enable_clipping(rect.clone());
165             g.canvas.mark_covered_area(rect.clone());
166         } else {
167             g.canvas.mark_covered_area(self.top_level.rect.clone());
168         }
169 
170         // Debugging
171         if false {
172             g.fork_screenspace();
173             g.draw_polygon(Color::RED.alpha(0.5), self.top_level.rect.to_polygon());
174 
175             let top_left = g
176                 .canvas
177                 .align_window(self.container_dims, self.horiz, self.vert);
178             g.draw_polygon(
179                 Color::BLUE.alpha(0.5),
180                 Polygon::rectangle(self.container_dims.width, self.container_dims.height)
181                     .translate(top_left.x, top_left.y),
182             );
183         }
184 
185         g.unfork();
186 
187         self.top_level.draw(g);
188         if self.scrollable_x || self.scrollable_y {
189             g.disable_clipping();
190 
191             // Draw the scrollbars after clipping is disabled, because they actually live just
192             // outside the rectangle.
193             if self.scrollable_x {
194                 self.slider("horiz scrollbar").draw(g);
195             }
196             if self.scrollable_y {
197                 self.slider("vert scrollbar").draw(g);
198             }
199         }
200     }
201 
get_all_click_actions(&self) -> HashSet<String>202     pub fn get_all_click_actions(&self) -> HashSet<String> {
203         let mut actions = HashSet::new();
204         self.top_level.get_all_click_actions(&mut actions);
205         actions
206     }
207 
restore(&mut self, ctx: &mut EventCtx, prev: &Panel)208     pub fn restore(&mut self, ctx: &mut EventCtx, prev: &Panel) {
209         self.set_scroll_offset(ctx, prev.scroll_offset());
210 
211         self.top_level.restore(ctx, &prev);
212 
213         // Since we just moved things around, let all widgets respond to the mouse being somewhere
214         ctx.no_op_event(true, |ctx| assert_eq!(self.event(ctx), Outcome::Nothing));
215     }
216 
scroll_to_member(&mut self, ctx: &EventCtx, name: String)217     pub fn scroll_to_member(&mut self, ctx: &EventCtx, name: String) {
218         if let Some(w) = self.top_level.find(&name) {
219             let y1 = w.rect.y1;
220             self.set_scroll_offset(ctx, (0.0, y1));
221         } else {
222             panic!("Can't scroll_to_member of unknown {}", name);
223         }
224     }
225 
has_widget(&self, name: &str) -> bool226     pub fn has_widget(&self, name: &str) -> bool {
227         self.top_level.find(name).is_some()
228     }
229 
slider(&self, name: &str) -> &Slider230     pub fn slider(&self, name: &str) -> &Slider {
231         self.find(name)
232     }
slider_mut(&mut self, name: &str) -> &mut Slider233     pub fn slider_mut(&mut self, name: &str) -> &mut Slider {
234         self.find_mut(name)
235     }
area_slider(&self, name: &str) -> &AreaSlider236     pub fn area_slider(&self, name: &str) -> &AreaSlider {
237         self.find(name)
238     }
239 
take_menu_choice<T: 'static>(&mut self, name: &str) -> T240     pub fn take_menu_choice<T: 'static>(&mut self, name: &str) -> T {
241         self.find_mut::<Menu<T>>(name).take_current_choice()
242     }
243 
is_checked(&self, name: &str) -> bool244     pub fn is_checked(&self, name: &str) -> bool {
245         self.find::<Checkbox>(name).enabled
246     }
maybe_is_checked(&self, name: &str) -> Option<bool>247     pub fn maybe_is_checked(&self, name: &str) -> Option<bool> {
248         if self.has_widget(name) {
249             Some(self.find::<Checkbox>(name).enabled)
250         } else {
251             None
252         }
253     }
254 
text_box(&self, name: &str) -> String255     pub fn text_box(&self, name: &str) -> String {
256         self.find::<TextBox>(name).get_line()
257     }
258 
spinner(&self, name: &str) -> isize259     pub fn spinner(&self, name: &str) -> isize {
260         self.find::<Spinner>(name).current
261     }
262 
dropdown_value<T: 'static + PartialEq + Clone>(&self, name: &str) -> T263     pub fn dropdown_value<T: 'static + PartialEq + Clone>(&self, name: &str) -> T {
264         self.find::<Dropdown<T>>(name).current_value()
265     }
persistent_split_value<T: 'static + PartialEq + Clone>(&self, name: &str) -> T266     pub fn persistent_split_value<T: 'static + PartialEq + Clone>(&self, name: &str) -> T {
267         self.find::<PersistentSplit<T>>(name).current_value()
268     }
269 
autocomplete_done<T: 'static + Clone>(&self, name: &str) -> Option<Vec<T>>270     pub fn autocomplete_done<T: 'static + Clone>(&self, name: &str) -> Option<Vec<T>> {
271         self.find::<Autocomplete<T>>(name).final_value()
272     }
273 
find<T: WidgetImpl>(&self, name: &str) -> &T274     pub fn find<T: WidgetImpl>(&self, name: &str) -> &T {
275         if let Some(w) = self.top_level.find(name) {
276             if let Some(x) = w.widget.downcast_ref::<T>() {
277                 x
278             } else {
279                 panic!("Found widget {}, but wrong type", name);
280             }
281         } else {
282             panic!("Can't find widget {}", name);
283         }
284     }
find_mut<T: WidgetImpl>(&mut self, name: &str) -> &mut T285     pub fn find_mut<T: WidgetImpl>(&mut self, name: &str) -> &mut T {
286         if let Some(w) = self.top_level.find_mut(name) {
287             if let Some(x) = w.widget.downcast_mut::<T>() {
288                 x
289             } else {
290                 panic!("Found widget {}, but wrong type", name);
291             }
292         } else {
293             panic!("Can't find widget {}", name);
294         }
295     }
296 
rect_of(&self, name: &str) -> &ScreenRectangle297     pub fn rect_of(&self, name: &str) -> &ScreenRectangle {
298         &self.top_level.find(name).unwrap().rect
299     }
300     // TODO Deprecate
center_of(&self, name: &str) -> ScreenPt301     pub fn center_of(&self, name: &str) -> ScreenPt {
302         self.rect_of(name).center()
303     }
center_of_panel(&self) -> ScreenPt304     pub fn center_of_panel(&self) -> ScreenPt {
305         self.top_level.rect.center()
306     }
307 
align_above(&mut self, ctx: &mut EventCtx, other: &Panel)308     pub fn align_above(&mut self, ctx: &mut EventCtx, other: &Panel) {
309         // Small padding
310         self.vert = VerticalAlignment::Above(other.top_level.rect.y1 - 5.0);
311         self.recompute_layout(ctx, false);
312 
313         // Since we just moved things around, let all widgets respond to the mouse being somewhere
314         ctx.no_op_event(true, |ctx| assert_eq!(self.event(ctx), Outcome::Nothing));
315     }
align_below(&mut self, ctx: &mut EventCtx, other: &Panel, pad: f64)316     pub fn align_below(&mut self, ctx: &mut EventCtx, other: &Panel, pad: f64) {
317         self.vert = VerticalAlignment::Below(other.top_level.rect.y2 + pad);
318         self.recompute_layout(ctx, false);
319 
320         // Since we just moved things around, let all widgets respond to the mouse being somewhere
321         ctx.no_op_event(true, |ctx| assert_eq!(self.event(ctx), Outcome::Nothing));
322     }
323 
324     // All margins/padding/etc from the previous widget are retained.
replace(&mut self, ctx: &mut EventCtx, id: &str, mut new: Widget)325     pub fn replace(&mut self, ctx: &mut EventCtx, id: &str, mut new: Widget) {
326         let old = self.top_level.find_mut(id).unwrap();
327         new.layout.style = old.layout.style;
328         *old = new;
329         self.recompute_layout(ctx, true);
330 
331         // TODO Same no_op_event as align_above? Should we always do this in recompute_layout?
332     }
333 
clicked_outside(&self, ctx: &mut EventCtx) -> bool334     pub fn clicked_outside(&self, ctx: &mut EventCtx) -> bool {
335         // TODO No great way to populate OSD from here with "click to cancel"
336         !self.top_level.rect.contains(ctx.canvas.get_cursor()) && ctx.normal_left_click()
337     }
338 
currently_hovering(&self) -> Option<&String>339     pub fn currently_hovering(&self) -> Option<&String> {
340         self.top_level.currently_hovering()
341     }
342 }
343 
344 pub struct PanelBuilder {
345     top_level: Widget,
346     horiz: HorizontalAlignment,
347     vert: VerticalAlignment,
348     dims: Dims,
349 }
350 
351 enum Dims {
352     MaxPercent(Percent, Percent),
353     ExactPercent(f64, f64),
354 }
355 
356 impl PanelBuilder {
build(mut self, ctx: &mut EventCtx) -> Panel357     pub fn build(mut self, ctx: &mut EventCtx) -> Panel {
358         self.top_level = self.top_level.padding(16).bg(ctx.style.panel_bg);
359         self.build_custom(ctx)
360     }
361 
build_custom(self, ctx: &mut EventCtx) -> Panel362     pub fn build_custom(self, ctx: &mut EventCtx) -> Panel {
363         let mut c = Panel {
364             top_level: self.top_level,
365 
366             horiz: self.horiz,
367             vert: self.vert,
368             dims: self.dims,
369 
370             scrollable_x: false,
371             scrollable_y: false,
372             contents_dims: ScreenDims::new(0.0, 0.0),
373             container_dims: ScreenDims::new(0.0, 0.0),
374             clip_rect: None,
375         };
376         if let Dims::ExactPercent(w, h) = c.dims {
377             // Don't set size, because then scrolling breaks -- the actual size has to be based on
378             // the contents.
379             c.top_level.layout.style.min_size = Size {
380                 width: Dimension::Points((w * ctx.canvas.window_width) as f32),
381                 height: Dimension::Points((h * ctx.canvas.window_height) as f32),
382             };
383         }
384         c.recompute_layout(ctx, false);
385 
386         c.contents_dims = ScreenDims::new(c.top_level.rect.width(), c.top_level.rect.height());
387         c.container_dims = match c.dims {
388             Dims::MaxPercent(w, h) => ScreenDims::new(
389                 c.contents_dims
390                     .width
391                     .min(w.inner() * ctx.canvas.window_width),
392                 c.contents_dims
393                     .height
394                     .min(h.inner() * ctx.canvas.window_height),
395             ),
396             Dims::ExactPercent(w, h) => {
397                 ScreenDims::new(w * ctx.canvas.window_width, h * ctx.canvas.window_height)
398             }
399         };
400 
401         // If the panel fits without a scrollbar, don't add one.
402         let top_left = ctx.canvas.align_window(c.container_dims, c.horiz, c.vert);
403         if c.contents_dims.width > c.container_dims.width {
404             c.scrollable_x = true;
405             c.top_level = Widget::custom_col(vec![
406                 c.top_level,
407                 Slider::horizontal(
408                     ctx,
409                     c.container_dims.width,
410                     c.container_dims.width * (c.container_dims.width / c.contents_dims.width),
411                     0.0,
412                 )
413                 .named("horiz scrollbar")
414                 .abs(top_left.x, top_left.y + c.container_dims.height),
415             ]);
416         }
417         if c.contents_dims.height > c.container_dims.height {
418             c.scrollable_y = true;
419             c.top_level = Widget::custom_row(vec![
420                 c.top_level,
421                 Slider::vertical(
422                     ctx,
423                     c.container_dims.height,
424                     c.container_dims.height * (c.container_dims.height / c.contents_dims.height),
425                     0.0,
426                 )
427                 .named("vert scrollbar")
428                 .abs(top_left.x + c.container_dims.width, top_left.y),
429             ]);
430         }
431         if c.scrollable_x || c.scrollable_y {
432             c.recompute_layout(ctx, false);
433             c.clip_rect = Some(ScreenRectangle::top_left(top_left, c.container_dims));
434         }
435 
436         // Just trigger error if a button is double-defined
437         c.get_all_click_actions();
438         // Let all widgets initially respond to the mouse being somewhere
439         ctx.no_op_event(true, |ctx| assert_eq!(c.event(ctx), Outcome::Nothing));
440         c
441     }
442 
aligned(mut self, horiz: HorizontalAlignment, vert: VerticalAlignment) -> PanelBuilder443     pub fn aligned(mut self, horiz: HorizontalAlignment, vert: VerticalAlignment) -> PanelBuilder {
444         self.horiz = horiz;
445         self.vert = vert;
446         self
447     }
448 
max_size(mut self, width: Percent, height: Percent) -> PanelBuilder449     pub fn max_size(mut self, width: Percent, height: Percent) -> PanelBuilder {
450         if width == Percent::int(100) && height == Percent::int(100) {
451             panic!("By default, Panels are capped at 100% of the screen. This is redundant.");
452         }
453         self.dims = Dims::MaxPercent(width, height);
454         self
455     }
456 
exact_size_percent(mut self, pct_width: usize, pct_height: usize) -> PanelBuilder457     pub fn exact_size_percent(mut self, pct_width: usize, pct_height: usize) -> PanelBuilder {
458         self.dims = Dims::ExactPercent((pct_width as f64) / 100.0, (pct_height as f64) / 100.0);
459         self
460     }
461 }
462