Skip to main content

material_ui_rs/widget/component/
app_bar.rs

1//! Material 3 app bar constructors.
2
3use 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
14/// Status bar inset height used when previewing top app bars edge-to-edge.
15pub const STATUS_BAR_HEIGHT: f32 = 24.0;
16
17/// Creates a Material icon button suitable for app bars.
18pub 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
48/// Creates a Material icon action suitable for app bars.
49pub 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
61/// Creates Material icon actions suitable for app bars.
62pub 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
77/// Creates a small top app bar.
78pub 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
140/// Creates a small top app bar with the on-scroll container color/elevation.
141pub 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
203/// Creates a medium top app bar.
204pub 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
216/// Creates a large top app bar.
217pub 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
229/// Creates a bottom app bar.
230pub 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
268/// Creates a top app bar status inset using the top app bar surface color.
269pub 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
280/// Prepends a status inset to a top app bar.
281pub 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;