1use iced::{
2 Alignment, Border, Element, Font, Length, Size,
3 advanced::{Text, text::Paragraph},
4 padding,
5};
6use iced_widget::{
7 button, center, center_x, column, responsive, row,
8 text::{LineHeight, Shaping, Wrapping},
9};
10
11use crate::{style::mix_colors, theme::ColorScheme};
12
13const COMPACT_BUTTON_SIZE: Size<f32> = Size {
14 width: 56.0,
15 height: 32.0,
16};
17const LARGE_BUTTON_SIZE: Size<f32> = Size {
18 width: 100.0,
19 height: 40.0,
20};
21const LARGE_BUTTON_SPACING: f32 = 24.0;
22const DEFAULT_LABEL_SIZE: f32 = 14.0;
23const DEFAULT_ICON_SIZE: f32 = 24.0;
24const COMPACT_VERTICAL_PADDING: f32 = 6.0;
25const LARGE_VERTICAL_PADDING: f32 = 12.0;
26const LARGE_BUTTON_INTERNAL_SPACING: f32 = 4.0;
27const LARGE_BUTTON_PADDING: f32 = 16.0;
28
29#[derive(Default)]
30pub enum Mode {
31 Compact,
32 Large,
33 #[default]
34 Auto,
35}
36
37pub struct Item<'a, Message> {
38 pub icon: &'a char,
39 pub label: &'a str,
40 pub message: Message,
41}
42
43pub struct Navbar<'a, Message> {
44 compact: bool,
45 items: Vec<Item<'a, Message>>,
46 theme: &'a dyn ColorScheme,
47 font: Option<Font>,
48 icon_font_active: Option<Font>,
49 icon_font_inactive: Option<Font>,
50 mode: Mode,
51 focused_index: usize,
52 icon_size: f32,
53 label_size: f32,
54}
55
56impl<'a, Message> Navbar<'a, Message> {
57 #[must_use]
58 pub fn new(items: Vec<Item<'a, Message>>, theme: &'a impl ColorScheme) -> Self {
59 Self {
60 compact: false,
61 items,
62 theme,
63 font: None,
64 icon_font_active: None,
65 icon_font_inactive: None,
66 mode: Mode::default(),
67 focused_index: 0,
68 icon_size: DEFAULT_ICON_SIZE,
69 label_size: DEFAULT_LABEL_SIZE,
70 }
71 }
72
73 #[must_use]
74 pub fn compact(mut self, val: bool) -> Self {
75 self.compact = val;
76 self
77 }
78
79 #[must_use]
80 pub fn item(mut self, item: Item<'a, Message>) -> Self {
81 self.items.push(item);
82 self
83 }
84
85 #[must_use]
86 pub fn font(mut self, font: Font) -> Self {
87 self.font = Some(font);
88 self
89 }
90
91 #[must_use]
92 pub fn icon_font_active(mut self, font: Font) -> Self {
93 self.icon_font_active = Some(font);
94 self
95 }
96
97 #[must_use]
98 pub fn icon_font_inactive(mut self, font: Font) -> Self {
99 self.icon_font_inactive = Some(font);
100 self
101 }
102
103 #[must_use]
104 pub fn mode(mut self, mode: Mode) -> Self {
105 self.mode = mode;
106 self
107 }
108
109 #[must_use]
110 pub fn focused_index(mut self, index: usize) -> Self {
111 self.focused_index = index;
112 self
113 }
114
115 #[must_use]
116 pub fn icon_size(mut self, size: f32) -> Self {
117 self.icon_size = size;
118 self
119 }
120
121 #[must_use]
122 pub fn label_size(mut self, size: f32) -> Self {
123 self.label_size = size;
124 self
125 }
126}
127
128impl<'a, Message> From<Navbar<'a, Message>> for Element<'a, Message, iced::Theme, iced::Renderer>
129where
130 Message: 'a + Clone,
131{
132 fn from(navbar: Navbar<'a, Message>) -> Self {
133 let font = navbar.font.unwrap_or_default();
134
135 let mut max_width = 0.0;
136 for item in &navbar.items {
137 let p = iced::advanced::graphics::text::Paragraph::with_text(Text {
138 content: item.label,
139 bounds: Size::INFINITE,
140 size: navbar.label_size.into(),
141 line_height: LineHeight::default(),
142 font,
143 align_x: iced_widget::text::Alignment::Left,
144 align_y: iced::alignment::Vertical::Top,
145 shaping: Shaping::Advanced,
146 wrapping: Wrapping::None,
147 });
148 let width = p.min_bounds().width;
149
150 if width > max_width {
151 max_width = width
152 }
153 }
154
155 max_width += navbar.icon_size + LARGE_BUTTON_INTERNAL_SPACING + 2.0 * LARGE_BUTTON_PADDING;
162
163 responsive(move |size| {
164 let compact = match navbar.mode {
165 Mode::Compact => true,
166 Mode::Large => false,
167 Mode::Auto => {
168 navbar.items.len() as f32 * max_width
169 + (navbar.items.len() as i64 - 1) as f32 * LARGE_BUTTON_SPACING
170 > size.width
171 }
172 };
173
174 let font = navbar.font.unwrap_or_default();
175 let icon_font_active = navbar
176 .icon_font_active
177 .unwrap_or(navbar.icon_font_inactive.unwrap_or_default());
178 let icon_font_inactive = navbar
179 .icon_font_inactive
180 .unwrap_or(navbar.icon_font_active.unwrap_or_default());
181 let buttons = if navbar.items.is_empty() {
182 Vec::new()
183 } else {
184 if compact {
185 let mut buttons = Vec::with_capacity(navbar.items.len());
186 for (i, item) in navbar.items.iter().enumerate() {
187 let active = i == navbar.focused_index;
188 let label_color = if active {
189 navbar.theme.secondary()
190 } else {
191 navbar.theme.on_surface_variant()
192 };
193 let icon_color = if active {
194 navbar.theme.on_secondary_container()
195 } else {
196 navbar.theme.on_surface_variant()
197 };
198 let icon_font = if active {
199 icon_font_active
200 } else {
201 icon_font_inactive
202 };
203
204 buttons.push(
205 center_x(
206 column![
207 button(center(
208 iced_widget::text(item.icon)
209 .font(icon_font)
210 .color(icon_color)
211 .size(navbar.icon_size)
212 .align_x(Alignment::Center)
213 .align_y(Alignment::Center)
214 ))
215 .style(move |_, status| {
216 let state_layer_color =
217 navbar.theme.on_secondary_container();
218 let layer_opacity = match status {
219 button::Status::Active => 0.0,
220 button::Status::Hovered => 0.08,
221 button::Status::Pressed => 0.1,
222 button::Status::Disabled => unreachable!(),
223 };
224 let color = if active {
225 mix_colors(
226 state_layer_color,
227 navbar.theme.secondary_container(),
228 layer_opacity,
229 )
230 } else {
231 state_layer_color.scale_alpha(layer_opacity)
232 };
233 button::Style {
234 background: Some(iced::Background::Color(color)),
235 border: Border {
236 radius: f32::MAX.into(),
237 ..Default::default()
238 },
239 ..Default::default()
240 }
241 })
242 .on_press(item.message.clone())
243 .width(Length::Fixed(COMPACT_BUTTON_SIZE.width))
244 .height(Length::Fixed(COMPACT_BUTTON_SIZE.height)),
245 iced_widget::text(item.label)
246 .font(font)
247 .color(label_color)
248 .size(navbar.label_size)
249 .wrapping(Wrapping::None),
250 ]
251 .align_x(Alignment::Center)
252 .spacing(4.0),
253 )
254 .into(),
255 );
256 }
257 buttons
258 } else {
259 let mut buttons = Vec::with_capacity(navbar.items.len());
260 for (i, item) in navbar.items.iter().enumerate() {
261 let active = i == navbar.focused_index;
262 let label_color = if active {
263 navbar.theme.secondary()
264 } else {
265 navbar.theme.on_surface_variant()
266 };
267 let icon_color = if active {
268 navbar.theme.on_secondary_container()
269 } else {
270 navbar.theme.on_surface_variant()
271 };
272 let icon_font = if active {
273 icon_font_active
274 } else {
275 icon_font_inactive
276 };
277
278 buttons.push(
279 button(center(
280 row![
281 iced_widget::text(item.icon)
282 .font(icon_font)
283 .color(icon_color)
284 .size(navbar.icon_size),
285 iced_widget::text(item.label)
286 .font(font)
287 .color(label_color)
288 .size(navbar.label_size)
289 ]
290 .align_y(Alignment::Center)
291 .spacing(LARGE_BUTTON_INTERNAL_SPACING),
292 ))
293 .style(move |_, status| {
294 let state_layer_color = navbar.theme.on_secondary_container();
295 let layer_opacity = match status {
296 button::Status::Active => 0.0,
297 button::Status::Hovered => 0.08,
298 button::Status::Pressed => 0.1,
299 button::Status::Disabled => unreachable!(),
300 };
301 let color = if active {
302 mix_colors(
303 state_layer_color,
304 navbar.theme.secondary_container(),
305 layer_opacity,
306 )
307 } else {
308 state_layer_color.scale_alpha(layer_opacity)
309 };
310 button::Style {
311 background: Some(iced::Background::Color(color)),
312 border: Border {
313 radius: f32::MAX.into(),
314 ..Default::default()
315 },
316 ..Default::default()
317 }
318 })
319 .on_press(item.message.clone())
320 .width(Length::Fixed(max_width))
321 .height(Length::Fixed(LARGE_BUTTON_SIZE.height))
322 .into(),
323 )
324 }
325 buttons
326 }
327 };
328
329 let padding = if compact {
330 COMPACT_VERTICAL_PADDING
331 } else {
332 LARGE_VERTICAL_PADDING
333 };
334 let spacing = if compact { 0.0 } else { LARGE_BUTTON_SPACING };
335 center_x(row(buttons).spacing(spacing))
336 .padding(padding::vertical(padding))
337 .into()
338 })
339 .height(Length::Shrink)
340 .into()
341 }
342}