1use iced_widget::core::text as core_text;
4use iced_widget::core::{Background, Element, Length, Padding, alignment, border};
5use iced_widget::graphics::geometry;
6use iced_widget::renderer::wgpu::primitive;
7use iced_widget::text;
8use iced_widget::{Column, Container, Row, Space, Text};
9
10use super::{absolute_line_height, button::Button};
11use crate::utils::shadow_from_level;
12use crate::{Theme, fonts, style::button as button_style, tokens};
13
14pub const STATUS_BAR_HEIGHT: f32 = 24.0;
16
17pub fn icon_button<'a, Message, Renderer>(
19 icon_name: impl text::IntoFragment<'a>,
20) -> Button<'a, Message, Renderer>
21where
22 Message: Clone + 'a,
23 Renderer: geometry::Renderer + core_text::Renderer + 'a,
24 iced_widget::core::Font: Into<Renderer::Font>,
25{
26 Button::new(
27 Container::new(fonts::icon(
28 icon_name,
29 tokens::component::app_bar::ICON_SIZE,
30 ))
31 .center_x(Length::Fixed(
32 tokens::component::icon_button::CONTAINER_WIDTH,
33 ))
34 .center_y(Length::Fixed(
35 tokens::component::icon_button::CONTAINER_HEIGHT,
36 )),
37 )
38 .width(Length::Fixed(
39 tokens::component::icon_button::CONTAINER_WIDTH,
40 ))
41 .height(Length::Fixed(
42 tokens::component::icon_button::CONTAINER_HEIGHT,
43 ))
44 .padding(Padding::ZERO)
45 .style(button_style::icon)
46}
47
48pub fn icon_action<'a, Message, Renderer>(
50 icon_name: impl text::IntoFragment<'a>,
51 on_press: Message,
52) -> Element<'a, Message, Theme, Renderer>
53where
54 Message: Clone + 'a,
55 Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
56 iced_widget::core::Font: Into<Renderer::Font>,
57{
58 icon_button(icon_name).on_press(on_press).into()
59}
60
61pub fn icon_actions<'a, Message, Renderer, Icon>(
63 actions: impl IntoIterator<Item = (Icon, Message)>,
64) -> Vec<Element<'a, Message, Theme, Renderer>>
65where
66 Icon: text::IntoFragment<'a>,
67 Message: Clone + 'a,
68 Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
69 iced_widget::core::Font: Into<Renderer::Font>,
70{
71 actions
72 .into_iter()
73 .map(|(icon_name, on_press)| icon_action(icon_name, on_press))
74 .collect()
75}
76
77pub fn small<'a, Message, Renderer>(
79 title: impl text::IntoFragment<'a>,
80 leading: Option<Element<'a, Message, Theme, Renderer>>,
81 actions: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
82) -> Container<'a, Message, Theme, Renderer>
83where
84 Message: 'a,
85 Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
86{
87 let title_text = tokens::component::app_bar::SMALL_TITLE_TEXT;
88 let mut content = Row::new()
89 .spacing(tokens::component::app_bar::ICON_BUTTON_SPACE)
90 .align_y(alignment::Vertical::Center)
91 .width(Length::Fill);
92
93 if let Some(leading) = leading {
94 content = content.push(Container::new(leading).padding(Padding {
95 top: 0.0,
96 right: 0.0,
97 bottom: 0.0,
98 left: tokens::component::app_bar::LEADING_SPACE,
99 }));
100 } else {
101 content = content.push(Space::new().width(Length::Fixed(
102 tokens::component::app_bar::TITLE_LEADING_SPACE
103 - tokens::component::app_bar::LEADING_SPACE,
104 )));
105 }
106
107 content = content.push(
108 Container::new(
109 Text::new(title)
110 .size(title_text.size)
111 .line_height(absolute_line_height(title_text.line_height)),
112 )
113 .width(Length::Fill)
114 .padding(Padding {
115 top: 0.0,
116 right: tokens::component::app_bar::LEADING_SPACE,
117 bottom: 0.0,
118 left: tokens::component::app_bar::LEADING_SPACE,
119 }),
120 );
121
122 let mut action_content = Row::new();
123 for action in actions {
124 action_content = action_content.push(action);
125 }
126 content = content.push(Container::new(action_content).padding(Padding {
127 top: 0.0,
128 right: tokens::component::app_bar::TRAILING_SPACE,
129 bottom: 0.0,
130 left: 0.0,
131 }));
132
133 top_container(
134 content,
135 tokens::component::app_bar::SMALL_CONTAINER_HEIGHT,
136 false,
137 )
138}
139
140pub fn small_on_scroll<'a, Message, Renderer>(
142 title: impl text::IntoFragment<'a>,
143 leading: Option<Element<'a, Message, Theme, Renderer>>,
144 actions: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
145) -> Container<'a, Message, Theme, Renderer>
146where
147 Message: 'a,
148 Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
149{
150 let title_text = tokens::component::app_bar::SMALL_TITLE_TEXT;
151 let mut content = Row::new()
152 .spacing(tokens::component::app_bar::ICON_BUTTON_SPACE)
153 .align_y(alignment::Vertical::Center)
154 .width(Length::Fill);
155
156 if let Some(leading) = leading {
157 content = content.push(Container::new(leading).padding(Padding {
158 top: 0.0,
159 right: 0.0,
160 bottom: 0.0,
161 left: tokens::component::app_bar::LEADING_SPACE,
162 }));
163 } else {
164 content = content.push(Space::new().width(Length::Fixed(
165 tokens::component::app_bar::TITLE_LEADING_SPACE
166 - tokens::component::app_bar::LEADING_SPACE,
167 )));
168 }
169
170 content = content.push(
171 Container::new(
172 Text::new(title)
173 .size(title_text.size)
174 .line_height(absolute_line_height(title_text.line_height)),
175 )
176 .width(Length::Fill)
177 .padding(Padding {
178 top: 0.0,
179 right: tokens::component::app_bar::LEADING_SPACE,
180 bottom: 0.0,
181 left: tokens::component::app_bar::LEADING_SPACE,
182 }),
183 );
184
185 let mut action_content = Row::new();
186 for action in actions {
187 action_content = action_content.push(action);
188 }
189 content = content.push(Container::new(action_content).padding(Padding {
190 top: 0.0,
191 right: tokens::component::app_bar::TRAILING_SPACE,
192 bottom: 0.0,
193 left: 0.0,
194 }));
195
196 top_container(
197 content,
198 tokens::component::app_bar::SMALL_CONTAINER_HEIGHT,
199 true,
200 )
201}
202
203pub fn medium<'a, Message, Renderer>(
205 title: impl text::IntoFragment<'a>,
206 leading: Option<Element<'a, Message, Theme, Renderer>>,
207 actions: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
208) -> Container<'a, Message, Theme, Renderer>
209where
210 Message: 'a,
211 Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
212{
213 flexible(title, leading, actions, FlexibleSize::Medium)
214}
215
216pub fn large<'a, Message, Renderer>(
218 title: impl text::IntoFragment<'a>,
219 leading: Option<Element<'a, Message, Theme, Renderer>>,
220 actions: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
221) -> Container<'a, Message, Theme, Renderer>
222where
223 Message: 'a,
224 Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
225{
226 flexible(title, leading, actions, FlexibleSize::Large)
227}
228
229pub fn bottom<'a, Message, Renderer>(
231 actions: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
232 floating_action: Option<Element<'a, Message, Theme, Renderer>>,
233) -> Container<'a, Message, Theme, Renderer>
234where
235 Message: 'a,
236 Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
237{
238 let mut content = Row::new()
239 .spacing(tokens::component::app_bar::ICON_BUTTON_SPACE)
240 .align_y(alignment::Vertical::Center)
241 .width(Length::Fill);
242
243 for action in actions {
244 content = content.push(action);
245 }
246
247 content = content.push(Space::new().width(Length::Fill));
248
249 if let Some(fab) = floating_action {
250 content = content.push(fab);
251 }
252
253 Container::new(content)
254 .height(Length::Fixed(
255 tokens::component::bottom_app_bar::CONTAINER_HEIGHT,
256 ))
257 .width(Length::Fill)
258 .padding(Padding {
259 top: 0.0,
260 right: 16.0,
261 bottom: 0.0,
262 left: 4.0,
263 })
264 .align_y(alignment::Vertical::Center)
265 .style(bottom_style)
266}
267
268pub fn status_bar<'a, Message, Renderer>() -> Container<'a, Message, Theme, Renderer>
270where
271 Message: 'a,
272 Renderer: iced_widget::core::Renderer + 'a,
273{
274 Container::new(Space::new())
275 .height(Length::Fixed(STATUS_BAR_HEIGHT))
276 .width(Length::Fill)
277 .style(|theme| top_style(theme, false))
278}
279
280pub fn with_status_bar<'a, Message, Renderer>(
282 app_bar: impl Into<Element<'a, Message, Theme, Renderer>>,
283) -> Column<'a, Message, Theme, Renderer>
284where
285 Message: 'a,
286 Renderer: iced_widget::core::Renderer + 'a,
287{
288 Column::new()
289 .push(status_bar())
290 .push(app_bar)
291 .spacing(0)
292 .width(Length::Fill)
293}
294
295#[derive(Debug, Clone, Copy, PartialEq, Eq)]
296enum FlexibleSize {
297 Medium,
298 Large,
299}
300
301impl FlexibleSize {
302 fn container_height(self) -> f32 {
303 match self {
304 Self::Medium => tokens::component::app_bar::MEDIUM_CONTAINER_HEIGHT,
305 Self::Large => tokens::component::app_bar::LARGE_CONTAINER_HEIGHT,
306 }
307 }
308
309 fn title_text(self) -> tokens::typography::TypeScale {
310 match self {
311 Self::Medium => tokens::component::app_bar::MEDIUM_TITLE_TEXT,
312 Self::Large => tokens::component::app_bar::LARGE_TITLE_TEXT,
313 }
314 }
315}
316
317fn flexible<'a, Message, Renderer>(
318 title: impl text::IntoFragment<'a>,
319 leading: Option<Element<'a, Message, Theme, Renderer>>,
320 actions: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
321 size: FlexibleSize,
322) -> Container<'a, Message, Theme, Renderer>
323where
324 Message: 'a,
325 Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
326{
327 let title_text = size.title_text();
328 let content = Column::new()
329 .push(navigation_row(leading, actions).height(Length::Fixed(
330 tokens::component::app_bar::SMALL_CONTAINER_HEIGHT,
331 )))
332 .push(
333 Container::new(
334 Text::new(title)
335 .size(title_text.size)
336 .line_height(absolute_line_height(title_text.line_height)),
337 )
338 .width(Length::Fill)
339 .height(Length::Fill)
340 .padding(Padding {
341 top: 0.0,
342 right: 16.0,
343 bottom: 20.0,
344 left: 16.0,
345 })
346 .align_y(alignment::Vertical::Bottom),
347 )
348 .width(Length::Fill);
349
350 top_container(content, size.container_height(), false)
351}
352
353fn navigation_row<'a, Message, Renderer>(
354 leading: Option<Element<'a, Message, Theme, Renderer>>,
355 actions: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
356) -> Row<'a, Message, Theme, Renderer>
357where
358 Message: 'a,
359 Renderer: iced_widget::core::Renderer + 'a,
360{
361 let mut content = Row::new()
362 .spacing(tokens::component::app_bar::ICON_BUTTON_SPACE)
363 .padding(app_bar_padding())
364 .align_y(alignment::Vertical::Center)
365 .width(Length::Fill);
366
367 if let Some(leading) = leading {
368 content = content.push(leading);
369 }
370
371 content = content.push(Space::new().width(Length::Fill));
372
373 for action in actions {
374 content = content.push(action);
375 }
376
377 content
378}
379
380fn app_bar_padding() -> Padding {
381 Padding {
382 top: 0.0,
383 right: tokens::component::app_bar::TRAILING_SPACE,
384 bottom: 0.0,
385 left: tokens::component::app_bar::LEADING_SPACE,
386 }
387}
388
389fn top_container<'a, Message, Renderer>(
390 content: impl Into<Element<'a, Message, Theme, Renderer>>,
391 height: f32,
392 on_scroll: bool,
393) -> Container<'a, Message, Theme, Renderer>
394where
395 Message: 'a,
396 Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
397{
398 Container::new(content)
399 .height(Length::Fixed(height))
400 .width(Length::Fill)
401 .style(move |theme| top_style(theme, on_scroll))
402}
403
404fn top_style(theme: &Theme, on_scroll: bool) -> iced_widget::container::Style {
405 let colors = theme.colors();
406 let (background, elevation) = if on_scroll {
407 (
408 colors.surface.container.base,
409 tokens::component::app_bar::ON_SCROLL_CONTAINER_ELEVATION_LEVEL,
410 )
411 } else {
412 (
413 colors.surface.color,
414 tokens::component::app_bar::CONTAINER_ELEVATION_LEVEL,
415 )
416 };
417
418 iced_widget::container::Style {
419 background: Some(Background::Color(background)),
420 text_color: Some(colors.surface.text),
421 border: border::rounded(tokens::component::app_bar::CONTAINER_SHAPE),
422 shadow: shadow_from_level(elevation, colors.shadow),
423 snap: cfg!(feature = "crisp"),
424 }
425}
426
427fn bottom_style(theme: &Theme) -> iced_widget::container::Style {
428 let colors = theme.colors();
429
430 iced_widget::container::Style {
431 background: Some(Background::Color(colors.surface.container.base)),
432 text_color: Some(colors.surface.text),
433 border: border::rounded(tokens::component::bottom_app_bar::CONTAINER_SHAPE),
434 shadow: shadow_from_level(
435 tokens::component::bottom_app_bar::CONTAINER_ELEVATION_LEVEL,
436 colors.shadow,
437 ),
438 snap: cfg!(feature = "crisp"),
439 }
440}
441
442#[cfg(test)]
443#[path = "../../../tests/widget/component/app_bar.rs"]
444mod tests;