tessera_ui_basic_components/
dialog.rs1use std::{
7 sync::Arc,
8 time::{Duration, Instant},
9};
10
11use derive_builder::Builder;
12use parking_lot::RwLock;
13use tessera_ui::{Color, DimensionValue, Dp, tessera, winit};
14
15use crate::{
16 alignment::Alignment,
17 animation,
18 boxed::{BoxedArgsBuilder, boxed},
19 fluid_glass::{FluidGlassArgsBuilder, fluid_glass},
20 pipelines::ShadowProps,
21 shape_def::Shape,
22 surface::{SurfaceArgsBuilder, surface},
23};
24
25const ANIM_TIME: Duration = Duration::from_millis(300);
27
28fn compute_dialog_progress(timer_opt: Option<Instant>) -> f32 {
31 timer_opt.as_ref().map_or(1.0, |timer| {
32 let elapsed = timer.elapsed();
33 if elapsed >= ANIM_TIME {
34 1.0
35 } else {
36 elapsed.as_secs_f32() / ANIM_TIME.as_secs_f32()
37 }
38 })
39}
40
41fn blur_radius_for(progress: f32, is_open: bool, max_blur_radius: f32) -> f32 {
43 if is_open {
44 progress * max_blur_radius
45 } else {
46 max_blur_radius * (1.0 - progress)
47 }
48}
49
50fn scrim_alpha_for(progress: f32, is_open: bool) -> f32 {
52 if is_open {
53 progress * 0.5
54 } else {
55 0.5 * (1.0 - progress)
56 }
57}
58
59#[derive(Default, Clone, Copy)]
61pub enum DialogStyle {
62 Glass,
64 #[default]
66 Material,
67}
68
69#[derive(Builder)]
71#[builder(pattern = "owned")]
72pub struct DialogProviderArgs {
73 pub on_close_request: Arc<dyn Fn() + Send + Sync>,
76 #[builder(default = "Dp(16.0)")]
78 pub padding: Dp,
79 #[builder(default)]
81 pub style: DialogStyle,
82}
83
84#[derive(Default)]
85struct DialogProviderStateInner {
86 is_open: bool,
87 timer: Option<Instant>,
88}
89
90#[derive(Clone, Default)]
91pub struct DialogProviderState {
92 inner: Arc<RwLock<DialogProviderStateInner>>,
93}
94
95impl DialogProviderState {
96 pub fn new() -> Self {
98 Self::default()
99 }
100
101 pub fn open(&self) {
103 let mut inner = self.inner.write();
104 if !inner.is_open {
105 inner.is_open = true;
106 let mut timer = Instant::now();
107 if let Some(old_timer) = inner.timer {
108 let elapsed = old_timer.elapsed();
109 if elapsed < ANIM_TIME {
110 timer += ANIM_TIME - elapsed;
111 }
112 }
113 inner.timer = Some(timer);
114 }
115 }
116
117 pub fn close(&self) {
119 let mut inner = self.inner.write();
120 if inner.is_open {
121 inner.is_open = false;
122 let mut timer = Instant::now();
123 if let Some(old_timer) = inner.timer {
124 let elapsed = old_timer.elapsed();
125 if elapsed < ANIM_TIME {
126 timer += ANIM_TIME - elapsed;
127 }
128 }
129 inner.timer = Some(timer);
130 }
131 }
132
133 pub fn is_open(&self) -> bool {
135 self.inner.read().is_open
136 }
137
138 pub fn is_animating(&self) -> bool {
140 self.inner
141 .read()
142 .timer
143 .is_some_and(|t| t.elapsed() < ANIM_TIME)
144 }
145
146 fn snapshot(&self) -> (bool, Option<Instant>) {
147 let inner = self.inner.read();
148 (inner.is_open, inner.timer)
149 }
150}
151
152fn render_scrim(args: &DialogProviderArgs, is_open: bool, progress: f32) {
153 match args.style {
154 DialogStyle::Glass => {
155 let blur_radius = blur_radius_for(progress, is_open, 5.0);
156 fluid_glass(
157 FluidGlassArgsBuilder::default()
158 .on_click(args.on_close_request.clone())
159 .tint_color(Color::TRANSPARENT)
160 .width(DimensionValue::Fill {
161 min: None,
162 max: None,
163 })
164 .height(DimensionValue::Fill {
165 min: None,
166 max: None,
167 })
168 .dispersion_height(Dp(0.0))
169 .refraction_height(Dp(0.0))
170 .block_input(true)
171 .blur_radius(Dp(blur_radius as f64))
172 .border(None)
173 .shape(Shape::RoundedRectangle {
174 top_left: Dp(0.0),
175 top_right: Dp(0.0),
176 bottom_right: Dp(0.0),
177 bottom_left: Dp(0.0),
178 g2_k_value: 3.0,
179 })
180 .noise_amount(0.0)
181 .build()
182 .unwrap(),
183 None,
184 || {},
185 );
186 }
187 DialogStyle::Material => {
188 let alpha = scrim_alpha_for(progress, is_open);
189 surface(
190 SurfaceArgsBuilder::default()
191 .style(Color::BLACK.with_alpha(alpha).into())
192 .on_click(args.on_close_request.clone())
193 .width(DimensionValue::Fill {
194 min: None,
195 max: None,
196 })
197 .height(DimensionValue::Fill {
198 min: None,
199 max: None,
200 })
201 .block_input(true)
202 .build()
203 .unwrap(),
204 None,
205 || {},
206 );
207 }
208 }
209}
210
211fn make_keyboard_input_handler(
212 on_close: Arc<dyn Fn() + Send + Sync>,
213) -> Box<dyn for<'a> Fn(tessera_ui::InputHandlerInput<'a>) + Send + Sync + 'static> {
214 Box::new(move |input| {
215 input.keyboard_events.drain(..).for_each(|event| {
216 if event.state == winit::event::ElementState::Pressed
217 && let winit::keyboard::PhysicalKey::Code(winit::keyboard::KeyCode::Escape) =
218 event.physical_key
219 {
220 (on_close)();
221 }
222 });
223 })
224}
225
226#[tessera]
227fn dialog_content_wrapper(
228 style: DialogStyle,
229 alpha: f32,
230 padding: Dp,
231 content: impl FnOnce() + Send + Sync + 'static,
232) {
233 boxed(
234 BoxedArgsBuilder::default()
235 .width(DimensionValue::FILLED)
236 .height(DimensionValue::FILLED)
237 .alignment(Alignment::Center)
238 .build()
239 .unwrap(),
240 |scope| {
241 scope.child(move || match style {
242 DialogStyle::Glass => {
243 fluid_glass(
244 FluidGlassArgsBuilder::default()
245 .tint_color(Color::WHITE.with_alpha(alpha / 2.5))
246 .blur_radius(Dp(5.0 * alpha as f64))
247 .shape(Shape::RoundedRectangle {
248 top_left: Dp(25.0),
249 top_right: Dp(25.0),
250 bottom_right: Dp(25.0),
251 bottom_left: Dp(25.0),
252 g2_k_value: 3.0,
253 })
254 .refraction_amount(32.0 * alpha)
255 .block_input(true)
256 .padding(padding)
257 .build()
258 .unwrap(),
259 None,
260 content,
261 );
262 }
263 DialogStyle::Material => {
264 surface(
265 SurfaceArgsBuilder::default()
266 .style(Color::WHITE.with_alpha(alpha).into())
267 .shadow(ShadowProps {
268 color: Color::BLACK.with_alpha(alpha / 4.0),
269 ..Default::default()
270 })
271 .shape(Shape::RoundedRectangle {
272 top_left: Dp(25.0),
273 top_right: Dp(25.0),
274 bottom_right: Dp(25.0),
275 bottom_left: Dp(25.0),
276 g2_k_value: 3.0,
277 })
278 .padding(padding)
279 .block_input(true)
280 .build()
281 .unwrap(),
282 None,
283 content,
284 );
285 }
286 });
287 },
288 );
289}
290
291#[tessera]
318pub fn dialog_provider(
319 args: DialogProviderArgs,
320 state: DialogProviderState,
321 main_content: impl FnOnce(),
322 dialog_content: impl FnOnce(f32) + Send + Sync + 'static,
323) {
324 main_content();
326
327 let (is_open, timer_opt) = state.snapshot();
330
331 let is_animating = timer_opt.is_some_and(|t| t.elapsed() < ANIM_TIME);
332
333 if is_open || is_animating {
334 let progress = animation::easing(compute_dialog_progress(timer_opt));
335
336 let content_alpha = if is_open {
337 progress * 1.0 } else {
339 1.0 * (1.0 - progress) };
341
342 render_scrim(&args, is_open, progress);
344
345 let handler = make_keyboard_input_handler(args.on_close_request.clone());
347 input_handler(handler);
348
349 dialog_content_wrapper(args.style, content_alpha, args.padding, move || {
352 dialog_content(content_alpha);
353 });
354 }
355}