use std::{
sync::Arc,
time::{Duration, Instant},
};
use derive_builder::Builder;
use parking_lot::RwLock;
use tessera_ui::{Color, DimensionValue, Dp, tessera, winit};
use crate::{
alignment::Alignment,
animation,
boxed::{BoxedArgsBuilder, boxed},
fluid_glass::{FluidGlassArgsBuilder, fluid_glass},
pipelines::ShadowProps,
shape_def::Shape,
surface::{SurfaceArgsBuilder, surface},
};
const ANIM_TIME: Duration = Duration::from_millis(300);
fn compute_dialog_progress(timer_opt: Option<Instant>) -> f32 {
timer_opt.as_ref().map_or(1.0, |timer| {
let elapsed = timer.elapsed();
if elapsed >= ANIM_TIME {
1.0
} else {
elapsed.as_secs_f32() / ANIM_TIME.as_secs_f32()
}
})
}
fn blur_radius_for(progress: f32, is_open: bool, max_blur_radius: f32) -> f32 {
if is_open {
progress * max_blur_radius
} else {
max_blur_radius * (1.0 - progress)
}
}
fn scrim_alpha_for(progress: f32, is_open: bool) -> f32 {
if is_open {
progress * 0.5
} else {
0.5 * (1.0 - progress)
}
}
#[derive(Default, Clone, Copy)]
pub enum DialogStyle {
Glass,
#[default]
Material,
}
#[derive(Builder)]
#[builder(pattern = "owned")]
pub struct DialogProviderArgs {
pub on_close_request: Arc<dyn Fn() + Send + Sync>,
#[builder(default = "Dp(16.0)")]
pub padding: Dp,
#[builder(default)]
pub style: DialogStyle,
}
#[derive(Default)]
struct DialogProviderStateInner {
is_open: bool,
timer: Option<Instant>,
}
#[derive(Clone, Default)]
pub struct DialogProviderState {
inner: Arc<RwLock<DialogProviderStateInner>>,
}
impl DialogProviderState {
pub fn new() -> Self {
Self::default()
}
pub fn open(&self) {
let mut inner = self.inner.write();
if !inner.is_open {
inner.is_open = true;
let mut timer = Instant::now();
if let Some(old_timer) = inner.timer {
let elapsed = old_timer.elapsed();
if elapsed < ANIM_TIME {
timer += ANIM_TIME - elapsed;
}
}
inner.timer = Some(timer);
}
}
pub fn close(&self) {
let mut inner = self.inner.write();
if inner.is_open {
inner.is_open = false;
let mut timer = Instant::now();
if let Some(old_timer) = inner.timer {
let elapsed = old_timer.elapsed();
if elapsed < ANIM_TIME {
timer += ANIM_TIME - elapsed;
}
}
inner.timer = Some(timer);
}
}
pub fn is_open(&self) -> bool {
self.inner.read().is_open
}
pub fn is_animating(&self) -> bool {
self.inner
.read()
.timer
.is_some_and(|t| t.elapsed() < ANIM_TIME)
}
fn snapshot(&self) -> (bool, Option<Instant>) {
let inner = self.inner.read();
(inner.is_open, inner.timer)
}
}
fn render_scrim(args: &DialogProviderArgs, is_open: bool, progress: f32) {
match args.style {
DialogStyle::Glass => {
let blur_radius = blur_radius_for(progress, is_open, 5.0);
fluid_glass(
FluidGlassArgsBuilder::default()
.on_click(args.on_close_request.clone())
.tint_color(Color::TRANSPARENT)
.width(DimensionValue::Fill {
min: None,
max: None,
})
.height(DimensionValue::Fill {
min: None,
max: None,
})
.dispersion_height(Dp(0.0))
.refraction_height(Dp(0.0))
.block_input(true)
.blur_radius(Dp(blur_radius as f64))
.border(None)
.shape(Shape::RoundedRectangle {
top_left: Dp(0.0),
top_right: Dp(0.0),
bottom_right: Dp(0.0),
bottom_left: Dp(0.0),
g2_k_value: 3.0,
})
.noise_amount(0.0)
.build()
.unwrap(),
None,
|| {},
);
}
DialogStyle::Material => {
let alpha = scrim_alpha_for(progress, is_open);
surface(
SurfaceArgsBuilder::default()
.style(Color::BLACK.with_alpha(alpha).into())
.on_click(args.on_close_request.clone())
.width(DimensionValue::Fill {
min: None,
max: None,
})
.height(DimensionValue::Fill {
min: None,
max: None,
})
.block_input(true)
.build()
.unwrap(),
None,
|| {},
);
}
}
}
fn make_keyboard_input_handler(
on_close: Arc<dyn Fn() + Send + Sync>,
) -> Box<dyn for<'a> Fn(tessera_ui::InputHandlerInput<'a>) + Send + Sync + 'static> {
Box::new(move |input| {
input.keyboard_events.drain(..).for_each(|event| {
if event.state == winit::event::ElementState::Pressed
&& let winit::keyboard::PhysicalKey::Code(winit::keyboard::KeyCode::Escape) =
event.physical_key
{
(on_close)();
}
});
})
}
#[tessera]
fn dialog_content_wrapper(
style: DialogStyle,
alpha: f32,
padding: Dp,
content: impl FnOnce() + Send + Sync + 'static,
) {
boxed(
BoxedArgsBuilder::default()
.width(DimensionValue::FILLED)
.height(DimensionValue::FILLED)
.alignment(Alignment::Center)
.build()
.unwrap(),
|scope| {
scope.child(move || match style {
DialogStyle::Glass => {
fluid_glass(
FluidGlassArgsBuilder::default()
.tint_color(Color::WHITE.with_alpha(alpha / 2.5))
.blur_radius(Dp(5.0 * alpha as f64))
.shape(Shape::RoundedRectangle {
top_left: Dp(25.0),
top_right: Dp(25.0),
bottom_right: Dp(25.0),
bottom_left: Dp(25.0),
g2_k_value: 3.0,
})
.refraction_amount(32.0 * alpha)
.block_input(true)
.padding(padding)
.build()
.unwrap(),
None,
content,
);
}
DialogStyle::Material => {
surface(
SurfaceArgsBuilder::default()
.style(Color::WHITE.with_alpha(alpha).into())
.shadow(ShadowProps {
color: Color::BLACK.with_alpha(alpha / 4.0),
..Default::default()
})
.shape(Shape::RoundedRectangle {
top_left: Dp(25.0),
top_right: Dp(25.0),
bottom_right: Dp(25.0),
bottom_left: Dp(25.0),
g2_k_value: 3.0,
})
.padding(padding)
.block_input(true)
.build()
.unwrap(),
None,
content,
);
}
});
},
);
}
#[tessera]
pub fn dialog_provider(
args: DialogProviderArgs,
state: DialogProviderState,
main_content: impl FnOnce(),
dialog_content: impl FnOnce(f32) + Send + Sync + 'static,
) {
main_content();
let (is_open, timer_opt) = state.snapshot();
let is_animating = timer_opt.is_some_and(|t| t.elapsed() < ANIM_TIME);
if is_open || is_animating {
let progress = animation::easing(compute_dialog_progress(timer_opt));
let content_alpha = if is_open {
progress * 1.0 } else {
1.0 * (1.0 - progress) };
render_scrim(&args, is_open, progress);
let handler = make_keyboard_input_handler(args.on_close_request.clone());
input_handler(handler);
dialog_content_wrapper(args.style, content_alpha, args.padding, move || {
dialog_content(content_alpha);
});
}
}