#![allow(non_snake_case)]
use std::cell::Cell;
use std::rc::Rc;
use repose_core::NestedScrollConnection;
use repose_core::*;
use repose_ui::{Box, Column, Row, ViewExt, ZStack};
use super::*;
use super::util::lerp_color;
#[derive(Clone, Copy, Debug)]
pub struct TopAppBarColors {
pub container_color: Color,
pub scrolled_container_color: Color,
pub navigation_icon_content_color: Color,
pub title_content_color: Color,
pub subtitle_content_color: Color,
pub action_icon_content_color: Color,
}
impl TopAppBarColors {
pub fn container_color(&self, scroll_fraction: f32) -> Color {
lerp_color(
self.container_color,
self.scrolled_container_color,
scroll_fraction.clamp(0.0, 1.0),
)
}
}
impl Default for TopAppBarColors {
fn default() -> Self {
Self {
container_color: TopAppBarDefaults::container_color(),
scrolled_container_color: TopAppBarDefaults::scrolled_container_color(),
navigation_icon_content_color: TopAppBarDefaults::navigation_icon_content_color(),
title_content_color: TopAppBarDefaults::title_content_color(),
subtitle_content_color: TopAppBarDefaults::subtitle_content_color(),
action_icon_content_color: TopAppBarDefaults::action_icon_content_color(),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub enum TopAppBarScrollMode {
Pinned,
EnterAlways,
ExitUntilCollapsed,
}
#[derive(Clone)]
pub struct TopAppBarScrollBehavior {
pub collapsed_offset: Signal<f32>,
pub height: f32,
pub collapsed_height: f32,
pub mode: TopAppBarScrollMode,
_pending: Rc<Cell<f32>>,
}
impl std::fmt::Debug for TopAppBarScrollBehavior {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("TopAppBarScrollBehavior")
.field("offset", &self.offset())
.field("height", &self.height)
.field("collapsed_height", &self.collapsed_height)
.field("mode", &self.mode)
.finish()
}
}
impl TopAppBarScrollBehavior {
pub fn new(height: f32, collapsed_height: f32, mode: TopAppBarScrollMode) -> Self {
Self {
collapsed_offset: signal(0.0),
height,
collapsed_height,
mode,
_pending: Rc::new(Cell::new(0.0)),
}
}
pub fn nested_scroll_connection(&self) -> NestedScrollConnection {
let off = self.collapsed_offset.clone();
let max_collapse = -(self.height - self.collapsed_height);
match self.mode {
TopAppBarScrollMode::Pinned => NestedScrollConnection::new(),
TopAppBarScrollMode::EnterAlways => {
NestedScrollConnection::new().on_pre_scroll(move |d: Vec2, _source| -> Vec2 {
let mut consumed = Vec2::ZERO;
let current = off.get();
if d.y > 0.0 {
if current > max_collapse {
let consume = d.y.min(current - max_collapse);
off.set(current - consume);
consumed.y = consume;
}
} else if current < 0.0 {
let consume = (-d.y).min(-current);
off.set(current + consume);
consumed.y = consume;
}
if consumed.y != 0.0 {
repose_core::request_frame();
}
consumed
})
}
TopAppBarScrollMode::ExitUntilCollapsed => NestedScrollConnection::new()
.on_pre_scroll({
let off = off.clone();
move |d: Vec2, _source| -> Vec2 {
let mut consumed = Vec2::ZERO;
if d.y > 0.0 {
let current = off.get();
if current > max_collapse {
let consume = d.y.min(current - max_collapse);
off.set(current - consume);
consumed.y = consume;
repose_core::request_frame();
}
}
consumed
}
})
.on_post_scroll(move |_consumed: Vec2, available: Vec2, _source| -> Vec2 {
let mut expanded = Vec2::ZERO;
if available.y < 0.0 {
let current = off.get();
if current < 0.0 {
let consume = (-available.y).min(-current);
off.set(current + consume);
expanded.y = consume;
repose_core::request_frame();
}
}
expanded
}),
}
}
pub fn offset(&self) -> f32 {
self.collapsed_offset.get()
}
pub fn collapsed_fraction(&self) -> f32 {
let range = (self.height - self.collapsed_height).max(f32::EPSILON);
((-self.collapsed_offset.get()) / range).clamp(0.0, 1.0)
}
}
#[derive(Clone, Debug)]
pub struct TopAppBarConfig {
pub modifier: Modifier,
pub colors: TopAppBarColors,
pub height: f32,
pub scroll_fraction: f32,
pub scroll_offset: f32,
pub scroll_behavior: Option<Rc<TopAppBarScrollBehavior>>,
pub window_insets: WindowInsets,
pub content_padding: PaddingValues,
}
#[derive(Clone, Copy, Debug)]
pub struct WindowInsets {
pub top: f32,
pub bottom: f32,
pub left: f32,
pub right: f32,
}
impl Default for WindowInsets {
fn default() -> Self {
Self {
top: 0.0,
bottom: 0.0,
left: 0.0,
right: 0.0,
}
}
}
impl Default for TopAppBarConfig {
fn default() -> Self {
Self {
modifier: Modifier::new(),
colors: TopAppBarColors::default(),
height: TopAppBarDefaults::HEIGHT,
scroll_fraction: 0.0,
scroll_offset: 0.0,
scroll_behavior: None,
window_insets: WindowInsets::default(),
content_padding: PaddingValues {
left: 4.0,
right: 4.0,
top: 0.0,
bottom: 0.0,
},
}
}
}
fn top_app_bar_layout(
title: View,
subtitle: Option<View>,
navigation_icon: Option<View>,
actions: Vec<View>,
config: TopAppBarConfig,
centered: bool,
) -> View {
let insets = config.window_insets;
let (scroll_offset, scroll_fraction) = if let Some(ref sb) = config.scroll_behavior {
(sb.offset(), sb.collapsed_fraction())
} else {
(config.scroll_offset, config.scroll_fraction)
};
let bg = config.colors.container_color(scroll_fraction);
let colors = config.colors;
let root_m = Modifier::new()
.fill_max_width()
.height(config.height + insets.top)
.background(bg)
.translate(0.0, scroll_offset)
.semantics(Semantics::new(Role::Container));
let nav = navigation_icon
.map(|icon| with_content_color(colors.navigation_icon_content_color, move || icon))
.unwrap_or(Box(Modifier::new().width(16.0).fill_max_height()));
let actions_row = Row(Modifier::new()
.align_items(AlignItems::CENTER)
.flex_shrink(0.0))
.child(
actions
.into_iter()
.map(|a| with_content_color(colors.action_icon_content_color, move || a.clone()))
.collect::<Vec<_>>(),
);
let title_column = Column(Modifier::new().justify_content(JustifyContent::CENTER)).child((
Box(Modifier::new()).child(with_content_color(colors.title_content_color, || title)),
subtitle
.map(|s| {
Box(Modifier::new()).child(with_content_color(colors.subtitle_content_color, || s))
})
.unwrap_or(Box(Modifier::new())),
));
let content_padding = PaddingValues {
left: config.content_padding.left + insets.left,
right: config.content_padding.right + insets.right,
top: config.content_padding.top + insets.top,
bottom: config.content_padding.bottom + insets.bottom,
};
if centered {
ZStack(root_m.then(config.modifier)).child((
Row(Modifier::new()
.fill_max_width()
.align_items(AlignItems::CENTER)
.padding_values(content_padding))
.child((nav, Box(Modifier::new().flex_grow(1.0)), actions_row)),
Box(Modifier::new()
.absolute()
.offset(Some(0.0), Some(0.0), Some(0.0), None)
.fill_max_width()
.justify_content(JustifyContent::CENTER)
.align_items(AlignItems::CENTER))
.child(title_column),
))
} else {
Row(root_m.padding_values(content_padding).then(config.modifier)).child((
nav,
Box(Modifier::new()
.padding_values(PaddingValues {
left: 16.0,
right: 0.0,
top: 0.0,
bottom: 0.0,
})
.flex_grow(1.0))
.child(title_column),
actions_row,
))
}
}
pub fn TopAppBar(
title: View,
subtitle: Option<View>,
navigation_icon: Option<View>,
actions: Vec<View>,
config: TopAppBarConfig,
) -> View {
top_app_bar_layout(title, subtitle, navigation_icon, actions, config, false)
}
pub fn CenterAlignedTopAppBar(
title: View,
subtitle: Option<View>,
navigation_icon: Option<View>,
actions: Vec<View>,
config: TopAppBarConfig,
) -> View {
top_app_bar_layout(title, subtitle, navigation_icon, actions, config, true)
}