use iced::widget::canvas;
use iced::{Color, Pixels, Point, Radians, Size, Vector, alignment};
use serde_json::Value;
use plushie_core::types::{self as canvas_types, CanvasShape, ClipRect, Transform};
use plushie_core::types::{Color as CoreColor, PlushieType};
use super::types::MAX_SHAPES_PER_LAYER;
use crate::PlushieRenderer;
use crate::iced_convert;
use crate::iced_convert::hex_to_iced_color;
pub(super) fn parse_fill_rule(value: Option<&Value>) -> canvas::fill::Rule {
match value.and_then(|v| v.as_str()) {
Some("even_odd") => canvas::fill::Rule::EvenOdd,
_ => canvas::fill::Rule::NonZero,
}
}
#[allow(dead_code)] pub(crate) fn parse_canvas_fill(value: &Value, shape: &Value) -> canvas::Fill {
parse_canvas_fill_themed(value, shape, None)
}
pub(super) fn parse_canvas_fill_themed(
value: &Value,
shape: &Value,
theme: Option<&iced::Theme>,
) -> canvas::Fill {
let rule = parse_fill_rule(shape.get("fill_rule"));
match value {
Value::String(_) => {
let color = theme
.and_then(|t| resolve_color(value, t))
.or_else(|| CoreColor::wire_decode(value).map(|c| iced_convert::color(&c)))
.unwrap_or(Color::WHITE);
canvas::Fill {
style: canvas::Style::Solid(color),
rule,
}
}
Value::Object(obj) => match obj.get("type").and_then(|v| v.as_str()) {
Some("linear") => {
let valid_keys: &[&str] = &["type", "start", "end", "stops"];
for key in obj.keys() {
if !valid_keys.contains(&key.as_str()) {
log::warn!(
"unrecognized canvas gradient key '{}' (valid: {:?})",
key,
valid_keys
);
}
}
let start = obj
.get("start")
.and_then(|v| v.as_array())
.map(|a| {
Point::new(
a.first().and_then(|v| v.as_f64()).unwrap_or(0.0) as f32,
a.get(1).and_then(|v| v.as_f64()).unwrap_or(0.0) as f32,
)
})
.unwrap_or(Point::ORIGIN);
let end = obj
.get("end")
.and_then(|v| v.as_array())
.map(|a| {
Point::new(
a.first().and_then(|v| v.as_f64()).unwrap_or(0.0) as f32,
a.get(1).and_then(|v| v.as_f64()).unwrap_or(0.0) as f32,
)
})
.unwrap_or(Point::ORIGIN);
let mut linear = canvas::gradient::Linear::new(start, end);
if let Some(stops) = obj.get("stops").and_then(|v| v.as_array()) {
for stop in stops {
if let Some(arr) = stop.as_array() {
let offset = arr.first().and_then(|v| v.as_f64()).unwrap_or(0.0) as f32;
let color = arr
.get(1)
.and_then(|v| {
theme.and_then(|t| resolve_color(v, t)).or_else(|| {
CoreColor::wire_decode(v).map(|c| iced_convert::color(&c))
})
})
.unwrap_or(Color::TRANSPARENT);
linear = linear.add_stop(offset, color);
}
}
}
canvas::Fill {
style: canvas::Style::Gradient(canvas::Gradient::Linear(linear)),
rule,
}
}
Some(other) => {
log::warn!(
"unrecognized canvas gradient type '{}' (supported: \"linear\")",
other
);
let color = CoreColor::wire_decode(value)
.map(|c| iced_convert::color(&c))
.unwrap_or(Color::WHITE);
canvas::Fill {
style: canvas::Style::Solid(color),
rule,
}
}
_ => {
let color = CoreColor::wire_decode(value)
.map(|c| iced_convert::color(&c))
.unwrap_or(Color::WHITE);
canvas::Fill {
style: canvas::Style::Solid(color),
rule,
}
}
},
_ => canvas::Fill {
style: canvas::Style::Solid(Color::WHITE),
rule,
},
}
}
#[allow(dead_code)] pub(crate) fn parse_canvas_stroke(value: &Value) -> canvas::Stroke<'static> {
parse_canvas_stroke_themed(value, None)
}
pub(super) fn parse_canvas_stroke_themed(
value: &Value,
theme: Option<&iced::Theme>,
) -> canvas::Stroke<'static> {
let obj = match value.as_object() {
Some(o) => o,
None => return canvas::Stroke::default(),
};
let color = theme
.and_then(|t| obj.get("color").and_then(|v| resolve_color(v, t)))
.or_else(|| {
obj.get("color")
.and_then(CoreColor::wire_decode)
.map(|c| iced_convert::color(&c))
})
.unwrap_or(Color::WHITE);
let width = obj
.get("width")
.and_then(|v| v.as_f64())
.map(|v| v as f32)
.unwrap_or(1.0);
let cap = match obj.get("cap").and_then(|v| v.as_str()).unwrap_or("butt") {
"round" => canvas::LineCap::Round,
"square" => canvas::LineCap::Square,
_ => canvas::LineCap::Butt,
};
let join = match obj.get("join").and_then(|v| v.as_str()).unwrap_or("miter") {
"round" => canvas::LineJoin::Round,
"bevel" => canvas::LineJoin::Bevel,
_ => canvas::LineJoin::Miter,
};
let mut stroke = canvas::Stroke::default()
.with_color(color)
.with_width(width)
.with_line_cap(cap)
.with_line_join(join);
if let Some(dash_obj) = obj.get("dash").and_then(|v| v.as_object()) {
let segments_val = dash_obj
.get("segments")
.and_then(|v| v.as_array())
.cloned()
.unwrap_or_default();
let segments: Vec<f32> = segments_val
.iter()
.filter_map(|v| v.as_f64().map(|n| n as f32))
.collect();
let offset = dash_obj
.get("offset")
.and_then(|v| v.as_u64())
.map(|v| v as usize)
.unwrap_or(0);
if let Some(segments) = intern_dash_segments(segments) {
stroke.line_dash = canvas::LineDash { segments, offset };
}
}
stroke
}
const MAX_DASH_CACHE: usize = 1024;
const MAX_DASH_SEGMENTS: usize = 64;
fn intern_dash_segments(segments: Vec<f32>) -> Option<&'static [f32]> {
use std::collections::HashMap;
use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{LazyLock, Mutex};
static CACHE: LazyLock<Mutex<HashMap<Vec<u32>, &'static [f32]>>> =
LazyLock::new(|| Mutex::new(HashMap::new()));
static CACHE_WARNED: AtomicBool = AtomicBool::new(false);
static SEGMENTS_WARNED: AtomicBool = AtomicBool::new(false);
if segments.len() > MAX_DASH_SEGMENTS {
if !SEGMENTS_WARNED.swap(true, Ordering::Relaxed) {
crate::diagnostics::info(plushie_core::Diagnostic::DashSegmentsCapExceeded {
max: MAX_DASH_SEGMENTS,
});
}
return None;
}
let key: Vec<u32> = segments.iter().map(|s| s.to_bits()).collect();
let mut cache = CACHE.lock().unwrap_or_else(|e| e.into_inner());
if let Some(existing) = cache.get(&key) {
return Some(existing);
}
if cache.len() >= MAX_DASH_CACHE {
if !CACHE_WARNED.swap(true, Ordering::Relaxed) {
crate::diagnostics::info(plushie_core::Diagnostic::DashCacheCapExceeded {
max: MAX_DASH_CACHE,
});
}
return None;
}
let leaked: &'static [f32] = Box::leak(segments.into_boxed_slice());
cache.insert(key, leaked);
Some(leaked)
}
pub(super) fn build_path_from_commands(commands: &[canvas_types::PathCommand]) -> canvas::Path {
use canvas_types::PathCommand;
canvas::Path::new(|builder| {
for cmd in commands {
match cmd {
PathCommand::MoveTo { x, y } => {
builder.move_to(Point::new(*x, *y));
}
PathCommand::LineTo { x, y } => {
builder.line_to(Point::new(*x, *y));
}
PathCommand::BezierTo {
cp1x,
cp1y,
cp2x,
cp2y,
x,
y,
} => {
builder.bezier_curve_to(
Point::new(*cp1x, *cp1y),
Point::new(*cp2x, *cp2y),
Point::new(*x, *y),
);
}
PathCommand::QuadraticTo { cpx, cpy, x, y } => {
builder.quadratic_curve_to(Point::new(*cpx, *cpy), Point::new(*x, *y));
}
PathCommand::Arc {
cx,
cy,
radius,
start_angle,
end_angle,
} => {
builder.arc(canvas::path::Arc {
center: Point::new(*cx, *cy),
radius: *radius,
start_angle: Radians(start_angle.radians()),
end_angle: Radians(end_angle.radians()),
});
}
PathCommand::ArcTo {
x1,
y1,
x2,
y2,
radius,
} => {
builder.arc_to(Point::new(*x1, *y1), Point::new(*x2, *y2), *radius);
}
PathCommand::Ellipse {
cx,
cy,
rx,
ry,
rotation,
start_angle,
end_angle,
} => {
builder.ellipse(canvas::path::arc::Elliptical {
center: Point::new(*cx, *cy),
radii: Vector::new(*rx, *ry),
rotation: Radians(rotation.radians()),
start_angle: Radians(start_angle.radians()),
end_angle: Radians(end_angle.radians()),
});
}
PathCommand::RoundedRect { x, y, w, h, radius } => {
let iced_radius = match radius {
plushie_core::types::Radius::Uniform(r) => iced::border::Radius::new(*r),
plushie_core::types::Radius::PerCorner {
top_left,
top_right,
bottom_right,
bottom_left,
} => iced::border::Radius::new(0.0)
.top_left(*top_left)
.top_right(*top_right)
.bottom_right(*bottom_right)
.bottom_left(*bottom_left),
};
builder.rounded_rectangle(Point::new(*x, *y), Size::new(*w, *h), iced_radius);
}
PathCommand::Close => {
builder.close();
}
}
}
})
}
pub(super) fn draw_canvas_shapes<R: PlushieRenderer>(
frame: &mut canvas::Frame<R>,
shapes: &[&CanvasShape],
images: &crate::image_registry::ImageRegistry,
theme: &iced::Theme,
) {
for shape in shapes {
draw_canvas_shape(frame, shape, images, theme);
}
}
fn apply_opacity_to_style(opacity: Option<f32>, mut style: canvas::Style) -> canvas::Style {
if let Some(a) = opacity {
match &mut style {
canvas::Style::Solid(c) => c.a *= a,
canvas::Style::Gradient(canvas::Gradient::Linear(linear)) => {
for stop in &mut linear.stops {
if let Some(stop) = stop.as_mut() {
stop.color.a *= a;
}
}
}
}
}
style
}
pub(super) fn apply_opacity_to_fill(opacity: Option<f32>, mut fill: canvas::Fill) -> canvas::Fill {
fill.style = apply_opacity_to_style(opacity, fill.style);
fill
}
pub(super) fn apply_opacity_to_stroke(
opacity: Option<f32>,
mut stroke: canvas::Stroke<'static>,
) -> canvas::Stroke<'static> {
stroke.style = apply_opacity_to_style(opacity, stroke.style);
stroke
}
pub(super) fn apply_opacity_to_color(opacity: Option<f32>, mut color: Color) -> Color {
if let Some(a) = opacity {
color.a *= a;
}
color
}
#[cfg(test)]
pub(super) fn parse_canvas_text_align_x(value: Option<&Value>) -> iced::widget::text::Alignment {
match value.and_then(|v| v.as_str()) {
Some("left") => iced::widget::text::Alignment::Left,
Some("center") => iced::widget::text::Alignment::Center,
Some("right") => iced::widget::text::Alignment::Right,
_ => iced::widget::text::Alignment::Default,
}
}
#[cfg(test)]
pub(super) fn parse_canvas_text_align_y(value: Option<&Value>) -> alignment::Vertical {
match value.and_then(|v| v.as_str()) {
Some("center") => alignment::Vertical::Center,
Some("bottom") => alignment::Vertical::Bottom,
_ => alignment::Vertical::Top,
}
}
fn parse_text_align_x(value: Option<&str>) -> iced::widget::text::Alignment {
match value {
Some("left") => iced::widget::text::Alignment::Left,
Some("center") => iced::widget::text::Alignment::Center,
Some("right") => iced::widget::text::Alignment::Right,
_ => iced::widget::text::Alignment::Default,
}
}
fn parse_text_align_y(value: Option<&str>) -> alignment::Vertical {
match value {
Some("center") => alignment::Vertical::Center,
Some("bottom") => alignment::Vertical::Bottom,
_ => alignment::Vertical::Top,
}
}
fn decode_font_str(s: &str) -> iced::Font {
plushie_core::types::Font::wire_decode(&Value::String(s.to_string()))
.map(|f| iced_convert::font(&f))
.unwrap_or(iced::Font::DEFAULT)
}
pub(super) fn pick_action(
existing: Option<iced::widget::Action<crate::message::Message>>,
new: iced::widget::Action<crate::message::Message>,
) -> iced::widget::Action<crate::message::Message> {
existing.unwrap_or(new)
}
pub(super) fn apply_group_transforms<R: PlushieRenderer>(
frame: &mut canvas::Frame<R>,
transforms: &[Transform],
) {
for t in transforms {
match t {
Transform::Translate { x, y } => {
frame.translate(Vector::new(*x, *y));
}
Transform::Rotate { angle } => {
frame.rotate(Radians(angle.radians()));
}
Transform::Scale { x, y } => {
frame.scale_nonuniform(Vector::new(*x, *y));
}
Transform::ScaleUniform { factor } => {
frame.scale(*factor);
}
}
}
}
pub(super) fn draw_with_group_clip<R: PlushieRenderer>(
frame: &mut canvas::Frame<R>,
clip: Option<&ClipRect>,
images: &crate::image_registry::ImageRegistry,
theme: &iced::Theme,
children: &[&CanvasShape],
draw_fn: impl FnOnce(
&mut canvas::Frame<R>,
&[&CanvasShape],
&crate::image_registry::ImageRegistry,
&iced::Theme,
),
) {
if let Some(c) = clip {
let clip_rect = iced::Rectangle {
x: c.x,
y: c.y,
width: c.w,
height: c.h,
};
frame.with_clip(clip_rect, |f| {
draw_fn(f, children, images, theme);
});
} else {
draw_fn(frame, children, images, theme);
}
}
pub(super) fn truncate_shapes(name: &str, mut shapes: Vec<CanvasShape>) -> Vec<CanvasShape> {
if shapes.len() > MAX_SHAPES_PER_LAYER {
log::warn!(
"canvas layer `{name}` has {} shapes, truncating to {MAX_SHAPES_PER_LAYER}",
shapes.len(),
);
shapes.truncate(MAX_SHAPES_PER_LAYER);
}
shapes
}
pub(crate) fn resolve_color(value: &Value, theme: &iced::Theme) -> Option<Color> {
let s = value.as_str()?;
if s.starts_with('#') {
return CoreColor::wire_decode(value).map(|c| iced_convert::color(&c));
}
let palette = theme.palette();
match s {
"primary" => Some(palette.primary.base.color),
"text" => Some(palette.background.base.text),
"background" => Some(palette.background.base.color),
"success" => Some(palette.success.base.color),
"danger" => Some(palette.danger.base.color),
"warning" => Some(palette.warning.base.color),
_ => {
CoreColor::wire_decode(value).map(|c| iced_convert::color(&c))
}
}
}
#[cfg(test)]
#[allow(dead_code)]
pub(super) fn json_color_themed(val: &Value, key: &str, theme: &iced::Theme) -> Color {
val.get(key)
.and_then(|v| resolve_color(v, theme))
.unwrap_or(Color::WHITE)
}
fn resolve_core_color(c: &plushie_core::types::Color, theme: &iced::Theme) -> Color {
let s = c.as_hex();
if s.starts_with('#') {
return hex_to_iced_color(s).unwrap_or(Color::WHITE);
}
let palette = theme.palette();
match s {
"primary" => palette.primary.base.color,
"text" => palette.background.base.text,
"background" => palette.background.base.color,
"success" => palette.success.base.color,
"danger" => palette.danger.base.color,
"warning" => palette.warning.base.color,
_ => hex_to_iced_color(s).unwrap_or(Color::WHITE),
}
}
fn typed_canvas_fill(
fill: &canvas_types::CanvasFill,
fill_rule: Option<&canvas_types::FillRule>,
theme: &iced::Theme,
) -> canvas::Fill {
let rule = fill_rule
.map(|r| iced_convert::fill_rule(*r))
.unwrap_or(canvas::fill::Rule::NonZero);
match fill {
canvas_types::CanvasFill::Color(c) => canvas::Fill {
style: canvas::Style::Solid(resolve_core_color(c, theme)),
rule,
},
canvas_types::CanvasFill::Gradient(g) => canvas::Fill {
style: canvas::Style::Gradient(iced_convert::canvas_gradient(g)),
rule,
},
}
}
fn typed_canvas_stroke(s: &canvas_types::Stroke, theme: &iced::Theme) -> canvas::Stroke<'static> {
let color = resolve_core_color(&s.color, theme);
let mut out = canvas::Stroke::default()
.with_color(color)
.with_width(s.width)
.with_line_cap(
s.cap
.map(iced_convert::line_cap)
.unwrap_or(canvas::LineCap::Butt),
)
.with_line_join(
s.join
.map(iced_convert::line_join)
.unwrap_or(canvas::LineJoin::Miter),
);
if let Some(ref dash) = s.dash
&& let Some(segments) = intern_dash_segments(dash.segments.clone())
{
out.line_dash = canvas::LineDash {
segments,
offset: dash.offset as usize,
};
}
out
}
pub(super) fn draw_canvas_shape<R: PlushieRenderer>(
frame: &mut canvas::Frame<R>,
shape: &CanvasShape,
images: &crate::image_registry::ImageRegistry,
theme: &iced::Theme,
) {
match shape {
CanvasShape::Rect(r) => {
let rect_path = match &r.radius {
Some(radius) => {
let iced_radius = iced_convert::radius(*radius);
canvas::Path::rounded_rectangle(
Point::new(r.x, r.y),
Size::new(r.w, r.h),
iced_radius,
)
}
None => canvas::Path::rectangle(Point::new(r.x, r.y), Size::new(r.w, r.h)),
};
if let Some(ref fill) = r.fill {
let iced_fill = apply_opacity_to_fill(
r.opacity,
typed_canvas_fill(fill, r.fill_rule.as_ref(), theme),
);
frame.fill(&rect_path, iced_fill);
} else if r.stroke.is_none() {
let color = apply_opacity_to_color(r.opacity, Color::WHITE);
frame.fill_rectangle(Point::new(r.x, r.y), Size::new(r.w, r.h), color);
}
if let Some(ref stroke) = r.stroke {
let iced_stroke =
apply_opacity_to_stroke(r.opacity, typed_canvas_stroke(stroke, theme));
frame.stroke(&rect_path, iced_stroke);
}
}
CanvasShape::Circle(c) => {
let circle_path = canvas::Path::circle(Point::new(c.x, c.y), c.r);
if let Some(ref fill) = c.fill {
let iced_fill = apply_opacity_to_fill(
c.opacity,
typed_canvas_fill(fill, c.fill_rule.as_ref(), theme),
);
frame.fill(&circle_path, iced_fill);
} else if c.stroke.is_none() {
let color = apply_opacity_to_color(c.opacity, Color::WHITE);
frame.fill(&circle_path, color);
}
if let Some(ref stroke) = c.stroke {
let iced_stroke =
apply_opacity_to_stroke(c.opacity, typed_canvas_stroke(stroke, theme));
frame.stroke(&circle_path, iced_stroke);
}
}
CanvasShape::Line(l) => {
let line_path = canvas::Path::line(Point::new(l.x1, l.y1), Point::new(l.x2, l.y2));
if let Some(ref stroke) = l.stroke {
let iced_stroke =
apply_opacity_to_stroke(l.opacity, typed_canvas_stroke(stroke, theme));
frame.stroke(&line_path, iced_stroke);
} else {
let color = apply_opacity_to_color(l.opacity, Color::WHITE);
frame.stroke(
&line_path,
canvas::Stroke::default().with_color(color).with_width(1.0),
);
}
}
CanvasShape::Text(t) => {
let fill_color = t
.fill
.as_ref()
.and_then(|f| match f {
canvas_types::CanvasFill::Color(c) => Some(resolve_core_color(c, theme)),
_ => None,
})
.unwrap_or(Color::WHITE);
let fill_color = apply_opacity_to_color(t.opacity, fill_color);
let align_x = parse_text_align_x(t.align_x.as_deref());
let align_y = parse_text_align_y(t.align_y.as_deref());
let mut canvas_text = canvas::Text {
content: t.content.clone(),
position: Point::new(t.x, t.y),
color: fill_color,
align_x,
align_y,
..canvas::Text::default()
};
if let Some(s) = t.size {
canvas_text.size = Pixels(s);
}
if let Some(ref font_name) = t.font {
canvas_text.font = decode_font_str(font_name);
}
frame.fill_text(canvas_text);
}
CanvasShape::Path(p) => {
let path = build_path_from_commands(&p.commands);
if let Some(ref fill) = p.fill {
let iced_fill = apply_opacity_to_fill(
p.opacity,
typed_canvas_fill(fill, p.fill_rule.as_ref(), theme),
);
frame.fill(&path, iced_fill);
}
if let Some(ref stroke) = p.stroke {
let iced_stroke =
apply_opacity_to_stroke(p.opacity, typed_canvas_stroke(stroke, theme));
frame.stroke(&path, iced_stroke);
}
}
CanvasShape::Image(img_shape) => {
let bounds = iced::Rectangle {
x: img_shape.x,
y: img_shape.y,
width: img_shape.w,
height: img_shape.h,
};
let handle = if let Some(h) = images.get(&img_shape.source) {
h.clone()
} else {
iced::widget::image::Handle::from_path(&img_shape.source)
};
let rotation = img_shape
.rotation
.map(|a| Radians(a.radians()))
.unwrap_or(Radians(0.0));
let opacity = img_shape.opacity.unwrap_or(1.0);
let img = iced::advanced::image::Image {
handle,
filter_method: iced::advanced::image::FilterMethod::default(),
rotation,
border_radius: Default::default(),
opacity,
};
frame.draw_image(bounds, img);
}
CanvasShape::Svg(s) => {
let bounds = iced::Rectangle {
x: s.x,
y: s.y,
width: s.w,
height: s.h,
};
let handle = iced::widget::svg::Handle::from_path(&s.source);
frame.draw_svg(bounds, &handle);
}
CanvasShape::Group(g) => {
let child_refs: Vec<&CanvasShape> = g.children.iter().collect();
let has_transforms = !g.transforms.is_empty();
if has_transforms {
frame.push_transform();
apply_group_transforms(frame, &g.transforms);
}
draw_with_group_clip(
frame,
g.clip.as_ref(),
images,
theme,
&child_refs,
|f, c, img, theme| {
draw_canvas_shapes(f, c, img, theme);
},
);
if has_transforms {
frame.pop_transform();
}
}
}
}
pub(super) fn draw_canvas_shape_with_overrides<R: PlushieRenderer>(
frame: &mut canvas::Frame<R>,
shape: &CanvasShape,
images: &crate::image_registry::ImageRegistry,
theme: &iced::Theme,
overrides: &plushie_core::types::ShapeStyle,
) {
let modified = apply_style_overrides(shape, overrides);
draw_canvas_shape(frame, &modified, images, theme);
}
fn apply_style_overrides(
shape: &CanvasShape,
overrides: &plushie_core::types::ShapeStyle,
) -> CanvasShape {
use plushie_core::types::CanvasFill;
use plushie_core::types::Color as CoreColor;
let override_fill = overrides
.fill
.as_ref()
.map(|f| CanvasFill::Color(CoreColor::hex(f)));
let override_stroke = overrides.stroke.clone();
let override_opacity = overrides.opacity;
match shape {
CanvasShape::Rect(r) => {
let mut r = r.clone();
if let Some(f) = override_fill {
r.fill = Some(f);
}
if let Some(s) = override_stroke {
r.stroke = Some(s);
}
if let Some(o) = override_opacity {
r.opacity = Some(o);
}
CanvasShape::Rect(r)
}
CanvasShape::Circle(c) => {
let mut c = c.clone();
if let Some(f) = override_fill {
c.fill = Some(f);
}
if let Some(s) = override_stroke {
c.stroke = Some(s);
}
if let Some(o) = override_opacity {
c.opacity = Some(o);
}
CanvasShape::Circle(c)
}
CanvasShape::Line(l) => {
let mut l = l.clone();
if let Some(s) = override_stroke {
l.stroke = Some(s);
}
if let Some(o) = override_opacity {
l.opacity = Some(o);
}
CanvasShape::Line(l)
}
CanvasShape::Text(t) => {
let mut t = t.clone();
if let Some(f) = override_fill {
t.fill = Some(f);
}
if let Some(o) = override_opacity {
t.opacity = Some(o);
}
CanvasShape::Text(t)
}
CanvasShape::Path(p) => {
let mut p = p.clone();
if let Some(f) = override_fill {
p.fill = Some(f);
}
if let Some(s) = override_stroke {
p.stroke = Some(s);
}
if let Some(o) = override_opacity {
p.opacity = Some(o);
}
CanvasShape::Path(p)
}
CanvasShape::Image(i) => {
let mut i = i.clone();
if let Some(o) = override_opacity {
i.opacity = Some(o);
}
CanvasShape::Image(i)
}
_ => shape.clone(),
}
}