use std::sync::Arc;
use derive_builder::Builder;
use tessera_ui::{
Color, ComputedData, Constraint, CursorEventContent, DimensionValue, Dp, GestureState,
InputHandlerInput, PressKeyEventType, Px, PxPosition, PxSize,
accesskit::{Action, Role},
tessera,
winit::window::CursorIcon,
};
use crate::{
padding_utils::remove_padding_from_dimension,
pipelines::{RippleProps, ShadowProps, ShapeCommand, SimpleRectCommand},
pos_misc::is_position_in_component,
ripple_state::RippleState,
shape_def::Shape,
};
#[derive(Clone)]
pub enum SurfaceStyle {
Filled { color: Color },
Outlined { color: Color, width: Dp },
FilledOutlined {
fill_color: Color,
border_color: Color,
border_width: Dp,
},
}
impl Default for SurfaceStyle {
fn default() -> Self {
SurfaceStyle::Filled {
color: Color::new(0.4745, 0.5255, 0.7961, 1.0),
}
}
}
impl From<Color> for SurfaceStyle {
fn from(color: Color) -> Self {
SurfaceStyle::Filled { color }
}
}
#[derive(Builder, Clone)]
#[builder(pattern = "owned")]
pub struct SurfaceArgs {
#[builder(default)]
pub style: SurfaceStyle,
#[builder(default)]
pub hover_style: Option<SurfaceStyle>,
#[builder(default)]
pub shape: Shape,
#[builder(default, setter(strip_option))]
pub shadow: Option<ShadowProps>,
#[builder(default = "Dp(0.0)")]
pub padding: Dp,
#[builder(default = "DimensionValue::WRAP", setter(into))]
pub width: DimensionValue,
#[builder(default = "DimensionValue::WRAP", setter(into))]
pub height: DimensionValue,
#[builder(default, setter(strip_option))]
pub on_click: Option<Arc<dyn Fn() + Send + Sync>>,
#[builder(default = "Color::from_rgb(1.0, 1.0, 1.0)")]
pub ripple_color: Color,
#[builder(default = "false")]
pub block_input: bool,
#[builder(default, setter(strip_option))]
pub accessibility_role: Option<Role>,
#[builder(default, setter(strip_option, into))]
pub accessibility_label: Option<String>,
#[builder(default, setter(strip_option, into))]
pub accessibility_description: Option<String>,
#[builder(default)]
pub accessibility_focusable: bool,
}
impl Default for SurfaceArgs {
fn default() -> Self {
SurfaceArgsBuilder::default().build().unwrap()
}
}
fn build_ripple_props(args: &SurfaceArgs, ripple_state: Option<&RippleState>) -> RippleProps {
if let Some(state) = ripple_state
&& let Some((progress, click_pos)) = state.get_animation_progress()
{
let radius = progress;
let alpha = (1.0 - progress) * 0.3;
return RippleProps {
center: click_pos,
radius,
alpha,
color: args.ripple_color,
};
}
RippleProps::default()
}
fn build_rounded_rectangle_command(
args: &SurfaceArgs,
style: &SurfaceStyle,
ripple_props: RippleProps,
corner_radii: [f32; 4],
g2_k_value: f32,
interactive: bool,
) -> ShapeCommand {
match style {
SurfaceStyle::Filled { color } => {
if interactive {
ShapeCommand::RippleRect {
color: *color,
corner_radii,
g2_k_value,
shadow: args.shadow,
ripple: ripple_props,
}
} else {
ShapeCommand::Rect {
color: *color,
corner_radii,
g2_k_value,
shadow: args.shadow,
}
}
}
SurfaceStyle::Outlined { color, width } => {
if interactive {
ShapeCommand::RippleOutlinedRect {
color: *color,
corner_radii,
g2_k_value,
shadow: args.shadow,
border_width: width.to_pixels_f32(),
ripple: ripple_props,
}
} else {
ShapeCommand::OutlinedRect {
color: *color,
corner_radii,
g2_k_value,
shadow: args.shadow,
border_width: width.to_pixels_f32(),
}
}
}
SurfaceStyle::FilledOutlined {
fill_color,
border_color,
border_width,
} => {
if interactive {
ShapeCommand::RippleFilledOutlinedRect {
color: *fill_color,
border_color: *border_color,
corner_radii,
g2_k_value,
shadow: args.shadow,
border_width: border_width.to_pixels_f32(),
ripple: ripple_props,
}
} else {
ShapeCommand::FilledOutlinedRect {
color: *fill_color,
border_color: *border_color,
corner_radii,
g2_k_value,
shadow: args.shadow,
border_width: border_width.to_pixels_f32(),
}
}
}
}
}
fn build_ellipse_command(
args: &SurfaceArgs,
style: &SurfaceStyle,
ripple_props: RippleProps,
interactive: bool,
) -> ShapeCommand {
let corner_marker = [-1.0, -1.0, -1.0, -1.0];
match style {
SurfaceStyle::Filled { color } => {
if interactive {
ShapeCommand::RippleRect {
color: *color,
corner_radii: corner_marker,
g2_k_value: 0.0,
shadow: args.shadow,
ripple: ripple_props,
}
} else {
ShapeCommand::Ellipse {
color: *color,
shadow: args.shadow,
}
}
}
SurfaceStyle::Outlined { color, width } => {
if interactive {
ShapeCommand::RippleOutlinedRect {
color: *color,
corner_radii: corner_marker,
g2_k_value: 0.0,
shadow: args.shadow,
border_width: width.to_pixels_f32(),
ripple: ripple_props,
}
} else {
ShapeCommand::OutlinedEllipse {
color: *color,
shadow: args.shadow,
border_width: width.to_pixels_f32(),
}
}
}
SurfaceStyle::FilledOutlined {
fill_color,
border_color,
border_width,
} => {
ShapeCommand::FilledOutlinedEllipse {
color: *fill_color,
border_color: *border_color,
shadow: args.shadow,
border_width: border_width.to_pixels_f32(),
}
}
}
}
fn build_shape_command(
args: &SurfaceArgs,
style: &SurfaceStyle,
ripple_props: RippleProps,
size: PxSize,
) -> ShapeCommand {
let interactive = args.on_click.is_some();
match args.shape {
Shape::RoundedRectangle {
top_left,
top_right,
bottom_right,
bottom_left,
g2_k_value,
} => {
let corner_radii = [
top_left.to_pixels_f32(),
top_right.to_pixels_f32(),
bottom_right.to_pixels_f32(),
bottom_left.to_pixels_f32(),
];
build_rounded_rectangle_command(
args,
style,
ripple_props,
corner_radii,
g2_k_value,
interactive,
)
}
Shape::Ellipse => build_ellipse_command(args, style, ripple_props, interactive),
Shape::HorizontalCapsule => {
let radius = size.height.to_f32() / 2.0;
let corner_radii = [radius, radius, radius, radius];
build_rounded_rectangle_command(
args,
style,
ripple_props,
corner_radii,
2.0, interactive,
)
}
Shape::VerticalCapsule => {
let radius = size.width.to_f32() / 2.0;
let corner_radii = [radius, radius, radius, radius];
build_rounded_rectangle_command(
args,
style,
ripple_props,
corner_radii,
2.0, interactive,
)
}
}
}
fn make_surface_drawable(
args: &SurfaceArgs,
style: &SurfaceStyle,
ripple_state: Option<&RippleState>,
size: PxSize,
) -> ShapeCommand {
let ripple_props = build_ripple_props(args, ripple_state);
build_shape_command(args, style, ripple_props, size)
}
fn try_build_simple_rect_command(
args: &SurfaceArgs,
style: &SurfaceStyle,
ripple_state: Option<&RippleState>,
) -> Option<SimpleRectCommand> {
if args.shadow.is_some() {
return None;
}
if args.on_click.is_some() {
return None;
}
if let Some(state) = ripple_state
&& state.get_animation_progress().is_some()
{
return None;
}
let color = match style {
SurfaceStyle::Filled { color } => *color,
_ => return None,
};
match args.shape {
Shape::RoundedRectangle {
top_left,
top_right,
bottom_right,
bottom_left,
..
} => {
let radii = [
top_left.to_pixels_f32(),
top_right.to_pixels_f32(),
bottom_right.to_pixels_f32(),
bottom_left.to_pixels_f32(),
];
let zero_eps = 0.0001;
if radii.iter().all(|r| r.abs() <= zero_eps) {
Some(SimpleRectCommand { color })
} else {
None
}
}
_ => None,
}
}
fn compute_surface_size(
effective_surface_constraint: Constraint,
child_measurement: ComputedData,
padding_px: Px,
) -> (Px, Px) {
let min_width = child_measurement.width + padding_px * 2;
let min_height = child_measurement.height + padding_px * 2;
fn clamp_wrap(min: Option<Px>, max: Option<Px>, min_measure: Px) -> Px {
min.unwrap_or(Px(0))
.max(min_measure)
.min(max.unwrap_or(Px::MAX))
}
fn fill_value(min: Option<Px>, max: Option<Px>, min_measure: Px) -> Px {
max.expect("Seems that you are trying to fill an infinite dimension, which is not allowed")
.max(min_measure)
.max(min.unwrap_or(Px(0)))
}
let width = match effective_surface_constraint.width {
DimensionValue::Fixed(value) => value,
DimensionValue::Wrap { min, max } => clamp_wrap(min, max, min_width),
DimensionValue::Fill { min, max } => fill_value(min, max, min_width),
};
let height = match effective_surface_constraint.height {
DimensionValue::Fixed(value) => value,
DimensionValue::Wrap { min, max } => clamp_wrap(min, max, min_height),
DimensionValue::Fill { min, max } => fill_value(min, max, min_height),
};
(width, height)
}
#[tessera]
pub fn surface(args: SurfaceArgs, ripple_state: Option<RippleState>, child: impl FnOnce()) {
(child)();
let ripple_state_for_measure = ripple_state.clone();
let args_measure_clone = args.clone();
let args_for_handler = args.clone();
measure(Box::new(move |input| {
let surface_intrinsic_width = args_measure_clone.width;
let surface_intrinsic_height = args_measure_clone.height;
let surface_intrinsic_constraint =
Constraint::new(surface_intrinsic_width, surface_intrinsic_height);
let effective_surface_constraint =
surface_intrinsic_constraint.merge(input.parent_constraint);
let padding_px: Px = args_measure_clone.padding.into();
let child_constraint = Constraint::new(
remove_padding_from_dimension(effective_surface_constraint.width, padding_px),
remove_padding_from_dimension(effective_surface_constraint.height, padding_px),
);
let child_measurement = if !input.children_ids.is_empty() {
let child_measurements = input.measure_children(
input
.children_ids
.iter()
.copied()
.map(|node_id| (node_id, child_constraint))
.collect(),
)?;
input.place_child(
input.children_ids[0],
PxPosition {
x: args.padding.into(),
y: args.padding.into(),
},
);
let mut max_width = Px::ZERO;
let mut max_height = Px::ZERO;
for measurement in child_measurements.values() {
max_width = max_width.max(measurement.width);
max_height = max_height.max(measurement.height);
}
ComputedData {
width: max_width,
height: max_height,
}
} else {
ComputedData {
width: Px(0),
height: Px(0),
}
};
let is_hovered = ripple_state_for_measure
.as_ref()
.map(|state| state.is_hovered())
.unwrap_or(false);
let effective_style = if is_hovered && args_measure_clone.hover_style.is_some() {
args_measure_clone.hover_style.as_ref().unwrap()
} else {
&args_measure_clone.style
};
let padding_px: Px = args_measure_clone.padding.into();
let (width, height) =
compute_surface_size(effective_surface_constraint, child_measurement, padding_px);
if let Some(simple) = try_build_simple_rect_command(
&args_measure_clone,
effective_style,
ripple_state_for_measure.as_ref(),
) {
input.metadata_mut().push_draw_command(simple);
} else {
let drawable = make_surface_drawable(
&args_measure_clone,
effective_style,
ripple_state_for_measure.as_ref(),
PxSize::new(width, height),
);
input.metadata_mut().push_draw_command(drawable);
}
Ok(ComputedData { width, height })
}));
if args.on_click.is_some() {
let args_for_handler = args.clone();
let state_for_handler = ripple_state;
input_handler(Box::new(move |mut input| {
apply_surface_accessibility(
&mut input,
&args_for_handler,
true,
args_for_handler.on_click.clone(),
);
let size = input.computed_data;
let cursor_pos_option = input.cursor_position_rel;
let is_cursor_in_surface = cursor_pos_option
.map(|pos| is_position_in_component(size, pos))
.unwrap_or(false);
if let Some(ref state) = state_for_handler {
state.set_hovered(is_cursor_in_surface);
}
if is_cursor_in_surface && args_for_handler.on_click.is_some() {
input.requests.cursor_icon = CursorIcon::Pointer;
}
if is_cursor_in_surface {
let press_events: Vec<_> = input
.cursor_events
.iter()
.filter(|event| {
matches!(
event.content,
CursorEventContent::Pressed(PressKeyEventType::Left)
)
})
.collect();
let release_events: Vec<_> = input
.cursor_events
.iter()
.filter(|event| event.gesture_state == GestureState::TapCandidate)
.filter(|event| {
matches!(
event.content,
CursorEventContent::Released(PressKeyEventType::Left)
)
})
.collect();
if !press_events.is_empty()
&& let (Some(cursor_pos), Some(state)) =
(cursor_pos_option, state_for_handler.as_ref())
{
let normalized_x = (cursor_pos.x.to_f32() / size.width.to_f32()) - 0.5;
let normalized_y = (cursor_pos.y.to_f32() / size.height.to_f32()) - 0.5;
state.start_animation([normalized_x, normalized_y]);
}
if !release_events.is_empty()
&& let Some(ref on_click) = args_for_handler.on_click
{
on_click();
}
if args_for_handler.block_input {
input.block_all();
}
}
}));
} else {
input_handler(Box::new(move |mut input| {
apply_surface_accessibility(&mut input, &args_for_handler, false, None);
let size = input.computed_data;
let cursor_pos_option = input.cursor_position_rel;
let is_cursor_in_surface = cursor_pos_option
.map(|pos| is_position_in_component(size, pos))
.unwrap_or(false);
if args_for_handler.block_input && is_cursor_in_surface {
input.block_all();
}
}));
}
}
fn apply_surface_accessibility(
input: &mut InputHandlerInput<'_>,
args: &SurfaceArgs,
interactive: bool,
on_click: Option<Arc<dyn Fn() + Send + Sync>>,
) {
let has_metadata = args.accessibility_role.is_some()
|| args.accessibility_label.is_some()
|| args.accessibility_description.is_some()
|| args.accessibility_focusable
|| interactive;
if !has_metadata {
return;
}
let mut builder = input.accessibility();
let role = args
.accessibility_role
.or_else(|| interactive.then_some(Role::Button));
if let Some(role) = role {
builder = builder.role(role);
}
if let Some(label) = args.accessibility_label.as_ref() {
builder = builder.label(label.clone());
}
if let Some(description) = args.accessibility_description.as_ref() {
builder = builder.description(description.clone());
}
if args.accessibility_focusable || interactive {
builder = builder.focusable();
}
if interactive {
builder = builder.action(Action::Click);
}
builder.commit();
if interactive && let Some(on_click) = on_click {
input.set_accessibility_action_handler(move |action| {
if action == Action::Click {
on_click();
}
});
}
}