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