tessera_ui_basic_components/
fluid_glass.rs1use std::sync::Arc;
11
12use derive_builder::Builder;
13use tessera_ui::{
14 Color, ComputedData, Constraint, CursorEventContent, DimensionValue, Dp, PressKeyEventType, Px,
15 PxPosition, renderer::DrawCommand, winit::window::CursorIcon,
16};
17use tessera_ui_macros::tessera;
18
19use crate::{
20 padding_utils::remove_padding_from_dimension,
21 pipelines::{blur::command::BlurCommand, contrast::ContrastCommand, mean::MeanCommand},
22 pos_misc::is_position_in_component,
23 ripple_state::RippleState,
24 shape_def::Shape,
25};
26
27#[derive(Clone, Copy, Debug, Default)]
28pub struct GlassBorder {
29 pub width: Px,
30}
31
32impl GlassBorder {
33 pub fn new(width: Px) -> Self {
34 Self { width }
35 }
36}
37
38#[derive(Builder, Clone)]
42#[builder(build_fn(validate = "Self::validate"), pattern = "owned", setter(into))]
43pub struct FluidGlassArgs {
44 #[builder(default = "Color::TRANSPARENT")]
49 pub tint_color: Color,
50 #[builder(default = "Shape::RoundedRectangle { corner_radius: 25.0, g2_k_value: 3.0 }")]
52 pub shape: Shape,
53 #[builder(default = "0.0")]
55 pub blur_radius: f32,
56 #[builder(default = "25.0")]
58 pub dispersion_height: f32,
59 #[builder(default = "1.0")]
61 pub chroma_multiplier: f32,
62 #[builder(default = "24.0")]
64 pub refraction_height: f32,
65 #[builder(default = "32.0")]
67 pub refraction_amount: f32,
68 #[builder(default = "0.2")]
70 pub eccentric_factor: f32,
71 #[builder(default = "0.02")]
73 pub noise_amount: f32,
74 #[builder(default = "1.0")]
76 pub noise_scale: f32,
77 #[builder(default = "0.0")]
79 pub time: f32,
80 #[builder(default, setter(strip_option))]
82 pub contrast: Option<f32>,
83 #[builder(default, setter(strip_option))]
85 pub width: Option<DimensionValue>,
86 #[builder(default, setter(strip_option))]
88 pub height: Option<DimensionValue>,
89
90 #[builder(default = "Dp(0.0)")]
91 pub padding: Dp,
92
93 #[builder(default, setter(strip_option))]
95 pub ripple_center: Option<[f32; 2]>,
96 #[builder(default, setter(strip_option))]
97 pub ripple_radius: Option<f32>,
98 #[builder(default, setter(strip_option))]
99 pub ripple_alpha: Option<f32>,
100 #[builder(default, setter(strip_option))]
101 pub ripple_strength: Option<f32>,
102
103 #[builder(default, setter(strip_option, into = false))]
104 pub on_click: Option<Arc<dyn Fn() + Send + Sync>>,
105
106 #[builder(default = "Some(GlassBorder { width: Px(1) })")]
107 pub border: Option<GlassBorder>,
108
109 #[builder(default = "false")]
112 pub block_input: bool,
113}
114
115impl FluidGlassArgsBuilder {
116 fn validate(&self) -> Result<(), String> {
117 Ok(())
118 }
119}
120
121impl Default for FluidGlassArgs {
123 fn default() -> Self {
124 FluidGlassArgsBuilder::default().build().unwrap()
125 }
126}
127
128#[derive(Clone)]
129pub struct FluidGlassCommand {
130 pub args: FluidGlassArgs,
131}
132
133impl DrawCommand for FluidGlassCommand {
134 fn barrier(&self) -> Option<tessera_ui::BarrierRequirement> {
135 Some(tessera_ui::BarrierRequirement::SampleBackground)
137 }
138}
139
140#[tessera]
141pub fn fluid_glass(
173 mut args: FluidGlassArgs,
174 ripple_state: Option<Arc<RippleState>>,
175 child: impl FnOnce(),
176) {
177 if let Some(ripple_state) = &ripple_state {
178 if let Some((progress, center)) = ripple_state.get_animation_progress() {
179 args.ripple_center = Some(center);
180 args.ripple_radius = Some(progress);
181 args.ripple_alpha = Some((1.0 - progress) * 0.3);
182 args.ripple_strength = Some(progress);
183 }
184 }
185 (child)();
186 let args_measure_clone = args.clone();
187 measure(Box::new(move |input| {
188 let glass_intrinsic_width = args_measure_clone.width.unwrap_or(DimensionValue::Wrap {
189 min: None,
190 max: None,
191 });
192 let glass_intrinsic_height = args_measure_clone.height.unwrap_or(DimensionValue::Wrap {
193 min: None,
194 max: None,
195 });
196 let glass_intrinsic_constraint =
197 Constraint::new(glass_intrinsic_width, glass_intrinsic_height);
198 let effective_glass_constraint = glass_intrinsic_constraint.merge(input.parent_constraint);
199
200 let child_constraint = Constraint::new(
201 remove_padding_from_dimension(
202 effective_glass_constraint.width,
203 args_measure_clone.padding.into(),
204 ),
205 remove_padding_from_dimension(
206 effective_glass_constraint.height,
207 args_measure_clone.padding.into(),
208 ),
209 );
210
211 let child_measurement = if !input.children_ids.is_empty() {
212 let child_measurement =
213 input.measure_child(input.children_ids[0], &child_constraint)?;
214 input.place_child(
215 input.children_ids[0],
216 PxPosition {
217 x: args.padding.into(),
218 y: args.padding.into(),
219 },
220 );
221 child_measurement
222 } else {
223 ComputedData {
224 width: Px(0),
225 height: Px(0),
226 }
227 };
228
229 if args.blur_radius > 0.0 {
230 let blur_command = BlurCommand {
231 radius: args.blur_radius,
232 direction: (1.0, 0.0), };
234 let blur_command2 = BlurCommand {
235 radius: args.blur_radius,
236 direction: (0.0, 1.0), };
238 let mut metadata = input.metadata_mut();
239 metadata.push_compute_command(blur_command);
240 metadata.push_compute_command(blur_command2);
241 }
242
243 if let Some(contrast_value) = args.contrast {
244 let mean_command =
245 MeanCommand::new(input.gpu, &mut input.compute_resource_manager.write());
246 let contrast_command =
247 ContrastCommand::new(contrast_value, mean_command.result_buffer_ref());
248 let mut metadata = input.metadata_mut();
249 metadata.push_compute_command(mean_command);
250 metadata.push_compute_command(contrast_command);
251 }
252
253 let drawable = FluidGlassCommand {
254 args: args_measure_clone.clone(),
255 };
256
257 input.metadata_mut().push_draw_command(drawable);
258
259 let padding_px: Px = args_measure_clone.padding.into();
260 let min_width = child_measurement.width + padding_px * 2;
261 let min_height = child_measurement.height + padding_px * 2;
262 let width = match effective_glass_constraint.width {
263 DimensionValue::Fixed(value) => value,
264 DimensionValue::Wrap { min, max } => min
265 .unwrap_or(Px(0))
266 .max(min_width)
267 .min(max.unwrap_or(Px::MAX)),
268 DimensionValue::Fill { min, max } => max
269 .expect("Seems that you are trying to fill an infinite width, which is not allowed")
270 .max(min_width)
271 .max(min.unwrap_or(Px(0))),
272 };
273 let height = match effective_glass_constraint.height {
274 DimensionValue::Fixed(value) => value,
275 DimensionValue::Wrap { min, max } => min
276 .unwrap_or(Px(0))
277 .max(min_height)
278 .min(max.unwrap_or(Px::MAX)),
279 DimensionValue::Fill { min, max } => max
280 .expect(
281 "Seems that you are trying to fill an infinite height, which is not allowed",
282 )
283 .max(min_height)
284 .max(min.unwrap_or(Px(0))),
285 };
286 Ok(ComputedData { width, height })
287 }));
288
289 if let Some(on_click) = args.on_click {
290 let ripple_state = ripple_state.clone();
291 state_handler(Box::new(move |mut input| {
292 let size = input.computed_data;
293 let cursor_pos_option = input.cursor_position_rel;
294 let is_cursor_in = cursor_pos_option
295 .map(|pos| is_position_in_component(size, pos))
296 .unwrap_or(false);
297
298 if is_cursor_in {
299 input.requests.cursor_icon = CursorIcon::Pointer;
300 }
301
302 if is_cursor_in {
303 if let Some(_event) = input.cursor_events.iter().find(|e| {
304 matches!(
305 e.content,
306 CursorEventContent::Pressed(PressKeyEventType::Left)
307 )
308 }) {
309 if let Some(ripple_state) = &ripple_state {
310 if let Some(pos) = input.cursor_position_rel {
311 let size = input.computed_data;
312 let normalized_pos = [
313 pos.x.to_f32() / size.width.to_f32(),
314 pos.y.to_f32() / size.height.to_f32(),
315 ];
316 ripple_state.start_animation(normalized_pos);
317 }
318 }
319 on_click();
320 }
321
322 if args.block_input {
323 input.block_all();
325 }
326 }
327 }));
328 } else if args.block_input {
329 state_handler(Box::new(move |mut input| {
330 let size = input.computed_data;
331 let cursor_pos_option = input.cursor_position_rel;
332 let is_cursor_in = cursor_pos_option
333 .map(|pos| is_position_in_component(size, pos))
334 .unwrap_or(false);
335
336 if is_cursor_in {
337 input.block_all();
339 }
340 }));
341 }
342}