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,
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}
58
59pub struct TopAppBarScrollBehavior {
66 pub collapsed_offset: Signal<f32>,
67 pub height: f32,
68 pub collapsed_height: f32,
69 pub mode: TopAppBarScrollMode,
70 _pending: Rc<Cell<f32>>,
71}
72
73impl TopAppBarScrollBehavior {
74 pub fn new(height: f32, collapsed_height: f32, mode: TopAppBarScrollMode) -> Self {
75 Self {
76 collapsed_offset: signal(0.0),
77 height,
78 collapsed_height,
79 mode,
80 _pending: Rc::new(Cell::new(0.0)),
81 }
82 }
83
84 pub fn nested_scroll_connection(&self) -> NestedScrollConnection {
87 let off = self.collapsed_offset.clone();
88 let max_collapse = -(self.height - self.collapsed_height);
89 let mode = self.mode;
90
91 NestedScrollConnection::new().on_pre_scroll(move |d: Vec2, _source| -> Vec2 {
92 if mode == TopAppBarScrollMode::Pinned {
93 return Vec2::ZERO;
94 }
95 let current = off.get();
96 if d.y > 0.0 {
97 if current <= max_collapse {
99 return Vec2::ZERO;
100 }
101 let collapse_room = current - max_collapse;
102 let consume = d.y.min(collapse_room);
103 off.set(current - consume);
104 repose_core::request_frame();
105 Vec2 { x: 0.0, y: consume }
106 } else {
107 if current >= 0.0 {
109 return Vec2::ZERO;
110 }
111 let expansion_room = -current;
112 let consume = (-d.y).min(expansion_room);
113 off.set(current + consume);
114 repose_core::request_frame();
115 Vec2 { x: 0.0, y: consume }
116 }
117 })
118 }
119
120 pub fn offset(&self) -> f32 {
122 self.collapsed_offset.get()
123 }
124}
125
126#[derive(Clone, Debug)]
128pub struct TopAppBarConfig {
129 pub modifier: Modifier,
130 pub colors: TopAppBarColors,
131 pub height: f32,
132 pub scroll_fraction: f32,
133 pub scroll_offset: f32,
136 pub window_insets: WindowInsets,
137 pub content_padding: PaddingValues,
138}
139
140#[derive(Clone, Copy, Debug)]
142pub struct WindowInsets {
143 pub top: f32,
144 pub bottom: f32,
145 pub left: f32,
146 pub right: f32,
147}
148
149impl Default for WindowInsets {
150 fn default() -> Self {
151 Self {
152 top: 0.0,
153 bottom: 0.0,
154 left: 0.0,
155 right: 0.0,
156 }
157 }
158}
159
160impl Default for TopAppBarConfig {
161 fn default() -> Self {
162 Self {
163 modifier: Modifier::new(),
164 colors: TopAppBarColors::default(),
165 height: TopAppBarDefaults::HEIGHT,
166 scroll_fraction: 0.0,
167 scroll_offset: 0.0,
168 window_insets: WindowInsets::default(),
169 content_padding: PaddingValues {
170 left: 4.0,
171 right: 4.0,
172 top: 0.0,
173 bottom: 0.0,
174 },
175 }
176 }
177}
178
179fn top_app_bar_layout(
180 title: View,
181 subtitle: Option<View>,
182 navigation_icon: Option<View>,
183 actions: Vec<View>,
184 config: TopAppBarConfig,
185 centered: bool,
186) -> View {
187 let insets = config.window_insets;
188 let bg = config.colors.container_color(config.scroll_fraction);
189 let mut m = Modifier::new()
190 .min_width(200.0)
191 .height(config.height + insets.top)
192 .background(bg)
193 .translate(0.0, config.scroll_offset)
194 .padding_values(PaddingValues {
195 left: config.content_padding.left + insets.left,
196 right: config.content_padding.right + insets.right,
197 top: config.content_padding.top + insets.top,
198 bottom: config.content_padding.bottom + insets.bottom,
199 })
200 .align_items(AlignItems::CENTER)
201 .then(config.modifier);
202 if centered {
203 m = m.justify_content(JustifyContent::CENTER);
204 }
205 Row(m).child((
206 navigation_icon.unwrap_or(Box(Modifier::new().width(16.0).fill_max_height())),
207 Box(Modifier::new()
208 .padding_values(PaddingValues {
209 left: 16.0,
210 right: 0.0,
211 top: 0.0,
212 bottom: 0.0,
213 })
214 .flex_grow(1.0))
215 .child(
216 Column(Modifier::new().justify_content(JustifyContent::CENTER)).child((
217 Box(Modifier::new()).child(with_content_color(
218 config.colors.title_content_color,
219 || title,
220 )),
221 subtitle
222 .map(|s| {
223 Box(Modifier::new()).child(with_content_color(
224 config.colors.subtitle_content_color,
225 || s,
226 ))
227 })
228 .unwrap_or(Box(Modifier::new())),
229 )),
230 ),
231 Row(Modifier::new()
232 .align_items(AlignItems::CENTER)
233 .clip_rounded(20.0))
234 .child(
235 actions
236 .into_iter()
237 .map(|a| {
238 with_content_color(config.colors.action_icon_content_color, move || a.clone())
239 })
240 .collect::<Vec<_>>(),
241 ),
242 ))
243}
244
245pub fn TopAppBar(
248 title: View,
249 subtitle: Option<View>,
250 navigation_icon: Option<View>,
251 actions: Vec<View>,
252 config: TopAppBarConfig,
253) -> View {
254 top_app_bar_layout(title, subtitle, navigation_icon, actions, config, false)
255}
256
257pub fn CenterAlignedTopAppBar(
259 title: View,
260 subtitle: Option<View>,
261 navigation_icon: Option<View>,
262 actions: Vec<View>,
263 config: TopAppBarConfig,
264) -> View {
265 top_app_bar_layout(title, subtitle, navigation_icon, actions, config, true)
266}