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        .padding(app_bar_padding())
91        .align_y(alignment::Vertical::Center);
92
93    if let Some(leading) = leading {
94        content = content.push(leading);
95    }
96
97    content = content.push(
98        Text::new(title)
99            .size(title_text.size)
100            .line_height(absolute_line_height(title_text.line_height))
101            .width(Length::Fill),
102    );
103
104    for action in actions {
105        content = content.push(action);
106    }
107
108    top_container(
109        content,
110        tokens::component::app_bar::SMALL_CONTAINER_HEIGHT,
111        false,
112    )
113}
114
115/// Creates a small top app bar with the on-scroll container color/elevation.
116pub fn small_on_scroll<'a, Message, Renderer>(
117    title: impl text::IntoFragment<'a>,
118    leading: Option<Element<'a, Message, Theme, Renderer>>,
119    actions: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
120) -> Container<'a, Message, Theme, Renderer>
121where
122    Message: 'a,
123    Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
124{
125    let title_text = tokens::component::app_bar::SMALL_TITLE_TEXT;
126    let mut content = Row::new()
127        .spacing(tokens::component::app_bar::ICON_BUTTON_SPACE)
128        .padding(app_bar_padding())
129        .align_y(alignment::Vertical::Center);
130
131    if let Some(leading) = leading {
132        content = content.push(leading);
133    }
134
135    content = content.push(
136        Text::new(title)
137            .size(title_text.size)
138            .line_height(absolute_line_height(title_text.line_height))
139            .width(Length::Fill),
140    );
141
142    for action in actions {
143        content = content.push(action);
144    }
145
146    top_container(
147        content,
148        tokens::component::app_bar::SMALL_CONTAINER_HEIGHT,
149        true,
150    )
151}
152
153/// Creates a medium top app bar.
154pub fn medium<'a, Message, Renderer>(
155    title: impl text::IntoFragment<'a>,
156    leading: Option<Element<'a, Message, Theme, Renderer>>,
157    actions: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
158) -> Container<'a, Message, Theme, Renderer>
159where
160    Message: 'a,
161    Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
162{
163    flexible(title, leading, actions, FlexibleSize::Medium)
164}
165
166/// Creates a large top app bar.
167pub fn large<'a, Message, Renderer>(
168    title: impl text::IntoFragment<'a>,
169    leading: Option<Element<'a, Message, Theme, Renderer>>,
170    actions: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
171) -> Container<'a, Message, Theme, Renderer>
172where
173    Message: 'a,
174    Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
175{
176    flexible(title, leading, actions, FlexibleSize::Large)
177}
178
179/// Creates a bottom app bar.
180pub fn bottom<'a, Message, Renderer>(
181    actions: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
182    floating_action: Option<Element<'a, Message, Theme, Renderer>>,
183) -> Container<'a, Message, Theme, Renderer>
184where
185    Message: 'a,
186    Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
187{
188    let mut content = Row::new()
189        .spacing(tokens::component::app_bar::ICON_BUTTON_SPACE)
190        .align_y(alignment::Vertical::Center)
191        .width(Length::Fill);
192
193    for action in actions {
194        content = content.push(action);
195    }
196
197    content = content.push(Space::new().width(Length::Fill));
198
199    if let Some(fab) = floating_action {
200        content = content.push(fab);
201    }
202
203    Container::new(content)
204        .height(Length::Fixed(
205            tokens::component::bottom_app_bar::CONTAINER_HEIGHT,
206        ))
207        .width(Length::Fill)
208        .padding(Padding {
209            top: 0.0,
210            right: 16.0,
211            bottom: 0.0,
212            left: 4.0,
213        })
214        .align_y(alignment::Vertical::Center)
215        .style(bottom_style)
216}
217
218/// Creates a top app bar status inset using the top app bar surface color.
219pub fn status_bar<'a, Message, Renderer>() -> Container<'a, Message, Theme, Renderer>
220where
221    Message: 'a,
222    Renderer: iced_widget::core::Renderer + 'a,
223{
224    Container::new(Space::new())
225        .height(Length::Fixed(STATUS_BAR_HEIGHT))
226        .width(Length::Fill)
227        .style(|theme| top_style(theme, false))
228}
229
230/// Prepends a status inset to a top app bar.
231pub fn with_status_bar<'a, Message, Renderer>(
232    app_bar: impl Into<Element<'a, Message, Theme, Renderer>>,
233) -> Column<'a, Message, Theme, Renderer>
234where
235    Message: 'a,
236    Renderer: iced_widget::core::Renderer + 'a,
237{
238    Column::new()
239        .push(status_bar())
240        .push(app_bar)
241        .spacing(0)
242        .width(Length::Fill)
243}
244
245#[derive(Debug, Clone, Copy, PartialEq, Eq)]
246enum FlexibleSize {
247    Medium,
248    Large,
249}
250
251impl FlexibleSize {
252    fn container_height(self) -> f32 {
253        match self {
254            Self::Medium => tokens::component::app_bar::MEDIUM_CONTAINER_HEIGHT,
255            Self::Large => tokens::component::app_bar::LARGE_CONTAINER_HEIGHT,
256        }
257    }
258
259    fn title_text(self) -> tokens::typography::TypeScale {
260        match self {
261            Self::Medium => tokens::component::app_bar::MEDIUM_TITLE_TEXT,
262            Self::Large => tokens::component::app_bar::LARGE_TITLE_TEXT,
263        }
264    }
265}
266
267fn flexible<'a, Message, Renderer>(
268    title: impl text::IntoFragment<'a>,
269    leading: Option<Element<'a, Message, Theme, Renderer>>,
270    actions: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
271    size: FlexibleSize,
272) -> Container<'a, Message, Theme, Renderer>
273where
274    Message: 'a,
275    Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
276{
277    let title_text = size.title_text();
278    let content = Column::new()
279        .push(navigation_row(leading, actions).height(Length::Fixed(
280            tokens::component::app_bar::SMALL_CONTAINER_HEIGHT,
281        )))
282        .push(
283            Container::new(
284                Text::new(title)
285                    .size(title_text.size)
286                    .line_height(absolute_line_height(title_text.line_height)),
287            )
288            .width(Length::Fill)
289            .height(Length::Fill)
290            .padding(Padding {
291                top: 0.0,
292                right: 16.0,
293                bottom: 20.0,
294                left: 16.0,
295            })
296            .align_y(alignment::Vertical::Bottom),
297        )
298        .width(Length::Fill);
299
300    top_container(content, size.container_height(), false)
301}
302
303fn navigation_row<'a, Message, Renderer>(
304    leading: Option<Element<'a, Message, Theme, Renderer>>,
305    actions: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
306) -> Row<'a, Message, Theme, Renderer>
307where
308    Message: 'a,
309    Renderer: iced_widget::core::Renderer + 'a,
310{
311    let mut content = Row::new()
312        .spacing(tokens::component::app_bar::ICON_BUTTON_SPACE)
313        .padding(app_bar_padding())
314        .align_y(alignment::Vertical::Center)
315        .width(Length::Fill);
316
317    if let Some(leading) = leading {
318        content = content.push(leading);
319    }
320
321    content = content.push(Space::new().width(Length::Fill));
322
323    for action in actions {
324        content = content.push(action);
325    }
326
327    content
328}
329
330fn app_bar_padding() -> Padding {
331    Padding {
332        top: 0.0,
333        right: tokens::component::app_bar::TRAILING_SPACE,
334        bottom: 0.0,
335        left: tokens::component::app_bar::LEADING_SPACE,
336    }
337}
338
339fn top_container<'a, Message, Renderer>(
340    content: impl Into<Element<'a, Message, Theme, Renderer>>,
341    height: f32,
342    on_scroll: bool,
343) -> Container<'a, Message, Theme, Renderer>
344where
345    Message: 'a,
346    Renderer: iced_widget::core::Renderer + core_text::Renderer + 'a,
347{
348    Container::new(content)
349        .height(Length::Fixed(height))
350        .width(Length::Fill)
351        .style(move |theme| top_style(theme, on_scroll))
352}
353
354fn top_style(theme: &Theme, on_scroll: bool) -> iced_widget::container::Style {
355    let colors = theme.colors();
356    let (background, elevation) = if on_scroll {
357        (
358            colors.surface.container.base,
359            tokens::component::app_bar::ON_SCROLL_CONTAINER_ELEVATION_LEVEL,
360        )
361    } else {
362        (
363            colors.surface.color,
364            tokens::component::app_bar::CONTAINER_ELEVATION_LEVEL,
365        )
366    };
367
368    iced_widget::container::Style {
369        background: Some(Background::Color(background)),
370        text_color: Some(colors.surface.text),
371        border: border::rounded(tokens::component::app_bar::CONTAINER_SHAPE),
372        shadow: shadow_from_level(elevation, colors.shadow),
373        snap: cfg!(feature = "crisp"),
374    }
375}
376
377fn bottom_style(theme: &Theme) -> iced_widget::container::Style {
378    let colors = theme.colors();
379
380    iced_widget::container::Style {
381        background: Some(Background::Color(colors.surface.container.base)),
382        text_color: Some(colors.surface.text),
383        border: border::rounded(tokens::component::bottom_app_bar::CONTAINER_SHAPE),
384        shadow: shadow_from_level(
385            tokens::component::bottom_app_bar::CONTAINER_ELEVATION_LEVEL,
386            colors.shadow,
387        ),
388        snap: cfg!(feature = "crisp"),
389    }
390}
391
392#[cfg(test)]
393#[path = "../../../tests/widget/component/app_bar.rs"]
394mod tests;