use derive_setters::Setters;
use tessera_ui::{
Color, ComputedData, Constraint, DimensionValue, Dp, LayoutInput, LayoutOutput, LayoutSpec,
MeasurementError, Px, PxPosition, PxSize, RenderInput, provide_context, tessera, use_context,
};
use crate::{
alignment::{CrossAxisAlignment, MainAxisAlignment},
pipelines::shape::command::ShapeCommand,
row::{RowArgs, RowScope, row},
shape_def::{ResolvedShape, Shape},
theme::{ContentColor, MaterialTheme, content_color_for, provide_text_style},
};
fn clamp_wrap(min: Option<Px>, max: Option<Px>, measure: Px) -> Px {
min.unwrap_or(Px(0))
.max(measure)
.min(max.unwrap_or(Px::MAX))
}
fn fill_value(min: Option<Px>, max: Option<Px>, measure: Px) -> Px {
max.expect("Seems that you are trying to fill an infinite dimension, which is not allowed")
.max(measure)
.max(min.unwrap_or(Px(0)))
}
fn resolve_dimension(dim: DimensionValue, measure: Px) -> Px {
match dim {
DimensionValue::Fixed(v) => v,
DimensionValue::Wrap { min, max } => clamp_wrap(min, max, measure),
DimensionValue::Fill { min, max } => fill_value(min, max, measure),
}
}
fn dimension_max(dim: DimensionValue) -> Option<Px> {
match dim {
DimensionValue::Fixed(v) => Some(v),
DimensionValue::Wrap { max, .. } | DimensionValue::Fill { max, .. } => max,
}
}
fn relax_min_constraint(dim: DimensionValue) -> DimensionValue {
match dim {
DimensionValue::Fixed(v) => DimensionValue::Wrap {
min: Some(Px(0)),
max: Some(v),
},
DimensionValue::Wrap { max, .. } => DimensionValue::Wrap {
min: Some(Px(0)),
max,
},
DimensionValue::Fill { max, .. } => DimensionValue::Fill {
min: Some(Px(0)),
max,
},
}
}
#[derive(Clone, Copy, Default, PartialEq, Eq, Hash)]
struct BadgedBoxLayout;
impl LayoutSpec for BadgedBoxLayout {
fn measure(
&self,
input: &LayoutInput<'_>,
output: &mut LayoutOutput<'_>,
) -> Result<ComputedData, MeasurementError> {
debug_assert_eq!(
input.children_ids().len(),
2,
"badged_box expects exactly two children: anchor and badge",
);
let parent_constraint = Constraint::new(
input.parent_constraint().width(),
input.parent_constraint().height(),
);
let badge_constraint = Constraint::new(
input.parent_constraint().width(),
relax_min_constraint(input.parent_constraint().height()),
);
let anchor_id = input.children_ids()[0];
let badge_id = input.children_ids()[1];
let to_measure = vec![(badge_id, badge_constraint), (anchor_id, parent_constraint)];
let results = input.measure_children(to_measure)?;
let anchor = results
.get(&anchor_id)
.copied()
.expect("badged_box anchor must be measured");
let badge_data = results
.get(&badge_id)
.copied()
.expect("badged_box badge must be measured");
output.place_child(anchor_id, PxPosition::new(Px(0), Px(0)));
let badge_size_px = BadgeDefaults::SIZE.to_px();
let has_content = badge_data.width > badge_size_px;
let horizontal_offset = if has_content {
BadgeDefaults::WITH_CONTENT_HORIZONTAL_OFFSET
} else {
BadgeDefaults::OFFSET
}
.to_px();
let vertical_offset = if has_content {
BadgeDefaults::WITH_CONTENT_VERTICAL_OFFSET
} else {
BadgeDefaults::OFFSET
}
.to_px();
let badge_x = anchor.width - horizontal_offset;
let badge_y = -badge_data.height + vertical_offset;
output.place_child(badge_id, PxPosition::new(badge_x, badge_y));
Ok(ComputedData {
width: anchor.width,
height: anchor.height,
})
}
}
#[derive(Clone, Copy, PartialEq)]
struct BadgeLayout {
container_color: Color,
}
impl LayoutSpec for BadgeLayout {
fn measure(
&self,
input: &LayoutInput<'_>,
_output: &mut LayoutOutput<'_>,
) -> Result<ComputedData, MeasurementError> {
let size_px = BadgeDefaults::SIZE.to_px();
let intrinsic = Constraint::new(
DimensionValue::Wrap {
min: Some(size_px),
max: None,
},
DimensionValue::Wrap {
min: Some(size_px),
max: None,
},
);
let effective = intrinsic.merge(input.parent_constraint());
let width = resolve_dimension(effective.width, size_px);
let height = resolve_dimension(effective.height, size_px);
Ok(ComputedData { width, height })
}
fn record(&self, input: &RenderInput<'_>) {
let mut metadata = input.metadata_mut();
let size = metadata
.computed_data
.expect("badge must have computed size before record");
let ResolvedShape::Rounded {
corner_radii,
corner_g2,
} = BadgeDefaults::SHAPE.resolve_for_size(PxSize::new(size.width, size.height))
else {
unreachable!("badge shape must resolve to a rounded rectangle");
};
metadata.push_draw_command(ShapeCommand::Rect {
color: self.container_color,
corner_radii,
corner_g2,
shadow: None,
});
}
}
#[derive(Clone, Copy, PartialEq)]
struct BadgeWithContentLayout {
container_color: Color,
padding_px: Px,
}
impl LayoutSpec for BadgeWithContentLayout {
fn measure(
&self,
input: &LayoutInput<'_>,
output: &mut LayoutOutput<'_>,
) -> Result<ComputedData, MeasurementError> {
debug_assert_eq!(
input.children_ids().len(),
1,
"badge_with_content expects a single row child",
);
let min_size_px = BadgeDefaults::LARGE_SIZE.to_px();
let intrinsic = Constraint::new(
DimensionValue::Wrap {
min: Some(min_size_px),
max: None,
},
DimensionValue::Wrap {
min: Some(min_size_px),
max: None,
},
);
let effective = intrinsic.merge(input.parent_constraint());
let max_width =
dimension_max(effective.width).map(|v| (v - self.padding_px * 2).max(Px(0)));
let max_height = dimension_max(effective.height);
let child_constraint = Constraint::new(
DimensionValue::Wrap {
min: None,
max: max_width,
},
DimensionValue::Wrap {
min: None,
max: max_height,
},
);
let row_id = input.children_ids()[0];
let row_data = input.measure_child(row_id, &child_constraint)?;
let measured_width = (row_data.width + self.padding_px * 2).max(min_size_px);
let measured_height = row_data.height.max(min_size_px);
let width = resolve_dimension(effective.width, measured_width);
let height = resolve_dimension(effective.height, measured_height);
let x = (width - row_data.width).max(Px(0)) / 2;
let y = (height - row_data.height).max(Px(0)) / 2;
output.place_child(row_id, PxPosition::new(x, y));
Ok(ComputedData { width, height })
}
fn record(&self, input: &RenderInput<'_>) {
let mut metadata = input.metadata_mut();
let size = metadata
.computed_data
.expect("badge_with_content must have computed size before record");
let ResolvedShape::Rounded {
corner_radii,
corner_g2,
} = BadgeDefaults::SHAPE.resolve_for_size(PxSize::new(size.width, size.height))
else {
unreachable!("badge shape must resolve to a rounded rectangle");
};
metadata.push_draw_command(ShapeCommand::Rect {
color: self.container_color,
corner_radii,
corner_g2,
shadow: None,
});
}
}
pub struct BadgeDefaults;
impl BadgeDefaults {
pub const SIZE: Dp = Dp(6.0);
pub const LARGE_SIZE: Dp = Dp(16.0);
pub const SHAPE: Shape = Shape::capsule();
pub const WITH_CONTENT_HORIZONTAL_PADDING: Dp = Dp(4.0);
pub const WITH_CONTENT_HORIZONTAL_OFFSET: Dp = Dp(12.0);
pub const WITH_CONTENT_VERTICAL_OFFSET: Dp = Dp(14.0);
pub const OFFSET: Dp = Dp(6.0);
pub fn container_color() -> Color {
use_context::<MaterialTheme>()
.expect("MaterialTheme must be provided")
.get()
.color_scheme
.error
}
}
#[derive(Clone, Debug, Setters)]
pub struct BadgeArgs {
pub container_color: Color,
#[setters(strip_option)]
pub content_color: Option<Color>,
}
impl Default for BadgeArgs {
fn default() -> Self {
Self {
container_color: BadgeDefaults::container_color(),
content_color: None,
}
}
}
#[tessera]
pub fn badged_box<F1, F2>(badge: F1, content: F2)
where
F1: FnOnce() + Send + Sync + 'static,
F2: FnOnce() + Send + Sync + 'static,
{
layout(BadgedBoxLayout);
content();
badge();
}
#[tessera]
pub fn badge(args: impl Into<BadgeArgs>) {
let args: BadgeArgs = args.into();
let container_color = args.container_color;
layout(BadgeLayout { container_color });
}
#[tessera]
pub fn badge_with_content<F>(args: impl Into<BadgeArgs>, content: F)
where
F: FnOnce(&mut RowScope),
{
let args: BadgeArgs = args.into();
let theme = use_context::<MaterialTheme>()
.expect("MaterialTheme must be provided")
.get();
let scheme = theme.color_scheme;
let typography = theme.typography;
let container_color = args.container_color;
let content_color = args.content_color.unwrap_or_else(|| {
content_color_for(container_color, &scheme).unwrap_or(
use_context::<ContentColor>()
.map(|c| c.get().current)
.unwrap_or(ContentColor::default().current),
)
});
let padding_px = BadgeDefaults::WITH_CONTENT_HORIZONTAL_PADDING.to_px();
layout(BadgeWithContentLayout {
container_color,
padding_px,
});
provide_context(
|| ContentColor {
current: content_color,
},
|| {
provide_text_style(typography.label_small, || {
row(
RowArgs::default()
.main_axis_alignment(MainAxisAlignment::Center)
.cross_axis_alignment(CrossAxisAlignment::Center),
content,
);
});
},
);
}