use std::{
sync::Arc,
time::{Duration, Instant},
};
use derive_builder::Builder;
use parking_lot::RwLock;
use tessera_ui::{Color, DimensionValue, Dp, Px, PxPosition, tessera, winit};
use crate::{
animation,
fluid_glass::{FluidGlassArgsBuilder, fluid_glass},
shape_def::Shape,
surface::{SurfaceArgsBuilder, surface},
};
const ANIM_TIME: Duration = Duration::from_millis(300);
#[derive(Default, Clone, Copy)]
pub enum SideBarStyle {
Glass,
#[default]
Material,
}
#[derive(Builder)]
pub struct SideBarProviderArgs {
pub on_close_request: Arc<dyn Fn() + Send + Sync>,
#[builder(default)]
pub style: SideBarStyle,
}
#[derive(Default)]
struct SideBarProviderStateInner {
is_open: bool,
timer: Option<Instant>,
}
#[derive(Clone, Default)]
pub struct SideBarProviderState {
inner: Arc<RwLock<SideBarProviderStateInner>>,
}
impl SideBarProviderState {
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 calc_progress_from_timer(timer: Option<&Instant>) -> f32 {
let raw = match timer {
None => 1.0,
Some(t) => {
let elapsed = t.elapsed();
if elapsed >= ANIM_TIME {
1.0
} else {
elapsed.as_secs_f32() / ANIM_TIME.as_secs_f32()
}
}
};
animation::easing(raw)
}
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)
}
}
fn compute_side_bar_x(child_width: Px, progress: f32, is_open: bool) -> i32 {
let child = child_width.0 as f32;
let x = if is_open {
-child * (1.0 - progress)
} else {
-child * progress
};
x as i32
}
fn render_glass_scrim(args: &SideBarProviderArgs, progress: f32, is_open: bool) {
let max_blur_radius = 5.0;
let blur_radius = blur_radius_for(progress, is_open, max_blur_radius);
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,
|| {},
);
}
fn render_material_scrim(args: &SideBarProviderArgs, progress: f32, is_open: bool) {
let scrim_alpha = scrim_alpha_for(progress, is_open);
surface(
SurfaceArgsBuilder::default()
.style(Color::BLACK.with_alpha(scrim_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 render_scrim(args: &SideBarProviderArgs, progress: f32, is_open: bool) {
match args.style {
SideBarStyle::Glass => render_glass_scrim(args, progress, is_open),
SideBarStyle::Material => render_material_scrim(args, progress, is_open),
}
}
fn snapshot_state(state: &SideBarProviderState) -> (bool, Option<Instant>) {
state.snapshot()
}
fn make_keyboard_closure(
on_close: Arc<dyn Fn() + Send + Sync>,
) -> Box<dyn Fn(tessera_ui::InputHandlerInput<'_>) + Send + Sync> {
Box::new(move |input: tessera_ui::InputHandlerInput<'_>| {
for event in input.keyboard_events.drain(..) {
if event.state == winit::event::ElementState::Pressed
&& let winit::keyboard::PhysicalKey::Code(winit::keyboard::KeyCode::Escape) =
event.physical_key
{
(on_close)();
}
}
})
}
fn place_side_bar_if_present(
input: &tessera_ui::MeasureInput<'_>,
state_for_measure: &SideBarProviderState,
progress: f32,
) {
if input.children_ids.len() <= 2 {
return;
}
let side_bar_id = input.children_ids[2];
let child_size = match input.measure_child(side_bar_id, input.parent_constraint) {
Ok(s) => s,
Err(_) => return,
};
let current_is_open = state_for_measure.is_open();
let x = compute_side_bar_x(child_size.width, progress, current_is_open);
input.place_child(side_bar_id, PxPosition::new(Px(x), Px(0)));
}
#[tessera]
pub fn side_bar_provider(
args: SideBarProviderArgs,
state: SideBarProviderState,
main_content: impl FnOnce() + Send + Sync + 'static,
side_bar_content: impl FnOnce() + Send + Sync + 'static,
) {
main_content();
let (is_open, timer_opt) = snapshot_state(&state);
if !(is_open || timer_opt.is_some_and(|t| t.elapsed() < ANIM_TIME)) {
return;
}
let on_close_for_keyboard = args.on_close_request.clone();
let progress = calc_progress_from_timer(timer_opt.as_ref());
render_scrim(&args, progress, is_open);
let keyboard_closure = make_keyboard_closure(on_close_for_keyboard);
input_handler(keyboard_closure);
side_bar_content_wrapper(args.style, side_bar_content);
let state_for_measure = state.clone();
let measure_closure = Box::new(move |input: &tessera_ui::MeasureInput<'_>| {
let main_content_id = input.children_ids[0];
let main_content_size = input.measure_child(main_content_id, input.parent_constraint)?;
input.place_child(main_content_id, PxPosition::new(Px(0), Px(0)));
if input.children_ids.len() > 1 {
let scrim_id = input.children_ids[1];
input.measure_child(scrim_id, input.parent_constraint)?;
input.place_child(scrim_id, PxPosition::new(Px(0), Px(0)));
}
place_side_bar_if_present(input, &state_for_measure, progress);
Ok(main_content_size)
});
measure(measure_closure);
}
#[tessera]
fn side_bar_content_wrapper(style: SideBarStyle, content: impl FnOnce() + Send + Sync + 'static) {
match style {
SideBarStyle::Glass => {
fluid_glass(
FluidGlassArgsBuilder::default()
.shape(Shape::RoundedRectangle {
top_left: Dp(0.0),
top_right: Dp(25.0),
bottom_right: Dp(25.0),
bottom_left: Dp(0.0),
g2_k_value: 3.0,
})
.tint_color(Color::new(0.6, 0.8, 1.0, 0.3))
.width(DimensionValue::from(Dp(250.0)))
.height(tessera_ui::DimensionValue::Fill {
min: None,
max: None,
})
.blur_radius(Dp(10.0))
.padding(Dp(16.0))
.block_input(true)
.build()
.unwrap(),
None,
content,
);
}
SideBarStyle::Material => {
surface(
SurfaceArgsBuilder::default()
.style(Color::new(0.9, 0.9, 0.9, 1.0).into())
.width(DimensionValue::from(Dp(250.0)))
.height(tessera_ui::DimensionValue::Fill {
min: None,
max: None,
})
.padding(Dp(16.0))
.shape(Shape::RoundedRectangle {
top_left: Dp(0.0),
top_right: Dp(25.0),
bottom_right: Dp(25.0),
bottom_left: Dp(0.0),
g2_k_value: 3.0,
})
.block_input(true)
.build()
.unwrap(),
None,
content,
);
}
}
}