1use std::sync::Arc;
16
17use derive_builder::Builder;
18use tessera_ui::{
19 Color, ComputedData, Constraint, CursorEventContent, DimensionValue, Dp, PressKeyEventType, Px,
20 PxPosition, PxSize, tessera, winit::window::CursorIcon,
21};
22
23use crate::{
24 padding_utils::remove_padding_from_dimension,
25 pipelines::{RippleProps, ShadowProps, ShapeCommand},
26 pos_misc::is_position_in_component,
27 ripple_state::RippleState,
28 shape_def::Shape,
29};
30
31#[derive(Clone)]
33pub enum SurfaceStyle {
34 Filled { color: Color },
36 Outlined { color: Color, width: Dp },
38 FilledOutlined {
40 fill_color: Color,
41 border_color: Color,
42 border_width: Dp,
43 },
44}
45
46impl Default for SurfaceStyle {
47 fn default() -> Self {
48 SurfaceStyle::Filled {
49 color: Color::new(0.4745, 0.5255, 0.7961, 1.0),
50 }
51 }
52}
53
54impl From<Color> for SurfaceStyle {
55 fn from(color: Color) -> Self {
56 SurfaceStyle::Filled { color }
57 }
58}
59
60#[derive(Builder, Clone)]
61#[builder(pattern = "owned")]
62pub struct SurfaceArgs {
63 #[builder(default)]
65 pub style: SurfaceStyle,
66
67 #[builder(default)]
70 pub hover_style: Option<SurfaceStyle>,
71
72 #[builder(default)]
74 pub shape: Shape,
75
76 #[builder(default, setter(strip_option))]
78 pub shadow: Option<ShadowProps>,
79
80 #[builder(default = "Dp(0.0)")]
83 pub padding: Dp,
84
85 #[builder(default = "DimensionValue::WRAP", setter(into))]
87 pub width: DimensionValue,
88
89 #[builder(default = "DimensionValue::WRAP", setter(into))]
91 pub height: DimensionValue,
92
93 #[builder(default, setter(strip_option))]
98 pub on_click: Option<Arc<dyn Fn() + Send + Sync>>,
99
100 #[builder(default = "Color::from_rgb(1.0, 1.0, 1.0)")]
102 pub ripple_color: Color,
103
104 #[builder(default = "false")]
107 pub block_input: bool,
108}
109
110impl Default for SurfaceArgs {
111 fn default() -> Self {
112 SurfaceArgsBuilder::default().build().unwrap()
113 }
114}
115
116fn build_ripple_props(args: &SurfaceArgs, ripple_state: Option<&Arc<RippleState>>) -> RippleProps {
117 if let Some(state) = ripple_state
118 && let Some((progress, click_pos)) = state.get_animation_progress()
119 {
120 let radius = progress;
121 let alpha = (1.0 - progress) * 0.3;
122 return RippleProps {
123 center: click_pos,
124 radius,
125 alpha,
126 color: args.ripple_color,
127 };
128 }
129 RippleProps::default()
130}
131
132fn build_rounded_rectangle_command(
133 args: &SurfaceArgs,
134 style: &SurfaceStyle,
135 ripple_props: RippleProps,
136 corner_radii: [f32; 4],
137 g2_k_value: f32,
138 interactive: bool,
139) -> ShapeCommand {
140 match style {
141 SurfaceStyle::Filled { color } => {
142 if interactive {
143 ShapeCommand::RippleRect {
144 color: *color,
145 corner_radii,
146 g2_k_value,
147 shadow: args.shadow,
148 ripple: ripple_props,
149 }
150 } else {
151 ShapeCommand::Rect {
152 color: *color,
153 corner_radii,
154 g2_k_value,
155 shadow: args.shadow,
156 }
157 }
158 }
159 SurfaceStyle::Outlined { color, width } => {
160 if interactive {
161 ShapeCommand::RippleOutlinedRect {
162 color: *color,
163 corner_radii,
164 g2_k_value,
165 shadow: args.shadow,
166 border_width: width.to_pixels_f32(),
167 ripple: ripple_props,
168 }
169 } else {
170 ShapeCommand::OutlinedRect {
171 color: *color,
172 corner_radii,
173 g2_k_value,
174 shadow: args.shadow,
175 border_width: width.to_pixels_f32(),
176 }
177 }
178 }
179 SurfaceStyle::FilledOutlined {
180 fill_color,
181 border_color,
182 border_width,
183 } => {
184 if interactive {
185 ShapeCommand::RippleFilledOutlinedRect {
186 color: *fill_color,
187 border_color: *border_color,
188 corner_radii,
189 g2_k_value,
190 shadow: args.shadow,
191 border_width: border_width.to_pixels_f32(),
192 ripple: ripple_props,
193 }
194 } else {
195 ShapeCommand::FilledOutlinedRect {
196 color: *fill_color,
197 border_color: *border_color,
198 corner_radii,
199 g2_k_value,
200 shadow: args.shadow,
201 border_width: border_width.to_pixels_f32(),
202 }
203 }
204 }
205 }
206}
207
208fn build_ellipse_command(
209 args: &SurfaceArgs,
210 style: &SurfaceStyle,
211 ripple_props: RippleProps,
212 interactive: bool,
213) -> ShapeCommand {
214 let corner_marker = [-1.0, -1.0, -1.0, -1.0];
215 match style {
216 SurfaceStyle::Filled { color } => {
217 if interactive {
218 ShapeCommand::RippleRect {
219 color: *color,
220 corner_radii: corner_marker,
221 g2_k_value: 0.0,
222 shadow: args.shadow,
223 ripple: ripple_props,
224 }
225 } else {
226 ShapeCommand::Ellipse {
227 color: *color,
228 shadow: args.shadow,
229 }
230 }
231 }
232 SurfaceStyle::Outlined { color, width } => {
233 if interactive {
234 ShapeCommand::RippleOutlinedRect {
235 color: *color,
236 corner_radii: corner_marker,
237 g2_k_value: 0.0,
238 shadow: args.shadow,
239 border_width: width.to_pixels_f32(),
240 ripple: ripple_props,
241 }
242 } else {
243 ShapeCommand::OutlinedEllipse {
244 color: *color,
245 shadow: args.shadow,
246 border_width: width.to_pixels_f32(),
247 }
248 }
249 }
250 SurfaceStyle::FilledOutlined {
251 fill_color,
252 border_color,
253 border_width,
254 } => {
255 ShapeCommand::FilledOutlinedEllipse {
257 color: *fill_color,
258 border_color: *border_color,
259 shadow: args.shadow,
260 border_width: border_width.to_pixels_f32(),
261 }
262 }
263 }
264}
265
266fn build_shape_command(
267 args: &SurfaceArgs,
268 style: &SurfaceStyle,
269 ripple_props: RippleProps,
270 size: PxSize,
271) -> ShapeCommand {
272 let interactive = args.on_click.is_some();
273
274 match args.shape {
275 Shape::RoundedRectangle {
276 top_left,
277 top_right,
278 bottom_right,
279 bottom_left,
280 g2_k_value,
281 } => {
282 let corner_radii = [
283 top_left.to_pixels_f32(),
284 top_right.to_pixels_f32(),
285 bottom_right.to_pixels_f32(),
286 bottom_left.to_pixels_f32(),
287 ];
288 build_rounded_rectangle_command(
289 args,
290 style,
291 ripple_props,
292 corner_radii,
293 g2_k_value,
294 interactive,
295 )
296 }
297 Shape::Ellipse => build_ellipse_command(args, style, ripple_props, interactive),
298 Shape::HorizontalCapsule => {
299 let radius = size.height.to_f32() / 2.0;
300 let corner_radii = [radius, radius, radius, radius];
301 build_rounded_rectangle_command(
302 args,
303 style,
304 ripple_props,
305 corner_radii,
306 2.0, interactive,
308 )
309 }
310 Shape::VerticalCapsule => {
311 let radius = size.width.to_f32() / 2.0;
312 let corner_radii = [radius, radius, radius, radius];
313 build_rounded_rectangle_command(
314 args,
315 style,
316 ripple_props,
317 corner_radii,
318 2.0, interactive,
320 )
321 }
322 }
323}
324
325fn make_surface_drawable(
326 args: &SurfaceArgs,
327 style: &SurfaceStyle,
328 ripple_state: Option<&Arc<RippleState>>,
329 size: PxSize,
330) -> ShapeCommand {
331 let ripple_props = build_ripple_props(args, ripple_state);
332 build_shape_command(args, style, ripple_props, size)
333}
334
335fn compute_surface_size(
336 effective_surface_constraint: Constraint,
337 child_measurement: ComputedData,
338 padding_px: Px,
339) -> (Px, Px) {
340 let min_width = child_measurement.width + padding_px * 2;
341 let min_height = child_measurement.height + padding_px * 2;
342
343 fn clamp_wrap(min: Option<Px>, max: Option<Px>, min_measure: Px) -> Px {
344 min.unwrap_or(Px(0))
345 .max(min_measure)
346 .min(max.unwrap_or(Px::MAX))
347 }
348
349 fn fill_value(min: Option<Px>, max: Option<Px>, min_measure: Px) -> Px {
350 max.expect("Seems that you are trying to fill an infinite dimension, which is not allowed")
351 .max(min_measure)
352 .max(min.unwrap_or(Px(0)))
353 }
354
355 let width = match effective_surface_constraint.width {
356 DimensionValue::Fixed(value) => value,
357 DimensionValue::Wrap { min, max } => clamp_wrap(min, max, min_width),
358 DimensionValue::Fill { min, max } => fill_value(min, max, min_width),
359 };
360
361 let height = match effective_surface_constraint.height {
362 DimensionValue::Fixed(value) => value,
363 DimensionValue::Wrap { min, max } => clamp_wrap(min, max, min_height),
364 DimensionValue::Fill { min, max } => fill_value(min, max, min_height),
365 };
366
367 (width, height)
368}
369
370#[tessera]
421pub fn surface(args: SurfaceArgs, ripple_state: Option<Arc<RippleState>>, child: impl FnOnce()) {
422 (child)();
423 let ripple_state_for_measure = ripple_state.clone();
424 let args_measure_clone = args.clone();
425 let args_for_handler = args.clone();
426
427 measure(Box::new(move |input| {
428 let surface_intrinsic_width = args_measure_clone.width;
429 let surface_intrinsic_height = args_measure_clone.height;
430 let surface_intrinsic_constraint =
431 Constraint::new(surface_intrinsic_width, surface_intrinsic_height);
432 let effective_surface_constraint =
433 surface_intrinsic_constraint.merge(input.parent_constraint);
434 let padding_px: Px = args_measure_clone.padding.into();
435 let child_constraint = Constraint::new(
436 remove_padding_from_dimension(effective_surface_constraint.width, padding_px),
437 remove_padding_from_dimension(effective_surface_constraint.height, padding_px),
438 );
439
440 let child_measurement = if !input.children_ids.is_empty() {
441 let child_measurements = input.measure_children(
442 input
443 .children_ids
444 .iter()
445 .copied()
446 .map(|node_id| (node_id, child_constraint))
447 .collect(),
448 )?;
449 input.place_child(
450 input.children_ids[0],
451 PxPosition {
452 x: args.padding.into(),
453 y: args.padding.into(),
454 },
455 );
456 let mut max_width = Px::ZERO;
457 let mut max_height = Px::ZERO;
458 for measurement in child_measurements.values() {
459 max_width = max_width.max(measurement.width);
460 max_height = max_height.max(measurement.height);
461 }
462 ComputedData {
463 width: max_width,
464 height: max_height,
465 }
466 } else {
467 ComputedData {
468 width: Px(0),
469 height: Px(0),
470 }
471 };
472
473 let is_hovered = ripple_state_for_measure
474 .as_ref()
475 .map(|state| state.is_hovered())
476 .unwrap_or(false);
477
478 let effective_style = if is_hovered && args_measure_clone.hover_style.is_some() {
479 args_measure_clone.hover_style.as_ref().unwrap()
480 } else {
481 &args_measure_clone.style
482 };
483
484 let padding_px: Px = args_measure_clone.padding.into();
485 let (width, height) =
486 compute_surface_size(effective_surface_constraint, child_measurement, padding_px);
487
488 let drawable = make_surface_drawable(
489 &args_measure_clone,
490 effective_style,
491 ripple_state_for_measure.as_ref(),
492 PxSize::new(width, height),
493 );
494
495 input.metadata_mut().push_draw_command(drawable);
496
497 Ok(ComputedData { width, height })
498 }));
499
500 if args.on_click.is_some() {
501 let args_for_handler = args.clone();
502 let state_for_handler = ripple_state;
503 input_handler(Box::new(move |mut input| {
504 let size = input.computed_data;
505 let cursor_pos_option = input.cursor_position_rel;
506 let is_cursor_in_surface = cursor_pos_option
507 .map(|pos| is_position_in_component(size, pos))
508 .unwrap_or(false);
509
510 if let Some(ref state) = state_for_handler {
511 state.set_hovered(is_cursor_in_surface);
512 }
513
514 if is_cursor_in_surface && args_for_handler.on_click.is_some() {
515 input.requests.cursor_icon = CursorIcon::Pointer;
516 }
517
518 if is_cursor_in_surface {
519 let press_events: Vec<_> = input
520 .cursor_events
521 .iter()
522 .filter(|event| {
523 matches!(
524 event.content,
525 CursorEventContent::Pressed(PressKeyEventType::Left)
526 )
527 })
528 .collect();
529
530 let release_events: Vec<_> = input
531 .cursor_events
532 .iter()
533 .filter(|event| {
534 matches!(
535 event.content,
536 CursorEventContent::Released(PressKeyEventType::Left)
537 )
538 })
539 .collect();
540
541 if !press_events.is_empty()
542 && let (Some(cursor_pos), Some(state)) =
543 (cursor_pos_option, state_for_handler.as_ref())
544 {
545 let normalized_x = (cursor_pos.x.to_f32() / size.width.to_f32()) - 0.5;
546 let normalized_y = (cursor_pos.y.to_f32() / size.height.to_f32()) - 0.5;
547
548 state.start_animation([normalized_x, normalized_y]);
549 }
550
551 if !release_events.is_empty()
552 && let Some(ref on_click) = args_for_handler.on_click
553 {
554 on_click();
555 }
556
557 if args_for_handler.block_input {
558 input.block_all();
559 }
560 }
561 }));
562 } else {
563 input_handler(Box::new(move |mut input| {
564 let size = input.computed_data;
565 let cursor_pos_option = input.cursor_position_rel;
566 let is_cursor_in_surface = cursor_pos_option
567 .map(|pos| is_position_in_component(size, pos))
568 .unwrap_or(false);
569 if args_for_handler.block_input && is_cursor_in_surface {
570 input.block_all();
571 }
572 }));
573 }
574}