bones_framework/render/ui/widgets/
bordered_button.rs

1#![allow(dead_code)] // We haven't used all of these methods yet but we still want them there
2
3use egui::{
4    NumExt, Response, RichText, Sense, TextStyle, Ui, Widget, WidgetInfo, WidgetText, WidgetType,
5};
6
7use crate::prelude::*;
8
9/// A button rendered with a [`BorderImageMeta`]
10pub struct BorderedButton<'a> {
11    text: WidgetText,
12    wrap: Option<bool>,
13    sense: Sense,
14    min_size: Vec2,
15    focus_on_hover: bool,
16    default_border: Option<&'a BorderImageMeta>,
17    on_focus_border: Option<&'a BorderImageMeta>,
18    on_click_border: Option<&'a BorderImageMeta>,
19    margin: egui::style::Margin,
20    padding: egui::style::Margin,
21}
22
23impl<'a> BorderedButton<'a> {
24    /// Create a new button
25    #[must_use = "You must call .show() to render the button"]
26    pub fn new(text: impl Into<WidgetText>) -> Self {
27        Self {
28            text: text.into(),
29            sense: Sense::click(),
30            min_size: Vec2::ZERO,
31            focus_on_hover: true,
32            wrap: None,
33            default_border: None,
34            on_focus_border: None,
35            on_click_border: None,
36            margin: Default::default(),
37            padding: Default::default(),
38        }
39    }
40
41    /// Create a button with the given theme.
42    #[must_use = "You must call .show() to render the button"]
43    pub fn themed(
44        button_theme: &'a ButtonThemeMeta,
45        label: impl Into<RichText>,
46    ) -> BorderedButton<'a> {
47        BorderedButton::new(
48            label
49                .into()
50                .font(button_theme.font.id())
51                .color(button_theme.font.color),
52        )
53        .border(&button_theme.borders.default)
54        .on_click_border(Some(&button_theme.borders.clicked))
55        .on_focus_border(Some(&button_theme.borders.focused))
56        .padding(button_theme.padding)
57    }
58
59    /// Set whether or not the button focuses itself automatically when it is hovered over.
60    pub fn focus_on_hover(mut self, focus_on_hover: bool) -> Self {
61        self.focus_on_hover = focus_on_hover;
62        self
63    }
64
65    /// If `true`, the text will wrap to stay within the max width of the [`Ui`].
66    ///
67    /// By default [`Self::wrap`] will be true in vertical layouts
68    /// and horizontal layouts with wrapping,
69    /// and false on non-wrapping horizontal layouts.
70    ///
71    /// Note that any `\n` in the text will always produce a new line.
72    #[inline]
73    #[must_use = "You must call .show() to render the button"]
74    pub fn wrap(mut self, wrap: bool) -> Self {
75        self.wrap = Some(wrap);
76        self
77    }
78
79    /// Set the margin. This will be applied on the outside of the border.
80    #[must_use = "You must call .show() to render the button"]
81    pub fn margin(mut self, margin: impl Into<egui::style::Margin>) -> Self {
82        self.margin = margin.into();
83
84        self
85    }
86
87    /// Set the padding. This will be applied on the inside of the border.
88    #[must_use = "You must call .show() to render the button"]
89    pub fn padding(mut self, padding: impl Into<egui::style::Margin>) -> Self {
90        self.padding = padding.into();
91
92        self
93    }
94
95    /// Set the button border image
96    #[must_use = "You must call .show() to render the button"]
97    pub fn border(mut self, border: &'a BorderImageMeta) -> Self {
98        self.default_border = Some(border);
99        self
100    }
101
102    /// Set a different border to use when focusing / hovering over the button
103    #[must_use = "You must call .show() to render the button"]
104    pub fn on_focus_border(mut self, border: Option<&'a BorderImageMeta>) -> Self {
105        self.on_focus_border = border;
106        self
107    }
108
109    /// Set a different border to use when the mouse is clicking on the button
110    #[must_use = "You must call .show() to render the button"]
111    pub fn on_click_border(mut self, border: Option<&'a BorderImageMeta>) -> Self {
112        self.on_click_border = border;
113        self
114    }
115
116    /// By default, buttons senses clicks.
117    /// Change this to a drag-button with `Sense::drag()`.
118    #[must_use = "You must call .show() to render the button"]
119    pub fn sense(mut self, sense: Sense) -> Self {
120        self.sense = sense;
121        self
122    }
123
124    /// Set the minimum size for the button
125    #[must_use = "You must call .show() to render the button"]
126    pub fn min_size(mut self, min_size: Vec2) -> Self {
127        self.min_size = min_size;
128        self
129    }
130
131    /// Render the button
132    #[must_use = "You must call .show() to render the button"]
133    pub fn show(self, ui: &mut Ui) -> egui::Response {
134        self.ui(ui)
135    }
136}
137
138impl<'a> Widget for BorderedButton<'a> {
139    fn ui(self, ui: &mut Ui) -> Response {
140        let BorderedButton {
141            text,
142            sense,
143            min_size,
144            focus_on_hover,
145            wrap,
146            default_border,
147            on_focus_border,
148            on_click_border,
149            margin,
150            padding,
151        }: BorderedButton = self;
152
153        let total_extra = padding.sum() + margin.sum();
154
155        let wrap_width = ui.available_width() - total_extra.x;
156        let text = text.into_galley(ui, wrap, wrap_width, TextStyle::Button);
157
158        let mut desired_size = text.size() + total_extra;
159        desired_size = desired_size.at_least(egui::vec2(min_size.x, min_size.y));
160
161        let (rect, response) = ui.allocate_at_least(desired_size, sense);
162        response.widget_info(|| WidgetInfo::labeled(WidgetType::Button, text.text()));
163
164        // Focus the button automatically when it is hovered and the mouse is moving
165        if response.hovered()
166            && ui.ctx().input(|i| i.pointer.velocity().length_sq() > 0.0)
167            && focus_on_hover
168        {
169            response.request_focus();
170        }
171
172        if ui.is_rect_visible(rect) {
173            let visuals = ui.style().interact(&response);
174
175            let mut text_rect = rect;
176            text_rect.min += padding.left_top() + margin.left_top();
177            text_rect.max -= padding.right_bottom() + margin.right_bottom();
178            text_rect.max.x = text_rect.max.x.max(text_rect.min.x);
179            text_rect.max.y = text_rect.max.y.max(text_rect.min.y);
180
181            let label_pos = ui
182                .layout()
183                .align_size_within_rect(text.size(), text_rect)
184                .min;
185
186            let border = if response.is_pointer_button_down_on() {
187                on_click_border.or(default_border)
188            } else if response.has_focus() || response.hovered() {
189                on_focus_border.or(default_border)
190            } else {
191                default_border
192            };
193
194            let mut border_rect = rect;
195            border_rect.min += margin.left_top();
196            border_rect.max -= margin.right_bottom();
197            border_rect.max.x = border_rect.max.x.max(border_rect.min.x);
198            border_rect.max.y = border_rect.max.y.max(border_rect.min.y);
199
200            if let Some(border) = border {
201                let texture = ui.data(|map| {
202                    map.get_temp::<AtomicResource<EguiTextures>>(egui::Id::null())
203                        .unwrap()
204                        .borrow()
205                        .unwrap()
206                        .get(border.image)
207                });
208                ui.painter()
209                    .add(BorderedFrame::new(border).paint(texture, border_rect));
210            }
211
212            text.paint_with_visuals(ui.painter(), label_pos, visuals);
213        }
214
215        response
216    }
217}