use iced::widget::canvas;
use iced::{Point, Radians, Vector};
use crate::PlushieRenderer;
pub(crate) const MAX_SHAPES_PER_LAYER: usize = 10_000;
#[derive(Debug, Clone)]
#[allow(dead_code)] pub(crate) enum HitRegion {
Rect {
x: f32,
y: f32,
w: f32,
h: f32,
},
Circle {
cx: f32,
cy: f32,
r: f32,
},
Line {
x1: f32,
y1: f32,
x2: f32,
y2: f32,
half_width: f32,
},
}
fn clean_coordinate(value: f32) -> f32 {
if value.is_finite() { value } else { 0.0 }
}
fn clean_extent(value: f32) -> f32 {
if value.is_finite() {
value.max(0.0)
} else {
0.0
}
}
impl HitRegion {
pub(crate) fn normalized(&self) -> Self {
match *self {
Self::Rect { x, y, w, h } => Self::Rect {
x: clean_coordinate(x),
y: clean_coordinate(y),
w: clean_extent(w),
h: clean_extent(h),
},
Self::Circle { cx, cy, r } => Self::Circle {
cx: clean_coordinate(cx),
cy: clean_coordinate(cy),
r: clean_extent(r),
},
Self::Line {
x1,
y1,
x2,
y2,
half_width,
} => Self::Line {
x1: clean_coordinate(x1),
y1: clean_coordinate(y1),
x2: clean_coordinate(x2),
y2: clean_coordinate(y2),
half_width: clean_extent(half_width),
},
}
}
}
#[derive(Debug, Clone, Copy)]
pub(crate) struct TransformMatrix {
pub a: f32,
pub b: f32,
pub c: f32,
pub d: f32,
pub tx: f32,
pub ty: f32,
}
impl TransformMatrix {
pub fn identity() -> Self {
Self {
a: 1.0,
b: 0.0,
c: 0.0,
d: 1.0,
tx: 0.0,
ty: 0.0,
}
}
pub fn translate(self, x: f32, y: f32) -> Self {
Self {
a: self.a,
b: self.b,
c: self.c,
d: self.d,
tx: self.a * x + self.b * y + self.tx,
ty: self.c * x + self.d * y + self.ty,
}
}
pub fn rotate(self, angle: f32) -> Self {
let cos = angle.cos();
let sin = angle.sin();
Self {
a: self.a * cos + self.b * sin,
b: self.b * cos - self.a * sin,
c: self.c * cos + self.d * sin,
d: self.d * cos - self.c * sin,
tx: self.tx,
ty: self.ty,
}
}
pub fn scale(self, sx: f32, sy: f32) -> Self {
Self {
a: self.a * sx,
b: self.b * sy,
c: self.c * sx,
d: self.d * sy,
tx: self.tx,
ty: self.ty,
}
}
pub fn transform_point(&self, x: f32, y: f32) -> (f32, f32) {
(
self.a * x + self.b * y + self.tx,
self.c * x + self.d * y + self.ty,
)
}
pub fn compose(&self, other: &Self) -> Self {
Self {
a: self.a * other.a + self.b * other.c,
b: self.a * other.b + self.b * other.d,
c: self.c * other.a + self.d * other.c,
d: self.c * other.b + self.d * other.d,
tx: self.a * other.tx + self.b * other.ty + self.tx,
ty: self.c * other.tx + self.d * other.ty + self.ty,
}
}
pub fn inverse(&self) -> Option<Self> {
let det = self.a * self.d - self.b * self.c;
if det.abs() < 1e-10 {
return None;
}
let inv_det = 1.0 / det;
Some(Self {
a: self.d * inv_det,
b: -self.b * inv_det,
c: -self.c * inv_det,
d: self.a * inv_det,
tx: (self.b * self.ty - self.d * self.tx) * inv_det,
ty: (self.c * self.tx - self.a * self.ty) * inv_det,
})
}
pub fn decompose(&self) -> (f32, f32, f32, f32, f32) {
const MIN_SCALE: f32 = 1e-10;
let tx = if self.tx.is_finite() { self.tx } else { 0.0 };
let ty = if self.ty.is_finite() { self.ty } else { 0.0 };
let angle = self.c.atan2(self.a);
let angle = if angle.is_finite() { angle } else { 0.0 };
let sx = (self.a * self.a + self.c * self.c).sqrt();
let sx = if sx.is_finite() {
sx.max(MIN_SCALE)
} else {
1.0
};
let det = self.a * self.d - self.b * self.c;
let sy = if det.is_finite() {
let sy = det / sx;
if sy.is_finite() {
let sign = if sy.is_sign_negative() { -1.0 } else { 1.0 };
sign * sy.abs().max(MIN_SCALE)
} else {
1.0
}
} else {
1.0
};
(tx, ty, angle, sx, sy)
}
pub fn apply_to_frame<R: PlushieRenderer>(&self, frame: &mut canvas::Frame<R>) {
let (tx, ty, angle, sx, sy) = self.decompose();
frame.translate(Vector::new(tx, ty));
if angle.abs() > 1e-6 {
frame.rotate(Radians(angle));
}
if (sx - 1.0).abs() > 1e-6 || (sy - 1.0).abs() > 1e-6 {
frame.scale_nonuniform(Vector::new(sx, sy));
}
}
#[cfg(test)]
pub fn from_transforms(transforms: &[serde_json::Value]) -> Self {
let mut m = Self::identity();
for t in transforms {
let t_type = t.get("type").and_then(|v| v.as_str()).unwrap_or("");
match t_type {
"translate" => {
let x = t.get("x").and_then(|v| v.as_f64()).unwrap_or(0.0) as f32;
let y = t.get("y").and_then(|v| v.as_f64()).unwrap_or(0.0) as f32;
m = m.translate(x, y);
}
"rotate" => {
let deg = t.get("angle").and_then(|v| v.as_f64()).unwrap_or(0.0) as f32;
m = m.rotate(deg.to_radians());
}
"scale" => {
if let Some(factor) = t.get("factor").and_then(|v| v.as_f64()) {
let f = factor as f32;
m = m.scale(f, f);
} else {
let sx = t.get("x").and_then(|v| v.as_f64()).unwrap_or(1.0) as f32;
let sy = t.get("y").and_then(|v| v.as_f64()).unwrap_or(1.0) as f32;
m = m.scale(sx, sy);
}
}
_ => {}
}
}
m
}
pub fn from_typed_transforms(transforms: &[plushie_core::types::Transform]) -> Self {
use plushie_core::types::Transform;
let mut m = Self::identity();
for t in transforms {
match t {
Transform::Translate { x, y } => m = m.translate(*x, *y),
Transform::Rotate { angle } => m = m.rotate(angle.radians()),
Transform::Scale { x, y } => m = m.scale(*x, *y),
Transform::ScaleUniform { factor } => m = m.scale(*factor, *factor),
}
}
m
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
pub(crate) enum ArrowMode {
#[default]
Wrap,
Clamp,
Linear,
None,
}
impl ArrowMode {
#[cfg(test)]
pub fn from_str(s: &str) -> Self {
match s {
"wrap" => Self::Wrap,
"clamp" => Self::Clamp,
"linear" => Self::Linear,
"none" => Self::None,
_ => {
log::warn!("canvas: unknown arrow_mode '{s}', defaulting to 'wrap'");
Self::Wrap
}
}
}
}
impl From<plushie_core::types::ArrowMode> for ArrowMode {
fn from(mode: plushie_core::types::ArrowMode) -> Self {
match mode {
plushie_core::types::ArrowMode::Wrap => Self::Wrap,
plushie_core::types::ArrowMode::Clamp => Self::Clamp,
plushie_core::types::ArrowMode::Linear => Self::Linear,
plushie_core::types::ArrowMode::None => Self::None,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub(crate) enum DragAxis {
Both,
X,
Y,
}
#[derive(Debug, Clone)]
pub(crate) struct DragBounds {
pub min_x: f32,
pub max_x: f32,
pub min_y: f32,
pub max_y: f32,
}
#[derive(Debug, Clone)]
pub(crate) struct InteractiveElement {
pub id: String,
pub layer: String,
pub hit_region: HitRegion,
pub transform: TransformMatrix,
pub inverse_transform: Option<TransformMatrix>,
pub clip_rect: Option<(f32, f32, f32, f32)>,
pub on_click: bool,
pub on_hover: bool,
pub draggable: bool,
pub drag_axis: DragAxis,
pub drag_bounds: Option<DragBounds>,
pub cursor: Option<String>,
pub has_hover_style: bool,
pub has_pressed_style: bool,
pub has_focus_style: bool,
pub show_focus_ring: bool,
pub focus_ring_radius: Option<f32>,
pub focusable: bool,
pub parent_group: Option<String>,
pub tooltip: Option<String>,
pub a11y: Option<crate::a11y::A11yOverrides>,
}
#[derive(Debug, Clone)]
pub(crate) struct DragState {
pub element_id: String,
pub last: Point,
}
#[derive(Default)]
pub(crate) struct CanvasState {
pub cursor_position: Option<Point>,
pub hovered_element: Option<String>,
pub pressed_element: Option<String>,
pub dragging: Option<DragState>,
pub focused_id: Option<String>,
pub focused_group: Option<String>,
pub last_consumed_pending: Option<String>,
pub canvas_focused: bool,
pub focus_visible: bool,
pub current_modifiers: iced::keyboard::Modifiers,
}