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
46pub trait WidgetImpl: downcast_rs::Downcast {
49 fn get_dims(&self) -> ScreenDims;
52 fn set_pos(&mut self, top_left: ScreenPt);
54 fn event(&mut self, ctx: &mut EventCtx, output: &mut WidgetOutput);
57 fn draw(&self, g: &mut GfxCtx);
59 fn can_restore(&self) -> bool {
62 false
63 }
64 fn restore(&mut self, _: &mut EventCtx, _prev: &dyn WidgetImpl) {
67 unreachable!()
68 }
69}
70
71pub enum Outcome {
73 Clicked(String),
75 ClickCustom(Box<dyn CloneableAny>),
77 Changed(String),
80 DragDropReleased(String, usize, usize),
83 Focused(String),
85 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
102pub enum ClickOutcome {
104 Label(String),
105 Custom(Box<dyn CloneableAny>),
106}
107
108pub struct WidgetOutput {
109 pub redo_layout: bool,
111 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 pub(crate) widget: Box<dyn WidgetImpl>,
130 layout: LayoutStyle,
131 pub(crate) rect: ScreenRectangle,
132 bg: Option<Drawable>,
133 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 outline: Option<(f64, Color)>,
167 corner_rounding: CornerRounding,
168 style: Style,
169}
170
171impl 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 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 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 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 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 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 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 tab_body.layout.style.min_size.height = Dimension::Points(200.0);
291 tab_body
292 }
293
294 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 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 pub fn hide(self, x: bool) -> Widget {
406 if x {
407 Widget::nothing()
408 } else {
409 self
410 }
411 }
412}
413
414impl 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 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 pub fn custom_row(widgets: Vec<Widget>) -> Widget {
454 Widget::new(Box::new(Container::new(true, widgets)))
455 }
456
457 pub fn row(widgets: Vec<Widget>) -> Widget {
459 Widget::evenly_spaced_row(10, widgets)
460 }
461
462 pub fn evenly_spaced_row(spacing: usize, widgets: Vec<Widget>) -> Widget {
464 let mut new = Vec::new();
465 let len = widgets.len();
466 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 pub fn custom_col(widgets: Vec<Widget>) -> Widget {
479 Widget::new(Box::new(Container::new(false, widgets)))
480 }
481
482 pub fn evenly_spaced_col(spacing: usize, widgets: Vec<Widget>) -> Widget {
484 let mut new = Vec::new();
485 let len = widgets.len();
486 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 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 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 self.layout.style.min_size.width =
517 Dimension::Points((w * ctx.canvas.window_width) as f32 - 35.0);
518 }
519
520 {
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 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 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
586impl Widget {
588 pub(crate) fn draw(&self, g: &mut GfxCtx) {
589 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 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 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 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 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 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 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}