widgetry/widgets/
button.rs

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    /// When a button is clicked, `Outcome::Clicked` with this string is produced.
14    pub action: String,
15
16    // These must have the same dimensions and are oriented with their top-left corner at
17    // 0, 0. Transformation happens later.
18    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    // Screenspace, top-left always at the origin. Also, probably not a box. :P
26    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        // dims are based on the hitbox, not the two drawables!
49        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
169// can we take 'b out? and make the func that uses it generic?
170impl<'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    /// Extra spacing around a button's items (label and/or image).
185    ///
186    /// If not specified, a default will be applied.
187    /// ```
188    /// # use widgetry::{ButtonBuilder, EdgeInsets};
189    /// // Custom padding for each inset
190    /// let b = ButtonBuilder::new().padding(EdgeInsets{ top: 1.0, bottom: 2.0,  left: 12.0, right: 14.0 });
191    /// // uniform padding
192    /// let b = ButtonBuilder::new().padding(6);
193    /// ```
194    pub fn padding<EI: Into<EdgeInsets>>(mut self, padding: EI) -> Self {
195        self.padding = padding.into();
196        self
197    }
198
199    /// Extra spacing around a button's items (label and/or image).
200    pub fn padding_top(mut self, padding: f64) -> Self {
201        self.padding.top = padding;
202        self
203    }
204
205    /// Extra spacing around a button's items (label and/or image).
206    pub fn padding_left(mut self, padding: f64) -> Self {
207        self.padding.left = padding;
208        self
209    }
210
211    /// Extra spacing around a button's items (label and/or image).
212    pub fn padding_bottom(mut self, padding: f64) -> Self {
213        self.padding.bottom = padding;
214        self
215    }
216
217    /// Extra spacing around a button's items (label and/or image).
218    pub fn padding_right(mut self, padding: f64) -> Self {
219        self.padding.right = padding;
220        self
221    }
222
223    /// Set the text of the button's label.
224    ///
225    /// If `label_text` is not set, the button will not have a label.
226    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    /// Set the text of the button's label. The text will be decorated with an underline.
234    ///
235    /// See `label_styled_text` if you need something more customizable text styling.
236    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    /// Assign a pre-styled `Text` instance if your button need something more than uniformly
246    /// colored text.
247    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        // Unset plain `text` to avoid confusion. Alternatively we could assign the inner text -
252        // something like:
253        //      label.text = styled_text.rows.map(|r|r.text).join(" ")
254        label.text = None;
255        state_style.label = Some(label);
256        self
257    }
258
259    /// Set the color of the button's label.
260    ///
261    /// If not specified, a default font color will be used.
262    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    /// Set the font used by the button's label.
271    ///
272    /// If not specified, a default font will be used.
273    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    /// Set the size of the font of the button's label.
281    ///
282    /// If not specified, a default font size will be used.
283    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    /// Set the image for the button. If not set, the button will have no image.
291    ///
292    /// This will replace any image previously set.
293    pub fn image_path(mut self, path: &'a str) -> Self {
294        // Currently we don't support setting image for other states like "hover", we easily
295        // could, but the API gets more verbose for a thing we don't currently need.
296        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        // Currently we don't support setting image for other states like "hover", we easily
304        // could, but the API gets more verbose for a thing we don't currently need.
305        self.default_style.image = Some(image);
306        self
307    }
308
309    /// Set the image for the button. If not set, the button will have no image.
310    ///
311    /// This will replace any image previously set.
312    ///
313    /// * `labeled_bytes`: is a (`label`, `bytes`) tuple you can generate with
314    ///   [`include_labeled_bytes!`]
315    /// * `label`: a label to describe the bytes for debugging purposes
316    /// * `bytes`: UTF-8 encoded bytes of the SVG
317    pub fn image_bytes(mut self, labeled_bytes: (&'a str, &'a [u8])) -> Self {
318        // Currently we don't support setting image for other states like "hover", we easily
319        // could, but the API gets more verbose for a thing we don't currently need.
320        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    /// Set the image for the button. If not set, the button will have no image.
327    ///
328    /// This will replace any image previously set.
329    ///
330    /// This method is useful when doing more complex transforms. For example, to re-write more than
331    /// one color for your button image, do so externally and pass in the resultant GeomBatch here.
332    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    /// Rewrite the color of the button's image.
340    ///
341    /// This has no effect if the button does not have an image.
342    ///
343    /// If the style hasn't been set for the current ControlState, the style for
344    /// `ControlState::Default` will be used.
345    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    /// Set a background color for the image, other than the buttons background.
354    ///
355    /// This has no effect if the button does not have an image.
356    ///
357    /// If the style hasn't been set for the current ControlState, the style for
358    /// `ControlState::Default` will be used.
359    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    /// Scale the bounds containing the image. If `image_dims` are not specified, the images
368    /// intrinsic size will be used.
369    ///
370    /// See [`ButtonBuilder::image_content_mode`] to control how the image scales to fit
371    /// its custom bounds.
372    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    /// If a custom `image_dims` was set, control how the image should be scaled to its new bounds
384    ///
385    /// If `image_dims` were not specified, the image will not be scaled, so content_mode has no
386    /// affect.
387    ///
388    /// The default, [`ContentMode::ScaleAspectFit`] will only grow as much as it can while
389    /// maintaining its aspect ratio and not exceeding its bounds.
390    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    /// Set independent rounding for each of the button's image's corners
398    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    /// Set padding for the image
406    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    /// Set a background color for the button based on the button's [`ControlState`].
414    ///
415    /// If the style hasn't been set for the current ControlState, the style for
416    /// `ControlState::Default` will be used.
417    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    /// Set an outline for the button based on the button's [`ControlState`].
423    ///
424    /// If the style hasn't been set for the current ControlState, the style for
425    /// `ControlState::Default` will be used.
426    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    /// Set a pre-rendered [GeomBatch] to use for the button instead of individual fields.
444    ///
445    /// This is useful for applying one-off button designs that can't be accommodated by the
446    /// the existing ButtonBuilder methods.
447    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    /// Set a hotkey for the button
453    pub fn hotkey<MK: Into<Option<MultiKey>>>(mut self, key: MK) -> Self {
454        self.hotkey = key.into();
455        self
456    }
457
458    /// Set a non-default tooltip [`Text`] to appear when hovering over the button.
459    ///
460    /// If a `tooltip` is not specified, a default tooltip will be applied.
461    pub fn tooltip(mut self, tooltip: impl Into<Text>) -> Self {
462        self.tooltip = Some(tooltip.into());
463        self
464    }
465
466    /// If a `tooltip` is not specified, a default tooltip will be applied. Use `no_tooltip` when
467    /// you do not want even the default tooltip to appear.
468    pub fn no_tooltip(mut self) -> Self {
469        // otherwise the widgets `name` is used
470        self.tooltip = Some(Text::new());
471        self
472    }
473
474    /// Set a tooltip [`Text`] to appear when hovering over the button, when the button is
475    /// disabled.
476    ///
477    /// This tooltip is only displayed when `disabled(true)` is also called.
478    pub fn disabled_tooltip(mut self, tooltip: impl Into<Text>) -> Self {
479        self.disabled_tooltip = Some(tooltip.into());
480        self
481    }
482
483    /// Like `disabled_tooltip`, but the tooltip may not exist.
484    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    /// Sets a tooltip to appear whether the button is disabled or not.
490    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    /// The button's items will be rendered in a vertical column
498    ///
499    /// If the button doesn't have both an image and label, this has no effect.
500    pub fn vertical(mut self) -> Self {
501        self.stack_axis = Some(Axis::Vertical);
502        self
503    }
504
505    /// The button's items will be rendered in a horizontal row
506    ///
507    /// If the button doesn't have both an image and label, this has no effect.
508    pub fn horizontal(mut self) -> Self {
509        self.stack_axis = Some(Axis::Horizontal);
510        self
511    }
512
513    /// The button cannot be clicked and will be styled as [`ControlState::Disabled`]
514    pub fn disabled(mut self, is_disabled: bool) -> Self {
515        self.is_disabled = is_disabled;
516        self
517    }
518
519    /// Display the button's label before the button's image.
520    ///
521    /// If the button doesn't have both an image and label, this has no effect.
522    pub fn label_first(mut self) -> Self {
523        self.is_label_before_image = true;
524        self
525    }
526
527    /// Display the button's image before the button's label.
528    ///
529    /// If the button doesn't have both an image and label, this has no effect.
530    pub fn image_first(mut self) -> Self {
531        self.is_label_before_image = false;
532        self
533    }
534
535    /// Spacing between the image and text of a button.
536    /// Has no effect if the button is text-only or image-only.
537    pub fn stack_spacing(mut self, value: f64) -> Self {
538        self.stack_spacing = value;
539        self
540    }
541
542    /// Set independent rounding for each of the button's corners
543    pub fn corner_rounding<R: Into<CornerRounding>>(mut self, value: R) -> Self {
544        self.corner_rounding = Some(value.into());
545        self
546    }
547
548    // Building
549
550    /// Build a button.
551    ///
552    /// `action`: The event that will be fired when clicked
553    /// ```
554    /// # use widgetry::{Color, ButtonBuilder, ControlState, EventCtx};
555    ///
556    /// fn build_some_buttons(ctx: &EventCtx) {
557    ///     let one_off_builder = ButtonBuilder::new().label_text("foo").build(ctx, "foo");
558    ///
559    ///     // If you'd like to build a series of similar buttons, `clone` the builder first.
560    ///     let red_builder = ButtonBuilder::new()
561    ///         .bg_color(Color::RED, ControlState::Default)
562    ///         .bg_color(Color::RED.alpha(0.3), ControlState::Disabled)
563    ///         .outline((2.0, Color::WHITE), ControlState::Default);
564    ///
565    ///     let red_button_1 = red_builder.clone().label_text("First red button").build(ctx, "first");
566    ///     let red_button_2 = red_builder.clone().label_text("Second red button").build(ctx, "second");
567    ///     let red_button_3 = red_builder.label_text("Last red button").build(ctx, "third");
568    /// }
569    /// ```
570    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    /// Shorthand method to build a Button wrapped in a Widget
595    ///
596    /// `action`: The event that will be fired when clicked
597    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    /// Get the button's text label, if defined
603    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    /// Shorthand method to build a default widget whose `action` is derived from the label's text.
611    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    // private  methods
619
620    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                        // Add a clear background to maintain a consistent amount of space for the
703                        // label based on the font, rather than the particular text.
704                        // Otherwise a button with text "YYY" will not line up with a button
705                        // with text "aaa".
706                        .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() // TODO: rename -> `widget` or `build_widget`
730            .container()
731            .padding(self.padding)
732            .bg(state_style
733                .bg_color
734                .or(default_style.bg_color)
735                // If we have *no* background, buttons will be cropped differently depending on
736                // their specific content, and it becomes impossible to have
737                // uniformly sized buttons.
738                .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}