cranpose_liquid/widgets/
nav_bar.rs1use crate::material::{Glass, LiquidModifierExt, LiquidShape};
6use crate::theme::{liquid_colors, liquid_typography};
7use cranpose_macros::composable;
8use cranpose_ui::text::{SpanStyle, TextStyle};
9use cranpose_ui::widgets::{Box, BoxSpec, Row, RowSpec, Text};
10use cranpose_ui::{Modifier, ScrollState};
11use cranpose_ui_graphics::GraphicsLayer;
12use cranpose_ui_layout::{Alignment, VerticalAlignment};
13use std::cell::RefCell;
14use std::rc::Rc;
15
16#[derive(Clone, Debug, PartialEq)]
18pub struct LiquidNavBarSpec {
19 pub title: String,
20 pub collapse_range: f32,
22}
23
24impl LiquidNavBarSpec {
25 pub fn new(title: impl Into<String>) -> Self {
26 Self {
27 title: title.into(),
28 collapse_range: 52.0,
29 }
30 }
31}
32
33const BAR_HEIGHT: f32 = 52.0;
34const LARGE_TITLE_HEIGHT: f32 = 52.0;
35
36#[composable]
48#[allow(non_snake_case)]
49pub fn LiquidNavBar(
50 modifier: Modifier,
51 spec: LiquidNavBarSpec,
52 scroll: ScrollState,
53 leading: impl FnMut() + 'static,
54 trailing: impl FnMut() + 'static,
55) {
56 let colors = liquid_colors();
57 let typography = liquid_typography();
58 let leading = Rc::new(RefCell::new(leading));
59 let trailing = Rc::new(RefCell::new(trailing));
60
61 let collapse_range = spec.collapse_range;
62 if std::env::var_os("CRANPOSE_DISABLE_NAV_SNAP").is_none() {
63 scroll.set_settle_policy(Some(large_title_settle_policy(collapse_range)));
64 }
65 let scroll_offset = scroll.value();
66
67 let progress = (scroll_offset / spec.collapse_range.max(1.0)).clamp(0.0, 1.0);
69 let title = spec.title.clone();
70
71 Box(modifier, BoxSpec::default(), move || {
72 let large_alpha = (1.0 - progress * 1.6).clamp(0.0, 1.0);
75 if large_alpha > 0.0 {
76 let style = TextStyle {
77 span_style: SpanStyle {
78 color: Some(colors.label.with_alpha(large_alpha)),
79 ..typography.large_title.span_style.clone()
80 },
81 ..typography.large_title.clone()
82 };
83 let slide = -scroll_offset.min(collapse_range);
84 let large_title = title.clone();
85 Box(
86 Modifier::empty()
87 .offset(16.0, BAR_HEIGHT)
88 .height(LARGE_TITLE_HEIGHT)
89 .graphics_layer(move || GraphicsLayer {
90 translation_y: slide,
91 ..Default::default()
92 }),
93 BoxSpec::default().content_alignment(Alignment::new(
94 cranpose_ui_layout::HorizontalAlignment::Start,
95 cranpose_ui_layout::VerticalAlignment::CenterVertically,
96 )),
97 move || {
98 Text(large_title.clone(), Modifier::empty(), style.clone());
99 },
100 );
101 }
102
103 if progress > 0.01 {
105 let band = Modifier::empty()
106 .graphics_layer(move || GraphicsLayer {
107 alpha: progress,
108 ..Default::default()
109 })
110 .fill_max_width()
111 .height(BAR_HEIGHT)
112 .glass_effect(
113 Glass::regular()
114 .shape(LiquidShape::RoundedRect(0.0))
115 .shadow(false),
116 );
117 Box(band, BoxSpec::default(), || {});
118 }
119
120 let inline_alpha = ((progress - 0.5) * 2.0).clamp(0.0, 1.0);
122 let inline_title = title.clone();
123 let inline_typography = typography.clone();
124 let leading = Rc::clone(&leading);
125 let trailing = Rc::clone(&trailing);
126 Row(
127 Modifier::empty()
128 .fill_max_width()
129 .height(BAR_HEIGHT)
130 .padding_horizontal(10.0),
131 RowSpec::default().vertical_alignment(VerticalAlignment::CenterVertically),
132 move || {
133 (leading.borrow_mut())();
134 let style = TextStyle {
135 span_style: SpanStyle {
136 color: Some(colors.label.with_alpha(inline_alpha)),
137 ..inline_typography.headline.span_style.clone()
138 },
139 ..inline_typography.headline.clone()
140 };
141 let inline_title = inline_title.clone();
142 Box(
143 Modifier::empty().weight(1.0),
144 BoxSpec::default().content_alignment(Alignment::CENTER),
145 move || {
146 Text(inline_title.clone(), Modifier::empty(), style.clone());
147 },
148 );
149 (trailing.borrow_mut())();
150 },
151 );
152 });
153}
154
155pub fn large_title_settle_policy(collapse_range: f32) -> cranpose_ui::ScrollSettlePolicy {
159 let range = collapse_range.max(1.0);
160 Rc::new(move |proposed: f32, _velocity: f32| {
161 if proposed <= 0.0 || proposed >= range {
162 proposed
163 } else if proposed < range * 0.5 {
164 0.0
165 } else {
166 range
167 }
168 })
169}
170
171pub fn liquid_nav_bar_expanded_height() -> f32 {
174 BAR_HEIGHT + LARGE_TITLE_HEIGHT
175}