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