cranpose_liquid/widgets/
nav_bar.rs1use crate::material::{Glass, GlassDynamics, 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 let band = Modifier::empty()
107 .fill_max_width()
108 .height(BAR_HEIGHT)
109 .glass_effect_with(
110 Glass::regular()
111 .shape(LiquidShape::RoundedRect(0.0))
112 .blur_radius(30.0)
116 .saturation(1.15)
117 .refraction_depth(0.0)
118 .transmission_refraction(0.0)
119 .highlight(0.18)
120 .adaptive_frost(colors.label, 0.75)
121 .shadow(false),
122 move || GlassDynamics {
123 activity: Some(progress),
124 ..Default::default()
125 },
126 );
127 Box(band, BoxSpec::default(), || {});
128
129 let inline_alpha = ((progress - 0.5) * 2.0).clamp(0.0, 1.0);
131 let inline_title = title.clone();
132 let inline_typography = typography.clone();
133 let leading = Rc::clone(&leading);
134 let trailing = Rc::clone(&trailing);
135 Row(
136 Modifier::empty()
137 .fill_max_width()
138 .height(BAR_HEIGHT)
139 .padding_horizontal(10.0),
140 RowSpec::default().vertical_alignment(VerticalAlignment::CenterVertically),
141 move || {
142 (leading.borrow_mut())();
143 let style = TextStyle {
144 span_style: SpanStyle {
145 color: Some(colors.label.with_alpha(inline_alpha)),
146 ..inline_typography.headline.span_style.clone()
147 },
148 ..inline_typography.headline.clone()
149 };
150 let inline_title = inline_title.clone();
151 Box(
152 Modifier::empty().weight(1.0),
153 BoxSpec::default().content_alignment(Alignment::CENTER),
154 move || {
155 Text(inline_title.clone(), Modifier::empty(), style.clone());
156 },
157 );
158 (trailing.borrow_mut())();
159 },
160 );
161 });
162}
163
164pub fn large_title_settle_policy(collapse_range: f32) -> cranpose_ui::ScrollSettlePolicy {
168 let range = collapse_range.max(1.0);
169 Rc::new(move |proposed: f32, _velocity: f32| {
170 if proposed <= 0.0 || proposed >= range {
171 proposed
172 } else if proposed < range * 0.5 {
173 0.0
174 } else {
175 range
176 }
177 })
178}
179
180pub fn liquid_nav_bar_expanded_height() -> f32 {
183 BAR_HEIGHT + LARGE_TITLE_HEIGHT
184}