cadmus_core/view/settings_editor/
bottom_bar.rs

1use crate::color::WHITE;
2use crate::font::Fonts;
3use crate::framebuffer::Framebuffer;
4use crate::geom::Rectangle;
5use crate::view::filler::Filler;
6use crate::view::icon::Icon;
7use crate::view::{Event, Id, View, ID_FEEDER};
8
9/// Defines the layout variant for the settings editor bottom bar
10#[derive(Debug, Clone)]
11pub enum BottomBarVariant {
12    /// Single button centered in the bar (typically for save/validate)
13    SingleButton {
14        /// The event to emit when the button is clicked
15        event: Event,
16        /// Icon name for the button
17        icon: &'static str,
18    },
19    /// Two buttons with 50/50 split (typically cancel/save pattern)
20    TwoButtons {
21        /// Event emitted by left button
22        left_event: Event,
23        /// Icon name for left button
24        left_icon: &'static str,
25        /// Event emitted by right button
26        right_event: Event,
27        /// Icon name for right button
28        right_icon: &'static str,
29    },
30}
31
32/// Reusable bottom bar component for settings editor views
33///
34/// Provides a consistent bottom bar with white background and configurable
35/// button layout. Supports single centered button or two buttons with 50/50 split.
36pub struct SettingsEditorBottomBar {
37    id: Id,
38    rect: Rectangle,
39    children: Vec<Box<dyn View>>,
40}
41
42impl SettingsEditorBottomBar {
43    /// Creates a new settings editor bottom bar
44    ///
45    /// # Arguments
46    ///
47    /// * `rect` - The rectangle defining the bottom bar's position and size
48    /// * `variant` - The button layout variant to use
49    ///
50    /// # Returns
51    ///
52    /// A new `SettingsEditorBottomBar` instance
53    ///
54    /// # Examples
55    ///
56    /// ```
57    /// use cadmus_core::view::settings_editor::{SettingsEditorBottomBar, BottomBarVariant};
58    /// use cadmus_core::view::Event;
59    /// use cadmus_core::geom::{Rectangle, Point};
60    ///
61    /// let rect = Rectangle::new(Point { x: 0, y: 0 }, Point { x: 100, y: 50 });
62    /// let bottom_bar = SettingsEditorBottomBar::new(
63    ///     rect,
64    ///     BottomBarVariant::SingleButton {
65    ///         event: Event::Validate,
66    ///         icon: "check_mark-large",
67    ///     },
68    /// );
69    /// ```
70    pub fn new(rect: Rectangle, variant: BottomBarVariant) -> Self {
71        let id = ID_FEEDER.next();
72        let mut children = Vec::new();
73
74        let background = Filler::new(rect, WHITE);
75        children.push(Box::new(background) as Box<dyn View>);
76
77        match variant {
78            BottomBarVariant::SingleButton {
79                event,
80                icon: icon_name,
81            } => {
82                let icon = Icon::new(icon_name, rect, event);
83                children.push(Box::new(icon) as Box<dyn View>);
84            }
85            BottomBarVariant::TwoButtons {
86                left_event,
87                left_icon,
88                right_event,
89                right_icon,
90            } => {
91                let button_width = rect.width() as i32 / 2;
92
93                let left_rect = rect![
94                    rect.min.x,
95                    rect.min.y,
96                    rect.min.x + button_width,
97                    rect.max.y
98                ];
99                let left_button = Icon::new(left_icon, left_rect, left_event);
100                children.push(Box::new(left_button) as Box<dyn View>);
101
102                let right_rect = rect![
103                    rect.min.x + button_width,
104                    rect.min.y,
105                    rect.max.x,
106                    rect.max.y
107                ];
108                let right_button = Icon::new(right_icon, right_rect, right_event);
109                children.push(Box::new(right_button) as Box<dyn View>);
110            }
111        }
112
113        SettingsEditorBottomBar { id, rect, children }
114    }
115}
116
117impl View for SettingsEditorBottomBar {
118    #[cfg_attr(feature = "otel", tracing::instrument(skip(self, _hub, _bus, _rq, _context), fields(event = ?_evt), ret(level=tracing::Level::TRACE)))]
119    fn handle_event(
120        &mut self,
121        _evt: &Event,
122        _hub: &crate::view::Hub,
123        _bus: &mut crate::view::Bus,
124        _rq: &mut crate::view::RenderQueue,
125        _context: &mut crate::context::Context,
126    ) -> bool {
127        false
128    }
129
130    #[cfg_attr(feature = "otel", tracing::instrument(skip(self, _fb, _fonts), fields(rect = ?_rect)))]
131    fn render(&self, _fb: &mut dyn Framebuffer, _rect: Rectangle, _fonts: &mut Fonts) {}
132
133    fn rect(&self) -> &Rectangle {
134        &self.rect
135    }
136
137    fn rect_mut(&mut self) -> &mut Rectangle {
138        &mut self.rect
139    }
140
141    fn children(&self) -> &Vec<Box<dyn View>> {
142        &self.children
143    }
144
145    fn children_mut(&mut self) -> &mut Vec<Box<dyn View>> {
146        &mut self.children
147    }
148
149    fn id(&self) -> Id {
150        self.id
151    }
152}
153
154#[cfg(test)]
155mod tests {
156    use super::*;
157    use crate::geom::Point;
158
159    #[test]
160    fn test_single_button_creates_two_children() {
161        let rect = Rectangle::new(Point { x: 0, y: 0 }, Point { x: 100, y: 50 });
162
163        let bottom_bar = SettingsEditorBottomBar::new(
164            rect,
165            BottomBarVariant::SingleButton {
166                event: Event::Back,
167                icon: "back",
168            },
169        );
170
171        assert_eq!(
172            bottom_bar.children().len(),
173            2,
174            "SingleButton variant should have 2 children: background filler and icon"
175        );
176    }
177
178    #[test]
179    fn test_two_buttons_creates_three_children() {
180        let rect = Rectangle::new(Point { x: 0, y: 0 }, Point { x: 100, y: 50 });
181
182        let bottom_bar = SettingsEditorBottomBar::new(
183            rect,
184            BottomBarVariant::TwoButtons {
185                left_event: Event::Back,
186                left_icon: "back",
187                right_event: Event::Validate,
188                right_icon: "check_mark",
189            },
190        );
191
192        assert_eq!(
193            bottom_bar.children().len(),
194            3,
195            "TwoButtons variant should have 3 children: background filler, left icon, and right icon"
196        );
197    }
198
199    #[test]
200    fn test_two_buttons_split_width_evenly() {
201        let rect = Rectangle::new(Point { x: 0, y: 0 }, Point { x: 200, y: 50 });
202
203        let bottom_bar = SettingsEditorBottomBar::new(
204            rect,
205            BottomBarVariant::TwoButtons {
206                left_event: Event::Back,
207                left_icon: "back",
208                right_event: Event::Validate,
209                right_icon: "check_mark",
210            },
211        );
212
213        let children = bottom_bar.children();
214        let left_button_rect = children[1].rect();
215        let right_button_rect = children[2].rect();
216
217        assert_eq!(
218            left_button_rect.width(),
219            100,
220            "Left button should be 100 units wide"
221        );
222        assert_eq!(
223            right_button_rect.width(),
224            100,
225            "Right button should be 100 units wide"
226        );
227        assert_eq!(left_button_rect.min.x, 0);
228        assert_eq!(left_button_rect.max.x, 100);
229        assert_eq!(right_button_rect.min.x, 100);
230        assert_eq!(right_button_rect.max.x, 200);
231    }
232}