use std::{cell::RefCell, rc::Rc};
use cranpose_macros::composable;
use cranpose_ui::{
Dp, Modifier, ScrollState,
text::{SpanStyle, TextStyle},
widgets::{Box, BoxSpec, Row, RowSpec, Text},
};
use cranpose_ui_graphics::{Brush, GradientBlurDirection, GraphicsLayer};
use cranpose_ui_layout::{Alignment, VerticalAlignment};
use crate::theme::{liquid_colors, liquid_typography};
#[derive(Clone, Debug, PartialEq)]
pub struct LiquidNavBarSpec {
pub title: String,
pub collapse_range: f32,
}
impl LiquidNavBarSpec {
pub fn new(title: impl Into<String>) -> Self {
Self {
title: title.into(),
collapse_range: 52.0,
}
}
}
const BAR_HEIGHT: f32 = 52.0;
const LARGE_TITLE_HEIGHT: f32 = 52.0;
const BAND_BLUR_TOP_DP: f32 = 18.0;
const BAND_BLUR_BOTTOM_DP: f32 = 0.35;
const SCRIM_TOP_ALPHA: f32 = 0.82;
const SCRIM_MIDDLE_ALPHA: f32 = 0.40;
#[composable]
pub fn LiquidNavBar(
modifier: Modifier,
spec: LiquidNavBarSpec,
scroll: ScrollState,
leading: impl FnMut() + 'static,
trailing: impl FnMut() + 'static,
) {
let colors = liquid_colors();
let typography = liquid_typography();
let leading = Rc::new(RefCell::new(leading));
let trailing = Rc::new(RefCell::new(trailing));
let collapse_range = spec.collapse_range;
if !cranpose_core::env_flag!("CRANPOSE_DISABLE_NAV_SNAP") {
scroll.set_settle_policy(Some(large_title_settle_policy(collapse_range)));
}
let scroll_offset = scroll.value();
let progress = (scroll_offset / spec.collapse_range.max(1.0)).clamp(0.0, 1.0);
let title = spec.title;
Box(modifier, BoxSpec::default(), move || {
let large_alpha = (1.0 - progress * 1.6).clamp(0.0, 1.0);
if large_alpha > 0.0 {
let style = TextStyle {
span_style: SpanStyle {
color: Some(colors.label.with_alpha(large_alpha)),
..typography.large_title.span_style.clone()
},
..typography.large_title.clone()
};
let slide = -scroll_offset.min(collapse_range);
let large_title = title.clone();
Box(
Modifier::empty()
.offset(16.0, BAR_HEIGHT)
.height(LARGE_TITLE_HEIGHT)
.graphics_layer(move || GraphicsLayer {
translation_y: slide,
..Default::default()
}),
BoxSpec::default().content_alignment(Alignment::new(
cranpose_ui_layout::HorizontalAlignment::Start,
cranpose_ui_layout::VerticalAlignment::CenterVertically,
)),
move || {
let modifier = if progress < 0.5 {
Modifier::empty()
} else {
Modifier::empty().hide_from_accessibility()
};
Text(large_title.clone(), modifier, style.clone());
},
);
}
let background = colors.background;
let band = Modifier::empty()
.fill_max_width()
.height(BAR_HEIGHT)
.backdrop_gradient_blur(
Dp(BAND_BLUR_TOP_DP * progress),
Dp(BAND_BLUR_BOTTOM_DP * progress),
GradientBlurDirection::TopToBottom,
)
.draw_behind(move |scope| {
scope.draw_rect(Brush::vertical_gradient(
vec![
background.with_alpha(SCRIM_TOP_ALPHA * progress),
background.with_alpha(SCRIM_MIDDLE_ALPHA * progress),
background.with_alpha(0.0),
],
0.0,
scope.size().height,
));
});
Box(band, BoxSpec::default(), || {});
let inline_alpha = ((progress - 0.5) * 2.0).clamp(0.0, 1.0);
let inline_title = title.clone();
let inline_typography = typography.clone();
let leading = Rc::clone(&leading);
let trailing = Rc::clone(&trailing);
Row(
Modifier::empty()
.fill_max_width()
.height(BAR_HEIGHT)
.padding_horizontal(10.0),
RowSpec::default().vertical_alignment(VerticalAlignment::CenterVertically),
move || {
(leading.borrow_mut())();
let style = TextStyle {
span_style: SpanStyle {
color: Some(colors.label.with_alpha(inline_alpha)),
..inline_typography.headline.span_style.clone()
},
..inline_typography.headline.clone()
};
let inline_title = inline_title.clone();
Box(
Modifier::empty().weight(1.0),
BoxSpec::default().content_alignment(Alignment::CENTER),
move || {
let modifier = if progress >= 0.5 {
Modifier::empty()
} else {
Modifier::empty().hide_from_accessibility()
};
Text(inline_title.clone(), modifier, style.clone());
},
);
(trailing.borrow_mut())();
},
);
});
}
pub fn large_title_settle_policy(collapse_range: f32) -> cranpose_ui::ScrollSettlePolicy {
let range = collapse_range.max(1.0);
Rc::new(move |proposed: f32, _velocity: f32| {
if proposed <= 0.0 || proposed >= range {
proposed
} else if proposed < range * 0.5 {
0.0
} else {
range
}
})
}
pub fn liquid_nav_bar_expanded_height() -> f32 {
BAR_HEIGHT + LARGE_TITLE_HEIGHT
}
#[cfg(test)]
#[path = "tests/nav_bar_tests.rs"]
mod tests;