use std::fmt;
use std::sync::Arc;
use serde::{Deserialize, Serialize};
use crate::{
Color, ColorRole, Dp, Dpi, NativeDrawCommand, NativeDrawFill, NativeDrawIconCommand,
NativeDrawPlan, NativeIconColorMode, Rect, SemanticTextStyle, WidgetId, ZsIcon,
ZsPointerButton, ZsPointerModifiers,
};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum ZsCanvasPointerPhase {
Pressed,
Moved,
Released,
Cancelled,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct ZsCanvasPointerEvent {
pub widget: WidgetId,
pub phase: ZsCanvasPointerPhase,
pub position: ZsCanvasPoint,
pub button: ZsPointerButton,
pub modifiers: ZsPointerModifiers,
pub inside: bool,
#[serde(default)]
pub region: Option<u64>,
}
impl ZsCanvasPointerEvent {
pub const fn new(
widget: WidgetId,
phase: ZsCanvasPointerPhase,
position: ZsCanvasPoint,
button: ZsPointerButton,
modifiers: ZsPointerModifiers,
inside: bool,
) -> Self {
Self {
widget,
phase,
position,
button,
modifiers,
inside,
region: None,
}
}
pub const fn with_region(mut self, region: Option<u64>) -> Self {
self.region = region;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct ZsCanvasPoint {
pub x: Dp,
pub y: Dp,
}
impl ZsCanvasPoint {
pub const fn new(x: Dp, y: Dp) -> Self {
Self { x, y }
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct ZsCanvasRect {
pub x: Dp,
pub y: Dp,
pub width: Dp,
pub height: Dp,
}
impl ZsCanvasRect {
pub const fn new(x: Dp, y: Dp, width: Dp, height: Dp) -> Self {
Self {
x,
y,
width,
height,
}
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub enum ZsCanvasPrimitive {
FillRect {
rect: ZsCanvasRect,
fill: NativeDrawFill,
},
StrokeRect {
rect: ZsCanvasRect,
stroke: NativeDrawFill,
width: Dp,
},
StrokeArc {
rect: ZsCanvasRect,
stroke: NativeDrawFill,
width: Dp,
start_degrees: i16,
sweep_degrees: i16,
},
FillTriangle {
points: [ZsCanvasPoint; 3],
fill: NativeDrawFill,
},
RoundRect {
rect: ZsCanvasRect,
fill: NativeDrawFill,
stroke: Option<NativeDrawFill>,
radius: Dp,
},
RoundFill {
rect: ZsCanvasRect,
fill: NativeDrawFill,
radius: Dp,
},
Text {
text: String,
rect: ZsCanvasRect,
style: SemanticTextStyle,
},
Icon {
icon: ZsIcon,
rect: ZsCanvasRect,
color: ColorRole,
},
TextRun {
text: String,
rect: ZsCanvasRect,
style: SemanticTextStyle,
color: Option<Color>,
size: Option<Dp>,
},
ColoredIcon {
icon: ZsIcon,
rect: ZsCanvasRect,
color: Color,
fallback: ColorRole,
},
}
impl ZsCanvasPrimitive {
pub fn fill_rect(rect: ZsCanvasRect, fill: NativeDrawFill) -> Self {
Self::FillRect { rect, fill }
}
pub fn round_fill(rect: ZsCanvasRect, fill: NativeDrawFill, radius: Dp) -> Self {
Self::RoundFill { rect, fill, radius }
}
pub fn text(text: impl Into<String>, rect: ZsCanvasRect, style: SemanticTextStyle) -> Self {
Self::Text {
text: text.into(),
rect,
style,
}
}
pub const fn icon(icon: ZsIcon, rect: ZsCanvasRect, color: ColorRole) -> Self {
Self::Icon { icon, rect, color }
}
pub fn colored_text(
text: impl Into<String>,
rect: ZsCanvasRect,
style: SemanticTextStyle,
color: Color,
) -> Self {
Self::text_run(text, rect, style, Some(color), None)
}
pub fn text_run(
text: impl Into<String>,
rect: ZsCanvasRect,
style: SemanticTextStyle,
color: Option<Color>,
size: Option<Dp>,
) -> Self {
Self::TextRun {
text: text.into(),
rect,
style,
color,
size,
}
}
pub fn glyph(glyph: char, rect: ZsCanvasRect, color: Color, fallback: ColorRole) -> Self {
let mut style = SemanticTextStyle::for_role(crate::TextRole::Icon);
style.color = fallback;
style.horizontal_align = crate::HorizontalAlign::Center;
style.ellipsis = false;
Self::text_run(
glyph.to_string(),
rect,
style,
Some(color),
Some(Dp::new(rect.width.0.min(rect.height.0).max(1.0))),
)
}
pub const fn colored_icon(
icon: ZsIcon,
rect: ZsCanvasRect,
color: Color,
fallback: ColorRole,
) -> Self {
Self::ColoredIcon {
icon,
rect,
color,
fallback,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct ZsCanvasScene {
primitives: Vec<ZsCanvasPrimitive>,
#[serde(default, skip_serializing_if = "Option::is_none")]
extent_height: Option<Dp>,
#[serde(default, skip_serializing_if = "Vec::is_empty")]
hover_regions: Vec<ZsCanvasHoverRegion>,
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct ZsCanvasHoverRegion {
pub id: u64,
pub rect: ZsCanvasRect,
}
impl ZsCanvasScene {
pub const fn new() -> Self {
Self {
primitives: Vec::new(),
extent_height: None,
hover_regions: Vec::new(),
}
}
pub fn with_extent_height(mut self, height: Dp) -> Self {
self.extent_height = Some(Dp::new(sanitized_extent(height.0)));
self
}
pub fn set_extent_height(&mut self, height: Dp) {
self.extent_height = Some(Dp::new(sanitized_extent(height.0)));
}
pub fn extent_height(&self) -> Option<Dp> {
self.extent_height
}
pub fn with_hover_region(mut self, id: u64, rect: ZsCanvasRect) -> Self {
self.push_hover_region(id, rect);
self
}
pub fn push_hover_region(&mut self, id: u64, rect: ZsCanvasRect) {
self.hover_regions.push(ZsCanvasHoverRegion { id, rect });
}
pub fn hover_regions(&self) -> &[ZsCanvasHoverRegion] {
&self.hover_regions
}
pub fn hover_region_at(&self, point: ZsCanvasPoint) -> Option<u64> {
self.hover_regions
.iter()
.rev()
.find(|region| {
let rect = region.rect;
point.x.0 >= rect.x.0
&& point.y.0 >= rect.y.0
&& point.x.0 < rect.x.0 + rect.width.0
&& point.y.0 < rect.y.0 + rect.height.0
})
.map(|region| region.id)
}
pub fn with(mut self, primitive: ZsCanvasPrimitive) -> Self {
self.primitives.push(primitive);
self
}
pub fn push(&mut self, primitive: ZsCanvasPrimitive) {
self.primitives.push(primitive);
}
pub fn primitives(&self) -> &[ZsCanvasPrimitive] {
&self.primitives
}
pub fn primitive_count(&self) -> usize {
self.primitives.len()
}
pub fn is_empty(&self) -> bool {
self.primitives.is_empty()
}
}
impl FromIterator<ZsCanvasPrimitive> for ZsCanvasScene {
fn from_iter<T: IntoIterator<Item = ZsCanvasPrimitive>>(iter: T) -> Self {
Self {
primitives: iter.into_iter().collect(),
extent_height: None,
hover_regions: Vec::new(),
}
}
}
fn sanitized_extent(value: f32) -> f32 {
if value.is_finite() {
value.max(0.0)
} else {
0.0
}
}
#[derive(Debug, Clone, Copy, PartialEq, Serialize, Deserialize)]
pub struct ZsCanvasSize {
pub width: Dp,
pub height: Dp,
}
impl ZsCanvasSize {
pub const fn new(width: Dp, height: Dp) -> Self {
Self { width, height }
}
}
pub struct ZsCanvasLayoutContext<'a> {
size: ZsCanvasSize,
dpi: Dpi,
typography_scale: f32,
measurements: Option<&'a crate::view::ViewTextMeasurements>,
}
impl<'a> ZsCanvasLayoutContext<'a> {
pub(crate) fn new(
size: ZsCanvasSize,
dpi: Dpi,
typography_scale: f32,
measurements: Option<&'a crate::view::ViewTextMeasurements>,
) -> Self {
Self {
size,
dpi,
typography_scale,
measurements,
}
}
pub fn detached(size: ZsCanvasSize) -> ZsCanvasLayoutContext<'static> {
ZsCanvasLayoutContext {
size,
dpi: Dpi::standard(),
typography_scale: 1.0,
measurements: None,
}
}
pub fn size(&self) -> ZsCanvasSize {
self.size
}
pub fn measure_text(
&self,
text: &str,
style: SemanticTextStyle,
max_width: Option<Dp>,
) -> ZsCanvasSize {
let max_px = max_width
.filter(|_| style.wrap == crate::TextWrap::Word)
.map(|width| canvas_non_negative_px(width, self.dpi))
.unwrap_or(0);
if let Some(size) = self
.measurements
.and_then(|measurements| measurements.measure(text, style, max_px))
{
let scale = self.dpi.scale_factor().max(f32::EPSILON);
return ZsCanvasSize::new(
Dp::new(size.width.max(0) as f32 / scale),
Dp::new(size.height.max(0) as f32 / scale),
);
}
self.estimate_text(text, style, max_width)
}
fn estimate_text(
&self,
text: &str,
style: SemanticTextStyle,
max_width: Option<Dp>,
) -> ZsCanvasSize {
let metrics = style
.role
.metrics_for(crate::ZsTypographyPlatformStyle::current());
let scale = self.typography_scale.max(0.0);
let unit = metrics.size * 0.5 * scale;
let weight = match style.weight {
crate::TextWeight::Semibold | crate::TextWeight::Bold => 1.08,
crate::TextWeight::Medium => 1.04,
crate::TextWeight::Automatic | crate::TextWeight::Regular => 1.0,
};
let line_height = metrics.line_height * scale;
let wrap_width = max_width
.filter(|_| style.wrap == crate::TextWrap::Word)
.map(|width| width.0.max(1.0));
let mut widest = 0.0_f32;
let mut rows = 0_u32;
for line in text.split('\n') {
let width =
crate::widget_render::zs_estimated_text_width_units(line) as f32 * unit * weight;
match wrap_width {
Some(limit) if width > limit => {
rows += (width / limit).ceil().max(1.0) as u32;
widest = widest.max(limit);
}
_ => {
rows += 1;
widest = widest.max(width);
}
}
}
ZsCanvasSize::new(
Dp::new(widest.ceil()),
Dp::new(line_height * rows.max(1) as f32),
)
}
}
type ZsCanvasBuildFn = dyn Fn(&ZsCanvasLayoutContext<'_>) -> ZsCanvasScene + Send + Sync;
#[derive(Clone)]
pub struct ZsCanvasBuilder {
build: Arc<ZsCanvasBuildFn>,
}
impl ZsCanvasBuilder {
pub fn new(
build: impl Fn(&ZsCanvasLayoutContext<'_>) -> ZsCanvasScene + Send + Sync + 'static,
) -> Self {
Self {
build: Arc::new(build),
}
}
pub fn build(&self, context: &ZsCanvasLayoutContext<'_>) -> ZsCanvasScene {
(self.build)(context)
}
}
impl fmt::Debug for ZsCanvasBuilder {
fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
formatter.write_str("ZsCanvasBuilder")
}
}
pub fn zs_canvas_native_draw_plan(bounds: Rect, scene: &ZsCanvasScene, dpi: Dpi) -> NativeDrawPlan {
let mut plan = NativeDrawPlan::default();
plan.push(NativeDrawCommand::PushClip { rect: bounds });
for primitive in scene.primitives() {
plan.push(canvas_primitive_to_native(bounds, primitive, dpi));
}
plan.push(NativeDrawCommand::PopClip);
plan
}
fn canvas_primitive_to_native(
bounds: Rect,
primitive: &ZsCanvasPrimitive,
dpi: Dpi,
) -> NativeDrawCommand {
match primitive {
ZsCanvasPrimitive::FillRect { rect, fill } => NativeDrawCommand::FillRect {
rect: canvas_rect_to_native(bounds, *rect, dpi),
fill: *fill,
},
ZsCanvasPrimitive::StrokeRect {
rect,
stroke,
width,
} => NativeDrawCommand::StrokeRect {
rect: canvas_rect_to_native(bounds, *rect, dpi),
stroke: *stroke,
width: canvas_non_negative_px(*width, dpi).max(1),
},
ZsCanvasPrimitive::StrokeArc {
rect,
stroke,
width,
start_degrees,
sweep_degrees,
} => NativeDrawCommand::StrokeArc {
rect: canvas_rect_to_native(bounds, *rect, dpi),
stroke: *stroke,
width: canvas_non_negative_px(*width, dpi).max(1),
start_degrees: *start_degrees,
sweep_degrees: *sweep_degrees,
},
ZsCanvasPrimitive::FillTriangle { points, fill } => NativeDrawCommand::FillTriangle {
points: points.map(|point| canvas_point_to_native(bounds, point, dpi)),
fill: *fill,
},
ZsCanvasPrimitive::RoundRect {
rect,
fill,
stroke,
radius,
} => NativeDrawCommand::RoundRect {
rect: canvas_rect_to_native(bounds, *rect, dpi),
fill: *fill,
stroke: *stroke,
radius: canvas_non_negative_px(*radius, dpi),
},
ZsCanvasPrimitive::RoundFill { rect, fill, radius } => NativeDrawCommand::RoundFill {
rect: canvas_rect_to_native(bounds, *rect, dpi),
fill: *fill,
radius: canvas_non_negative_px(*radius, dpi),
},
ZsCanvasPrimitive::Text { text, rect, style } => {
NativeDrawCommand::Text(crate::NativeDrawTextCommand::new(
text,
canvas_rect_to_native(bounds, *rect, dpi),
*style,
))
}
ZsCanvasPrimitive::Icon { icon, rect, color } => NativeDrawCommand::Icon(
NativeDrawIconCommand::new(
*icon,
canvas_rect_to_native(bounds, *rect, dpi),
NativeIconColorMode::ThemeAware,
)
.with_color(*color),
),
ZsCanvasPrimitive::TextRun {
text,
rect,
style,
color,
size,
} => {
let mut command = crate::NativeDrawTextCommand::new(
text,
canvas_rect_to_native(bounds, *rect, dpi),
*style,
);
command.color = *color;
command.size = size.map(|size| crate::ZsFontSize::from_dip(size.0));
NativeDrawCommand::Text(command)
}
ZsCanvasPrimitive::ColoredIcon {
icon,
rect,
color,
fallback,
} => NativeDrawCommand::Icon(
NativeDrawIconCommand::new(
*icon,
canvas_rect_to_native(bounds, *rect, dpi),
NativeIconColorMode::ThemeAware,
)
.with_color(*fallback)
.with_custom_color(*color),
),
}
}
fn canvas_point_to_native(bounds: Rect, point: ZsCanvasPoint, dpi: Dpi) -> crate::Point {
crate::Point {
x: bounds.x.saturating_add(canvas_signed_px(point.x, dpi)),
y: bounds.y.saturating_add(canvas_signed_px(point.y, dpi)),
}
}
fn canvas_rect_to_native(bounds: Rect, rect: ZsCanvasRect, dpi: Dpi) -> Rect {
Rect {
x: bounds.x.saturating_add(canvas_signed_px(rect.x, dpi)),
y: bounds.y.saturating_add(canvas_signed_px(rect.y, dpi)),
width: canvas_non_negative_px(rect.width, dpi),
height: canvas_non_negative_px(rect.height, dpi),
}
}
fn canvas_signed_px(value: Dp, dpi: Dpi) -> i32 {
if value.0.is_finite() {
value.to_px(dpi).round_i32()
} else {
0
}
}
fn canvas_non_negative_px(value: Dp, dpi: Dpi) -> i32 {
canvas_signed_px(value, dpi).max(0)
}
pub(crate) fn zs_canvas_pointer_event(
widget: WidgetId,
bounds: Rect,
point: crate::Point,
dpi: Dpi,
phase: ZsCanvasPointerPhase,
button: ZsPointerButton,
modifiers: ZsPointerModifiers,
) -> ZsCanvasPointerEvent {
let scale = dpi.scale_factor();
ZsCanvasPointerEvent::new(
widget,
phase,
ZsCanvasPoint::new(
Dp::new((point.x.saturating_sub(bounds.x)) as f32 / scale),
Dp::new((point.y.saturating_sub(bounds.y)) as f32 / scale),
),
button,
modifiers,
bounds.contains(point),
)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn canvas_translates_local_dp_and_balances_clip() {
let scene = ZsCanvasScene::new()
.with(ZsCanvasPrimitive::fill_rect(
ZsCanvasRect::new(Dp::new(4.0), Dp::new(6.0), Dp::new(20.0), Dp::new(10.0)),
NativeDrawFill::role(ColorRole::Accent),
))
.with(ZsCanvasPrimitive::text(
"Canvas",
ZsCanvasRect::new(Dp::new(8.0), Dp::new(20.0), Dp::new(80.0), Dp::new(24.0)),
SemanticTextStyle::body(),
));
let plan = zs_canvas_native_draw_plan(
Rect {
x: 100,
y: 40,
width: 200,
height: 100,
},
&scene,
Dpi::new(192.0),
);
assert_eq!(plan.commands.len(), 4);
assert_eq!(
plan.commands.first(),
Some(&NativeDrawCommand::PushClip {
rect: Rect {
x: 100,
y: 40,
width: 200,
height: 100,
},
})
);
assert!(matches!(
&plan.commands[1],
NativeDrawCommand::FillRect {
rect: Rect {
x: 108,
y: 52,
width: 40,
height: 20,
},
..
}
));
assert_eq!(plan.commands.last(), Some(&NativeDrawCommand::PopClip));
}
#[test]
fn canvas_sanitizes_non_finite_and_negative_extents() {
let scene = ZsCanvasScene::new().with(ZsCanvasPrimitive::RoundFill {
rect: ZsCanvasRect::new(
Dp::new(f32::NAN),
Dp::new(f32::INFINITY),
Dp::new(-20.0),
Dp::new(12.0),
),
fill: NativeDrawFill::role(ColorRole::Control),
radius: Dp::new(-4.0),
});
let plan = zs_canvas_native_draw_plan(
Rect {
x: 5,
y: 7,
width: 80,
height: 40,
},
&scene,
Dpi::standard(),
);
assert!(matches!(
&plan.commands[1],
NativeDrawCommand::RoundFill {
rect: Rect {
x: 5,
y: 7,
width: 0,
height: 12,
},
radius: 0,
..
}
));
}
#[test]
fn canvas_pointer_events_use_local_dp_and_preserve_outside_drag_positions() {
let event = zs_canvas_pointer_event(
WidgetId::new(7),
Rect {
x: 100,
y: 40,
width: 200,
height: 100,
},
crate::Point { x: 80, y: 260 },
Dpi::new(192.0),
ZsCanvasPointerPhase::Moved,
ZsPointerButton::Secondary,
ZsPointerModifiers::new(true, false, true, false),
);
assert_eq!(
event.position,
ZsCanvasPoint::new(Dp::new(-10.0), Dp::new(110.0))
);
assert!(!event.inside);
assert_eq!(event.button, ZsPointerButton::Secondary);
assert!(event.modifiers.shift);
assert!(event.modifiers.alt);
}
}