tessera_ui_basic_components/
fluid_glass.rs1use std::sync::Arc;
2
3use derive_builder::Builder;
4use tessera_ui::{
5 Color, ComputedData, Constraint, CursorEventContent, DimensionValue, Dp, PressKeyEventType, Px,
6 PxPosition, measure_node, place_node, renderer::DrawCommand, winit::window::CursorIcon,
7};
8use tessera_ui_macros::tessera;
9
10use crate::{
11 padding_utils::remove_padding_from_dimension,
12 pipelines::{blur::command::BlurCommand, contrast::ContrastCommand, mean::MeanCommand},
13 pos_misc::is_position_in_component,
14 ripple_state::RippleState,
15 shape_def::Shape,
16};
17
18#[derive(Clone, Copy, Debug, Default)]
19pub struct GlassBorder {
20 pub width: Dp,
21 pub color: Color,
22}
23
24impl GlassBorder {
25 pub fn new(width: Dp, color: Color) -> Self {
26 Self { width, color }
27 }
28}
29
30#[derive(Builder, Clone)]
34#[builder(build_fn(validate = "Self::validate"), pattern = "owned", setter(into))]
35pub struct FluidGlassArgs {
36 #[builder(default = "Color::new(0.5, 0.5, 0.5, 0.1)")]
41 pub tint_color: Color,
42 #[builder(default = "Shape::RoundedRectangle { corner_radius: 25.0, g2_k_value: 3.0 }")]
44 pub shape: Shape,
45 #[builder(default = "0.0")]
47 pub blur_radius: f32,
48 #[builder(default = "25.0")]
50 pub dispersion_height: f32,
51 #[builder(default = "1.2")]
53 pub chroma_multiplier: f32,
54 #[builder(default = "24.0")]
56 pub refraction_height: f32,
57 #[builder(default = "32.0")]
59 pub refraction_amount: f32,
60 #[builder(default = "0.2")]
62 pub eccentric_factor: f32,
63 #[builder(default = "0.02")]
65 pub noise_amount: f32,
66 #[builder(default = "1.0")]
68 pub noise_scale: f32,
69 #[builder(default = "0.0")]
71 pub time: f32,
72 #[builder(default, setter(strip_option))]
74 pub contrast: Option<f32>,
75 #[builder(default, setter(strip_option))]
77 pub width: Option<DimensionValue>,
78 #[builder(default, setter(strip_option))]
80 pub height: Option<DimensionValue>,
81
82 #[builder(default = "Dp(0.0)")]
83 pub padding: Dp,
84
85 #[builder(default, setter(strip_option))]
87 pub ripple_center: Option<[f32; 2]>,
88 #[builder(default, setter(strip_option))]
89 pub ripple_radius: Option<f32>,
90 #[builder(default, setter(strip_option))]
91 pub ripple_alpha: Option<f32>,
92 #[builder(default, setter(strip_option))]
93 pub ripple_strength: Option<f32>,
94
95 #[builder(default, setter(strip_option, into = false))]
96 pub on_click: Option<Arc<dyn Fn() + Send + Sync>>,
97
98 #[builder(default, setter(strip_option))]
99 pub border: Option<GlassBorder>,
100}
101
102impl FluidGlassArgsBuilder {
103 fn validate(&self) -> Result<(), String> {
104 Ok(())
105 }
106}
107
108impl Default for FluidGlassArgs {
110 fn default() -> Self {
111 FluidGlassArgsBuilder::default().build().unwrap()
112 }
113}
114
115#[derive(Clone)]
116pub struct FluidGlassCommand {
117 pub args: FluidGlassArgs,
118}
119
120impl DrawCommand for FluidGlassCommand {
121 fn barrier(&self) -> Option<tessera_ui::BarrierRequirement> {
122 Some(tessera_ui::BarrierRequirement::SampleBackground)
124 }
125}
126
127#[tessera]
128pub fn fluid_glass(
129 args: FluidGlassArgs,
130 ripple_state: Option<Arc<RippleState>>,
131 child: impl FnOnce(),
132) {
133 (child)();
134 let args_measure_clone = args.clone();
135 measure(Box::new(move |input| {
136 let glass_intrinsic_width = args_measure_clone.width.unwrap_or(DimensionValue::Wrap {
137 min: None,
138 max: None,
139 });
140 let glass_intrinsic_height = args_measure_clone.height.unwrap_or(DimensionValue::Wrap {
141 min: None,
142 max: None,
143 });
144 let glass_intrinsic_constraint =
145 Constraint::new(glass_intrinsic_width, glass_intrinsic_height);
146 let effective_glass_constraint = glass_intrinsic_constraint.merge(input.parent_constraint);
147
148 let child_constraint = Constraint::new(
149 remove_padding_from_dimension(
150 effective_glass_constraint.width,
151 args_measure_clone.padding.into(),
152 ),
153 remove_padding_from_dimension(
154 effective_glass_constraint.height,
155 args_measure_clone.padding.into(),
156 ),
157 );
158
159 let child_measurement = if !input.children_ids.is_empty() {
160 let child_measurement = measure_node(
161 input.children_ids[0],
162 &child_constraint,
163 input.tree,
164 input.metadatas,
165 input.compute_resource_manager.clone(),
166 input.gpu,
167 )?;
168 place_node(
169 input.children_ids[0],
170 PxPosition {
171 x: args.padding.into(),
172 y: args.padding.into(),
173 },
174 input.metadatas,
175 );
176 child_measurement
177 } else {
178 ComputedData {
179 width: Px(0),
180 height: Px(0),
181 }
182 };
183
184 if args.blur_radius > 0.0 {
185 let blur_command = BlurCommand {
186 radius: args.blur_radius,
187 direction: (1.0, 0.0), };
189 let blur_command2 = BlurCommand {
190 radius: args.blur_radius,
191 direction: (0.0, 1.0), };
193 if let Some(mut metadata) = input.metadatas.get_mut(&input.current_node_id) {
194 metadata.push_compute_command(blur_command);
195 metadata.push_compute_command(blur_command2);
196 }
197 }
198
199 if let Some(contrast_value) = args.contrast {
200 let mean_command =
201 MeanCommand::new(input.gpu, &mut input.compute_resource_manager.write());
202 let contrast_command =
203 ContrastCommand::new(contrast_value, mean_command.result_buffer_ref());
204 if let Some(mut metadata) = input.metadatas.get_mut(&input.current_node_id) {
205 metadata.push_compute_command(mean_command);
206 metadata.push_compute_command(contrast_command);
207 }
208 }
209
210 let drawable = FluidGlassCommand {
211 args: args_measure_clone.clone(),
212 };
213
214 if let Some(mut metadata) = input.metadatas.get_mut(&input.current_node_id) {
215 metadata.push_draw_command(drawable);
216 }
217
218 let padding_px: Px = args_measure_clone.padding.into();
219 let min_width = child_measurement.width + padding_px * 2;
220 let min_height = child_measurement.height + padding_px * 2;
221 let width = match effective_glass_constraint.width {
222 DimensionValue::Fixed(value) => value,
223 DimensionValue::Wrap { min, max } => min
224 .unwrap_or(Px(0))
225 .max(min_width)
226 .min(max.unwrap_or(Px::MAX)),
227 DimensionValue::Fill { min, max } => max
228 .expect("Seems that you are trying to fill an infinite width, which is not allowed")
229 .max(min_height)
230 .max(min.unwrap_or(Px(0))),
231 };
232 let height = match effective_glass_constraint.height {
233 DimensionValue::Fixed(value) => value,
234 DimensionValue::Wrap { min, max } => min
235 .unwrap_or(Px(0))
236 .max(min_height)
237 .min(max.unwrap_or(Px::MAX)),
238 DimensionValue::Fill { min, max } => max
239 .expect(
240 "Seems that you are trying to fill an infinite height, which is not allowed",
241 )
242 .max(min_height)
243 .max(min.unwrap_or(Px(0))),
244 };
245 Ok(ComputedData { width, height })
246 }));
247
248 if let Some(on_click) = args.on_click {
249 let ripple_state = ripple_state.clone();
250 state_handler(Box::new(move |input| {
251 let size = input.computed_data;
252 let cursor_pos_option = input.cursor_position;
253 let is_cursor_in = cursor_pos_option
254 .map(|pos| is_position_in_component(size, pos))
255 .unwrap_or(false);
256
257 if is_cursor_in {
258 input.requests.cursor_icon = CursorIcon::Pointer;
259 }
260
261 if is_cursor_in {
262 if let Some(_event) = input.cursor_events.iter().find(|e| {
263 matches!(
264 e.content,
265 CursorEventContent::Pressed(PressKeyEventType::Left)
266 )
267 }) {
268 if let Some(ripple_state) = &ripple_state {
269 if let Some(pos) = input.cursor_position {
270 let size = input.computed_data;
271 let normalized_pos = [
272 pos.x.to_f32() / size.width.to_f32(),
273 pos.y.to_f32() / size.height.to_f32(),
274 ];
275 ripple_state.start_animation(normalized_pos);
276 }
277 }
278 on_click();
279 }
280 }
281 }));
282 }
283}