ratatui_toolkit/primitives/menu_bar/methods/
mod.rs

1mod apply_theme;
2mod with_theme;
3
4use crate::primitives::menu_bar::functions::display_width;
5use crate::primitives::menu_bar::MenuBar;
6use ratatui::layout::{Constraint, Direction, Layout, Rect};
7use ratatui::widgets::{Block, BorderType, Borders, Paragraph};
8use ratatui::Frame;
9
10impl MenuBar {
11    pub fn update_hover(&mut self, column: u16, row: u16) {
12        for item in &mut self.items {
13            item.hovered = if let Some(area) = item.area {
14                column >= area.x
15                    && column < area.x + area.width
16                    && row >= area.y
17                    && row < area.y + area.height
18            } else {
19                false
20            };
21        }
22    }
23
24    pub fn handle_click(&mut self, column: u16, row: u16) -> Option<usize> {
25        let clicked_index = self.items.iter().enumerate().find_map(|(i, item)| {
26            if let Some(area) = item.area {
27                if column >= area.x
28                    && column < area.x + area.width
29                    && row >= area.y
30                    && row < area.y + area.height
31                {
32                    return Some(i);
33                }
34            }
35            None
36        });
37
38        if let Some(clicked) = clicked_index {
39            for (i, item) in self.items.iter_mut().enumerate() {
40                item.selected = i == clicked;
41            }
42        }
43
44        clicked_index
45    }
46
47    pub fn selected(&self) -> Option<usize> {
48        self.items.iter().position(|item| item.selected)
49    }
50
51    /// Render the menu bar as a connected series of items with rounded border
52    pub fn render(&mut self, frame: &mut Frame, area: Rect) {
53        self.render_with_offset(frame, area, 0);
54    }
55
56    /// Render the menu bar with a left offset to make room for other components
57    pub fn render_with_offset(&mut self, frame: &mut Frame, area: Rect, left_offset: u16) {
58        if self.items.is_empty() {
59            return;
60        }
61
62        // Calculate required width based on menu items using proper display width
63        // Items have no padding, separators have spacing, 1 space at start and end
64        let total_label_width: usize = self
65            .items
66            .iter()
67            .map(|item| display_width(&item.display_label()))
68            .sum();
69        let separators = (self.items.len() - 1) * 3; // " │ " between items (1 space + separator + 1 space)
70        let needed_width = (total_label_width + separators + 4) as u16; // +2 for borders + 2 for start/end spaces
71
72        // Adjust area to account for left offset
73        let available_width = area.width.saturating_sub(left_offset);
74
75        // Create a fixed-width area for the button group, shifted by the offset
76        let button_group_area = Rect {
77            x: area.x + left_offset,
78            y: area.y,
79            width: needed_width.min(available_width),
80            height: area.height,
81        };
82
83        self.area = Some(button_group_area);
84
85        // Create a block with rounded border around the entire group
86        let block = Block::default()
87            .borders(Borders::ALL)
88            .border_type(BorderType::Rounded);
89
90        let inner_area = block.inner(button_group_area);
91        frame.render_widget(block, button_group_area);
92
93        // Start with 1 space padding from the left border
94        let mut x_offset = inner_area.x + 1;
95        let button_count = self.items.len();
96
97        for (i, item) in self.items.iter_mut().enumerate() {
98            // No padding on menu item - use custom display width for Nerd Font icons
99            let label = item.display_label();
100            let item_width = display_width(&label) as u16;
101
102            // Check if we have space left to render this item
103            let available_width = (inner_area.x + inner_area.width).saturating_sub(x_offset);
104            if available_width == 0 {
105                break; // No space left
106            }
107
108            // Limit item width to available space
109            let actual_item_width = item_width.min(available_width);
110
111            let item_area = Rect {
112                x: x_offset,
113                y: inner_area.y,
114                width: actual_item_width,
115                height: inner_area.height,
116            };
117
118            item.area = Some(item_area);
119
120            // Determine style based on state
121            let style = match (item.selected, item.hovered) {
122                (true, true) => self.selected_hover_style,
123                (true, false) => self.selected_style,
124                (false, true) => self.hover_style,
125                (false, false) => self.normal_style,
126            };
127
128            // Create menu item text with no padding
129            // Truncate label if needed to fit available space
130            let display_label = if actual_item_width < item_width {
131                // Truncate the label to fit
132                label
133                    .chars()
134                    .take(actual_item_width as usize)
135                    .collect::<String>()
136            } else {
137                label
138            };
139            let paragraph = Paragraph::new(display_label).style(style);
140            frame.render_widget(paragraph, item_area);
141
142            x_offset += actual_item_width;
143
144            // Render separator after item (except for last item)
145            // Check if there's enough space left before rendering
146            if i < button_count - 1 && x_offset + 3 <= inner_area.x + inner_area.width {
147                let separator_area = Rect {
148                    x: x_offset,
149                    y: inner_area.y,
150                    width: 3, // " │ " (1 space + separator + 1 space)
151                    height: inner_area.height,
152                };
153                let separator = Paragraph::new(" │ ");
154                frame.render_widget(separator, separator_area);
155                x_offset += 3;
156            }
157        }
158    }
159
160    /// Render with a centered layout (useful for menu bars)
161    pub fn render_centered(&mut self, frame: &mut Frame, area: Rect) {
162        // Calculate total width needed for all menu items
163        let total_chars: usize = self
164            .items
165            .iter()
166            .map(|item| display_width(&item.display_label()) + 4)
167            .sum(); // +4 for borders/padding
168        let needed_width = total_chars as u16;
169
170        // Create a centered layout
171        let chunks = Layout::default()
172            .direction(Direction::Horizontal)
173            .constraints([
174                Constraint::Length((area.width.saturating_sub(needed_width)) / 2),
175                Constraint::Length(needed_width.min(area.width)),
176                Constraint::Min(0),
177            ])
178            .split(area);
179
180        self.render(frame, chunks[1]);
181    }
182}