Skip to main content

armas_basic/components/
icon_button.rs

1//! Icon Button Component
2//!
3//! A button variant specifically designed for rendering icons.
4
5use crate::components::ButtonVariant;
6use crate::ext::ArmasContextExt;
7use crate::icon::{render_icon_data, IconData, OwnedIconData};
8use egui::{Color32, Response, Sense, Ui, Vec2};
9
10/// Icon Button component
11///
12/// `size` controls the **total widget size** (allocation), not the icon drawing area.
13/// The icon is drawn inside after subtracting padding.
14/// For Ghost/Link variants, padding defaults to 0 so `size` ≈ icon size.
15/// For other variants, padding defaults to `ui.spacing().button_padding`.
16pub struct IconButton<'a> {
17    vertices: &'a [(f32, f32)],
18    indices: &'a [u32],
19    viewbox_width: f32,
20    viewbox_height: f32,
21    variant: ButtonVariant,
22    size: f32,
23    padding: Option<f32>,
24    enabled: bool,
25    icon_color: Option<Color32>,
26    hover_icon_color: Option<Color32>,
27}
28
29impl<'a> IconButton<'a> {
30    /// Create a new icon button from static [`IconData`].
31    #[must_use]
32    pub const fn new(icon_data: &'a IconData) -> Self {
33        Self {
34            vertices: icon_data.vertices,
35            indices: icon_data.indices,
36            viewbox_width: icon_data.viewbox_width,
37            viewbox_height: icon_data.viewbox_height,
38            variant: ButtonVariant::Default,
39            size: 16.0,
40            padding: None,
41            enabled: true,
42            icon_color: None,
43            hover_icon_color: None,
44        }
45    }
46
47    /// Create a new icon button from [`OwnedIconData`].
48    #[must_use]
49    pub fn from_owned(data: &'a OwnedIconData) -> Self {
50        Self {
51            vertices: &data.vertices,
52            indices: &data.indices,
53            viewbox_width: data.viewbox_width,
54            viewbox_height: data.viewbox_height,
55            variant: ButtonVariant::Default,
56            size: 16.0,
57            padding: None,
58            enabled: true,
59            icon_color: None,
60            hover_icon_color: None,
61        }
62    }
63
64    /// Set the button variant
65    #[must_use]
66    pub const fn variant(mut self, variant: ButtonVariant) -> Self {
67        self.variant = variant;
68        self
69    }
70
71    /// Set the total widget size (icon + padding). Default 16.
72    #[must_use]
73    pub const fn size(mut self, size: f32) -> Self {
74        self.size = size;
75        self
76    }
77
78    /// Set padding between widget edge and icon. Default: 0 for Ghost/Link, `button_padding` for others.
79    #[must_use]
80    pub const fn padding(mut self, padding: f32) -> Self {
81        self.padding = Some(padding);
82        self
83    }
84
85    /// Set enabled state
86    #[must_use]
87    pub const fn enabled(mut self, enabled: bool) -> Self {
88        self.enabled = enabled;
89        self
90    }
91
92    /// Set custom icon color (overrides default)
93    #[must_use]
94    pub const fn icon_color(mut self, color: Color32) -> Self {
95        self.icon_color = Some(color);
96        self
97    }
98
99    /// Set custom hover icon color (overrides default)
100    #[must_use]
101    pub const fn hover_icon_color(mut self, color: Color32) -> Self {
102        self.hover_icon_color = Some(color);
103        self
104    }
105
106    /// Show the icon button
107    pub fn show(self, ui: &mut Ui) -> Response {
108        let theme = ui.ctx().armas_theme();
109
110        // Ghost/Link: no background, so default padding = 0 (size ≈ icon).
111        // Others: use egui's button_padding so icon has breathing room inside bg.
112        let padding = self.padding.unwrap_or_else(|| match self.variant {
113            ButtonVariant::Ghost | ButtonVariant::Link => 0.0,
114            _ => {
115                let bp = ui.spacing().button_padding;
116                bp.x.max(bp.y)
117            }
118        });
119
120        // `size` is the total widget allocation.
121        let total_size = Vec2::splat(self.size);
122        // Icon draws in the inner rect after subtracting padding.
123        let icon_draw_size = (self.size - padding * 2.0).max(1.0);
124
125        let sense = if self.enabled {
126            Sense::click()
127        } else {
128            Sense::hover()
129        };
130
131        let (rect, response) = ui.allocate_exact_size(total_size, sense);
132
133        if ui.is_rect_visible(rect) {
134            let (bg_color, mut icon_color) = match self.variant {
135                ButtonVariant::Default => {
136                    let bg = if response.is_pointer_button_down_on() {
137                        theme.primary().linear_multiply(0.9)
138                    } else if response.hovered() {
139                        theme.primary().linear_multiply(1.08)
140                    } else {
141                        theme.primary()
142                    };
143                    (Some(bg), theme.primary_foreground())
144                }
145                ButtonVariant::Secondary => {
146                    let bg = if response.is_pointer_button_down_on() {
147                        theme.secondary()
148                    } else if response.hovered() {
149                        theme.secondary().linear_multiply(1.08)
150                    } else {
151                        theme.secondary()
152                    };
153                    (Some(bg), theme.secondary_foreground())
154                }
155                ButtonVariant::Outline => {
156                    let bg = if response.hovered() {
157                        Some(theme.accent())
158                    } else {
159                        None
160                    };
161                    let icon = if response.hovered() {
162                        theme.accent_foreground()
163                    } else {
164                        theme.foreground()
165                    };
166                    (bg, icon)
167                }
168                ButtonVariant::Ghost | ButtonVariant::Link => {
169                    let bg = if response.hovered() {
170                        Some(theme.muted())
171                    } else {
172                        None
173                    };
174                    let icon = if response.hovered() {
175                        theme.foreground()
176                    } else {
177                        theme.muted_foreground()
178                    };
179                    (bg, icon)
180                }
181            };
182
183            if response.hovered() {
184                if let Some(custom_hover_color) = self.hover_icon_color {
185                    icon_color = custom_hover_color;
186                }
187            } else if let Some(custom_color) = self.icon_color {
188                icon_color = custom_color;
189            }
190
191            if !self.enabled {
192                icon_color = icon_color.linear_multiply(0.5);
193            }
194
195            if let Some(bg) = bg_color {
196                let rounding = match self.variant {
197                    ButtonVariant::Default | ButtonVariant::Secondary => total_size.x / 2.0,
198                    ButtonVariant::Ghost | ButtonVariant::Link => 2.0,
199                    ButtonVariant::Outline => 6.0,
200                };
201                let final_bg = if self.enabled {
202                    bg
203                } else {
204                    bg.linear_multiply(0.5)
205                };
206                ui.painter().rect_filled(rect, rounding, final_bg);
207            }
208
209            if self.variant == ButtonVariant::Outline {
210                let stroke = egui::Stroke::new(1.0, theme.border());
211                ui.painter()
212                    .rect_stroke(rect, 6.0, stroke, egui::epaint::StrokeKind::Inside);
213            }
214
215            let icon_rect =
216                egui::Rect::from_center_size(rect.center(), Vec2::splat(icon_draw_size));
217            render_icon_data(
218                ui.painter(),
219                icon_rect,
220                self.vertices,
221                self.indices,
222                self.viewbox_width,
223                self.viewbox_height,
224                icon_color,
225            );
226        }
227
228        response
229    }
230}