use std::{rc::Rc, sync::Arc};
use cranpose_ui::text::{AnnotatedString, TextStyle, drawn_text_style};
use cranpose_ui_graphics::{
ArcGeometry, BlendMode, Brush, Color, ColorFilter, CornerRadii, DrawPrimitive, GraphicsLayer,
ImageBitmap, ImageSampling, LineGeometry, Point, Rect, RoundedCornerShape, ShadowPrimitive,
Stroke, TextPrimitive, Trapezoid, arc_band, inflate_rect,
};
use crate::{
graph::quad_bounds,
layer_transform::{
apply_layer_affine_to_point, apply_layer_affine_to_rect, apply_layer_to_quad,
apply_layer_to_rect, layer_uniform_scale,
},
style_shared::{
ResolvedBrush, apply_layer_to_brush, apply_layer_to_color, compose_color_filters,
resolve_layer_brush, scale_corner_radii,
},
};
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PrimitiveClipSpace {
Local,
LayerTransformed,
}
pub struct ShapeDrawParams {
pub rect: Rect,
pub local_rect: Rect,
pub quad: [[f32; 2]; 4],
pub brush: ResolvedBrush,
pub shape: Option<RoundedCornerShape>,
pub stroke: Option<Stroke>,
pub arc: Option<ArcGeometry>,
pub line: Option<LineGeometry>,
pub trapezoid: Option<Trapezoid>,
pub clip: Option<Rect>,
pub blend_mode: BlendMode,
pub motion_context_animated: bool,
}
fn stroked_draw_rect(
local_rect: Rect,
stroke: Option<Stroke>,
layer_bounds: Rect,
layer: &GraphicsLayer,
) -> Option<(Rect, Option<Stroke>)> {
let draw_rect = local_rect.translate(layer_bounds.x, layer_bounds.y);
let Some(stroke) = stroke else {
return Some((draw_rect, None));
};
if !stroke.is_visible() {
return None;
}
let outset = stroke.half_width();
Some((
inflate_rect(draw_rect, outset),
Some(stroke.scaled(layer_uniform_scale(layer))),
))
}
pub struct ImageDrawParams {
pub rect: Rect,
pub local_rect: Rect,
pub quad: [[f32; 2]; 4],
pub image: ImageBitmap,
pub alpha: f32,
pub color_filter: Option<ColorFilter>,
pub sampling: ImageSampling,
pub clip: Option<Rect>,
pub src_rect: Option<Rect>,
pub blend_mode: BlendMode,
pub motion_context_animated: bool,
}
pub struct TextDrawParams {
pub rect: Rect,
pub text: Rc<AnnotatedString>,
pub layout: cranpose_text::TextLayout,
pub color: Color,
pub text_style: Arc<TextStyle>,
pub style_hash: u64,
pub font_size: f32,
pub scale: f32,
pub clip: Option<Rect>,
}
pub trait DrawPrimitiveSink {
fn push_shape(&mut self, params: ShapeDrawParams);
fn push_image(&mut self, params: ImageDrawParams);
fn push_shadow(
&mut self,
shadow_primitive: &ShadowPrimitive,
layer_bounds: Rect,
layer: &GraphicsLayer,
clip: Option<Rect>,
);
fn push_text(&mut self, params: TextDrawParams) {
let _ = params;
}
}
pub fn draw_shape_params_for_primitive(
primitive: &DrawPrimitive,
layer_bounds: Rect,
layer: &GraphicsLayer,
clip: Option<Rect>,
blend_mode: BlendMode,
) -> Option<ShapeDrawParams> {
struct SingleShapeSink {
shape: Option<ShapeDrawParams>,
}
impl DrawPrimitiveSink for SingleShapeSink {
fn push_shape(&mut self, params: ShapeDrawParams) {
if self.shape.is_none() {
self.shape = Some(params);
}
}
fn push_image(&mut self, _params: ImageDrawParams) {}
fn push_shadow(
&mut self,
_shadow_primitive: &ShadowPrimitive,
_layer_bounds: Rect,
_layer: &GraphicsLayer,
_clip: Option<Rect>,
) {
}
}
let mut sink = SingleShapeSink { shape: None };
emit_draw_primitive(
primitive,
layer_bounds,
layer,
clip,
&mut sink,
Some(blend_mode),
false,
);
sink.shape
}
pub fn resolve_clip(parent_clip: Option<Rect>, requested_clip: Option<Rect>) -> Option<Rect> {
match (parent_clip, requested_clip) {
(Some(parent), Some(current)) => Some(parent.intersect(current).unwrap_or(Rect::EMPTY)),
(Some(parent), None) => Some(parent),
(None, Some(current)) => Some(current),
(None, None) => None,
}
}
pub fn resolve_primitive_clip(
local_clip: Option<Rect>,
layer_bounds: Rect,
layer: &GraphicsLayer,
parent_clip: Option<Rect>,
clip_space: PrimitiveClipSpace,
) -> Option<Rect> {
let Some(local_clip) = local_clip else {
return parent_clip;
};
let clip_rect = Rect {
x: layer_bounds.x + local_clip.x,
y: layer_bounds.y + local_clip.y,
width: local_clip.width,
height: local_clip.height,
};
let requested_clip = match clip_space {
PrimitiveClipSpace::Local => clip_rect,
PrimitiveClipSpace::LayerTransformed => apply_layer_to_rect(clip_rect, layer_bounds, layer),
};
resolve_clip(parent_clip, Some(requested_clip))
}
struct ShapePlacement<'a> {
layer_bounds: Rect,
layer: &'a GraphicsLayer,
clip: Option<Rect>,
blend_mode: BlendMode,
motion_context_animated: bool,
}
impl ShapePlacement<'_> {
fn fill(&self, draw_rect: Rect, brush: &Brush) -> ShapeDrawParams {
let quad = apply_layer_to_quad(draw_rect, self.layer_bounds, self.layer);
ShapeDrawParams {
rect: quad_bounds(quad),
local_rect: apply_layer_affine_to_rect(draw_rect, self.layer_bounds, self.layer),
quad,
brush: resolve_layer_brush(brush, self.layer),
shape: None,
stroke: None,
arc: None,
line: None,
trapezoid: None,
clip: self.clip,
blend_mode: self.blend_mode,
motion_context_animated: self.motion_context_animated,
}
}
fn translate(&self, rect: Rect) -> Rect {
rect.translate(self.layer_bounds.x, self.layer_bounds.y)
}
fn place(&self, point: Point) -> Point {
apply_layer_affine_to_point(
Point::new(point.x + self.layer_bounds.x, point.y + self.layer_bounds.y),
self.layer_bounds,
self.layer,
)
}
}
fn rect_shape_params(
local_rect: Rect,
brush: &Brush,
stroke: Option<Stroke>,
placement: &ShapePlacement,
) -> Option<ShapeDrawParams> {
let (draw_rect, stroke) =
stroked_draw_rect(local_rect, stroke, placement.layer_bounds, placement.layer)?;
Some(ShapeDrawParams {
stroke,
..placement.fill(draw_rect, brush)
})
}
fn round_rect_shape_params(
local_rect: Rect,
brush: &Brush,
radii: CornerRadii,
stroke: Option<Stroke>,
placement: &ShapePlacement,
) -> Option<ShapeDrawParams> {
let (draw_rect, stroke) =
stroked_draw_rect(local_rect, stroke, placement.layer_bounds, placement.layer)?;
let radii = scale_corner_radii(radii, layer_uniform_scale(placement.layer));
Some(ShapeDrawParams {
shape: Some(RoundedCornerShape::with_radii(radii)),
stroke,
..placement.fill(draw_rect, brush)
})
}
fn arc_shape_params(
local_rect: Rect,
brush: &Brush,
arc: ArcGeometry,
placement: &ShapePlacement,
) -> Option<ShapeDrawParams> {
if arc.is_degenerate() {
return None;
}
let scale = layer_uniform_scale(placement.layer);
Some(ShapeDrawParams {
arc: Some(arc.scaled_about(placement.place(arc.center), scale)),
..placement.fill(placement.translate(local_rect), brush)
})
}
fn line_shape_params(
brush: &Brush,
start: Point,
end: Point,
stroke: Stroke,
placement: &ShapePlacement,
) -> Option<ShapeDrawParams> {
let line = LineGeometry::new(start, end, stroke);
if line.is_degenerate() {
return None;
}
let scale = layer_uniform_scale(placement.layer);
Some(ShapeDrawParams {
line: Some(line.placed(placement.place(start), placement.place(end), scale)),
..placement.fill(placement.translate(line.bounds()), brush)
})
}
fn trapezoid_shape_params(
brush_rect: Rect,
brush: &Brush,
trapezoid: Trapezoid,
placement: &ShapePlacement,
) -> Option<ShapeDrawParams> {
if trapezoid.right <= trapezoid.left {
return None;
}
let place = |x: f32, y: f32| placement.place(Point::new(x, y));
let [top_left, top_right, bottom_left, bottom_right] = [
place(trapezoid.left, trapezoid.top[0]),
place(trapezoid.right, trapezoid.top[1]),
place(trapezoid.left, trapezoid.bottom[0]),
place(trapezoid.right, trapezoid.bottom[1]),
];
Some(ShapeDrawParams {
trapezoid: Some(Trapezoid {
left: top_left.x,
right: top_right.x,
top: [top_left.y, top_right.y],
bottom: [bottom_left.y, bottom_right.y],
..trapezoid
}),
..placement.fill(placement.translate(brush_rect), brush)
})
}
pub fn emit_draw_primitive<S: DrawPrimitiveSink>(
primitive: &DrawPrimitive,
layer_bounds: Rect,
layer: &GraphicsLayer,
clip: Option<Rect>,
sink: &mut S,
blend_mode: Option<BlendMode>,
motion_context_animated: bool,
) {
let placement = ShapePlacement {
layer_bounds,
layer,
clip,
blend_mode: blend_mode.unwrap_or(BlendMode::SrcOver),
motion_context_animated,
};
match primitive {
DrawPrimitive::Content | DrawPrimitive::Group(_) => {}
DrawPrimitive::Blend {
primitive,
blend_mode: nested,
} => emit_draw_primitive(
primitive,
layer_bounds,
layer,
clip,
sink,
blend_mode.or(Some(*nested)),
motion_context_animated,
),
DrawPrimitive::Rect {
rect,
brush,
stroke,
} => {
if let Some(params) = rect_shape_params(*rect, brush, *stroke, &placement) {
sink.push_shape(params);
}
}
DrawPrimitive::RoundRect {
rect,
brush,
radii,
stroke,
} => {
if let Some(params) = round_rect_shape_params(*rect, brush, *radii, *stroke, &placement)
{
sink.push_shape(params);
}
}
DrawPrimitive::Arc {
rect,
brush,
center,
radius,
start_angle,
sweep_angle,
stroke,
inner_radius,
} => {
let (band_inner, band_outer, cap) = arc_band(*radius, *inner_radius, *stroke);
let arc = ArcGeometry::new(
*center,
band_inner,
band_outer,
*start_angle,
*sweep_angle,
cap,
);
if let Some(params) = arc_shape_params(*rect, brush, arc, &placement) {
sink.push_shape(params);
}
}
DrawPrimitive::Line {
brush,
start,
end,
stroke,
..
} => {
if let Some(params) = line_shape_params(brush, *start, *end, *stroke, &placement) {
sink.push_shape(params);
}
}
DrawPrimitive::Trapezoid {
rect,
brush,
trapezoid,
} => {
if let Some(params) = trapezoid_shape_params(*rect, brush, *trapezoid, &placement) {
sink.push_shape(params);
}
}
DrawPrimitive::Image {
rect: local_rect,
image,
alpha,
color_filter,
sampling,
src_rect,
} => {
let draw_rect = local_rect.translate(layer_bounds.x, layer_bounds.y);
let local_rect = apply_layer_affine_to_rect(draw_rect, layer_bounds, layer);
let quad = apply_layer_to_quad(draw_rect, layer_bounds, layer);
sink.push_image(ImageDrawParams {
rect: quad_bounds(quad),
local_rect,
quad,
image: image.clone(),
alpha: (alpha * layer.alpha).clamp(0.0, 1.0),
color_filter: compose_color_filters(
color_filter.as_ref(),
layer.color_filter.as_ref(),
),
sampling: *sampling,
clip,
src_rect: *src_rect,
blend_mode: blend_mode.unwrap_or(BlendMode::SrcOver),
motion_context_animated,
});
}
DrawPrimitive::Text(text) => {
if let Some(params) = text_draw_params((**text).clone(), layer_bounds, layer, clip) {
sink.push_text(params);
}
}
DrawPrimitive::Shadow(shadow_primitive) => {
sink.push_shadow(shadow_primitive, layer_bounds, layer, clip);
}
}
}
fn text_draw_params(
text: TextPrimitive,
layer_bounds: Rect,
layer: &GraphicsLayer,
clip: Option<Rect>,
) -> Option<TextDrawParams> {
if text.text.is_empty() {
return None;
}
let layout = cranpose_text::TextLayout::clone(
&*text
.layout
.as_ref()?
.downcast::<cranpose_text::TextLayout>()?,
);
let draw_rect = text.rect.translate(layer_bounds.x, layer_bounds.y);
let rect = apply_layer_to_rect(draw_rect, layer_bounds, layer);
if !(rect.width > 0.0 && rect.height > 0.0) {
return None;
}
let scale = layer_uniform_scale(layer);
if !scale.is_finite() || scale <= 0.0 {
return None;
}
let (mut text_style, mut style_hash) = drawn_text_style(&text.style);
let color = match text.brush {
Brush::Solid(color) => apply_layer_to_color(color, layer),
gradient => {
let gradient = apply_layer_to_brush(gradient, layer);
let first = gradient
.colors()
.first()
.copied()
.unwrap_or(Color::TRANSPARENT);
let style = Arc::make_mut(&mut text_style);
style.span_style.brush = Some(gradient);
style_hash = style.render_hash();
first
}
};
if !text_style
.span_style
.brush
.as_ref()
.map_or(color.3 > 0.0, Brush::paints)
{
return None;
}
Some(TextDrawParams {
rect,
text: cranpose_ui::text::shared_plain_annotated_string(text.text.as_ref()),
layout,
color,
text_style,
style_hash,
font_size: text.style.resolved_font_size(),
scale,
clip,
})
}
#[cfg(test)]
#[path = "tests/primitive_emit_tests.rs"]
mod tests;