Skip to main content

repose_material/material3/
app_bar.rs

1#![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/// Color slots for [`TopAppBar`].
17#[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/// Scroll response mode for [`TopAppBarScrollBehavior`].
51#[derive(Clone, Copy, Debug, PartialEq)]
52pub enum TopAppBarScrollMode {
53    /// Always visible, no scroll response.
54    Pinned,
55    /// Collapses upward when scrolling down, expands when scrolling up.
56    EnterAlways,
57}
58
59/// Drives scroll-based collapsing/expanding of a TopAppBar.
60///
61/// Create one, pass its [`nested_scroll_connection`](TopAppBarScrollBehavior::nested_scroll_connection)
62/// to a lazy list's [`set_nested_scroll_parent`] method, and set the
63/// resulting [`collapsed_offset`](TopAppBarScrollBehavior::collapsed_offset)
64/// on the TopAppBar via [`TopAppBarConfig::scroll_offset`].
65pub 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    /// Returns a [`NestedScrollConnection`] that collapses the bar on
85    /// downward scroll and expands on upward scroll.
86    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                // Scrolling down → collapse bar
98                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                // Scrolling up → expand bar
108                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    /// Returns the current collapsed offset (0 = fully expanded, negative = collapsed).
121    pub fn offset(&self) -> f32 {
122        self.collapsed_offset.get()
123    }
124}
125
126/// Configuration for [`TopAppBar`].
127#[derive(Clone, Debug)]
128pub struct TopAppBarConfig {
129    pub modifier: Modifier,
130    pub colors: TopAppBarColors,
131    pub height: f32,
132    pub scroll_fraction: f32,
133    /// Vertical translate offset (negative = collapsed upward).
134    /// Set this from [`TopAppBarScrollBehavior::collapsed_offset`].
135    pub scroll_offset: f32,
136    pub window_insets: WindowInsets,
137    pub content_padding: PaddingValues,
138}
139
140/// System window insets for top app bar padding.
141#[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
245/// M3 Top App Bar (small). Displays a title with optional navigation icon,
246/// subtitle, and trailing action buttons.
247pub 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
257/// M3 Center-Aligned Top App Bar - same as TopAppBar but title is centered.
258pub 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}