Skip to main content

cadmus_core/view/settings_editor/
bottom_bar.rs

1use crate::color::WHITE;
2use crate::font::Fonts;
3use crate::framebuffer::Framebuffer;
4use crate::geom::{CycleDir, 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    /// Navigation bar with prev/next arrows and a center action button.
31    /// Used for Libraries pagination: prev | center icon | next.
32    PaginationWithButton {
33        prev_enabled: bool,
34        next_enabled: bool,
35        center_event: Event,
36        center_icon: &'static str,
37    },
38    /// Navigation bar with prev/next arrows only (no center content).
39    /// Used for non-Libraries pagination: prev | spacer | next.
40    Pagination {
41        prev_enabled: bool,
42        next_enabled: bool,
43    },
44}
45
46/// Reusable bottom bar component for settings editor views
47///
48/// Provides a consistent bottom bar with white background and configurable
49/// button layout. Supports single centered button or two buttons with 50/50 split.
50pub struct SettingsEditorBottomBar {
51    id: Id,
52    rect: Rectangle,
53    children: Vec<Box<dyn View>>,
54}
55
56impl SettingsEditorBottomBar {
57    /// Creates a new settings editor bottom bar
58    ///
59    /// # Arguments
60    ///
61    /// * `rect` - The rectangle defining the bottom bar's position and size
62    /// * `variant` - The button layout variant to use
63    ///
64    /// # Returns
65    ///
66    /// A new `SettingsEditorBottomBar` instance
67    ///
68    /// # Examples
69    ///
70    /// ```
71    /// use cadmus_core::view::settings_editor::{SettingsEditorBottomBar, BottomBarVariant};
72    /// use cadmus_core::view::Event;
73    /// use cadmus_core::geom::{Rectangle, Point};
74    ///
75    /// let rect = Rectangle::new(Point { x: 0, y: 0 }, Point { x: 100, y: 50 });
76    /// let bottom_bar = SettingsEditorBottomBar::new(
77    ///     rect,
78    ///     BottomBarVariant::SingleButton {
79    ///         event: Event::Validate,
80    ///         icon: "check_mark-large",
81    ///     },
82    /// );
83    /// ```
84    #[cfg_attr(feature = "tracing", tracing::instrument(skip_all))]
85    pub fn new(rect: Rectangle, variant: BottomBarVariant) -> Self {
86        let id = ID_FEEDER.next();
87        let mut children = Vec::new();
88
89        let background = Filler::new(rect, WHITE);
90        children.push(Box::new(background) as Box<dyn View>);
91
92        match variant {
93            BottomBarVariant::SingleButton {
94                event,
95                icon: icon_name,
96            } => {
97                let icon = Icon::new(icon_name, rect, event);
98                children.push(Box::new(icon) as Box<dyn View>);
99            }
100            BottomBarVariant::TwoButtons {
101                left_event,
102                left_icon,
103                right_event,
104                right_icon,
105            } => {
106                let button_width = rect.width() as i32 / 2;
107
108                let left_rect = rect![
109                    rect.min.x,
110                    rect.min.y,
111                    rect.min.x + button_width,
112                    rect.max.y
113                ];
114                let left_button = Icon::new(left_icon, left_rect, left_event);
115                children.push(Box::new(left_button) as Box<dyn View>);
116
117                let right_rect = rect![
118                    rect.min.x + button_width,
119                    rect.min.y,
120                    rect.max.x,
121                    rect.max.y
122                ];
123                let right_button = Icon::new(right_icon, right_rect, right_event);
124                children.push(Box::new(right_button) as Box<dyn View>);
125            }
126            BottomBarVariant::PaginationWithButton {
127                prev_enabled,
128                next_enabled,
129                center_event,
130                center_icon,
131            } => {
132                let (left_rect, center_rect, right_rect) = Self::pagination_rects(rect);
133                Self::push_prev_arrow(&mut children, left_rect, prev_enabled);
134                children
135                    .push(Box::new(Icon::new(center_icon, center_rect, center_event))
136                        as Box<dyn View>);
137                Self::push_next_arrow(&mut children, right_rect, next_enabled);
138            }
139            BottomBarVariant::Pagination {
140                prev_enabled,
141                next_enabled,
142            } => {
143                let (left_rect, center_rect, right_rect) = Self::pagination_rects(rect);
144                Self::push_prev_arrow(&mut children, left_rect, prev_enabled);
145                children.push(Box::new(Filler::new(center_rect, WHITE)) as Box<dyn View>);
146                Self::push_next_arrow(&mut children, right_rect, next_enabled);
147            }
148        }
149
150        SettingsEditorBottomBar { id, rect, children }
151    }
152
153    /// Splits `rect` into equal left, center, and right thirds for pagination layouts.
154    fn pagination_rects(rect: Rectangle) -> (Rectangle, Rectangle, Rectangle) {
155        let third_width = rect.width() as i32 / 3;
156        let left_rect = rect![rect.min.x, rect.min.y, rect.min.x + third_width, rect.max.y];
157        let center_rect = rect![
158            rect.min.x + third_width,
159            rect.min.y,
160            rect.max.x - third_width,
161            rect.max.y
162        ];
163        let right_rect = rect![rect.max.x - third_width, rect.min.y, rect.max.x, rect.max.y];
164        (left_rect, center_rect, right_rect)
165    }
166
167    fn push_prev_arrow(children: &mut Vec<Box<dyn View>>, rect: Rectangle, enabled: bool) {
168        if enabled {
169            children.push(Box::new(Icon::new(
170                "arrow-left",
171                rect,
172                Event::Page(CycleDir::Previous),
173            )) as Box<dyn View>);
174        } else {
175            children.push(Box::new(Filler::new(rect, WHITE)) as Box<dyn View>);
176        }
177    }
178
179    fn push_next_arrow(children: &mut Vec<Box<dyn View>>, rect: Rectangle, enabled: bool) {
180        if enabled {
181            children.push(
182                Box::new(Icon::new("arrow-right", rect, Event::Page(CycleDir::Next)))
183                    as Box<dyn View>,
184            );
185        } else {
186            children.push(Box::new(Filler::new(rect, WHITE)) as Box<dyn View>);
187        }
188    }
189}
190
191impl View for SettingsEditorBottomBar {
192    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, _hub, _bus, _rq, _context), fields(event = ?_evt), ret(level=tracing::Level::TRACE)))]
193    fn handle_event(
194        &mut self,
195        _evt: &Event,
196        _hub: &crate::view::Hub,
197        _bus: &mut crate::view::Bus,
198        _rq: &mut crate::view::RenderQueue,
199        _context: &mut crate::context::Context,
200    ) -> bool {
201        false
202    }
203
204    #[cfg_attr(feature = "tracing", tracing::instrument(skip(self, _fb, _fonts), fields(rect = ?_rect)))]
205    fn render(&self, _fb: &mut dyn Framebuffer, _rect: Rectangle, _fonts: &mut Fonts) {}
206
207    fn rect(&self) -> &Rectangle {
208        &self.rect
209    }
210
211    fn rect_mut(&mut self) -> &mut Rectangle {
212        &mut self.rect
213    }
214
215    fn children(&self) -> &Vec<Box<dyn View>> {
216        &self.children
217    }
218
219    fn children_mut(&mut self) -> &mut Vec<Box<dyn View>> {
220        &mut self.children
221    }
222
223    fn id(&self) -> Id {
224        self.id
225    }
226}
227
228#[cfg(test)]
229mod tests {
230    use super::*;
231    use crate::geom::Point;
232
233    #[test]
234    fn test_single_button_creates_two_children() {
235        let rect = Rectangle::new(Point { x: 0, y: 0 }, Point { x: 100, y: 50 });
236
237        let bottom_bar = SettingsEditorBottomBar::new(
238            rect,
239            BottomBarVariant::SingleButton {
240                event: Event::Back,
241                icon: "back",
242            },
243        );
244
245        assert_eq!(
246            bottom_bar.children().len(),
247            2,
248            "SingleButton variant should have 2 children: background filler and icon"
249        );
250    }
251
252    #[test]
253    fn test_two_buttons_creates_three_children() {
254        let rect = Rectangle::new(Point { x: 0, y: 0 }, Point { x: 100, y: 50 });
255
256        let bottom_bar = SettingsEditorBottomBar::new(
257            rect,
258            BottomBarVariant::TwoButtons {
259                left_event: Event::Back,
260                left_icon: "back",
261                right_event: Event::Validate,
262                right_icon: "check_mark",
263            },
264        );
265
266        assert_eq!(
267            bottom_bar.children().len(),
268            3,
269            "TwoButtons variant should have 3 children: background filler, left icon, and right icon"
270        );
271    }
272
273    #[test]
274    fn test_two_buttons_split_width_evenly() {
275        let rect = Rectangle::new(Point { x: 0, y: 0 }, Point { x: 200, y: 50 });
276
277        let bottom_bar = SettingsEditorBottomBar::new(
278            rect,
279            BottomBarVariant::TwoButtons {
280                left_event: Event::Back,
281                left_icon: "back",
282                right_event: Event::Validate,
283                right_icon: "check_mark",
284            },
285        );
286
287        let children = bottom_bar.children();
288        let left_button_rect = children[1].rect();
289        let right_button_rect = children[2].rect();
290
291        assert_eq!(
292            left_button_rect.width(),
293            100,
294            "Left button should be 100 units wide"
295        );
296        assert_eq!(
297            right_button_rect.width(),
298            100,
299            "Right button should be 100 units wide"
300        );
301        assert_eq!(left_button_rect.min.x, 0);
302        assert_eq!(left_button_rect.max.x, 100);
303        assert_eq!(right_button_rect.min.x, 100);
304        assert_eq!(right_button_rect.max.x, 200);
305    }
306}