Skip to main content

material_ui_rs/widget/component/
toolbar.rs

1//! Material 3 docked and floating toolbar constructors.
2
3use iced_widget::button::{Status, Style};
4use iced_widget::core::text as core_text;
5use iced_widget::core::{Background, Color, Element, Length, Padding, alignment, border};
6use iced_widget::graphics::geometry;
7use iced_widget::renderer::wgpu::primitive;
8use iced_widget::text;
9use iced_widget::{Column, Container, Row};
10
11use super::{absolute_line_height, button::Button};
12use crate::utils::{HOVERED_LAYER_OPACITY, PRESSED_LAYER_OPACITY, mix, shadow_from_level};
13use crate::{Theme, fonts, tokens};
14
15/// The Material color configuration used by a toolbar and its actions.
16#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum ColorMode {
18    /// Low-emphasis toolbar color for keeping attention on page content.
19    Standard,
20    /// High-emphasis toolbar color for prominent or temporary page modes.
21    Vibrant,
22}
23
24/// The layout direction used by a floating toolbar.
25#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum Orientation {
27    Horizontal,
28    Vertical,
29}
30
31/// Creates a standard docked toolbar.
32pub fn docked<'a, Message, Renderer>(
33    actions: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
34) -> Container<'a, Message, Theme, Renderer>
35where
36    Message: 'a,
37    Renderer: iced_widget::core::Renderer + 'a,
38{
39    docked_with_color(actions, ColorMode::Standard)
40}
41
42/// Creates a vibrant docked toolbar.
43pub fn docked_vibrant<'a, Message, Renderer>(
44    actions: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
45) -> Container<'a, Message, Theme, Renderer>
46where
47    Message: 'a,
48    Renderer: iced_widget::core::Renderer + 'a,
49{
50    docked_with_color(actions, ColorMode::Vibrant)
51}
52
53/// Creates a docked toolbar with an explicit color configuration.
54pub fn docked_with_color<'a, Message, Renderer>(
55    actions: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
56    color: ColorMode,
57) -> Container<'a, Message, Theme, Renderer>
58where
59    Message: 'a,
60    Renderer: iced_widget::core::Renderer + 'a,
61{
62    let content = actions
63        .into_iter()
64        .fold(Row::new(), Row::push)
65        .spacing(tokens::component::toolbar::ACTION_SPACE)
66        .align_y(alignment::Vertical::Center)
67        .width(Length::Fill);
68
69    Container::new(content)
70        .height(Length::Fixed(
71            tokens::component::toolbar::DOCKED_CONTAINER_HEIGHT,
72        ))
73        .width(Length::Fill)
74        .padding(Padding {
75            top: 0.0,
76            right: tokens::component::toolbar::DOCKED_TRAILING_SPACE,
77            bottom: 0.0,
78            left: tokens::component::toolbar::DOCKED_LEADING_SPACE,
79        })
80        .align_y(alignment::Vertical::Center)
81        .style(move |theme| docked_style(theme, color))
82}
83
84/// Creates a standard horizontal floating toolbar.
85pub fn floating<'a, Message, Renderer>(
86    actions: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
87) -> Container<'a, Message, Theme, Renderer>
88where
89    Message: 'a,
90    Renderer: iced_widget::core::Renderer + 'a,
91{
92    floating_with_options(actions, Orientation::Horizontal, ColorMode::Standard)
93}
94
95/// Creates a vibrant horizontal floating toolbar.
96pub fn floating_vibrant<'a, Message, Renderer>(
97    actions: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
98) -> Container<'a, Message, Theme, Renderer>
99where
100    Message: 'a,
101    Renderer: iced_widget::core::Renderer + 'a,
102{
103    floating_with_options(actions, Orientation::Horizontal, ColorMode::Vibrant)
104}
105
106/// Creates a standard vertical floating toolbar.
107pub fn vertical_floating<'a, Message, Renderer>(
108    actions: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
109) -> Container<'a, Message, Theme, Renderer>
110where
111    Message: 'a,
112    Renderer: iced_widget::core::Renderer + 'a,
113{
114    floating_with_options(actions, Orientation::Vertical, ColorMode::Standard)
115}
116
117/// Creates a vibrant vertical floating toolbar.
118pub fn vertical_floating_vibrant<'a, Message, 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 + 'a,
124{
125    floating_with_options(actions, Orientation::Vertical, ColorMode::Vibrant)
126}
127
128/// Creates a floating toolbar with explicit orientation and color configuration.
129pub fn floating_with_options<'a, Message, Renderer>(
130    actions: impl IntoIterator<Item = Element<'a, Message, Theme, Renderer>>,
131    orientation: Orientation,
132    color: ColorMode,
133) -> Container<'a, Message, Theme, Renderer>
134where
135    Message: 'a,
136    Renderer: iced_widget::core::Renderer + 'a,
137{
138    let content: Element<'a, Message, Theme, Renderer> = match orientation {
139        Orientation::Horizontal => actions
140            .into_iter()
141            .fold(Row::new(), Row::push)
142            .spacing(tokens::component::toolbar::ACTION_SPACE)
143            .align_y(alignment::Vertical::Center)
144            .into(),
145        Orientation::Vertical => actions
146            .into_iter()
147            .fold(Column::new(), Column::push)
148            .spacing(tokens::component::toolbar::ACTION_SPACE)
149            .align_x(alignment::Horizontal::Center)
150            .into(),
151    };
152
153    let mut container = Container::new(content)
154        .padding(Padding::from([
155            tokens::component::toolbar::FLOATING_CONTAINER_LEADING_SPACE,
156            tokens::component::toolbar::FLOATING_CONTAINER_TRAILING_SPACE,
157        ]))
158        .style(move |theme| floating_style(theme, color));
159
160    match orientation {
161        Orientation::Horizontal => {
162            container = container
163                .height(Length::Fixed(
164                    tokens::component::toolbar::FLOATING_HORIZONTAL_CONTAINER_HEIGHT,
165                ))
166                .align_y(alignment::Vertical::Center);
167        }
168        Orientation::Vertical => {
169            container = container
170                .width(Length::Fixed(
171                    tokens::component::toolbar::FLOATING_VERTICAL_CONTAINER_WIDTH,
172                ))
173                .align_x(alignment::Horizontal::Center);
174        }
175    }
176
177    container
178}
179
180/// Places a floating toolbar next to a floating action button.
181pub fn floating_with_fab<'a, Message, Renderer>(
182    toolbar: impl Into<Element<'a, Message, Theme, Renderer>>,
183    fab: impl Into<Element<'a, Message, Theme, Renderer>>,
184) -> Row<'a, Message, Theme, Renderer>
185where
186    Message: 'a,
187    Renderer: iced_widget::core::Renderer + 'a,
188{
189    Row::new()
190        .push(toolbar)
191        .push(fab)
192        .spacing(tokens::component::toolbar::FAB_SPACE)
193        .align_y(alignment::Vertical::Center)
194}
195
196/// Places a vertical floating toolbar above a floating action button.
197pub fn vertical_floating_with_fab<'a, Message, Renderer>(
198    toolbar: impl Into<Element<'a, Message, Theme, Renderer>>,
199    fab: impl Into<Element<'a, Message, Theme, Renderer>>,
200) -> Column<'a, Message, Theme, Renderer>
201where
202    Message: 'a,
203    Renderer: iced_widget::core::Renderer + 'a,
204{
205    Column::new()
206        .push(toolbar)
207        .push(fab)
208        .spacing(tokens::component::toolbar::FAB_SPACE)
209        .align_x(alignment::Horizontal::Center)
210}
211
212/// Creates a toolbar icon button using the standard color configuration.
213pub fn icon_button<'a, Message, Renderer>(
214    icon_name: impl text::IntoFragment<'a>,
215) -> Button<'a, Message, Renderer>
216where
217    Message: Clone + 'a,
218    Renderer: geometry::Renderer + core_text::Renderer + 'a,
219    iced_widget::core::Font: Into<Renderer::Font>,
220{
221    icon_button_with_color(icon_name, ColorMode::Standard, false)
222}
223
224/// Creates a selected toolbar icon button using the standard color configuration.
225pub fn selected_icon_button<'a, Message, Renderer>(
226    icon_name: impl text::IntoFragment<'a>,
227) -> Button<'a, Message, Renderer>
228where
229    Message: Clone + 'a,
230    Renderer: geometry::Renderer + core_text::Renderer + 'a,
231    iced_widget::core::Font: Into<Renderer::Font>,
232{
233    icon_button_with_color(icon_name, ColorMode::Standard, true)
234}
235
236/// Creates a toolbar icon button using the vibrant color configuration.
237pub fn vibrant_icon_button<'a, Message, Renderer>(
238    icon_name: impl text::IntoFragment<'a>,
239) -> Button<'a, Message, Renderer>
240where
241    Message: Clone + 'a,
242    Renderer: geometry::Renderer + core_text::Renderer + 'a,
243    iced_widget::core::Font: Into<Renderer::Font>,
244{
245    icon_button_with_color(icon_name, ColorMode::Vibrant, false)
246}
247
248/// Creates a selected toolbar icon button using the vibrant color configuration.
249pub fn selected_vibrant_icon_button<'a, Message, Renderer>(
250    icon_name: impl text::IntoFragment<'a>,
251) -> Button<'a, Message, Renderer>
252where
253    Message: Clone + 'a,
254    Renderer: geometry::Renderer + core_text::Renderer + 'a,
255    iced_widget::core::Font: Into<Renderer::Font>,
256{
257    icon_button_with_color(icon_name, ColorMode::Vibrant, true)
258}
259
260/// Creates a toolbar icon action using the standard color configuration.
261pub fn icon_action<'a, Message, Renderer>(
262    icon_name: impl text::IntoFragment<'a>,
263    on_press: Message,
264) -> Element<'a, Message, Theme, Renderer>
265where
266    Message: Clone + 'a,
267    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
268    iced_widget::core::Font: Into<Renderer::Font>,
269{
270    icon_button(icon_name).on_press(on_press).into()
271}
272
273/// Creates a selected toolbar icon action using the standard color configuration.
274pub fn selected_icon_action<'a, Message, Renderer>(
275    icon_name: impl text::IntoFragment<'a>,
276    on_press: Message,
277) -> Element<'a, Message, Theme, Renderer>
278where
279    Message: Clone + 'a,
280    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
281    iced_widget::core::Font: Into<Renderer::Font>,
282{
283    selected_icon_button(icon_name).on_press(on_press).into()
284}
285
286/// Creates a toolbar icon action using the vibrant color configuration.
287pub fn vibrant_icon_action<'a, Message, Renderer>(
288    icon_name: impl text::IntoFragment<'a>,
289    on_press: Message,
290) -> Element<'a, Message, Theme, Renderer>
291where
292    Message: Clone + 'a,
293    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
294    iced_widget::core::Font: Into<Renderer::Font>,
295{
296    vibrant_icon_button(icon_name).on_press(on_press).into()
297}
298
299/// Creates a selected toolbar icon action using the vibrant color configuration.
300pub fn selected_vibrant_icon_action<'a, Message, Renderer>(
301    icon_name: impl text::IntoFragment<'a>,
302    on_press: Message,
303) -> Element<'a, Message, Theme, Renderer>
304where
305    Message: Clone + 'a,
306    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
307    iced_widget::core::Font: Into<Renderer::Font>,
308{
309    selected_vibrant_icon_button(icon_name)
310        .on_press(on_press)
311        .into()
312}
313
314/// Creates standard toolbar icon actions.
315pub fn icon_actions<'a, Message, Renderer, Icon>(
316    actions: impl IntoIterator<Item = (Icon, Message)>,
317) -> Vec<Element<'a, Message, Theme, Renderer>>
318where
319    Icon: text::IntoFragment<'a>,
320    Message: Clone + 'a,
321    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
322    iced_widget::core::Font: Into<Renderer::Font>,
323{
324    actions
325        .into_iter()
326        .map(|(icon_name, on_press)| icon_action(icon_name, on_press))
327        .collect()
328}
329
330/// Creates vibrant toolbar icon actions.
331pub fn vibrant_icon_actions<'a, Message, Renderer, Icon>(
332    actions: impl IntoIterator<Item = (Icon, Message)>,
333) -> Vec<Element<'a, Message, Theme, Renderer>>
334where
335    Icon: text::IntoFragment<'a>,
336    Message: Clone + 'a,
337    Renderer: geometry::Renderer + primitive::Renderer + core_text::Renderer + 'a,
338    iced_widget::core::Font: Into<Renderer::Font>,
339{
340    actions
341        .into_iter()
342        .map(|(icon_name, on_press)| vibrant_icon_action(icon_name, on_press))
343        .collect()
344}
345
346fn icon_button_with_color<'a, Message, Renderer>(
347    icon_name: impl text::IntoFragment<'a>,
348    color: ColorMode,
349    selected: bool,
350) -> Button<'a, Message, Renderer>
351where
352    Message: Clone + 'a,
353    Renderer: geometry::Renderer + core_text::Renderer + 'a,
354    iced_widget::core::Font: Into<Renderer::Font>,
355{
356    Button::new(
357        Container::new(
358            fonts::icon(icon_name, tokens::component::toolbar::ACTION_ICON_SIZE).line_height(
359                absolute_line_height(tokens::component::toolbar::ACTION_ICON_SIZE),
360            ),
361        )
362        .center_x(Length::Fixed(
363            tokens::component::toolbar::ACTION_CONTAINER_WIDTH,
364        ))
365        .center_y(Length::Fixed(
366            tokens::component::toolbar::ACTION_CONTAINER_HEIGHT,
367        )),
368    )
369    .width(Length::Fixed(
370        tokens::component::toolbar::ACTION_CONTAINER_WIDTH,
371    ))
372    .height(Length::Fixed(
373        tokens::component::toolbar::ACTION_CONTAINER_HEIGHT,
374    ))
375    .padding(Padding::ZERO)
376    .style(move |theme, status| action_style(theme, status, color, selected))
377}
378
379fn docked_style(theme: &Theme, color: ColorMode) -> iced_widget::container::Style {
380    let colors = theme.colors();
381
382    iced_widget::container::Style {
383        background: Some(Background::Color(container_color(theme, color))),
384        text_color: Some(action_icon_color(theme, color, false)),
385        border: border::rounded(tokens::component::toolbar::DOCKED_CONTAINER_SHAPE),
386        shadow: shadow_from_level(0, colors.shadow),
387        snap: cfg!(feature = "crisp"),
388    }
389}
390
391fn floating_style(theme: &Theme, color: ColorMode) -> iced_widget::container::Style {
392    let colors = theme.colors();
393
394    iced_widget::container::Style {
395        background: Some(Background::Color(container_color(theme, color))),
396        text_color: Some(action_icon_color(theme, color, false)),
397        border: border::rounded(tokens::component::toolbar::FLOATING_CONTAINER_SHAPE),
398        shadow: shadow_from_level(
399            tokens::component::toolbar::FLOATING_CONTAINER_ELEVATION_LEVEL,
400            colors.shadow,
401        ),
402        snap: cfg!(feature = "crisp"),
403    }
404}
405
406fn action_style(theme: &Theme, status: Status, color: ColorMode, selected: bool) -> Style {
407    let colors = theme.colors();
408    let container = action_container_color(theme, color, selected);
409    let foreground = action_icon_color(theme, color, selected);
410    let state_layer = action_state_layer_color(theme, color, selected);
411    let shape = if selected {
412        tokens::component::toolbar::ACTION_SELECTED_CONTAINER_SHAPE
413    } else {
414        tokens::component::toolbar::ACTION_CONTAINER_SHAPE
415    };
416
417    let active = Style {
418        background: Some(Background::Color(container)),
419        text_color: foreground,
420        border: border::rounded(shape),
421        shadow: shadow_from_level(0, Color::TRANSPARENT),
422        snap: cfg!(feature = "crisp"),
423    };
424
425    match status {
426        Status::Active => active,
427        Status::Hovered => Style {
428            background: Some(Background::Color(mix(
429                container,
430                state_layer,
431                HOVERED_LAYER_OPACITY,
432            ))),
433            ..active
434        },
435        Status::Pressed => Style {
436            background: Some(Background::Color(mix(
437                container,
438                state_layer,
439                PRESSED_LAYER_OPACITY,
440            ))),
441            ..active
442        },
443        Status::Disabled => Style {
444            background: Some(Background::Color(container)),
445            text_color: Color {
446                a: tokens::component::toolbar::DISABLED_ICON_OPACITY,
447                ..colors.surface.text
448            },
449            border: border::rounded(shape),
450            shadow: shadow_from_level(0, Color::TRANSPARENT),
451            snap: cfg!(feature = "crisp"),
452        },
453    }
454}
455
456fn container_color(theme: &Theme, color: ColorMode) -> Color {
457    let colors = theme.colors();
458
459    match color {
460        ColorMode::Standard => colors.surface.container.base,
461        ColorMode::Vibrant => colors.primary.container,
462    }
463}
464
465fn action_container_color(theme: &Theme, color: ColorMode, selected: bool) -> Color {
466    let colors = theme.colors();
467
468    match (color, selected) {
469        (ColorMode::Standard, false) => colors.surface.container.base,
470        (ColorMode::Standard, true) => colors.secondary.container,
471        (ColorMode::Vibrant, false) => colors.primary.container,
472        (ColorMode::Vibrant, true) => colors.surface.container.base,
473    }
474}
475
476fn action_icon_color(theme: &Theme, color: ColorMode, selected: bool) -> Color {
477    let colors = theme.colors();
478
479    match (color, selected) {
480        (ColorMode::Standard, false) => colors.surface.text_variant,
481        (ColorMode::Standard, true) => colors.secondary.container_text,
482        (ColorMode::Vibrant, false) => colors.primary.container_text,
483        (ColorMode::Vibrant, true) => colors.surface.text,
484    }
485}
486
487fn action_state_layer_color(theme: &Theme, color: ColorMode, selected: bool) -> Color {
488    action_icon_color(theme, color, selected)
489}
490
491#[cfg(test)]
492#[path = "../../../tests/widget/component/toolbar.rs"]
493mod tests;