tessera_ui_basic_components/
surface.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, winit::window::CursorIcon,
7};
8use tessera_ui_macros::tessera;
9
10use crate::{
11 padding_utils::remove_padding_from_dimension,
12 pipelines::{RippleProps, ShadowProps, ShapeCommand},
13 pos_misc::is_position_in_component,
14 ripple_state::RippleState,
15 shape_def::Shape,
16};
17
18#[derive(Builder, Clone)]
20#[builder(pattern = "owned")]
21pub struct SurfaceArgs {
22 #[builder(default = "Color::new(0.4745, 0.5255, 0.7961, 1.0)")]
24 pub color: Color,
25 #[builder(default)]
27 pub hover_color: Option<Color>,
28 #[builder(default)]
30 pub shape: Shape,
31 #[builder(default)]
33 pub shadow: Option<ShadowProps>,
34 #[builder(default = "Dp(0.0)")]
36 pub padding: Dp,
37 #[builder(default, setter(strip_option))]
39 pub width: Option<DimensionValue>,
40 #[builder(default, setter(strip_option))]
42 pub height: Option<DimensionValue>,
43 #[builder(default = "0.0")]
45 pub border_width: f32,
46 #[builder(default)]
48 pub border_color: Option<Color>,
49 #[builder(default)]
51 pub on_click: Option<Arc<dyn Fn() + Send + Sync>>,
52 #[builder(default = "Color::from_rgb(1.0, 1.0, 1.0)")]
54 pub ripple_color: Color,
55}
56
57impl Default for SurfaceArgs {
59 fn default() -> Self {
60 SurfaceArgsBuilder::default().build().unwrap()
61 }
62}
63
64#[tessera]
67pub fn surface(args: SurfaceArgs, ripple_state: Option<Arc<RippleState>>, child: impl FnOnce()) {
68 (child)();
69 let ripple_state_for_measure = ripple_state.clone();
70 let args_measure_clone = args.clone();
71
72 measure(Box::new(move |input| {
73 let surface_intrinsic_width = args_measure_clone.width.unwrap_or(DimensionValue::Wrap {
75 min: None,
76 max: None,
77 });
78 let surface_intrinsic_height = args_measure_clone.height.unwrap_or(DimensionValue::Wrap {
79 min: None,
80 max: None,
81 });
82 let surface_intrinsic_constraint =
83 Constraint::new(surface_intrinsic_width, surface_intrinsic_height);
84 let effective_surface_constraint =
86 surface_intrinsic_constraint.merge(input.parent_constraint);
87 let child_constraint = Constraint::new(
89 remove_padding_from_dimension(
90 effective_surface_constraint.width,
91 args_measure_clone.padding.into(),
92 ),
93 remove_padding_from_dimension(
94 effective_surface_constraint.height,
95 args_measure_clone.padding.into(),
96 ),
97 );
98 let child_measurement = if !input.children_ids.is_empty() {
100 let child_measurement = measure_node(
101 input.children_ids[0],
102 &child_constraint,
103 input.tree,
104 input.metadatas,
105 input.compute_resource_manager.clone(),
106 input.gpu,
107 )?;
108 place_node(
110 input.children_ids[0],
111 PxPosition {
112 x: args.padding.into(),
113 y: args.padding.into(),
114 },
115 input.metadatas,
116 );
117 child_measurement
118 } else {
119 ComputedData {
120 width: Px(0),
121 height: Px(0),
122 }
123 };
124 let is_hovered = ripple_state_for_measure
126 .as_ref()
127 .map(|state| state.is_hovered())
128 .unwrap_or(false);
129
130 let effective_color = if is_hovered && args_measure_clone.hover_color.is_some() {
131 args_measure_clone.hover_color.unwrap()
132 } else {
133 args_measure_clone.color
134 };
135
136 let drawable = if args_measure_clone.on_click.is_some() {
137 let ripple_props = if let Some(ref state) = ripple_state_for_measure {
139 if let Some((progress, click_pos)) = state.get_animation_progress() {
140 let radius = progress; let alpha = (1.0 - progress) * 0.3; RippleProps {
144 center: click_pos,
145 radius,
146 alpha,
147 color: args_measure_clone.ripple_color,
148 }
149 } else {
150 RippleProps::default()
151 }
152 } else {
153 RippleProps::default()
154 };
155
156 match args_measure_clone.shape {
157 Shape::RoundedRectangle {
158 corner_radius,
159 g2_k_value,
160 } => {
161 if args_measure_clone.border_width > 0.0 {
162 ShapeCommand::RippleOutlinedRect {
163 color: args_measure_clone.border_color.unwrap_or(effective_color),
164 corner_radius,
165 g2_k_value,
166 shadow: args_measure_clone.shadow,
167 border_width: args_measure_clone.border_width,
168 ripple: ripple_props,
169 }
170 } else {
171 ShapeCommand::RippleRect {
172 color: effective_color,
173 corner_radius,
174 g2_k_value,
175 shadow: args_measure_clone.shadow,
176 ripple: ripple_props,
177 }
178 }
179 }
180 Shape::Ellipse => {
181 if args_measure_clone.border_width > 0.0 {
182 ShapeCommand::RippleOutlinedRect {
183 color: args_measure_clone.border_color.unwrap_or(effective_color),
184 corner_radius: -1.0, g2_k_value: 0.0, shadow: args_measure_clone.shadow,
187 border_width: args_measure_clone.border_width,
188 ripple: ripple_props,
189 }
190 } else {
191 ShapeCommand::RippleRect {
192 color: effective_color,
193 corner_radius: -1.0, g2_k_value: 0.0, shadow: args_measure_clone.shadow,
196 ripple: ripple_props,
197 }
198 }
199 }
200 }
201 } else {
202 match args_measure_clone.shape {
204 Shape::RoundedRectangle {
205 corner_radius,
206 g2_k_value,
207 } => {
208 if args_measure_clone.border_width > 0.0 {
209 ShapeCommand::OutlinedRect {
210 color: args_measure_clone.border_color.unwrap_or(effective_color),
211 corner_radius,
212 g2_k_value,
213 shadow: args_measure_clone.shadow,
214 border_width: args_measure_clone.border_width,
215 }
216 } else {
217 ShapeCommand::Rect {
218 color: effective_color,
219 corner_radius,
220 g2_k_value,
221 shadow: args_measure_clone.shadow,
222 }
223 }
224 }
225 Shape::Ellipse => {
226 if args_measure_clone.border_width > 0.0 {
227 ShapeCommand::OutlinedEllipse {
228 color: args_measure_clone.border_color.unwrap_or(effective_color),
229 shadow: args_measure_clone.shadow,
230 border_width: args_measure_clone.border_width,
231 }
232 } else {
233 ShapeCommand::Ellipse {
234 color: effective_color,
235 shadow: args_measure_clone.shadow,
236 }
237 }
238 }
239 }
240 };
241
242 if let Some(mut metadata) = input.metadatas.get_mut(&input.current_node_id) {
243 metadata.push_draw_command(drawable);
244 }
245
246 let padding_px: Px = args_measure_clone.padding.into();
248 let min_width = child_measurement.width + padding_px * 2;
249 let min_height = child_measurement.height + padding_px * 2;
250 let width = match effective_surface_constraint.width {
251 DimensionValue::Fixed(value) => value,
252 DimensionValue::Wrap { min, max } => min
253 .unwrap_or(Px(0))
254 .max(min_width)
255 .min(max.unwrap_or(Px::MAX)),
256 DimensionValue::Fill { min, max } => max
257 .expect("Seems that you are trying to fill an infinite width, which is not allowed")
258 .max(min_height)
259 .max(min.unwrap_or(Px(0))),
260 };
261 let height = match effective_surface_constraint.height {
262 DimensionValue::Fixed(value) => value,
263 DimensionValue::Wrap { min, max } => min
264 .unwrap_or(Px(0))
265 .max(min_height)
266 .min(max.unwrap_or(Px::MAX)),
267 DimensionValue::Fill { min, max } => max
268 .expect(
269 "Seems that you are trying to fill an infinite height, which is not allowed",
270 )
271 .max(min_height)
272 .max(min.unwrap_or(Px(0))),
273 };
274 Ok(ComputedData { width, height })
275 }));
276
277 if args.on_click.is_some() {
279 let args_for_handler = args.clone();
280 let state_for_handler = ripple_state;
281 state_handler(Box::new(move |input| {
282 let size = input.computed_data;
283 let cursor_pos_option = input.cursor_position;
284 let is_cursor_in_surface = cursor_pos_option
285 .map(|pos| is_position_in_component(size, pos))
286 .unwrap_or(false);
287
288 if let Some(ref state) = state_for_handler {
290 state.set_hovered(is_cursor_in_surface);
291 }
292
293 if is_cursor_in_surface && args_for_handler.on_click.is_some() {
295 input.requests.cursor_icon = CursorIcon::Pointer;
296 }
297
298 if is_cursor_in_surface {
300 let press_events: Vec<_> = input
302 .cursor_events
303 .iter()
304 .filter(|event| {
305 matches!(
306 event.content,
307 CursorEventContent::Pressed(PressKeyEventType::Left)
308 )
309 })
310 .collect();
311
312 let release_events: Vec<_> = input
314 .cursor_events
315 .iter()
316 .filter(|event| {
317 matches!(
318 event.content,
319 CursorEventContent::Released(PressKeyEventType::Left)
320 )
321 })
322 .collect();
323
324 if !press_events.is_empty()
325 && let (Some(cursor_pos), Some(state)) =
326 (cursor_pos_option, state_for_handler.as_ref())
327 {
328 let normalized_x = (cursor_pos.x.to_f32() / size.width.to_f32()) - 0.5;
330 let normalized_y = (cursor_pos.y.to_f32() / size.height.to_f32()) - 0.5;
331
332 state.start_animation([normalized_x, normalized_y]);
334 }
335
336 if !release_events.is_empty() {
337 if let Some(ref on_click) = args_for_handler.on_click {
339 on_click();
340 }
341 }
342
343 if !press_events.is_empty() || !release_events.is_empty() {
345 input.cursor_events.clear();
346 }
347 }
348 }));
349 }
350}