1use std::sync::Arc;
7
8use derive_builder::Builder;
9use tessera_ui::{
10 Color, ComputedData, Constraint, CursorEventContent, DimensionValue, Dp, GestureState,
11 InputHandlerInput, PressKeyEventType, Px, PxPosition, PxSize,
12 accesskit::{Action, Role},
13 tessera,
14 winit::window::CursorIcon,
15};
16
17use crate::{
18 padding_utils::remove_padding_from_dimension,
19 pipelines::{RippleProps, ShadowProps, ShapeCommand, SimpleRectCommand},
20 pos_misc::is_position_in_component,
21 ripple_state::RippleState,
22 shape_def::Shape,
23};
24
25#[derive(Clone)]
27pub enum SurfaceStyle {
28 Filled { color: Color },
30 Outlined { color: Color, width: Dp },
32 FilledOutlined {
34 fill_color: Color,
35 border_color: Color,
36 border_width: Dp,
37 },
38}
39
40impl Default for SurfaceStyle {
41 fn default() -> Self {
42 SurfaceStyle::Filled {
43 color: Color::new(0.4745, 0.5255, 0.7961, 1.0),
44 }
45 }
46}
47
48impl From<Color> for SurfaceStyle {
49 fn from(color: Color) -> Self {
50 SurfaceStyle::Filled { color }
51 }
52}
53
54#[derive(Builder, Clone)]
55#[builder(pattern = "owned")]
56pub struct SurfaceArgs {
57 #[builder(default)]
59 pub style: SurfaceStyle,
60
61 #[builder(default)]
64 pub hover_style: Option<SurfaceStyle>,
65
66 #[builder(default)]
68 pub shape: Shape,
69
70 #[builder(default, setter(strip_option))]
72 pub shadow: Option<ShadowProps>,
73
74 #[builder(default = "Dp(0.0)")]
77 pub padding: Dp,
78
79 #[builder(default = "DimensionValue::WRAP", setter(into))]
81 pub width: DimensionValue,
82
83 #[builder(default = "DimensionValue::WRAP", setter(into))]
85 pub height: DimensionValue,
86
87 #[builder(default, setter(strip_option))]
92 pub on_click: Option<Arc<dyn Fn() + Send + Sync>>,
93
94 #[builder(default = "Color::from_rgb(1.0, 1.0, 1.0)")]
96 pub ripple_color: Color,
97
98 #[builder(default = "false")]
101 pub block_input: bool,
102 #[builder(default, setter(strip_option))]
104 pub accessibility_role: Option<Role>,
105 #[builder(default, setter(strip_option, into))]
107 pub accessibility_label: Option<String>,
108 #[builder(default, setter(strip_option, into))]
110 pub accessibility_description: Option<String>,
111 #[builder(default)]
113 pub accessibility_focusable: bool,
114}
115
116impl Default for SurfaceArgs {
117 fn default() -> Self {
118 SurfaceArgsBuilder::default().build().unwrap()
119 }
120}
121
122fn build_ripple_props(args: &SurfaceArgs, ripple_state: Option<&RippleState>) -> RippleProps {
123 if let Some(state) = ripple_state
124 && let Some((progress, click_pos)) = state.get_animation_progress()
125 {
126 let radius = progress;
127 let alpha = (1.0 - progress) * 0.3;
128 return RippleProps {
129 center: click_pos,
130 radius,
131 alpha,
132 color: args.ripple_color,
133 };
134 }
135 RippleProps::default()
136}
137
138fn build_rounded_rectangle_command(
139 args: &SurfaceArgs,
140 style: &SurfaceStyle,
141 ripple_props: RippleProps,
142 corner_radii: [f32; 4],
143 g2_k_value: f32,
144 interactive: bool,
145) -> ShapeCommand {
146 match style {
147 SurfaceStyle::Filled { color } => {
148 if interactive {
149 ShapeCommand::RippleRect {
150 color: *color,
151 corner_radii,
152 g2_k_value,
153 shadow: args.shadow,
154 ripple: ripple_props,
155 }
156 } else {
157 ShapeCommand::Rect {
158 color: *color,
159 corner_radii,
160 g2_k_value,
161 shadow: args.shadow,
162 }
163 }
164 }
165 SurfaceStyle::Outlined { color, width } => {
166 if interactive {
167 ShapeCommand::RippleOutlinedRect {
168 color: *color,
169 corner_radii,
170 g2_k_value,
171 shadow: args.shadow,
172 border_width: width.to_pixels_f32(),
173 ripple: ripple_props,
174 }
175 } else {
176 ShapeCommand::OutlinedRect {
177 color: *color,
178 corner_radii,
179 g2_k_value,
180 shadow: args.shadow,
181 border_width: width.to_pixels_f32(),
182 }
183 }
184 }
185 SurfaceStyle::FilledOutlined {
186 fill_color,
187 border_color,
188 border_width,
189 } => {
190 if interactive {
191 ShapeCommand::RippleFilledOutlinedRect {
192 color: *fill_color,
193 border_color: *border_color,
194 corner_radii,
195 g2_k_value,
196 shadow: args.shadow,
197 border_width: border_width.to_pixels_f32(),
198 ripple: ripple_props,
199 }
200 } else {
201 ShapeCommand::FilledOutlinedRect {
202 color: *fill_color,
203 border_color: *border_color,
204 corner_radii,
205 g2_k_value,
206 shadow: args.shadow,
207 border_width: border_width.to_pixels_f32(),
208 }
209 }
210 }
211 }
212}
213
214fn build_ellipse_command(
215 args: &SurfaceArgs,
216 style: &SurfaceStyle,
217 ripple_props: RippleProps,
218 interactive: bool,
219) -> ShapeCommand {
220 let corner_marker = [-1.0, -1.0, -1.0, -1.0];
221 match style {
222 SurfaceStyle::Filled { color } => {
223 if interactive {
224 ShapeCommand::RippleRect {
225 color: *color,
226 corner_radii: corner_marker,
227 g2_k_value: 0.0,
228 shadow: args.shadow,
229 ripple: ripple_props,
230 }
231 } else {
232 ShapeCommand::Ellipse {
233 color: *color,
234 shadow: args.shadow,
235 }
236 }
237 }
238 SurfaceStyle::Outlined { color, width } => {
239 if interactive {
240 ShapeCommand::RippleOutlinedRect {
241 color: *color,
242 corner_radii: corner_marker,
243 g2_k_value: 0.0,
244 shadow: args.shadow,
245 border_width: width.to_pixels_f32(),
246 ripple: ripple_props,
247 }
248 } else {
249 ShapeCommand::OutlinedEllipse {
250 color: *color,
251 shadow: args.shadow,
252 border_width: width.to_pixels_f32(),
253 }
254 }
255 }
256 SurfaceStyle::FilledOutlined {
257 fill_color,
258 border_color,
259 border_width,
260 } => {
261 ShapeCommand::FilledOutlinedEllipse {
263 color: *fill_color,
264 border_color: *border_color,
265 shadow: args.shadow,
266 border_width: border_width.to_pixels_f32(),
267 }
268 }
269 }
270}
271
272fn build_shape_command(
273 args: &SurfaceArgs,
274 style: &SurfaceStyle,
275 ripple_props: RippleProps,
276 size: PxSize,
277) -> ShapeCommand {
278 let interactive = args.on_click.is_some();
279
280 match args.shape {
281 Shape::RoundedRectangle {
282 top_left,
283 top_right,
284 bottom_right,
285 bottom_left,
286 g2_k_value,
287 } => {
288 let corner_radii = [
289 top_left.to_pixels_f32(),
290 top_right.to_pixels_f32(),
291 bottom_right.to_pixels_f32(),
292 bottom_left.to_pixels_f32(),
293 ];
294 build_rounded_rectangle_command(
295 args,
296 style,
297 ripple_props,
298 corner_radii,
299 g2_k_value,
300 interactive,
301 )
302 }
303 Shape::Ellipse => build_ellipse_command(args, style, ripple_props, interactive),
304 Shape::HorizontalCapsule => {
305 let radius = size.height.to_f32() / 2.0;
306 let corner_radii = [radius, radius, radius, radius];
307 build_rounded_rectangle_command(
308 args,
309 style,
310 ripple_props,
311 corner_radii,
312 2.0, interactive,
314 )
315 }
316 Shape::VerticalCapsule => {
317 let radius = size.width.to_f32() / 2.0;
318 let corner_radii = [radius, radius, radius, radius];
319 build_rounded_rectangle_command(
320 args,
321 style,
322 ripple_props,
323 corner_radii,
324 2.0, interactive,
326 )
327 }
328 }
329}
330
331fn make_surface_drawable(
332 args: &SurfaceArgs,
333 style: &SurfaceStyle,
334 ripple_state: Option<&RippleState>,
335 size: PxSize,
336) -> ShapeCommand {
337 let ripple_props = build_ripple_props(args, ripple_state);
338 build_shape_command(args, style, ripple_props, size)
339}
340
341fn try_build_simple_rect_command(
342 args: &SurfaceArgs,
343 style: &SurfaceStyle,
344 ripple_state: Option<&RippleState>,
345) -> Option<SimpleRectCommand> {
346 if args.shadow.is_some() {
347 return None;
348 }
349 if args.on_click.is_some() {
350 return None;
351 }
352 if let Some(state) = ripple_state
353 && state.get_animation_progress().is_some()
354 {
355 return None;
356 }
357
358 let color = match style {
359 SurfaceStyle::Filled { color } => *color,
360 _ => return None,
361 };
362
363 match args.shape {
364 Shape::RoundedRectangle {
365 top_left,
366 top_right,
367 bottom_right,
368 bottom_left,
369 ..
370 } => {
371 let radii = [
372 top_left.to_pixels_f32(),
373 top_right.to_pixels_f32(),
374 bottom_right.to_pixels_f32(),
375 bottom_left.to_pixels_f32(),
376 ];
377 let zero_eps = 0.0001;
378 if radii.iter().all(|r| r.abs() <= zero_eps) {
379 Some(SimpleRectCommand { color })
380 } else {
381 None
382 }
383 }
384 _ => None,
385 }
386}
387
388fn compute_surface_size(
389 effective_surface_constraint: Constraint,
390 child_measurement: ComputedData,
391 padding_px: Px,
392) -> (Px, Px) {
393 let min_width = child_measurement.width + padding_px * 2;
394 let min_height = child_measurement.height + padding_px * 2;
395
396 fn clamp_wrap(min: Option<Px>, max: Option<Px>, min_measure: Px) -> Px {
397 min.unwrap_or(Px(0))
398 .max(min_measure)
399 .min(max.unwrap_or(Px::MAX))
400 }
401
402 fn fill_value(min: Option<Px>, max: Option<Px>, min_measure: Px) -> Px {
403 max.expect("Seems that you are trying to fill an infinite dimension, which is not allowed")
404 .max(min_measure)
405 .max(min.unwrap_or(Px(0)))
406 }
407
408 let width = match effective_surface_constraint.width {
409 DimensionValue::Fixed(value) => value,
410 DimensionValue::Wrap { min, max } => clamp_wrap(min, max, min_width),
411 DimensionValue::Fill { min, max } => fill_value(min, max, min_width),
412 };
413
414 let height = match effective_surface_constraint.height {
415 DimensionValue::Fixed(value) => value,
416 DimensionValue::Wrap { min, max } => clamp_wrap(min, max, min_height),
417 DimensionValue::Fill { min, max } => fill_value(min, max, min_height),
418 };
419
420 (width, height)
421}
422
423#[tessera]
463pub fn surface(args: SurfaceArgs, ripple_state: Option<RippleState>, child: impl FnOnce()) {
464 (child)();
465 let ripple_state_for_measure = ripple_state.clone();
466 let args_measure_clone = args.clone();
467 let args_for_handler = args.clone();
468
469 measure(Box::new(move |input| {
470 let surface_intrinsic_width = args_measure_clone.width;
471 let surface_intrinsic_height = args_measure_clone.height;
472 let surface_intrinsic_constraint =
473 Constraint::new(surface_intrinsic_width, surface_intrinsic_height);
474 let effective_surface_constraint =
475 surface_intrinsic_constraint.merge(input.parent_constraint);
476 let padding_px: Px = args_measure_clone.padding.into();
477 let child_constraint = Constraint::new(
478 remove_padding_from_dimension(effective_surface_constraint.width, padding_px),
479 remove_padding_from_dimension(effective_surface_constraint.height, padding_px),
480 );
481
482 let child_measurement = if !input.children_ids.is_empty() {
483 let child_measurements = input.measure_children(
484 input
485 .children_ids
486 .iter()
487 .copied()
488 .map(|node_id| (node_id, child_constraint))
489 .collect(),
490 )?;
491 input.place_child(
492 input.children_ids[0],
493 PxPosition {
494 x: args.padding.into(),
495 y: args.padding.into(),
496 },
497 );
498 let mut max_width = Px::ZERO;
499 let mut max_height = Px::ZERO;
500 for measurement in child_measurements.values() {
501 max_width = max_width.max(measurement.width);
502 max_height = max_height.max(measurement.height);
503 }
504 ComputedData {
505 width: max_width,
506 height: max_height,
507 }
508 } else {
509 ComputedData {
510 width: Px(0),
511 height: Px(0),
512 }
513 };
514
515 let is_hovered = ripple_state_for_measure
516 .as_ref()
517 .map(|state| state.is_hovered())
518 .unwrap_or(false);
519
520 let effective_style = if is_hovered && args_measure_clone.hover_style.is_some() {
521 args_measure_clone.hover_style.as_ref().unwrap()
522 } else {
523 &args_measure_clone.style
524 };
525
526 let padding_px: Px = args_measure_clone.padding.into();
527 let (width, height) =
528 compute_surface_size(effective_surface_constraint, child_measurement, padding_px);
529
530 if let Some(simple) = try_build_simple_rect_command(
531 &args_measure_clone,
532 effective_style,
533 ripple_state_for_measure.as_ref(),
534 ) {
535 input.metadata_mut().push_draw_command(simple);
536 } else {
537 let drawable = make_surface_drawable(
538 &args_measure_clone,
539 effective_style,
540 ripple_state_for_measure.as_ref(),
541 PxSize::new(width, height),
542 );
543
544 input.metadata_mut().push_draw_command(drawable);
545 }
546
547 Ok(ComputedData { width, height })
548 }));
549
550 if args.on_click.is_some() {
551 let args_for_handler = args.clone();
552 let state_for_handler = ripple_state;
553 input_handler(Box::new(move |mut input| {
554 apply_surface_accessibility(
556 &mut input,
557 &args_for_handler,
558 true,
559 args_for_handler.on_click.clone(),
560 );
561
562 let size = input.computed_data;
564 let cursor_pos_option = input.cursor_position_rel;
565 let is_cursor_in_surface = cursor_pos_option
566 .map(|pos| is_position_in_component(size, pos))
567 .unwrap_or(false);
568
569 if let Some(ref state) = state_for_handler {
570 state.set_hovered(is_cursor_in_surface);
571 }
572
573 if is_cursor_in_surface && args_for_handler.on_click.is_some() {
574 input.requests.cursor_icon = CursorIcon::Pointer;
575 }
576
577 if is_cursor_in_surface {
578 let press_events: Vec<_> = input
579 .cursor_events
580 .iter()
581 .filter(|event| {
582 matches!(
583 event.content,
584 CursorEventContent::Pressed(PressKeyEventType::Left)
585 )
586 })
587 .collect();
588
589 let release_events: Vec<_> = input
590 .cursor_events
591 .iter()
592 .filter(|event| event.gesture_state == GestureState::TapCandidate)
593 .filter(|event| {
594 matches!(
595 event.content,
596 CursorEventContent::Released(PressKeyEventType::Left)
597 )
598 })
599 .collect();
600
601 if !press_events.is_empty()
602 && let (Some(cursor_pos), Some(state)) =
603 (cursor_pos_option, state_for_handler.as_ref())
604 {
605 let normalized_x = (cursor_pos.x.to_f32() / size.width.to_f32()) - 0.5;
606 let normalized_y = (cursor_pos.y.to_f32() / size.height.to_f32()) - 0.5;
607
608 state.start_animation([normalized_x, normalized_y]);
609 }
610
611 if !release_events.is_empty()
612 && let Some(ref on_click) = args_for_handler.on_click
613 {
614 on_click();
615 }
616
617 if args_for_handler.block_input {
618 input.block_all();
619 }
620 }
621 }));
622 } else {
623 input_handler(Box::new(move |mut input| {
624 apply_surface_accessibility(&mut input, &args_for_handler, false, None);
626
627 let size = input.computed_data;
629 let cursor_pos_option = input.cursor_position_rel;
630 let is_cursor_in_surface = cursor_pos_option
631 .map(|pos| is_position_in_component(size, pos))
632 .unwrap_or(false);
633 if args_for_handler.block_input && is_cursor_in_surface {
634 input.block_all();
635 }
636 }));
637 }
638}
639
640fn apply_surface_accessibility(
641 input: &mut InputHandlerInput<'_>,
642 args: &SurfaceArgs,
643 interactive: bool,
644 on_click: Option<Arc<dyn Fn() + Send + Sync>>,
645) {
646 let has_metadata = args.accessibility_role.is_some()
647 || args.accessibility_label.is_some()
648 || args.accessibility_description.is_some()
649 || args.accessibility_focusable
650 || interactive;
651
652 if !has_metadata {
653 return;
654 }
655
656 let mut builder = input.accessibility();
657
658 let role = args
659 .accessibility_role
660 .or_else(|| interactive.then_some(Role::Button));
661 if let Some(role) = role {
662 builder = builder.role(role);
663 }
664 if let Some(label) = args.accessibility_label.as_ref() {
665 builder = builder.label(label.clone());
666 }
667 if let Some(description) = args.accessibility_description.as_ref() {
668 builder = builder.description(description.clone());
669 }
670 if args.accessibility_focusable || interactive {
671 builder = builder.focusable();
672 }
673 if interactive {
674 builder = builder.action(Action::Click);
675 }
676 builder.commit();
677
678 if interactive && let Some(on_click) = on_click {
679 input.set_accessibility_action_handler(move |action| {
680 if action == Action::Click {
681 on_click();
682 }
683 });
684 }
685}