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