Skip to main content

armas_basic/components/
button_group.rs

1//! Button Group Component (shadcn/ui style)
2//!
3//! Groups buttons with connected borders, removing redundant inner borders
4//! between adjacent buttons.
5//!
6//! ```rust,no_run
7//! # use egui::Ui;
8//! # fn example(ui: &mut Ui) {
9//! use armas_basic::prelude::*;
10//!
11//! ButtonGroup::new("actions").show(ui, |ui| {
12//!     Button::new("Bold").variant(ButtonVariant::Outline).show(ui);
13//!     Button::new("Italic").variant(ButtonVariant::Outline).show(ui);
14//!     Button::new("Underline").variant(ButtonVariant::Outline).show(ui);
15//! });
16//! # }
17//! ```
18
19use egui::{Id, Sense, Ui};
20
21/// Button group orientation.
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
23pub enum ButtonGroupOrientation {
24    /// Buttons arranged left to right.
25    Horizontal,
26    /// Buttons stacked top to bottom.
27    Vertical,
28}
29
30/// Button Group — groups buttons with connected borders.
31pub struct ButtonGroup {
32    id: Id,
33    orientation: ButtonGroupOrientation,
34}
35
36/// Response from a button group.
37pub struct ButtonGroupResponse {
38    /// The UI response.
39    pub response: egui::Response,
40}
41
42impl ButtonGroup {
43    /// Create a new button group with a unique ID.
44    pub fn new(id: impl Into<Id>) -> Self {
45        Self {
46            id: id.into(),
47            orientation: ButtonGroupOrientation::Horizontal,
48        }
49    }
50
51    /// Set the button group orientation.
52    #[must_use]
53    pub const fn orientation(mut self, o: ButtonGroupOrientation) -> Self {
54        self.orientation = o;
55        self
56    }
57
58    /// Show the button group. Render buttons inside the closure.
59    pub fn show(self, ui: &mut Ui, content: impl FnOnce(&mut Ui)) -> ButtonGroupResponse {
60        let is_horizontal = self.orientation == ButtonGroupOrientation::Horizontal;
61
62        // Create a child UI with zero item spacing so buttons are flush
63        let layout = if is_horizontal {
64            egui::Layout::left_to_right(egui::Align::Center)
65        } else {
66            egui::Layout::top_down(egui::Align::LEFT)
67        };
68
69        let inner_response = ui.with_layout(layout, |ui| {
70            // Remove spacing between items so buttons are flush
71            if is_horizontal {
72                ui.spacing_mut().item_spacing.x = 0.0;
73            } else {
74                ui.spacing_mut().item_spacing.y = 0.0;
75            }
76
77            // Suppress individual button corner radius and borders
78            // by setting all rounding to 0 — we'll draw our own outer border
79            ui.style_mut().visuals.widgets.inactive.corner_radius = 0.into();
80            ui.style_mut().visuals.widgets.hovered.corner_radius = 0.into();
81            ui.style_mut().visuals.widgets.active.corner_radius = 0.into();
82
83            content(ui);
84        });
85
86        let group_rect = inner_response.response.rect;
87        let response = ui.interact(group_rect, self.id, Sense::hover());
88
89        ButtonGroupResponse { response }
90    }
91}
92
93#[cfg(test)]
94mod tests {
95    use super::*;
96
97    #[test]
98    fn test_button_group_builder() {
99        let bg = ButtonGroup::new("test").orientation(ButtonGroupOrientation::Vertical);
100        assert_eq!(bg.orientation, ButtonGroupOrientation::Vertical);
101    }
102}