1use 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#[derive(Debug, Clone, Copy, PartialEq, Eq)]
17pub enum ColorMode {
18 Standard,
20 Vibrant,
22}
23
24#[derive(Debug, Clone, Copy, PartialEq, Eq)]
26pub enum Orientation {
27 Horizontal,
28 Vertical,
29}
30
31pub 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
42pub 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
53pub 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
84pub 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
95pub 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
106pub 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
117pub 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
128pub 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
180pub 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
196pub 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
212pub 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
224pub 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
236pub 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
248pub 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
260pub 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
273pub 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
286pub 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
299pub 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
314pub 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
330pub 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;