use crate::units::{Dp, Px, Sp};
use crate::{
BaselineShift, Brush, ClipOp, Color, DrawStyle, FontStyle, FontSynthesis, FontWeight, Modifier,
Rect, TextAlign, TextDecoration, TextDirection, TextSpan, Transform, Vec2,
};
use std::{fmt::Formatter, sync::Arc};
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SubcomposeScope {
pub min_width: Dp,
pub max_width: Dp,
pub min_height: Dp,
pub max_height: Dp,
}
impl SubcomposeScope {
pub const UNBOUNDED: Self = Self {
min_width: Dp(0.0),
max_width: Dp(f32::INFINITY),
min_height: Dp(0.0),
max_height: Dp(f32::INFINITY),
};
pub fn new(min_width: Dp, max_width: Dp, min_height: Dp, max_height: Dp) -> Self {
fn minimum(value: f32) -> f32 {
if value.is_nan() || value < 0.0 || !value.is_finite() {
0.0
} else {
value
}
}
fn maximum(value: f32) -> f32 {
if value.is_nan() || value < 0.0 {
if value.is_nan() { f32::INFINITY } else { 0.0 }
} else {
value
}
}
let mut scope = Self {
min_width: Dp(minimum(min_width.0)),
max_width: Dp(maximum(max_width.0)),
min_height: Dp(minimum(min_height.0)),
max_height: Dp(maximum(max_height.0)),
};
if scope.min_width.0 > scope.max_width.0 {
scope.min_width = scope.max_width;
}
if scope.min_height.0 > scope.max_height.0 {
scope.min_height = scope.max_height;
}
scope
}
pub fn has_bounded_width(&self) -> bool {
self.max_width.0.is_finite()
}
pub fn has_bounded_height(&self) -> bool {
self.max_height.0.is_finite()
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct BoxWithConstraintsScope {
pub min_width: Dp,
pub max_width: Dp,
pub min_height: Dp,
pub max_height: Dp,
}
impl BoxWithConstraintsScope {
pub fn has_bounded_width(&self) -> bool {
self.max_width.0.is_finite()
}
pub fn has_bounded_height(&self) -> bool {
self.max_height.0.is_finite()
}
}
pub type ViewId = u64;
pub type ImageHandle = u64;
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum ImageFilter {
#[default]
Linear,
Nearest,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct ImageSourceRect {
pub x: u32,
pub y: u32,
pub width: u32,
pub height: u32,
}
impl ImageSourceRect {
pub const fn new(x: u32, y: u32, width: u32, height: u32) -> Self {
Self {
x,
y,
width,
height,
}
}
pub const fn is_empty(self) -> bool {
self.width == 0 || self.height == 0
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Default)]
#[non_exhaustive]
pub enum ImageFit {
#[default]
Contain,
Cover,
FitWidth,
FitHeight,
FillBounds,
Inside,
None,
Tile,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum ImageAlignment {
#[default]
Center,
Begin,
End,
}
impl ImageFit {
pub fn scale_factor(self, src: (f32, f32), dst: (f32, f32)) -> (f32, f32) {
let fill_w = dst.0 / src.0;
let fill_h = dst.1 / src.1;
let uniform = |v: f32| (v, v);
match self {
Self::Contain => uniform(fill_w.min(fill_h)),
Self::Cover => uniform(fill_w.max(fill_h)),
Self::FitWidth => uniform(fill_w),
Self::FitHeight => uniform(fill_h),
Self::FillBounds => (fill_w, fill_h),
Self::Inside => {
if src.0 <= dst.0 && src.1 <= dst.1 {
uniform(1.0)
} else {
uniform(fill_w.min(fill_h))
}
}
Self::None | Self::Tile => uniform(1.0),
}
}
}
#[derive(Clone)]
pub struct OverlayEntry {
pub id: u64,
pub view: Box<View>,
}
#[derive(Clone)]
#[non_exhaustive]
pub enum ViewKind {
Box,
Row,
Column,
ZStack,
OverlayHost,
Text {
text: String,
color: Color,
font_size: Sp,
soft_wrap: bool,
max_lines: Option<usize>,
overflow: TextOverflow,
font_family: Option<&'static str>,
annotations: Option<Arc<[TextSpan]>>,
text_align: TextAlign,
font_weight: FontWeight,
font_style: FontStyle,
text_decoration: TextDecoration,
letter_spacing: Sp,
line_height: Sp,
url: Option<Arc<str>>,
font_variation_settings: Option<Arc<str>>,
draw_style: DrawStyle,
},
Image {
handle: ImageHandle,
tint: Color,
fit: ImageFit,
filter: ImageFilter,
source_rect: Option<ImageSourceRect>,
alignment: ImageAlignment,
},
SubcomposeLayout {
content: Arc<dyn Fn(&SubcomposeScope) -> Vec<(u64, View)>>,
},
}
impl std::fmt::Debug for ViewKind {
fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
match self {
Self::Box => f.write_str("Box"),
Self::Row => f.write_str("Row"),
Self::Column => f.write_str("Column"),
Self::ZStack => f.write_str("ZStack"),
Self::OverlayHost => f.write_str("OverlayHost"),
Self::Image {
handle,
tint,
fit,
filter,
source_rect,
alignment,
} => f
.debug_struct("Image")
.field("handle", handle)
.field("tint", tint)
.field("fit", fit)
.field("filter", filter)
.field("source_rect", source_rect)
.field("alignment", alignment)
.finish(),
Self::SubcomposeLayout { .. } => f.write_str("SubcomposeLayout"),
Self::Text { text, .. } => write!(f, "Text({:?})", text),
}
}
}
#[derive(Clone, Debug)]
pub struct View {
pub id: ViewId,
pub kind: ViewKind,
pub modifier: Modifier,
pub children: Vec<View>,
pub semantics: Option<crate::semantics::Semantics>,
pub scope_key: Option<String>,
}
impl View {
pub fn new(id: ViewId, kind: ViewKind) -> Self {
View {
id,
kind,
modifier: Modifier::default(),
children: vec![],
semantics: None,
scope_key: None,
}
}
pub fn modifier(mut self, m: Modifier) -> Self {
self.modifier = m;
self
}
pub fn disabled(mut self) -> Self {
self.modifier.disabled = true;
self
}
pub fn with_children(mut self, kids: Vec<View>) -> Self {
self.children = kids;
self
}
pub fn children(mut self, kids: impl Into<Vec<View>>) -> Self {
self.children = kids.into();
self
}
pub fn semantics(mut self, s: crate::semantics::Semantics) -> Self {
self.semantics = Some(s);
self
}
}
#[derive(Clone, Debug, Default)]
pub struct Scene {
pub clear_color: Color,
pub nodes: Vec<SceneNode>,
}
#[derive(Clone, Debug, PartialEq, Default)]
pub struct TextPaintStyle {
pub font_family: Option<&'static str>,
pub text_align: TextAlign,
pub font_weight: FontWeight,
pub font_style: FontStyle,
pub text_decoration: TextDecoration,
pub letter_spacing: Px,
pub line_height: Px,
pub text_direction: TextDirection,
pub font_synthesis: FontSynthesis,
pub baseline_shift: BaselineShift,
pub draw_style: DrawStyle,
pub url: Option<Arc<str>>,
pub font_variation_settings: Option<Arc<str>>,
}
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub struct ImagePaintStyle {
pub fit: ImageFit,
pub filter: ImageFilter,
pub source_rect: Option<ImageSourceRect>,
pub alignment: ImageAlignment,
}
#[derive(Clone, Copy, Debug)]
pub struct PaintCallbackInfo {
pub viewport: Rect,
pub clip_rect: Rect,
pub pixels_per_point: f32,
pub screen_size_px: [u32; 2],
}
pub type PaintCallbackPayload = Arc<dyn std::any::Any + Send + Sync>;
#[derive(Clone, Debug)]
#[non_exhaustive]
pub enum SceneNode {
Rect {
rect: Rect,
brush: Brush,
radius: [Px; 4],
},
Border {
rect: Rect,
brush: Brush,
width: Px,
radius: [Px; 4],
},
Text {
rect: Rect,
text: Arc<str>,
color: Color,
size: Px,
style: TextPaintStyle,
},
Ellipse {
rect: Rect,
brush: Brush,
},
EllipseBorder {
rect: Rect,
brush: Brush,
width: Px,
},
PushClip {
rect: Rect,
radius: [Px; 4],
op: ClipOp,
},
PopClip,
PushTransform {
transform: Transform,
},
PopTransform,
Image {
rect: Rect,
handle: ImageHandle,
tint: Color,
style: ImagePaintStyle,
},
ImageBlended {
rect: Rect,
handle: ImageHandle,
tint: Color,
style: ImagePaintStyle,
blend: BlendMode,
},
Coverage {
rect: Rect,
handle: ImageHandle,
color: Color,
},
Shadow {
rect: Rect,
radius: [Px; 4],
elevation: Px,
color: Color,
},
BeginLayer {
rect: Rect,
layer_id: u32,
alpha: f32,
blur_radius_x: Px,
blur_radius_y: Px,
rectangle_edge: bool,
},
EndLayer {
layer_id: u32,
},
CompositeShadow {
layer_id: u32,
blur_px: Px,
offset_px: (Px, Px),
color: Color,
shape_px: [Px; 4],
},
Arc {
rect: Rect,
start_angle: f32,
sweep_angle: f32,
stroke_width: Px,
brush: Brush,
cap: StrokeCap,
},
VectorMesh {
mesh: Arc<VectorMeshData>,
transform: [f32; 6],
paint: PaintDesc,
clip: Option<u32>,
blend: BlendMode,
},
VectorMeshInstanced {
mesh: Arc<VectorMeshData>,
transforms: Vec<[f32; 6]>,
paint: PaintDesc,
blend: BlendMode,
},
VectorOverlay {
meshes: Arc<[VectorMeshData]>,
},
PushVectorClip {
mesh: Arc<VectorMeshData>,
op: ClipOp,
},
PopVectorClip,
Callback {
rect: Rect,
payload: PaintCallbackPayload,
},
}
pub const CANVAS_LAYER_ID_BASE: u32 = 0x8000_0000;
#[derive(Clone, Debug, Default)]
pub struct VectorMeshData {
pub vertices: Arc<[VectorVertex]>,
pub indices: Arc<[u32]>,
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[repr(C)]
pub struct VectorVertex {
pub pos: [f32; 2],
pub color: [f32; 4],
pub uv: [f32; 2],
}
pub const MAX_GRADIENT_STOPS: usize = 8;
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub struct GradientStop {
pub offset: f32,
pub color: Color,
}
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub struct GradientStops {
stops: [GradientStop; MAX_GRADIENT_STOPS],
len: u8,
}
impl GradientStops {
pub fn from_slice(slice: &[GradientStop]) -> Self {
let mut stops = Self::default();
for stop in slice {
if !stops.push(*stop) {
log::warn!(
"gradient has {} stops; keeping the first {MAX_GRADIENT_STOPS}",
slice.len()
);
break;
}
}
stops
}
pub fn try_from_slice(slice: &[GradientStop]) -> Option<Self> {
if slice.len() > MAX_GRADIENT_STOPS {
return None;
}
let mut stops = Self::default();
for stop in slice {
if !stops.push(*stop) {
return None;
}
}
Some(stops)
}
pub fn push(&mut self, stop: GradientStop) -> bool {
let len = self.len as usize;
if len >= MAX_GRADIENT_STOPS {
return false;
}
self.stops[len] = stop;
self.len = (len + 1) as u8;
true
}
pub fn as_slice(&self) -> &[GradientStop] {
&self.stops[..self.len as usize]
}
pub fn as_mut_slice(&mut self) -> &mut [GradientStop] {
&mut self.stops[..self.len as usize]
}
pub fn len(&self) -> usize {
self.len as usize
}
pub fn is_empty(&self) -> bool {
self.len == 0
}
pub fn min_alpha(&self) -> f32 {
self.as_slice()
.iter()
.map(|stop| stop.color.3 as f32 / 255.0)
.fold(1.0, f32::min)
}
}
impl<'a> From<&'a [GradientStop]> for GradientStops {
fn from(slice: &'a [GradientStop]) -> Self {
Self::from_slice(slice)
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
#[non_exhaustive]
pub enum PaintDesc {
Solid,
Linear {
start: Vec2,
end: Vec2,
start_color: Color,
end_color: Color,
},
Radial {
center: Vec2,
radius: f32,
start_color: Color,
end_color: Color,
},
Sweep {
center: Vec2,
start_color: Color,
end_color: Color,
},
LinearStops {
start: Vec2,
end: Vec2,
stops: GradientStops,
},
RadialStops {
center: Vec2,
radius: f32,
stops: GradientStops,
},
}
impl PaintDesc {
pub fn min_alpha(self) -> Option<f32> {
match self {
PaintDesc::Solid => None,
PaintDesc::Linear {
start_color,
end_color,
..
}
| PaintDesc::Radial {
start_color,
end_color,
..
}
| PaintDesc::Sweep {
start_color,
end_color,
..
} => Some((start_color.3.min(end_color.3) as f32 / 255.0).clamp(0.0, 1.0)),
PaintDesc::LinearStops { stops, .. } | PaintDesc::RadialStops { stops, .. } => {
Some(stops.min_alpha())
}
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
#[derive(Default)]
pub enum BlendMode {
#[default]
Alpha,
Screen,
Overlay,
Darken,
Lighten,
ColorDodge,
ColorBurn,
HardLight,
SoftLight,
Difference,
Exclusion,
Hue,
Saturation,
Color,
Luminosity,
Add,
Multiply,
}
impl BlendMode {
pub fn needs_isolation(self) -> bool {
!matches!(
self,
BlendMode::Alpha
| BlendMode::Add
| BlendMode::Multiply
| BlendMode::Screen
| BlendMode::Darken
| BlendMode::Lighten
)
}
pub fn shader_mode(self) -> u32 {
match self {
BlendMode::Alpha => 0,
BlendMode::Add => 1,
BlendMode::Multiply => 2,
BlendMode::Screen => 3,
BlendMode::Overlay => 4,
BlendMode::Darken => 5,
BlendMode::Lighten => 6,
BlendMode::ColorDodge => 7,
BlendMode::ColorBurn => 8,
BlendMode::HardLight => 9,
BlendMode::SoftLight => 10,
BlendMode::Difference => 11,
BlendMode::Exclusion => 12,
BlendMode::Hue => 13,
BlendMode::Saturation => 14,
BlendMode::Color => 15,
BlendMode::Luminosity => 16,
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
#[non_exhaustive]
pub enum TextOverflow {
Visible,
Clip,
Ellipsis,
}
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub enum StrokeJoin {
#[default]
Miter,
Round,
Bevel,
}
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub enum StrokeCap {
#[default]
Butt,
Round,
Square,
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn subcompose_scope_unbounded_has_infinite_max() {
let s = SubcomposeScope::UNBOUNDED;
assert!(!s.max_width.0.is_finite());
assert!(!s.max_height.0.is_finite());
assert_eq!(s.min_width, Dp(0.0));
assert_eq!(s.min_height, Dp(0.0));
}
#[test]
fn subcompose_scope_new_round_trips() {
let s = SubcomposeScope::new(Dp(10.0), Dp(200.0), Dp(20.0), Dp(300.0));
assert_eq!(s.min_width, Dp(10.0));
assert_eq!(s.max_width, Dp(200.0));
assert_eq!(s.min_height, Dp(20.0));
assert_eq!(s.max_height, Dp(300.0));
}
#[test]
fn box_with_constraints_scope_bounded_predicates() {
let bounded = BoxWithConstraintsScope {
min_width: Dp(0.0),
max_width: Dp(360.0),
min_height: Dp(0.0),
max_height: Dp(640.0),
};
assert!(bounded.has_bounded_width());
assert!(bounded.has_bounded_height());
let unbounded = BoxWithConstraintsScope {
min_width: Dp(0.0),
max_width: Dp(f32::INFINITY),
min_height: Dp(0.0),
max_height: Dp(f32::INFINITY),
};
assert!(!unbounded.has_bounded_width());
assert!(!unbounded.has_bounded_height());
}
#[test]
fn view_kind_subcompose_layout_holds_closure() {
let v: View = View {
id: 0,
kind: ViewKind::SubcomposeLayout {
content: std::sync::Arc::new(|scope| {
let _ = scope.max_width;
vec![(0, View::new(0, ViewKind::Box))]
}),
},
modifier: Modifier::default(),
children: vec![],
scope_key: None,
semantics: None,
};
match &v.kind {
ViewKind::SubcomposeLayout { .. } => {}
_ => panic!("expected SubcomposeLayout"),
}
}
#[test]
fn view_kind_subcompose_layout_supports_multiple_slots() {
let v: View = View {
id: 0,
kind: ViewKind::SubcomposeLayout {
content: std::sync::Arc::new(|_scope| {
vec![
(1, View::new(0, ViewKind::Box)),
(2, View::new(0, ViewKind::Box)),
(3, View::new(0, ViewKind::Box)),
]
}),
},
modifier: Modifier::default(),
children: vec![],
scope_key: None,
semantics: None,
};
if let ViewKind::SubcomposeLayout { content } = &v.kind {
let slots = content(&SubcomposeScope::UNBOUNDED);
assert_eq!(slots.len(), 3);
assert_eq!(slots[0].0, 1);
assert_eq!(slots[1].0, 2);
assert_eq!(slots[2].0, 3);
} else {
panic!("expected SubcomposeLayout");
}
}
}