use std::{
sync::Arc,
time::{Duration, Instant},
};
use derive_setters::Setters;
use tessera_ui::{
Color, ComputedData, Dp, MeasurementError, Modifier, Px, PxPosition, State,
layout::{LayoutInput, LayoutOutput, LayoutSpec},
remember, tessera, winit,
};
use crate::{
animation,
fluid_glass::{FluidGlassArgs, fluid_glass},
modifier::ModifierExt,
shape_def::{RoundedCorner, Shape},
surface::{SurfaceArgs, surface},
};
const ANIM_TIME: Duration = Duration::from_millis(300);
#[derive(Default, Clone, Copy)]
pub enum SideBarStyle {
Glass,
#[default]
Material,
}
#[derive(Setters)]
pub struct SideBarProviderArgs {
#[setters(skip)]
pub on_close_request: Arc<dyn Fn() + Send + Sync>,
pub style: SideBarStyle,
pub is_open: bool,
}
impl SideBarProviderArgs {
pub fn new(on_close_request: impl Fn() + Send + Sync + 'static) -> Self {
Self {
on_close_request: Arc::new(on_close_request),
style: SideBarStyle::default(),
is_open: false,
}
}
pub fn on_close_request<F>(mut self, on_close_request: F) -> Self
where
F: Fn() + Send + Sync + 'static,
{
self.on_close_request = Arc::new(on_close_request);
self
}
pub fn on_close_request_shared(
mut self,
on_close_request: Arc<dyn Fn() + Send + Sync>,
) -> Self {
self.on_close_request = on_close_request;
self
}
}
#[derive(Clone)]
pub struct SideBarController {
is_open: bool,
timer: Option<Instant>,
}
impl SideBarController {
pub fn new(initial_open: bool) -> Self {
Self {
is_open: initial_open,
timer: None,
}
}
pub fn open(&mut self) {
if !self.is_open {
self.is_open = true;
let mut timer = Instant::now();
if let Some(old_timer) = self.timer {
let elapsed = old_timer.elapsed();
if elapsed < ANIM_TIME {
timer += ANIM_TIME - elapsed;
}
}
self.timer = Some(timer);
}
}
pub fn close(&mut self) {
if self.is_open {
self.is_open = false;
let mut timer = Instant::now();
if let Some(old_timer) = self.timer {
let elapsed = old_timer.elapsed();
if elapsed < ANIM_TIME {
timer += ANIM_TIME - elapsed;
}
}
self.timer = Some(timer);
}
}
pub fn is_open(&self) -> bool {
self.is_open
}
pub fn is_animating(&self) -> bool {
self.timer.is_some_and(|t| t.elapsed() < ANIM_TIME)
}
fn snapshot(&self) -> (bool, Option<Instant>) {
(self.is_open, self.timer)
}
}
impl Default for SideBarController {
fn default() -> Self {
Self::new(false)
}
}
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(
FluidGlassArgs::default()
.on_click_shared(args.on_close_request.clone())
.tint_color(Color::TRANSPARENT)
.modifier(Modifier::new().fill_max_size())
.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: RoundedCorner::manual(Dp(0.0), 3.0),
top_right: RoundedCorner::manual(Dp(0.0), 3.0),
bottom_right: RoundedCorner::manual(Dp(0.0), 3.0),
bottom_left: RoundedCorner::manual(Dp(0.0), 3.0),
})
.noise_amount(0.0),
|| {},
);
}
fn render_material_scrim(args: &SideBarProviderArgs, progress: f32, is_open: bool) {
let scrim_alpha = scrim_alpha_for(progress, is_open);
surface(
SurfaceArgs::default()
.style(Color::BLACK.with_alpha(scrim_alpha).into())
.on_click_shared(args.on_close_request.clone())
.modifier(Modifier::new().fill_max_size())
.block_input(true),
|| {},
);
}
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 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: &LayoutInput<'_>,
output: &mut LayoutOutput<'_>,
is_open: bool,
progress: f32,
) {
if input.children_ids().len() <= 2 {
return;
}
let side_bar_id = input.children_ids()[2];
let child_size = match input.measure_child_in_parent_constraint(side_bar_id) {
Ok(s) => s,
Err(_) => return,
};
let x = compute_side_bar_x(child_size.width, progress, is_open);
output.place_child(side_bar_id, PxPosition::new(Px(x), Px(0)));
}
#[tessera]
pub fn side_bar_provider(
args: impl Into<SideBarProviderArgs>,
main_content: impl FnOnce() + Send + Sync + 'static,
side_bar_content: impl FnOnce() + Send + Sync + 'static,
) {
let args: SideBarProviderArgs = args.into();
let controller = remember(|| SideBarController::new(args.is_open));
if args.is_open != controller.with(|c| c.is_open()) {
if args.is_open {
controller.with_mut(|c| c.open());
} else {
controller.with_mut(|c| c.close());
}
}
side_bar_provider_with_controller(args, controller, main_content, side_bar_content);
}
#[tessera]
pub fn side_bar_provider_with_controller(
args: impl Into<SideBarProviderArgs>,
controller: State<SideBarController>,
main_content: impl FnOnce() + Send + Sync + 'static,
side_bar_content: impl FnOnce() + Send + Sync + 'static,
) {
let args: SideBarProviderArgs = args.into();
main_content();
let (is_open, timer_opt) = controller.with(|c| c.snapshot());
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);
layout(SideBarLayout { progress, is_open });
}
#[tessera]
fn side_bar_content_wrapper(style: SideBarStyle, content: impl FnOnce() + Send + Sync + 'static) {
match style {
SideBarStyle::Glass => {
fluid_glass(
FluidGlassArgs::default()
.shape(Shape::RoundedRectangle {
top_left: RoundedCorner::manual(Dp(0.0), 3.0),
top_right: RoundedCorner::manual(Dp(25.0), 3.0),
bottom_right: RoundedCorner::manual(Dp(25.0), 3.0),
bottom_left: RoundedCorner::manual(Dp(0.0), 3.0),
})
.tint_color(Color::new(0.6, 0.8, 1.0, 0.3))
.modifier(Modifier::new().width(Dp(250.0)).fill_max_height())
.blur_radius(Dp(10.0))
.padding(Dp(16.0))
.block_input(true),
content,
);
}
SideBarStyle::Material => {
surface(
SurfaceArgs::default()
.style(Color::new(0.9, 0.9, 0.9, 1.0).into())
.modifier(Modifier::new().width(Dp(250.0)).fill_max_height())
.shape(Shape::RoundedRectangle {
top_left: RoundedCorner::manual(Dp(0.0), 3.0),
top_right: RoundedCorner::manual(Dp(25.0), 3.0),
bottom_right: RoundedCorner::manual(Dp(25.0), 3.0),
bottom_left: RoundedCorner::manual(Dp(0.0), 3.0),
})
.block_input(true),
move || {
Modifier::new().padding_all(Dp(16.0)).run(content);
},
);
}
}
}
#[derive(Clone, PartialEq)]
struct SideBarLayout {
progress: f32,
is_open: bool,
}
impl LayoutSpec for SideBarLayout {
fn measure(
&self,
input: &LayoutInput<'_>,
output: &mut LayoutOutput<'_>,
) -> Result<ComputedData, MeasurementError> {
let main_content_id = input.children_ids()[0];
let main_content_size = input.measure_child_in_parent_constraint(main_content_id)?;
output.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_in_parent_constraint(scrim_id)?;
output.place_child(scrim_id, PxPosition::new(Px(0), Px(0)));
}
place_side_bar_if_present(input, output, self.is_open, self.progress);
Ok(main_content_size)
}
}