tessera_ui_basic_components/
fluid_glass.rs1use std::sync::Arc;
11
12use derive_builder::Builder;
13use tessera_ui::{
14 BarrierRequirement, Color, ComputedData, Constraint, CursorEventContent, DimensionValue, Dp,
15 PressKeyEventType, Px, PxPosition, renderer::DrawCommand, tessera, winit::window::CursorIcon,
16};
17
18use crate::{
19 padding_utils::remove_padding_from_dimension,
20 pipelines::{blur::command::BlurCommand, contrast::ContrastCommand, mean::MeanCommand},
21 pos_misc::is_position_in_component,
22 ripple_state::RippleState,
23 shape_def::Shape,
24};
25
26#[derive(Clone, Copy, Debug, Default, PartialEq)]
27pub struct GlassBorder {
28 pub width: Px,
29}
30
31impl GlassBorder {
32 pub fn new(width: Px) -> Self {
33 Self { width }
34 }
35}
36
37#[derive(Builder, Clone)]
41#[builder(build_fn(validate = "Self::validate"), pattern = "owned", setter(into))]
42pub struct FluidGlassArgs {
43 #[builder(default = "Color::TRANSPARENT")]
48 pub tint_color: Color,
49 #[builder(
51 default = "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 }"
52 )]
53 pub shape: Shape,
54 #[builder(default = "0.0")]
56 pub blur_radius: f32,
57 #[builder(default = "25.0")]
59 pub dispersion_height: f32,
60 #[builder(default = "1.0")]
62 pub chroma_multiplier: f32,
63 #[builder(default = "24.0")]
65 pub refraction_height: f32,
66 #[builder(default = "32.0")]
68 pub refraction_amount: f32,
69 #[builder(default = "0.2")]
71 pub eccentric_factor: f32,
72 #[builder(default = "0.0")]
74 pub noise_amount: f32,
75 #[builder(default = "1.0")]
77 pub noise_scale: f32,
78 #[builder(default = "0.0")]
80 pub time: f32,
81 #[builder(default, setter(strip_option))]
83 pub contrast: Option<f32>,
84 #[builder(default = "DimensionValue::WRAP", setter(into))]
86 pub width: DimensionValue,
87 #[builder(default = "DimensionValue::WRAP", setter(into))]
89 pub height: DimensionValue,
90
91 #[builder(default = "Dp(0.0)")]
92 pub padding: Dp,
93
94 #[builder(default, setter(strip_option))]
96 pub ripple_center: Option<[f32; 2]>,
97 #[builder(default, setter(strip_option))]
98 pub ripple_radius: Option<f32>,
99 #[builder(default, setter(strip_option))]
100 pub ripple_alpha: Option<f32>,
101 #[builder(default, setter(strip_option))]
102 pub ripple_strength: Option<f32>,
103
104 #[builder(default, setter(strip_option, into = false))]
105 pub on_click: Option<Arc<dyn Fn() + Send + Sync>>,
106
107 #[builder(default = "Some(GlassBorder { width: Dp(1.0).into() })")]
108 pub border: Option<GlassBorder>,
109
110 #[builder(default = "false")]
113 pub block_input: bool,
114}
115
116impl PartialEq for FluidGlassArgs {
117 fn eq(&self, other: &Self) -> bool {
118 self.tint_color == other.tint_color
119 && self.shape == other.shape
120 && self.blur_radius == other.blur_radius
121 && self.dispersion_height == other.dispersion_height
122 && self.chroma_multiplier == other.chroma_multiplier
123 && self.refraction_height == other.refraction_height
124 && self.refraction_amount == other.refraction_amount
125 && self.eccentric_factor == other.eccentric_factor
126 && self.noise_amount == other.noise_amount
127 && self.noise_scale == other.noise_scale
128 && self.time == other.time
129 && self.contrast == other.contrast
130 && self.width == other.width
131 && self.height == other.height
132 && self.padding == other.padding
133 && self.ripple_center == other.ripple_center
134 && self.ripple_radius == other.ripple_radius
135 && self.ripple_alpha == other.ripple_alpha
136 && self.ripple_strength == other.ripple_strength
137 && self.border == other.border
138 && self.block_input == other.block_input
139 }
140}
141
142impl FluidGlassArgsBuilder {
143 fn validate(&self) -> Result<(), String> {
144 Ok(())
145 }
146}
147
148impl Default for FluidGlassArgs {
150 fn default() -> Self {
151 FluidGlassArgsBuilder::default().build().unwrap()
152 }
153}
154
155#[derive(Clone, PartialEq)]
156pub struct FluidGlassCommand {
157 pub args: FluidGlassArgs,
158}
159
160impl DrawCommand for FluidGlassCommand {
161 fn barrier(&self) -> Option<BarrierRequirement> {
162 Some(BarrierRequirement::ZERO_PADDING_LOCAL)
163 }
164}
165
166fn handle_click_state(
169 args: &FluidGlassArgs,
170 ripple_state: Option<Arc<RippleState>>,
171 on_click: Arc<dyn Fn() + Send + Sync>,
172 input: &mut tessera_ui::InputHandlerInput,
173) {
174 let size = input.computed_data;
175 let cursor_pos_option = input.cursor_position_rel;
176 let is_cursor_in = cursor_pos_option
177 .map(|pos| is_position_in_component(size, pos))
178 .unwrap_or(false);
179
180 if is_cursor_in {
181 input.requests.cursor_icon = CursorIcon::Pointer;
182
183 if let Some(_event) = input.cursor_events.iter().find(|e| {
184 matches!(
185 e.content,
186 CursorEventContent::Released(PressKeyEventType::Left)
187 )
188 }) {
189 if let Some(ripple_state) = &ripple_state
190 && let Some(pos) = input.cursor_position_rel
191 {
192 let size = input.computed_data;
193 let normalized_pos = [
194 pos.x.to_f32() / size.width.to_f32(),
195 pos.y.to_f32() / size.height.to_f32(),
196 ];
197 ripple_state.start_animation(normalized_pos);
198 }
199 on_click();
200 }
201
202 if args.block_input {
203 input.block_all();
205 }
206 }
207}
208
209fn handle_block_input(input: &mut tessera_ui::InputHandlerInput) {
210 let size = input.computed_data;
211 let cursor_pos_option = input.cursor_position_rel;
212 let is_cursor_in = cursor_pos_option
213 .map(|pos| is_position_in_component(size, pos))
214 .unwrap_or(false);
215
216 if is_cursor_in {
217 input.block_all();
219 }
220}
221
222#[tessera]
254pub fn fluid_glass(
255 mut args: FluidGlassArgs,
256 ripple_state: Option<Arc<RippleState>>,
257 child: impl FnOnce(),
258) {
259 if let Some(ripple_state) = &ripple_state
260 && let Some((progress, center)) = ripple_state.get_animation_progress()
261 {
262 args.ripple_center = Some(center);
263 args.ripple_radius = Some(progress);
264 args.ripple_alpha = Some((1.0 - progress) * 0.3);
265 args.ripple_strength = Some(progress);
266 }
267 (child)();
268 let args_measure_clone = args.clone();
269 measure(Box::new(move |input| {
270 let glass_intrinsic_width = args_measure_clone.width;
271 let glass_intrinsic_height = args_measure_clone.height;
272 let glass_intrinsic_constraint =
273 Constraint::new(glass_intrinsic_width, glass_intrinsic_height);
274 let effective_glass_constraint = glass_intrinsic_constraint.merge(input.parent_constraint);
275
276 let child_constraint = Constraint::new(
277 remove_padding_from_dimension(
278 effective_glass_constraint.width,
279 args_measure_clone.padding.into(),
280 ),
281 remove_padding_from_dimension(
282 effective_glass_constraint.height,
283 args_measure_clone.padding.into(),
284 ),
285 );
286
287 let child_measurement = if !input.children_ids.is_empty() {
288 let child_measurement =
289 input.measure_child(input.children_ids[0], &child_constraint)?;
290 input.place_child(
291 input.children_ids[0],
292 PxPosition {
293 x: args.padding.into(),
294 y: args.padding.into(),
295 },
296 );
297 child_measurement
298 } else {
299 ComputedData {
300 width: Px(0),
301 height: Px(0),
302 }
303 };
304
305 if args.blur_radius > 0.0 {
306 let blur_command = BlurCommand {
307 radius: args.blur_radius,
308 direction: (1.0, 0.0), padding: Px(args.refraction_height as i32),
310 };
311 let blur_command2 = BlurCommand {
312 radius: args.blur_radius,
313 direction: (0.0, 1.0), padding: Px(args.refraction_height as i32),
315 };
316 let mut metadata = input.metadata_mut();
317 metadata.push_compute_command(blur_command);
318 metadata.push_compute_command(blur_command2);
319 }
320
321 if let Some(contrast_value) = args.contrast {
322 let mean_command =
323 MeanCommand::new(input.gpu, &mut input.compute_resource_manager.write());
324 let contrast_command =
325 ContrastCommand::new(contrast_value, mean_command.result_buffer_ref());
326 let mut metadata = input.metadata_mut();
327 metadata.push_compute_command(mean_command);
328 metadata.push_compute_command(contrast_command);
329 }
330
331 let drawable = FluidGlassCommand {
332 args: args_measure_clone.clone(),
333 };
334
335 input.metadata_mut().push_draw_command(drawable);
336
337 let padding_px: Px = args_measure_clone.padding.into();
338 let min_width = child_measurement.width + padding_px * 2;
339 let min_height = child_measurement.height + padding_px * 2;
340 let width = match effective_glass_constraint.width {
341 DimensionValue::Fixed(value) => value,
342 DimensionValue::Wrap { min, max } => min
343 .unwrap_or(Px(0))
344 .max(min_width)
345 .min(max.unwrap_or(Px::MAX)),
346 DimensionValue::Fill { min, max } => max
347 .expect("Seems that you are trying to fill an infinite width, which is not allowed")
348 .max(min_width)
349 .max(min.unwrap_or(Px(0))),
350 };
351 let height = match effective_glass_constraint.height {
352 DimensionValue::Fixed(value) => value,
353 DimensionValue::Wrap { min, max } => min
354 .unwrap_or(Px(0))
355 .max(min_height)
356 .min(max.unwrap_or(Px::MAX)),
357 DimensionValue::Fill { min, max } => max
358 .expect(
359 "Seems that you are trying to fill an infinite height, which is not allowed",
360 )
361 .max(min_height)
362 .max(min.unwrap_or(Px(0))),
363 };
364 Ok(ComputedData { width, height })
365 }));
366
367 if let Some(ref on_click) = args.on_click {
368 let ripple_state = ripple_state.clone();
369 let on_click_arc = on_click.clone();
370 let args_for_handler = args.clone();
371 input_handler(Box::new(move |mut input: tessera_ui::InputHandlerInput| {
372 handle_click_state(
374 &args_for_handler,
375 ripple_state.clone(),
376 on_click_arc.clone(),
377 &mut input,
378 );
379 }));
380 } else if args.block_input {
381 input_handler(Box::new(move |mut input: tessera_ui::InputHandlerInput| {
382 handle_block_input(&mut input);
384 }));
385 }
386}