use std::sync::Arc;
use derive_setters::Setters;
use tessera_ui::{
Color, ComputedData, Constraint, DimensionValue, Dp, MeasurementError, Modifier, Px,
PxPosition, SampleRegion, State,
accesskit::Role,
layout::{LayoutInput, LayoutOutput, LayoutSpec, RenderInput},
remember,
renderer::DrawCommand,
tessera,
};
use crate::{
modifier::{ClickableArgs, InteractionState, ModifierExt, PointerEventContext, SemanticsArgs},
padding_utils::remove_padding_from_dimension,
pipelines::{
blur::command::DualBlurCommand, contrast::ContrastCommand, mean::command::MeanCommand,
},
pos_misc::is_position_in_component,
ripple_state::RippleState,
shape_def::{RoundedCorner, Shape},
};
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct GlassBorder {
pub width: Px,
}
impl GlassBorder {
pub fn new(width: Px) -> Self {
Self { width }
}
}
#[derive(Clone, Setters)]
#[setters(into)]
pub struct FluidGlassArgs {
pub tint_color: Color,
pub shape: Shape,
pub blur_radius: Dp,
pub dispersion_height: Dp,
pub chroma_multiplier: f32,
pub refraction_height: Dp,
pub refraction_amount: f32,
pub eccentric_factor: f32,
pub noise_amount: f32,
pub noise_scale: f32,
pub time: f32,
#[setters(strip_option)]
pub contrast: Option<f32>,
pub modifier: Modifier,
pub padding: Dp,
#[setters(strip_option)]
pub ripple_center: Option<[f32; 2]>,
#[setters(strip_option)]
pub ripple_radius: Option<f32>,
#[setters(strip_option)]
pub ripple_alpha: Option<f32>,
#[setters(strip_option)]
pub ripple_strength: Option<f32>,
#[setters(skip)]
pub on_click: Option<Arc<dyn Fn() + Send + Sync>>,
pub border: Option<GlassBorder>,
pub block_input: bool,
#[setters(strip_option)]
pub accessibility_role: Option<Role>,
#[setters(strip_option, into)]
pub accessibility_label: Option<String>,
#[setters(strip_option, into)]
pub accessibility_description: Option<String>,
pub accessibility_focusable: bool,
}
impl PartialEq for FluidGlassArgs {
fn eq(&self, other: &Self) -> bool {
self.tint_color == other.tint_color
&& self.shape == other.shape
&& self.blur_radius == other.blur_radius
&& self.dispersion_height == other.dispersion_height
&& self.chroma_multiplier == other.chroma_multiplier
&& self.refraction_height == other.refraction_height
&& self.refraction_amount == other.refraction_amount
&& self.eccentric_factor == other.eccentric_factor
&& self.noise_amount == other.noise_amount
&& self.noise_scale == other.noise_scale
&& self.time == other.time
&& self.contrast == other.contrast
&& self.padding == other.padding
&& self.ripple_center == other.ripple_center
&& self.ripple_radius == other.ripple_radius
&& self.ripple_alpha == other.ripple_alpha
&& self.ripple_strength == other.ripple_strength
&& self.border == other.border
&& self.block_input == other.block_input
}
}
impl FluidGlassArgs {
pub fn on_click<F>(mut self, on_click: F) -> Self
where
F: Fn() + Send + Sync + 'static,
{
self.on_click = Some(Arc::new(on_click));
self
}
pub fn on_click_shared(mut self, on_click: Arc<dyn Fn() + Send + Sync>) -> Self {
self.on_click = Some(on_click);
self
}
}
impl Default for FluidGlassArgs {
fn default() -> Self {
Self {
tint_color: Color::TRANSPARENT,
shape: Shape::RoundedRectangle {
top_left: RoundedCorner::manual(Dp(25.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(25.0), 3.0),
},
blur_radius: Dp(0.0),
dispersion_height: Dp(25.0),
chroma_multiplier: 1.1,
refraction_height: Dp(24.0),
refraction_amount: 32.0,
eccentric_factor: 0.2,
noise_amount: 0.0,
noise_scale: 1.0,
time: 0.0,
contrast: None,
modifier: Modifier::new(),
padding: Dp(0.0),
ripple_center: None,
ripple_radius: None,
ripple_alpha: None,
ripple_strength: None,
on_click: None,
border: Some(GlassBorder {
width: Dp(1.35).into(),
}),
block_input: false,
accessibility_role: None,
accessibility_label: None,
accessibility_description: None,
accessibility_focusable: false,
}
}
}
#[derive(Clone, PartialEq)]
pub struct FluidGlassCommand {
pub args: FluidGlassArgs,
}
impl DrawCommand for FluidGlassCommand {
fn sample_region(&self) -> Option<SampleRegion> {
Some(SampleRegion::uniform_padding_local(Px(10)))
}
fn apply_opacity(&mut self, opacity: f32) {
let factor = opacity.clamp(0.0, 1.0);
self.args.tint_color = self
.args
.tint_color
.with_alpha(self.args.tint_color.a * factor);
if let Some(ripple_alpha) = self.args.ripple_alpha.as_mut() {
*ripple_alpha *= factor;
}
}
}
fn handle_block_input(input: &mut tessera_ui::InputHandlerInput) {
let size = input.computed_data;
let cursor_pos_option = input.cursor_position_rel;
let is_cursor_in = cursor_pos_option
.map(|pos| is_position_in_component(size, pos))
.unwrap_or(false);
if is_cursor_in {
input.block_all();
}
}
#[tessera]
pub fn fluid_glass(args: FluidGlassArgs, child: impl FnOnce() + Send + Sync + 'static) {
let mut modifier = args.modifier;
let interactive = args.on_click.is_some();
let interaction_state = interactive.then(|| remember(InteractionState::new));
let ripple_state = interactive.then(|| remember(RippleState::new));
let has_semantics = args.accessibility_role.is_some()
|| args.accessibility_label.is_some()
|| args.accessibility_description.is_some();
if interactive {
let press_handler = ripple_state.map(|state| {
Arc::new(move |ctx: PointerEventContext| {
state.with_mut(|s| {
s.start_animation(ctx.normalized_pos);
});
})
});
let release_handler = ripple_state.map(|state| {
Arc::new(move |_ctx: PointerEventContext| {
state.with_mut(|s| s.release());
})
});
let mut clickable_args = ClickableArgs::new(
args.on_click
.clone()
.expect("interactive implies on_click is set"),
)
.block_input(args.block_input);
if let Some(role) = args.accessibility_role {
clickable_args = clickable_args.role(role);
}
if let Some(label) = args.accessibility_label.clone() {
clickable_args = clickable_args.label(label);
}
if let Some(description) = args.accessibility_description.clone() {
clickable_args = clickable_args.description(description);
}
if let Some(state) = interaction_state {
clickable_args = clickable_args.interaction_state(state);
}
if let Some(handler) = press_handler {
clickable_args = clickable_args.on_press(handler);
}
if let Some(handler) = release_handler {
clickable_args = clickable_args.on_release(handler);
}
modifier = modifier.clickable(clickable_args);
} else if args.block_input {
modifier = modifier.block_touch_propagation();
}
if !interactive && has_semantics {
let mut semantics = SemanticsArgs::new();
if let Some(role) = args.accessibility_role {
semantics = semantics.role(role);
}
if let Some(label) = args.accessibility_label.clone() {
semantics = semantics.label(label);
}
if let Some(desc) = args.accessibility_description.clone() {
semantics = semantics.description(desc);
}
modifier = modifier.semantics(semantics);
}
modifier.run(move || fluid_glass_inner(args, ripple_state, child));
}
#[tessera]
fn fluid_glass_inner(
mut args: FluidGlassArgs,
ripple_state: Option<State<RippleState>>,
child: impl FnOnce() + Send + Sync + 'static,
) {
if let Some((progress, center)) = ripple_state
.as_ref()
.and_then(|state| state.with_mut(|s| s.get_animation_progress()))
{
args.ripple_center = Some(center);
args.ripple_radius = Some(progress);
args.ripple_alpha = Some((1.0 - progress) * 0.3);
args.ripple_strength = Some(progress);
}
(child)();
layout(FluidGlassLayout { args: args.clone() });
if args.on_click.is_none() && args.block_input {
let args_for_handler = args.clone();
input_handler(move |mut input: tessera_ui::InputHandlerInput| {
if args_for_handler.block_input {
handle_block_input(&mut input);
}
});
}
}
#[derive(Clone, PartialEq)]
struct FluidGlassLayout {
args: FluidGlassArgs,
}
impl LayoutSpec for FluidGlassLayout {
fn measure(
&self,
input: &LayoutInput<'_>,
output: &mut LayoutOutput<'_>,
) -> Result<ComputedData, MeasurementError> {
let effective_glass_constraint = Constraint::new(
input.parent_constraint().width(),
input.parent_constraint().height(),
);
let child_constraint = Constraint::new(
remove_padding_from_dimension(
effective_glass_constraint.width,
self.args.padding.into(),
),
remove_padding_from_dimension(
effective_glass_constraint.height,
self.args.padding.into(),
),
);
let child_measurement = if !input.children_ids().is_empty() {
let child_measurement =
input.measure_child(input.children_ids()[0], &child_constraint)?;
output.place_child(
input.children_ids()[0],
PxPosition {
x: self.args.padding.into(),
y: self.args.padding.into(),
},
);
child_measurement
} else {
ComputedData {
width: Px(0),
height: Px(0),
}
};
let padding_px: Px = self.args.padding.into();
let min_width = child_measurement.width + padding_px * 2;
let min_height = child_measurement.height + padding_px * 2;
let width = match effective_glass_constraint.width {
DimensionValue::Fixed(value) => value,
DimensionValue::Wrap { min, max } => min
.unwrap_or(Px(0))
.max(min_width)
.min(max.unwrap_or(Px::MAX)),
DimensionValue::Fill { min, max } => max
.expect("Seems that you are trying to fill an infinite width, which is not allowed")
.max(min_width)
.max(min.unwrap_or(Px(0))),
};
let height = match effective_glass_constraint.height {
DimensionValue::Fixed(value) => value,
DimensionValue::Wrap { min, max } => min
.unwrap_or(Px(0))
.max(min_height)
.min(max.unwrap_or(Px::MAX)),
DimensionValue::Fill { min, max } => max
.expect(
"Seems that you are trying to fill an infinite height, which is not allowed",
)
.max(min_height)
.max(min.unwrap_or(Px(0))),
};
Ok(ComputedData { width, height })
}
fn record(&self, input: &RenderInput<'_>) {
if self.args.blur_radius > Dp(0.0) {
let blur_command =
DualBlurCommand::horizontal_then_vertical(self.args.blur_radius.to_pixels_f32());
let mut metadata = input.metadata_mut();
metadata.push_compute_command(blur_command);
}
if let Some(contrast_value) = self.args.contrast
&& contrast_value != 1.0
{
let mean_command =
MeanCommand::new(input.gpu, &mut input.compute_resource_manager.write());
let contrast_command =
ContrastCommand::new(contrast_value, mean_command.result_buffer_ref());
let mut metadata = input.metadata_mut();
metadata.push_compute_command(mean_command);
metadata.push_compute_command(contrast_command);
}
let drawable = FluidGlassCommand {
args: self.args.clone(),
};
input.metadata_mut().push_draw_command(drawable);
}
}