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 ShadowProps,
17 alignment::Alignment,
18 animation,
19 boxed::{BoxedArgsBuilder, boxed},
20 fluid_glass::{FluidGlassArgsBuilder, fluid_glass},
21 shape_def::{RoundedCorner, 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)]
103pub struct DialogProviderState {
104 inner: Arc<RwLock<DialogProviderStateInner>>,
105}
106
107impl DialogProviderState {
108 pub fn new() -> Self {
110 Self::default()
111 }
112
113 pub fn open(&self) {
115 let mut inner = self.inner.write();
116 if !inner.is_open {
117 inner.is_open = true;
118 let mut timer = Instant::now();
119 if let Some(old_timer) = inner.timer {
120 let elapsed = old_timer.elapsed();
121 if elapsed < ANIM_TIME {
122 timer += ANIM_TIME - elapsed;
123 }
124 }
125 inner.timer = Some(timer);
126 }
127 }
128
129 pub fn close(&self) {
131 let mut inner = self.inner.write();
132 if inner.is_open {
133 inner.is_open = false;
134 let mut timer = Instant::now();
135 if let Some(old_timer) = inner.timer {
136 let elapsed = old_timer.elapsed();
137 if elapsed < ANIM_TIME {
138 timer += ANIM_TIME - elapsed;
139 }
140 }
141 inner.timer = Some(timer);
142 }
143 }
144
145 pub fn is_open(&self) -> bool {
147 self.inner.read().is_open
148 }
149
150 pub fn is_animating(&self) -> bool {
152 self.inner
153 .read()
154 .timer
155 .is_some_and(|t| t.elapsed() < ANIM_TIME)
156 }
157
158 fn snapshot(&self) -> (bool, Option<Instant>) {
159 let inner = self.inner.read();
160 (inner.is_open, inner.timer)
161 }
162}
163
164fn render_scrim(args: &DialogProviderArgs, is_open: bool, progress: f32) {
165 match args.style {
166 DialogStyle::Glass => {
167 let blur_radius = blur_radius_for(progress, is_open, 5.0);
168 fluid_glass(
169 FluidGlassArgsBuilder::default()
170 .on_click(args.on_close_request.clone())
171 .tint_color(Color::TRANSPARENT)
172 .width(DimensionValue::Fill {
173 min: None,
174 max: None,
175 })
176 .height(DimensionValue::Fill {
177 min: None,
178 max: None,
179 })
180 .dispersion_height(Dp(0.0))
181 .refraction_height(Dp(0.0))
182 .block_input(true)
183 .blur_radius(Dp(blur_radius as f64))
184 .border(None)
185 .shape(Shape::RoundedRectangle {
186 top_left: RoundedCorner::manual(Dp(0.0), 3.0),
187 top_right: RoundedCorner::manual(Dp(0.0), 3.0),
188 bottom_right: RoundedCorner::manual(Dp(0.0), 3.0),
189 bottom_left: RoundedCorner::manual(Dp(0.0), 3.0),
190 })
191 .noise_amount(0.0)
192 .build()
193 .expect("builder construction failed"),
194 None,
195 || {},
196 );
197 }
198 DialogStyle::Material => {
199 let alpha = scrim_alpha_for(progress, is_open);
200 surface(
201 SurfaceArgsBuilder::default()
202 .style(Color::BLACK.with_alpha(alpha).into())
203 .on_click(args.on_close_request.clone())
204 .width(DimensionValue::Fill {
205 min: None,
206 max: None,
207 })
208 .height(DimensionValue::Fill {
209 min: None,
210 max: None,
211 })
212 .block_input(true)
213 .build()
214 .expect("builder construction failed"),
215 None,
216 || {},
217 );
218 }
219 }
220}
221
222fn make_keyboard_input_handler(
223 on_close: Arc<dyn Fn() + Send + Sync>,
224) -> Box<dyn for<'a> Fn(tessera_ui::InputHandlerInput<'a>) + Send + Sync + 'static> {
225 Box::new(move |input| {
226 input.keyboard_events.drain(..).for_each(|event| {
227 if event.state == winit::event::ElementState::Pressed
228 && let winit::keyboard::PhysicalKey::Code(winit::keyboard::KeyCode::Escape) =
229 event.physical_key
230 {
231 (on_close)();
232 }
233 });
234 })
235}
236
237#[tessera]
238fn dialog_content_wrapper(
239 style: DialogStyle,
240 alpha: f32,
241 padding: Dp,
242 content: impl FnOnce() + Send + Sync + 'static,
243) {
244 boxed(
245 BoxedArgsBuilder::default()
246 .width(DimensionValue::FILLED)
247 .height(DimensionValue::FILLED)
248 .alignment(Alignment::Center)
249 .build()
250 .expect("builder construction failed"),
251 |scope| {
252 scope.child(move || match style {
253 DialogStyle::Glass => {
254 fluid_glass(
255 FluidGlassArgsBuilder::default()
256 .tint_color(Color::WHITE.with_alpha(alpha / 2.5))
257 .blur_radius(Dp(5.0 * alpha as f64))
258 .shape(Shape::RoundedRectangle {
259 top_left: RoundedCorner::manual(Dp(25.0), 3.0),
260 top_right: RoundedCorner::manual(Dp(25.0), 3.0),
261 bottom_right: RoundedCorner::manual(Dp(25.0), 3.0),
262 bottom_left: RoundedCorner::manual(Dp(25.0), 3.0),
263 })
264 .refraction_amount(32.0 * alpha)
265 .block_input(true)
266 .padding(padding)
267 .build()
268 .expect("builder construction failed"),
269 None,
270 content,
271 );
272 }
273 DialogStyle::Material => {
274 surface(
275 SurfaceArgsBuilder::default()
276 .style(Color::WHITE.with_alpha(alpha).into())
277 .shadow(ShadowProps {
278 color: Color::BLACK.with_alpha(alpha / 4.0),
279 ..Default::default()
280 })
281 .shape(Shape::RoundedRectangle {
282 top_left: RoundedCorner::manual(Dp(25.0), 3.0),
283 top_right: RoundedCorner::manual(Dp(25.0), 3.0),
284 bottom_right: RoundedCorner::manual(Dp(25.0), 3.0),
285 bottom_left: RoundedCorner::manual(Dp(25.0), 3.0),
286 })
287 .padding(padding)
288 .block_input(true)
289 .build()
290 .expect("builder construction failed"),
291 None,
292 content,
293 );
294 }
295 });
296 },
297 );
298}
299
300#[tessera]
327pub fn dialog_provider(
328 args: DialogProviderArgs,
329 state: DialogProviderState,
330 main_content: impl FnOnce(),
331 dialog_content: impl FnOnce(f32) + Send + Sync + 'static,
332) {
333 main_content();
335
336 let (is_open, timer_opt) = state.snapshot();
339
340 let is_animating = timer_opt.is_some_and(|t| t.elapsed() < ANIM_TIME);
341
342 if is_open || is_animating {
343 let progress = animation::easing(compute_dialog_progress(timer_opt));
344
345 let content_alpha = if is_open {
346 progress * 1.0 } else {
348 1.0 * (1.0 - progress) };
350
351 render_scrim(&args, is_open, progress);
353
354 let handler = make_keyboard_input_handler(args.on_close_request.clone());
356 input_handler(handler);
357
358 dialog_content_wrapper(args.style, content_alpha, args.padding, move || {
361 dialog_content(content_alpha);
362 });
363 }
364}