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 .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
115pub 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
153pub 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
166pub 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
179pub 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
218pub 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
230pub 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;