repose_material/material3/
app_bar.rs1#![allow(non_snake_case)]
2
3use std::cell::Cell;
4use std::rc::Rc;
5
6use repose_core::NestedScrollConnection;
7use repose_core::*;
8use repose_ui::{
9 Box, Column, Row, ZStack,
10 ViewExt,
11};
12
13use super::*;
14
15use super::util::lerp_color;
16#[derive(Clone, Copy, Debug)]
18pub struct TopAppBarColors {
19 pub container_color: Color,
20 pub scrolled_container_color: Color,
21 pub navigation_icon_content_color: Color,
22 pub title_content_color: Color,
23 pub subtitle_content_color: Color,
24 pub action_icon_content_color: Color,
25}
26
27impl TopAppBarColors {
28 pub fn container_color(&self, scroll_fraction: f32) -> Color {
29 lerp_color(
30 self.container_color,
31 self.scrolled_container_color,
32 scroll_fraction.clamp(0.0, 1.0),
33 )
34 }
35}
36
37impl Default for TopAppBarColors {
38 fn default() -> Self {
39 Self {
40 container_color: TopAppBarDefaults::container_color(),
41 scrolled_container_color: TopAppBarDefaults::scrolled_container_color(),
42 navigation_icon_content_color: TopAppBarDefaults::navigation_icon_content_color(),
43 title_content_color: TopAppBarDefaults::title_content_color(),
44 subtitle_content_color: TopAppBarDefaults::subtitle_content_color(),
45 action_icon_content_color: TopAppBarDefaults::action_icon_content_color(),
46 }
47 }
48}
49
50#[derive(Clone, Copy, Debug, PartialEq)]
52pub enum TopAppBarScrollMode {
53 Pinned,
55 EnterAlways,
57 ExitUntilCollapsed,
60}
61
62#[derive(Clone)]
70pub struct TopAppBarScrollBehavior {
71 pub collapsed_offset: Signal<f32>,
72 pub height: f32,
73 pub collapsed_height: f32,
74 pub mode: TopAppBarScrollMode,
75 _pending: Rc<Cell<f32>>,
76}
77
78impl std::fmt::Debug for TopAppBarScrollBehavior {
79 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
80 f.debug_struct("TopAppBarScrollBehavior")
81 .field("offset", &self.offset())
82 .field("height", &self.height)
83 .field("collapsed_height", &self.collapsed_height)
84 .field("mode", &self.mode)
85 .finish()
86 }
87}
88
89impl TopAppBarScrollBehavior {
90 pub fn new(height: f32, collapsed_height: f32, mode: TopAppBarScrollMode) -> Self {
91 Self {
92 collapsed_offset: signal(0.0),
93 height,
94 collapsed_height,
95 mode,
96 _pending: Rc::new(Cell::new(0.0)),
97 }
98 }
99
100 pub fn nested_scroll_connection(&self) -> NestedScrollConnection {
104 let off = self.collapsed_offset.clone();
105 let max_collapse = -(self.height - self.collapsed_height);
106
107 match self.mode {
108 TopAppBarScrollMode::Pinned => NestedScrollConnection::new(),
109 TopAppBarScrollMode::EnterAlways => {
110 NestedScrollConnection::new().on_pre_scroll(move |d: Vec2, _source| -> Vec2 {
111 let mut consumed = Vec2::ZERO;
112 let current = off.get();
113 if d.y > 0.0 {
114 if current > max_collapse {
116 let consume = d.y.min(current - max_collapse);
117 off.set(current - consume);
118 consumed.y = consume;
119 }
120 } else if current < 0.0 {
121 let consume = (-d.y).min(-current);
123 off.set(current + consume);
124 consumed.y = consume;
125 }
126 if consumed.y != 0.0 {
127 repose_core::request_frame();
128 }
129 consumed
130 })
131 }
132 TopAppBarScrollMode::ExitUntilCollapsed => NestedScrollConnection::new()
133 .on_pre_scroll({
134 let off = off.clone();
135 move |d: Vec2, _source| -> Vec2 {
136 let mut consumed = Vec2::ZERO;
137 if d.y > 0.0 {
138 let current = off.get();
140 if current > max_collapse {
141 let consume = d.y.min(current - max_collapse);
142 off.set(current - consume);
143 consumed.y = consume;
144 repose_core::request_frame();
145 }
146 }
147 consumed
148 }
149 })
150 .on_post_scroll(
151 move |_consumed: Vec2, available: Vec2, _source| -> Vec2 {
152 let mut expanded = Vec2::ZERO;
155 if available.y < 0.0 {
156 let current = off.get();
157 if current < 0.0 {
158 let consume = (-available.y).min(-current);
159 off.set(current + consume);
160 expanded.y = consume;
161 repose_core::request_frame();
162 }
163 }
164 expanded
165 },
166 ),
167 }
168 }
169
170 pub fn offset(&self) -> f32 {
172 self.collapsed_offset.get()
173 }
174
175 pub fn collapsed_fraction(&self) -> f32 {
178 let range = (self.height - self.collapsed_height).max(f32::EPSILON);
179 ((-self.collapsed_offset.get()) / range).clamp(0.0, 1.0)
180 }
181}
182
183#[derive(Clone, Debug)]
185pub struct TopAppBarConfig {
186 pub modifier: Modifier,
187 pub colors: TopAppBarColors,
188 pub height: f32,
189 pub scroll_fraction: f32,
192 pub scroll_offset: f32,
195 pub scroll_behavior: Option<Rc<TopAppBarScrollBehavior>>,
200 pub window_insets: WindowInsets,
201 pub content_padding: PaddingValues,
202}
203
204#[derive(Clone, Copy, Debug)]
206pub struct WindowInsets {
207 pub top: f32,
208 pub bottom: f32,
209 pub left: f32,
210 pub right: f32,
211}
212
213impl Default for WindowInsets {
214 fn default() -> Self {
215 Self {
216 top: 0.0,
217 bottom: 0.0,
218 left: 0.0,
219 right: 0.0,
220 }
221 }
222}
223
224impl Default for TopAppBarConfig {
225 fn default() -> Self {
226 Self {
227 modifier: Modifier::new(),
228 colors: TopAppBarColors::default(),
229 height: TopAppBarDefaults::HEIGHT,
230 scroll_fraction: 0.0,
231 scroll_offset: 0.0,
232 scroll_behavior: None,
233 window_insets: WindowInsets::default(),
234 content_padding: PaddingValues {
235 left: 4.0,
236 right: 4.0,
237 top: 0.0,
238 bottom: 0.0,
239 },
240 }
241 }
242}
243
244fn top_app_bar_layout(
245 title: View,
246 subtitle: Option<View>,
247 navigation_icon: Option<View>,
248 actions: Vec<View>,
249 config: TopAppBarConfig,
250 centered: bool,
251) -> View {
252 let insets = config.window_insets;
253 let (scroll_offset, scroll_fraction) = if let Some(ref sb) = config.scroll_behavior {
256 (sb.offset(), sb.collapsed_fraction())
257 } else {
258 (config.scroll_offset, config.scroll_fraction)
259 };
260 let bg = config.colors.container_color(scroll_fraction);
261 let colors = config.colors;
262
263 let root_m = Modifier::new()
264 .min_width(200.0)
265 .height(config.height + insets.top)
266 .background(bg)
267 .translate(0.0, scroll_offset)
268 .semantics(Semantics::new(Role::Container));
269
270 let nav = navigation_icon
271 .map(|icon| with_content_color(colors.navigation_icon_content_color, move || icon))
272 .unwrap_or(Box(Modifier::new().width(16.0).fill_max_height()));
273
274 let actions_row = Row(Modifier::new()
275 .align_items(AlignItems::CENTER)
276 .flex_shrink(0.0))
277 .child(
278 actions
279 .into_iter()
280 .map(|a| {
281 with_content_color(colors.action_icon_content_color, move || a.clone())
282 })
283 .collect::<Vec<_>>(),
284 );
285
286 let title_column = Column(Modifier::new().justify_content(JustifyContent::CENTER)).child((
287 Box(Modifier::new()).child(with_content_color(
288 colors.title_content_color,
289 || title,
290 )),
291 subtitle
292 .map(|s| {
293 Box(Modifier::new()).child(with_content_color(
294 colors.subtitle_content_color,
295 || s,
296 ))
297 })
298 .unwrap_or(Box(Modifier::new())),
299 ));
300
301 let content_padding = PaddingValues {
302 left: config.content_padding.left + insets.left,
303 right: config.content_padding.right + insets.right,
304 top: config.content_padding.top + insets.top,
305 bottom: config.content_padding.bottom + insets.bottom,
306 };
307
308 if centered {
309 ZStack(root_m.then(config.modifier)).child((
313 Row(Modifier::new()
314 .fill_max_width()
315 .align_items(AlignItems::CENTER)
316 .padding_values(content_padding))
317 .child((
318 nav,
319 Box(Modifier::new().flex_grow(1.0)),
320 actions_row,
321 )),
322 Box(Modifier::new()
323 .absolute()
324 .offset(Some(0.0), Some(0.0), Some(0.0), None)
325 .fill_max_width()
326 .justify_content(JustifyContent::CENTER)
327 .align_items(AlignItems::CENTER))
328 .child(title_column),
329 ))
330 } else {
331 Row(root_m
332 .padding_values(content_padding)
333 .then(config.modifier))
334 .child((
335 nav,
336 Box(Modifier::new()
337 .padding_values(PaddingValues {
338 left: 16.0,
339 right: 0.0,
340 top: 0.0,
341 bottom: 0.0,
342 })
343 .flex_grow(1.0))
344 .child(title_column),
345 actions_row,
346 ))
347 }
348}
349
350pub fn TopAppBar(
353 title: View,
354 subtitle: Option<View>,
355 navigation_icon: Option<View>,
356 actions: Vec<View>,
357 config: TopAppBarConfig,
358) -> View {
359 top_app_bar_layout(title, subtitle, navigation_icon, actions, config, false)
360}
361
362pub fn CenterAlignedTopAppBar(
365 title: View,
366 subtitle: Option<View>,
367 navigation_icon: Option<View>,
368 actions: Vec<View>,
369 config: TopAppBarConfig,
370) -> View {
371 top_app_bar_layout(title, subtitle, navigation_icon, actions, config, true)
372}