1use geom::Polygon;
2
3use crate::{
4 style::DEFAULT_OUTLINE_THICKNESS, text::Font, ButtonStyle, Color, ContentMode, ControlState,
5 CornerRounding, Drawable, EdgeInsets, EventCtx, GeomBatch, GfxCtx, Image, Line, MultiKey,
6 Outcome, OutlineStyle, RewriteColor, ScreenDims, ScreenPt, Text, Widget, WidgetImpl,
7 WidgetOutput,
8};
9
10use crate::geom::geom_batch_stack::{Axis, GeomBatchStack};
11
12pub struct Button {
13 pub action: String,
15
16 draw_normal: Drawable,
19 draw_hovered: Drawable,
20 draw_disabled: Drawable,
21
22 pub(crate) hotkey: Option<MultiKey>,
23 tooltip: Option<Text>,
24 disabled_tooltip: Option<Text>,
25 hitbox: Polygon,
27
28 pub(crate) hovering: bool,
29 is_disabled: bool,
30
31 pub(crate) top_left: ScreenPt,
32 pub(crate) dims: ScreenDims,
33}
34
35impl Button {
36 fn new(
37 ctx: &EventCtx,
38 normal: GeomBatch,
39 hovered: GeomBatch,
40 disabled: GeomBatch,
41 hotkey: Option<MultiKey>,
42 action: &str,
43 maybe_tooltip: Option<Text>,
44 hitbox: Polygon,
45 is_disabled: bool,
46 disabled_tooltip: Option<Text>,
47 ) -> Button {
48 let bounds = hitbox.get_bounds();
50 let dims = ScreenDims::new(bounds.width(), bounds.height());
51 assert!(!action.is_empty());
52 Button {
53 action: action.to_string(),
54 draw_normal: ctx.upload(normal),
55 draw_hovered: ctx.upload(hovered),
56 draw_disabled: ctx.upload(disabled),
57 tooltip: if let Some(t) = maybe_tooltip {
58 if t.is_empty() {
59 None
60 } else {
61 Some(t)
62 }
63 } else {
64 Some(Text::tooltip(ctx, hotkey.clone(), action))
65 },
66 disabled_tooltip,
67 hotkey,
68 hitbox,
69
70 is_disabled,
71 hovering: false,
72
73 top_left: ScreenPt::new(0.0, 0.0),
74 dims,
75 }
76 }
77
78 pub fn is_enabled(&self) -> bool {
79 !self.is_disabled
80 }
81}
82
83impl WidgetImpl for Button {
84 fn get_dims(&self) -> ScreenDims {
85 self.dims
86 }
87
88 fn set_pos(&mut self, top_left: ScreenPt) {
89 self.top_left = top_left;
90 }
91
92 fn event(&mut self, ctx: &mut EventCtx, output: &mut WidgetOutput) {
93 if ctx.redo_mouseover() {
94 if let Some(pt) = ctx.canvas.get_cursor_in_screen_space() {
95 self.hovering = self
96 .hitbox
97 .translate(self.top_left.x, self.top_left.y)
98 .contains_pt(pt.to_pt());
99 } else {
100 self.hovering = false;
101 }
102 }
103
104 if self.is_disabled {
105 return;
106 }
107
108 if self.hovering && ctx.normal_left_click() {
109 self.hovering = false;
110 output.outcome = Outcome::Clicked(self.action.clone());
111 return;
112 }
113
114 if ctx.input.pressed(self.hotkey.clone()) {
115 self.hovering = false;
116 output.outcome = Outcome::Clicked(self.action.clone());
117 return;
118 }
119
120 if self.hovering {
121 ctx.cursor_clickable();
122 }
123 }
124
125 fn draw(&self, g: &mut GfxCtx) {
126 if self.is_disabled {
127 g.redraw_at(self.top_left, &self.draw_disabled);
128 if self.hovering {
129 if let Some(ref txt) = self.disabled_tooltip {
130 g.draw_mouse_tooltip(txt.clone());
131 }
132 }
133 } else if self.hovering {
134 g.redraw_at(self.top_left, &self.draw_hovered);
135 if let Some(ref txt) = self.tooltip {
136 g.draw_mouse_tooltip(txt.clone());
137 }
138 } else {
139 g.redraw_at(self.top_left, &self.draw_normal);
140 }
141 }
142}
143
144#[derive(Clone, Debug, Default)]
145pub struct ButtonBuilder<'a, 'c> {
146 padding: EdgeInsets,
147 stack_spacing: f64,
148 hotkey: Option<MultiKey>,
149 tooltip: Option<Text>,
150 stack_axis: Option<Axis>,
151 is_label_before_image: bool,
152 corner_rounding: Option<CornerRounding>,
153 is_disabled: bool,
154 default_style: ButtonStateStyle<'a, 'c>,
155 hover_style: ButtonStateStyle<'a, 'c>,
156 disable_style: ButtonStateStyle<'a, 'c>,
157 disabled_tooltip: Option<Text>,
158}
159
160#[derive(Clone, Debug, Default)]
161struct ButtonStateStyle<'a, 'c> {
162 image: Option<Image<'a, 'c>>,
163 label: Option<Label>,
164 outline: Option<OutlineStyle>,
165 bg_color: Option<Color>,
166 custom_batch: Option<GeomBatch>,
167}
168
169impl<'b, 'a: 'b, 'c> ButtonBuilder<'a, 'c> {
171 pub fn new() -> Self {
172 ButtonBuilder {
173 padding: EdgeInsets {
174 top: 8.0,
175 bottom: 8.0,
176 left: 16.0,
177 right: 16.0,
178 },
179 stack_spacing: 10.0,
180 ..Default::default()
181 }
182 }
183
184 pub fn padding<EI: Into<EdgeInsets>>(mut self, padding: EI) -> Self {
195 self.padding = padding.into();
196 self
197 }
198
199 pub fn padding_top(mut self, padding: f64) -> Self {
201 self.padding.top = padding;
202 self
203 }
204
205 pub fn padding_left(mut self, padding: f64) -> Self {
207 self.padding.left = padding;
208 self
209 }
210
211 pub fn padding_bottom(mut self, padding: f64) -> Self {
213 self.padding.bottom = padding;
214 self
215 }
216
217 pub fn padding_right(mut self, padding: f64) -> Self {
219 self.padding.right = padding;
220 self
221 }
222
223 pub fn label_text<I: Into<String>>(mut self, text: I) -> Self {
227 let mut label = self.default_style.label.take().unwrap_or_default();
228 label.text = Some(text.into());
229 self.default_style.label = Some(label);
230 self
231 }
232
233 pub fn label_underlined_text<I: Into<String>>(mut self, text: I) -> Self {
237 let text = text.into();
238 let mut label = self.default_style.label.take().unwrap_or_default();
239 label.text = Some(text.clone());
240 label.styled_text = Some(Text::from(Line(text).underlined()));
241 self.default_style.label = Some(label);
242 self
243 }
244
245 pub fn label_styled_text(mut self, styled_text: Text, for_state: ControlState) -> Self {
248 let state_style = self.style_mut(for_state);
249 let mut label = state_style.label.take().unwrap_or_default();
250 label.styled_text = Some(styled_text);
251 label.text = None;
255 state_style.label = Some(label);
256 self
257 }
258
259 pub fn label_color(mut self, color: Color, for_state: ControlState) -> Self {
263 let state_style = self.style_mut(for_state);
264 let mut label = state_style.label.take().unwrap_or_default();
265 label.color = Some(color);
266 state_style.label = Some(label);
267 self
268 }
269
270 pub fn font(mut self, font: Font) -> Self {
274 let mut label = self.default_style.label.take().unwrap_or_default();
275 label.font = Some(font);
276 self.default_style.label = Some(label);
277 self
278 }
279
280 pub fn font_size(mut self, font_size: usize) -> Self {
284 let mut label = self.default_style.label.take().unwrap_or_default();
285 label.font_size = Some(font_size);
286 self.default_style.label = Some(label);
287 self
288 }
289
290 pub fn image_path(mut self, path: &'a str) -> Self {
294 let mut image = self.default_style.image.take().unwrap_or_default();
297 image = image.source_path(path);
298 self.default_style.image = Some(image);
299 self
300 }
301
302 pub fn image(mut self, image: Image<'a, 'c>) -> Self {
303 self.default_style.image = Some(image);
306 self
307 }
308
309 pub fn image_bytes(mut self, labeled_bytes: (&'a str, &'a [u8])) -> Self {
318 let mut image = self.default_style.image.take().unwrap_or_default();
321 image = image.source_bytes(labeled_bytes);
322 self.default_style.image = Some(image);
323 self
324 }
325
326 pub fn image_batch(mut self, batch: GeomBatch, bounds: geom::Bounds) -> Self {
333 let mut image = self.default_style.image.take().unwrap_or_default();
334 image = image.source_batch(batch, bounds);
335 self.default_style.image = Some(image);
336 self
337 }
338
339 pub fn image_color<C: Into<RewriteColor>>(mut self, color: C, for_state: ControlState) -> Self {
346 let state_style = self.style_mut(for_state);
347 let mut image = state_style.image.take().unwrap_or_default();
348 image = image.color(color);
349 state_style.image = Some(image);
350 self
351 }
352
353 pub fn image_bg_color(mut self, color: Color, for_state: ControlState) -> Self {
360 let state_style = self.style_mut(for_state);
361 let mut image = state_style.image.take().unwrap_or_default();
362 image = image.bg_color(color);
363 state_style.image = Some(image);
364 self
365 }
366
367 pub fn image_dims<D: Into<ScreenDims>>(mut self, dims: D) -> Self {
373 let mut image = self.default_style.image.take().unwrap_or_default();
374 image = image.dims(dims);
375 self.default_style.image = Some(image);
376 self
377 }
378
379 pub fn override_style(self, style: &ButtonStyle) -> Self {
380 style.apply(self)
381 }
382
383 pub fn image_content_mode(mut self, content_mode: ContentMode) -> Self {
391 let mut image = self.default_style.image.take().unwrap_or_default();
392 image = image.content_mode(content_mode);
393 self.default_style.image = Some(image);
394 self
395 }
396
397 pub fn image_corner_rounding<R: Into<CornerRounding>>(mut self, value: R) -> Self {
399 let mut image = self.default_style.image.take().unwrap_or_default();
400 image = image.corner_rounding(value);
401 self.default_style.image = Some(image);
402 self
403 }
404
405 pub fn image_padding<EI: Into<EdgeInsets>>(mut self, value: EI) -> Self {
407 let mut image = self.default_style.image.take().unwrap_or_default();
408 image = image.padding(value);
409 self.default_style.image = Some(image);
410 self
411 }
412
413 pub fn bg_color(mut self, color: Color, for_state: ControlState) -> Self {
418 self.style_mut(for_state).bg_color = Some(color);
419 self
420 }
421
422 pub fn outline(mut self, outline: OutlineStyle, for_state: ControlState) -> Self {
427 self.style_mut(for_state).outline = Some(outline);
428 self
429 }
430
431 pub fn outline_color(mut self, color: Color, for_state: ControlState) -> Self {
432 let thickness: f64 = self
433 .style(for_state)
434 .outline
435 .map(|outline| outline.0)
436 .unwrap_or(DEFAULT_OUTLINE_THICKNESS);
437
438 self.style_mut(for_state).outline = Some((thickness, color));
439
440 self
441 }
442
443 pub fn custom_batch(mut self, batch: GeomBatch, for_state: ControlState) -> Self {
448 self.style_mut(for_state).custom_batch = Some(batch);
449 self
450 }
451
452 pub fn hotkey<MK: Into<Option<MultiKey>>>(mut self, key: MK) -> Self {
454 self.hotkey = key.into();
455 self
456 }
457
458 pub fn tooltip(mut self, tooltip: impl Into<Text>) -> Self {
462 self.tooltip = Some(tooltip.into());
463 self
464 }
465
466 pub fn no_tooltip(mut self) -> Self {
469 self.tooltip = Some(Text::new());
471 self
472 }
473
474 pub fn disabled_tooltip(mut self, tooltip: impl Into<Text>) -> Self {
479 self.disabled_tooltip = Some(tooltip.into());
480 self
481 }
482
483 pub fn maybe_disabled_tooltip(mut self, tooltip: Option<impl Into<Text>>) -> Self {
485 self.disabled_tooltip = tooltip.map(|x| x.into());
486 self
487 }
488
489 pub fn tooltip_and_disabled(mut self, tooltip: impl Into<Text>) -> Self {
491 let tooltip = tooltip.into();
492 self.tooltip = Some(tooltip.clone());
493 self.disabled_tooltip = Some(tooltip.clone());
494 self
495 }
496
497 pub fn vertical(mut self) -> Self {
501 self.stack_axis = Some(Axis::Vertical);
502 self
503 }
504
505 pub fn horizontal(mut self) -> Self {
509 self.stack_axis = Some(Axis::Horizontal);
510 self
511 }
512
513 pub fn disabled(mut self, is_disabled: bool) -> Self {
515 self.is_disabled = is_disabled;
516 self
517 }
518
519 pub fn label_first(mut self) -> Self {
523 self.is_label_before_image = true;
524 self
525 }
526
527 pub fn image_first(mut self) -> Self {
531 self.is_label_before_image = false;
532 self
533 }
534
535 pub fn stack_spacing(mut self, value: f64) -> Self {
538 self.stack_spacing = value;
539 self
540 }
541
542 pub fn corner_rounding<R: Into<CornerRounding>>(mut self, value: R) -> Self {
544 self.corner_rounding = Some(value.into());
545 self
546 }
547
548 pub fn build(&self, ctx: &EventCtx, action: &str) -> Button {
571 let normal = self.batch(ctx, ControlState::Default);
572 let hovered = self.batch(ctx, ControlState::Hovered);
573 let disabled = self.batch(ctx, ControlState::Disabled);
574
575 assert!(
576 normal.get_bounds() != geom::Bounds::zero(),
577 "button was empty"
578 );
579 let hitbox = normal.get_bounds().get_rectangle();
580 Button::new(
581 ctx,
582 normal,
583 hovered,
584 disabled,
585 self.hotkey.clone(),
586 action,
587 self.tooltip.clone(),
588 hitbox,
589 self.is_disabled,
590 self.disabled_tooltip.clone(),
591 )
592 }
593
594 pub fn build_widget<I: AsRef<str>>(&self, ctx: &EventCtx, action: I) -> Widget {
598 let action = action.as_ref();
599 Widget::new(Box::new(self.build(ctx, action))).named(action)
600 }
601
602 pub fn get_action(&self) -> Option<&String> {
604 self.default_style
605 .label
606 .as_ref()
607 .and_then(|label| label.text.as_ref())
608 }
609
610 pub fn build_def(&self, ctx: &EventCtx) -> Widget {
612 let action = self
613 .get_action()
614 .expect("Must set `label_text` before calling build_def");
615 self.build_widget(ctx, action)
616 }
617
618 fn style_mut(&'b mut self, state: ControlState) -> &'b mut ButtonStateStyle<'a, 'c> {
621 match state {
622 ControlState::Default => &mut self.default_style,
623 ControlState::Hovered => &mut self.hover_style,
624 ControlState::Disabled => &mut self.disable_style,
625 }
626 }
627
628 fn style(&'b self, state: ControlState) -> &'b ButtonStateStyle<'a, 'c> {
629 match state {
630 ControlState::Default => &self.default_style,
631 ControlState::Hovered => &self.hover_style,
632 ControlState::Disabled => &self.disable_style,
633 }
634 }
635
636 pub fn batch(&self, ctx: &EventCtx, for_state: ControlState) -> GeomBatch {
637 let state_style = self.style(for_state);
638 if let Some(custom_batch) = state_style.custom_batch.as_ref() {
639 return custom_batch.clone();
640 }
641
642 let default_style = &self.default_style;
643 if let Some(custom_batch) = default_style.custom_batch.as_ref() {
644 return custom_batch.clone();
645 }
646
647 let image_batch: Option<GeomBatch> = match (&state_style.image, &default_style.image) {
648 (Some(state_image), Some(default_image)) => default_image
649 .merged_image_style(state_image)
650 .build_batch(ctx),
651 (None, Some(default_image)) => default_image.build_batch(ctx),
652 (None, None) => None,
653 (Some(_), None) => {
654 debug_assert!(
655 false,
656 "unexpectedly found a per-state image with no default image"
657 );
658 None
659 }
660 }
661 .map(|b| b.0);
662
663 let label_batch = state_style
664 .label
665 .as_ref()
666 .or_else(|| default_style.label.as_ref())
667 .and_then(|label| {
668 let default = default_style.label.as_ref();
669
670 if let Some(styled_text) = label
671 .styled_text
672 .as_ref()
673 .or_else(|| default.and_then(|d| d.styled_text.as_ref()))
674 {
675 return Some(styled_text.clone().bg(Color::CLEAR).render(ctx));
676 }
677
678 let text = label
679 .text
680 .clone()
681 .or_else(|| default.and_then(|d| d.text.clone()))?;
682
683 let color = label
684 .color
685 .or_else(|| default.and_then(|d| d.color))
686 .unwrap_or_else(|| ctx.style().text_primary_color);
687 let mut line = Line(text).fg(color);
688
689 if let Some(font_size) = label
690 .font_size
691 .or_else(|| default.and_then(|d| d.font_size))
692 {
693 line = line.size(font_size);
694 }
695
696 if let Some(font) = label.font.or_else(|| default.and_then(|d| d.font)) {
697 line = line.font(font);
698 }
699
700 Some(
701 Text::from(line)
702 .bg(Color::CLEAR)
707 .render(ctx),
708 )
709 });
710
711 let mut items = vec![];
712 if let Some(image_batch) = image_batch {
713 items.push(image_batch);
714 }
715 if let Some(label_batch) = label_batch {
716 items.push(label_batch);
717 }
718 if self.is_label_before_image {
719 items.reverse()
720 }
721 let mut stack = GeomBatchStack::horizontal(items);
722 if let Some(stack_axis) = self.stack_axis {
723 stack.set_axis(stack_axis);
724 }
725 stack.set_spacing(self.stack_spacing);
726
727 let mut button_widget = stack
728 .batch()
729 .batch() .container()
731 .padding(self.padding)
732 .bg(state_style
733 .bg_color
734 .or(default_style.bg_color)
735 .unwrap_or(Color::CLEAR));
739
740 if let Some(outline) = state_style.outline.or(default_style.outline) {
741 button_widget = button_widget.outline(outline);
742 }
743
744 if let Some(corner_rounding) = self.corner_rounding {
745 button_widget = button_widget.corner_rounding(corner_rounding);
746 }
747
748 let (geom_batch, _hitbox) = button_widget.into_geom(ctx, None);
749 geom_batch
750 }
751}
752
753#[derive(Clone, Debug, Default)]
754struct Label {
755 text: Option<String>,
756 color: Option<Color>,
757 styled_text: Option<Text>,
758 font_size: Option<usize>,
759 font: Option<Font>,
760}