tessera_ui_basic_components/
fluid_glass.rs1use std::sync::Arc;
7
8use derive_builder::Builder;
9use tessera_ui::{
10 BarrierRequirement, Color, ComputedData, Constraint, CursorEventContent, DimensionValue, Dp,
11 GestureState, PressKeyEventType, Px, PxPosition,
12 accesskit::{Action, Role},
13 renderer::DrawCommand,
14 tessera,
15 winit::window::CursorIcon,
16};
17
18use crate::{
19 padding_utils::remove_padding_from_dimension,
20 pipelines::{
21 blur::command::DualBlurCommand, contrast::ContrastCommand, mean::command::MeanCommand,
22 },
23 pos_misc::is_position_in_component,
24 ripple_state::RippleState,
25 shape_def::{RoundedCorner, Shape},
26};
27
28#[derive(Clone, Copy, Debug, Default, PartialEq)]
39pub struct GlassBorder {
40 pub width: Px,
42}
43
44impl GlassBorder {
45 pub fn new(width: Px) -> Self {
47 Self { width }
48 }
49}
50
51#[derive(Builder, Clone)]
55#[builder(build_fn(validate = "Self::validate"), pattern = "owned", setter(into))]
56pub struct FluidGlassArgs {
57 #[builder(default = "Color::TRANSPARENT")]
62 pub tint_color: Color,
63 #[builder(default = "Shape::RoundedRectangle {
65 top_left: RoundedCorner::manual(Dp(25.0), 3.0),
66 top_right: RoundedCorner::manual(Dp(25.0), 3.0),
67 bottom_right: RoundedCorner::manual(Dp(25.0), 3.0),
68 bottom_left: RoundedCorner::manual(Dp(25.0), 3.0),
69 }")]
70 pub shape: Shape,
71 #[builder(default = "Dp(0.0)")]
73 pub blur_radius: Dp,
74 #[builder(default = "Dp(25.0)")]
76 pub dispersion_height: Dp,
77 #[builder(default = "1.1")]
79 pub chroma_multiplier: f32,
80 #[builder(default = "Dp(24.0)")]
82 pub refraction_height: Dp,
83 #[builder(default = "32.0")]
85 pub refraction_amount: f32,
86 #[builder(default = "0.2")]
88 pub eccentric_factor: f32,
89 #[builder(default = "0.0")]
91 pub noise_amount: f32,
92 #[builder(default = "1.0")]
94 pub noise_scale: f32,
95 #[builder(default = "0.0")]
97 pub time: f32,
98 #[builder(default, setter(strip_option))]
100 pub contrast: Option<f32>,
101 #[builder(default = "DimensionValue::WRAP", setter(into))]
103 pub width: DimensionValue,
104 #[builder(default = "DimensionValue::WRAP", setter(into))]
106 pub height: DimensionValue,
107 #[builder(default = "Dp(0.0)")]
109 pub padding: Dp,
110 #[builder(default, setter(strip_option))]
112 pub ripple_center: Option<[f32; 2]>,
113 #[builder(default, setter(strip_option))]
115 pub ripple_radius: Option<f32>,
116 #[builder(default, setter(strip_option))]
118 pub ripple_alpha: Option<f32>,
119 #[builder(default, setter(strip_option))]
121 pub ripple_strength: Option<f32>,
122
123 #[builder(default, setter(strip_option, into = false))]
125 pub on_click: Option<Arc<dyn Fn() + Send + Sync>>,
126
127 #[builder(default = "Some(GlassBorder { width: Dp(1.35).into() })")]
129 pub border: Option<GlassBorder>,
130
131 #[builder(default = "false")]
134 pub block_input: bool,
135 #[builder(default, setter(strip_option))]
137 pub accessibility_role: Option<Role>,
138 #[builder(default, setter(strip_option, into))]
140 pub accessibility_label: Option<String>,
141 #[builder(default, setter(strip_option, into))]
143 pub accessibility_description: Option<String>,
144 #[builder(default)]
146 pub accessibility_focusable: bool,
147}
148
149impl PartialEq for FluidGlassArgs {
150 fn eq(&self, other: &Self) -> bool {
151 self.tint_color == other.tint_color
152 && self.shape == other.shape
153 && self.blur_radius == other.blur_radius
154 && self.dispersion_height == other.dispersion_height
155 && self.chroma_multiplier == other.chroma_multiplier
156 && self.refraction_height == other.refraction_height
157 && self.refraction_amount == other.refraction_amount
158 && self.eccentric_factor == other.eccentric_factor
159 && self.noise_amount == other.noise_amount
160 && self.noise_scale == other.noise_scale
161 && self.time == other.time
162 && self.contrast == other.contrast
163 && self.width == other.width
164 && self.height == other.height
165 && self.padding == other.padding
166 && self.ripple_center == other.ripple_center
167 && self.ripple_radius == other.ripple_radius
168 && self.ripple_alpha == other.ripple_alpha
169 && self.ripple_strength == other.ripple_strength
170 && self.border == other.border
171 && self.block_input == other.block_input
172 }
173}
174
175impl FluidGlassArgsBuilder {
176 fn validate(&self) -> Result<(), String> {
177 Ok(())
178 }
179}
180
181impl Default for FluidGlassArgs {
183 fn default() -> Self {
184 FluidGlassArgsBuilder::default()
185 .build()
186 .expect("builder construction failed")
187 }
188}
189
190#[derive(Clone, PartialEq)]
192pub struct FluidGlassCommand {
193 pub args: FluidGlassArgs,
195}
196
197impl DrawCommand for FluidGlassCommand {
198 fn barrier(&self) -> Option<BarrierRequirement> {
199 Some(BarrierRequirement::uniform_padding_local(Px(10)))
200 }
201}
202
203fn handle_click_state(
206 args: &FluidGlassArgs,
207 ripple_state: Option<RippleState>,
208 on_click: Arc<dyn Fn() + Send + Sync>,
209 input: &mut tessera_ui::InputHandlerInput,
210) {
211 let size = input.computed_data;
212 let cursor_pos_option = input.cursor_position_rel;
213 let is_cursor_in = cursor_pos_option
214 .map(|pos| is_position_in_component(size, pos))
215 .unwrap_or(false);
216
217 if is_cursor_in {
218 input.requests.cursor_icon = CursorIcon::Pointer;
219
220 if let Some(_event) = input.cursor_events.iter().find(|e| {
221 e.gesture_state == GestureState::TapCandidate
222 && matches!(
223 e.content,
224 CursorEventContent::Released(PressKeyEventType::Left)
225 )
226 }) {
227 if let Some(ripple_state) = &ripple_state
228 && let Some(pos) = input.cursor_position_rel
229 {
230 let size = input.computed_data;
231 let normalized_pos = [
232 pos.x.to_f32() / size.width.to_f32(),
233 pos.y.to_f32() / size.height.to_f32(),
234 ];
235 ripple_state.start_animation(normalized_pos);
236 }
237 on_click();
238 }
239
240 if args.block_input {
241 input.block_all();
243 }
244 }
245}
246
247fn handle_block_input(input: &mut tessera_ui::InputHandlerInput) {
248 let size = input.computed_data;
249 let cursor_pos_option = input.cursor_position_rel;
250 let is_cursor_in = cursor_pos_option
251 .map(|pos| is_position_in_component(size, pos))
252 .unwrap_or(false);
253
254 if is_cursor_in {
255 input.block_all();
257 }
258}
259
260fn apply_fluid_glass_accessibility(
261 input: &mut tessera_ui::InputHandlerInput<'_>,
262 args: &FluidGlassArgs,
263 on_click: &Option<Arc<dyn Fn() + Send + Sync>>,
264) {
265 let interactive = on_click.is_some();
266 let has_metadata = interactive
267 || args.accessibility_role.is_some()
268 || args.accessibility_label.is_some()
269 || args.accessibility_description.is_some()
270 || args.accessibility_focusable;
271
272 if !has_metadata {
273 return;
274 }
275
276 let mut builder = input.accessibility();
277
278 let role = args
279 .accessibility_role
280 .or_else(|| interactive.then_some(Role::Button));
281 if let Some(role) = role {
282 builder = builder.role(role);
283 }
284 if let Some(label) = args.accessibility_label.as_ref() {
285 builder = builder.label(label.clone());
286 }
287 if let Some(description) = args.accessibility_description.as_ref() {
288 builder = builder.description(description.clone());
289 }
290 if args.accessibility_focusable || interactive {
291 builder = builder.focusable();
292 }
293 if interactive {
294 builder = builder.action(Action::Click);
295 }
296
297 builder.commit();
298
299 if interactive && let Some(on_click) = on_click.clone() {
300 input.set_accessibility_action_handler(move |action| {
301 if action == Action::Click {
302 on_click();
303 }
304 });
305 }
306}
307
308#[tessera]
335pub fn fluid_glass(
336 mut args: FluidGlassArgs,
337 ripple_state: Option<RippleState>,
338 child: impl FnOnce(),
339) {
340 if let Some(ripple_state) = &ripple_state
341 && let Some((progress, center)) = ripple_state.get_animation_progress()
342 {
343 args.ripple_center = Some(center);
344 args.ripple_radius = Some(progress);
345 args.ripple_alpha = Some((1.0 - progress) * 0.3);
346 args.ripple_strength = Some(progress);
347 }
348 (child)();
349 let args_measure_clone = args.clone();
350 measure(Box::new(move |input| {
351 let glass_intrinsic_width = args_measure_clone.width;
352 let glass_intrinsic_height = args_measure_clone.height;
353 let glass_intrinsic_constraint =
354 Constraint::new(glass_intrinsic_width, glass_intrinsic_height);
355 let effective_glass_constraint = glass_intrinsic_constraint.merge(input.parent_constraint);
356
357 let child_constraint = Constraint::new(
358 remove_padding_from_dimension(
359 effective_glass_constraint.width,
360 args_measure_clone.padding.into(),
361 ),
362 remove_padding_from_dimension(
363 effective_glass_constraint.height,
364 args_measure_clone.padding.into(),
365 ),
366 );
367
368 let child_measurement = if !input.children_ids.is_empty() {
369 let child_measurement =
370 input.measure_child(input.children_ids[0], &child_constraint)?;
371 input.place_child(
372 input.children_ids[0],
373 PxPosition {
374 x: args.padding.into(),
375 y: args.padding.into(),
376 },
377 );
378 child_measurement
379 } else {
380 ComputedData {
381 width: Px(0),
382 height: Px(0),
383 }
384 };
385
386 if args.blur_radius > Dp(0.0) {
387 let blur_command =
388 DualBlurCommand::horizontal_then_vertical(args.blur_radius.to_pixels_f32());
389 let mut metadata = input.metadata_mut();
390 metadata.push_compute_command(blur_command);
391 }
392
393 if let Some(contrast_value) = args.contrast
394 && contrast_value != 1.0
395 {
396 let mean_command =
397 MeanCommand::new(input.gpu, &mut input.compute_resource_manager.write());
398 let contrast_command =
399 ContrastCommand::new(contrast_value, mean_command.result_buffer_ref());
400 let mut metadata = input.metadata_mut();
401 metadata.push_compute_command(mean_command);
402 metadata.push_compute_command(contrast_command);
403 }
404
405 let drawable = FluidGlassCommand {
406 args: args_measure_clone.clone(),
407 };
408
409 input.metadata_mut().push_draw_command(drawable);
410
411 let padding_px: Px = args_measure_clone.padding.into();
412 let min_width = child_measurement.width + padding_px * 2;
413 let min_height = child_measurement.height + padding_px * 2;
414 let width = match effective_glass_constraint.width {
415 DimensionValue::Fixed(value) => value,
416 DimensionValue::Wrap { min, max } => min
417 .unwrap_or(Px(0))
418 .max(min_width)
419 .min(max.unwrap_or(Px::MAX)),
420 DimensionValue::Fill { min, max } => max
421 .expect("Seems that you are trying to fill an infinite width, which is not allowed")
422 .max(min_width)
423 .max(min.unwrap_or(Px(0))),
424 };
425 let height = match effective_glass_constraint.height {
426 DimensionValue::Fixed(value) => value,
427 DimensionValue::Wrap { min, max } => min
428 .unwrap_or(Px(0))
429 .max(min_height)
430 .min(max.unwrap_or(Px::MAX)),
431 DimensionValue::Fill { min, max } => max
432 .expect(
433 "Seems that you are trying to fill an infinite height, which is not allowed",
434 )
435 .max(min_height)
436 .max(min.unwrap_or(Px(0))),
437 };
438 Ok(ComputedData { width, height })
439 }));
440
441 if let Some(ref on_click) = args.on_click {
442 let ripple_state = ripple_state.clone();
443 let on_click_arc = on_click.clone();
444 let args_for_handler = args.clone();
445 input_handler(Box::new(move |mut input: tessera_ui::InputHandlerInput| {
446 apply_fluid_glass_accessibility(
448 &mut input,
449 &args_for_handler,
450 &args_for_handler.on_click,
451 );
452 handle_click_state(
454 &args_for_handler,
455 ripple_state.clone(),
456 on_click_arc.clone(),
457 &mut input,
458 );
459 }));
460 } else if args.block_input {
461 let args_for_handler = args.clone();
462 input_handler(Box::new(move |mut input: tessera_ui::InputHandlerInput| {
463 apply_fluid_glass_accessibility(&mut input, &args_for_handler, &None);
465 handle_block_input(&mut input);
467 }));
468 } else {
469 let args_for_handler = args.clone();
471 input_handler(Box::new(move |mut input: tessera_ui::InputHandlerInput| {
472 apply_fluid_glass_accessibility(&mut input, &args_for_handler, &None);
473 }));
474 }
475}