use skia_safe::{
BlendMode, ClipOp, Color, Contains, FilterMode, ISize, Image, ImageInfo, MipmapMode, Paint, Rect, SamplingOptions,
Size, color_filters, surfaces,
svg::{
Dom, Length, LengthUnit, PreserveAspectRatio,
preserve_aspect_ratio::{Align, Scale},
},
};
use crate::control::{Control, Has, LayoutCx, PaintCx, part, part_mut};
use crate::controls::image::{DrawImageAlignment, TransformAspect, rescale_aspect};
use crate::controls::sprite::picture_size;
use crate::props;
use crate::tree::{Build, Cx, Tree};
use crate::types::{CacheType, Dirty, IntoProp};
props!(SvgProps, SvgBuild, SvgSet {
source / set_source: String = String::new(), MEASURE_APPLY;
svg_string / set_svg_string: String = String::new(), MEASURE_APPLY;
tint_color / set_tint_color: Color = Color::TRANSPARENT, DRAW_APPLY;
aspect / set_aspect: TransformAspect = TransformAspect::AspectFitFill, DRAW;
horizontal_alignment / set_horizontal_alignment: DrawImageAlignment = DrawImageAlignment::Center, DRAW;
vertical_alignment / set_vertical_alignment: DrawImageAlignment = DrawImageAlignment::Center, DRAW;
});
#[derive(Default)]
pub struct SkiaSvg {
pub p: SvgProps,
dom: Option<Dom>,
intrinsic: Size,
resolved: String,
raster: Option<(ISize, Image)>,
loading: bool,
error: bool,
paint: Paint,
}
impl SkiaSvg {
#[allow(clippy::new_ret_no_self)]
pub fn new(source: impl IntoProp<String>) -> Build<SkiaSvg> {
Build::new(SkiaSvg::default()).source(source).use_cache(CacheType::Operations)
}
pub fn from_string(svg: impl IntoProp<String>) -> Build<SkiaSvg> {
Build::new(SkiaSvg::default()).svg_string(svg).use_cache(CacheType::Operations)
}
pub fn is_loading(&self) -> bool {
self.loading
}
pub fn has_error(&self) -> bool {
self.error
}
pub fn intrinsic_size(&self) -> Size {
self.intrinsic
}
fn take(&mut self, dom: Option<Dom>) {
self.intrinsic = dom.as_ref().map_or(Size::default(), fill_container);
(self.loading, self.error, self.raster) = (false, dom.is_none(), None);
self.dom = dom;
}
fn waits_for(&self, url: &str) -> bool {
self.p.svg_string.is_empty() && self.resolved == url
}
fn display_rect(&self, dest: Rect) -> Rect {
let (width, height) = (self.intrinsic.width, self.intrinsic.height);
let (sx, sy) = rescale_aspect(width, height, dest.size(), self.p.aspect);
let (w, h) = (width * sx, height * sy);
let along = |alignment: DrawImageAlignment, room: f32| match alignment {
DrawImageAlignment::Start => 0.0,
DrawImageAlignment::Center => room / 2.0,
DrawImageAlignment::End => room,
};
let x = dest.left + along(self.p.horizontal_alignment, dest.width() - w);
let y = dest.top + along(self.p.vertical_alignment, dest.height() - h);
Rect::from_xywh(x, y, w, h)
}
}
fn fill_container(dom: &Dom) -> Size {
let mut root = dom.root();
let size = match root.view_box() {
Some(view_box) if view_box.width() > 0.0 && view_box.height() > 0.0 => view_box.size(),
_ => root.intrinsic_size(),
};
if size.width > 0.0 && size.height > 0.0 {
if root.view_box().is_none() {
root.set_view_box(Rect::from_size(size));
}
root.set_width(Length::new(100.0, LengthUnit::Percentage));
root.set_height(Length::new(100.0, LengthUnit::Percentage));
root.set_preserve_aspect_ratio(PreserveAspectRatio::new(Align::None, Scale::Meet));
}
size
}
fn shared(tree: &Tree, url: &str) -> (Option<Dom>, bool) {
let mut flying = false;
for other in tree.nodes.iter().flatten().filter_map(|node| part::<SkiaSvg>(node.kind.as_deref()?)) {
if other.waits_for(url) {
if other.dom.is_some() {
return (other.dom.clone(), false);
}
flying |= other.loading;
}
}
(None, flying)
}
fn deliver(tree: &mut Tree, url: &str, bytes: &[u8]) {
let dom = (!bytes.is_empty()).then(|| Dom::from_bytes(bytes, crate::fonts::font_mgr()).ok()).flatten();
if dom.is_none() {
eprintln!("drawnui: svg {url} did not load");
}
let mut arrived = Vec::new();
for node in tree.nodes.iter_mut().flatten() {
let Some(svg) = node.kind.as_deref_mut().and_then(part_mut::<SkiaSvg>) else { continue };
if svg.loading && svg.waits_for(url) {
svg.take(dom.clone());
arrived.push(node.id);
}
}
for id in arrived {
tree.invalidate(id, Dirty::MEASURE);
}
}
impl Has<SvgProps> for SkiaSvg {
fn part(&self) -> &SvgProps {
&self.p
}
fn part_mut(&mut self) -> &mut SvgProps {
&mut self.p
}
}
impl Control for SkiaSvg {
fn on_props_changed(&mut self, cx: &mut Cx) {
self.paint.set_anti_alias(true);
let tint = self.p.tint_color;
self.paint.set_color_filter((tint.a() > 0).then(|| color_filters::blend(tint, BlendMode::SrcIn)).flatten());
let wanted = if self.p.svg_string.is_empty() { &self.p.source } else { &self.p.svg_string };
if self.resolved == *wanted {
return;
}
self.resolved.clone_from(wanted);
if !self.p.svg_string.is_empty() {
let dom = Dom::from_bytes(self.p.svg_string.as_bytes(), crate::fonts::font_mgr()).ok();
return self.take(dom);
}
self.take(None);
self.error = false;
if self.resolved.is_empty() {
return;
}
let (dom, flying) = shared(cx.tree, &self.resolved);
if dom.is_some() {
return self.take(dom);
}
self.loading = true;
if !flying {
let url = self.resolved.clone();
cx.tree.assets.fetch(&self.resolved, move |tree, bytes| deliver(tree, &url, &bytes));
}
}
fn measure(&mut self, cx: &mut LayoutCx, width: f32, height: f32) -> Size {
let (w, h) = (self.intrinsic.width, self.intrinsic.height);
let aspect = if w > 0.0 && h > 0.0 { w / h } else { 0.0 };
picture_size(&cx.base().p, aspect, width, height)
}
fn arrange(&mut self, cx: &mut LayoutCx) {
if self.dom.is_none() || self.intrinsic.width <= 0.0 || self.intrinsic.height <= 0.0 {
return;
}
let display = self.display_rect(cx.base().rect);
let size = ISize::new(display.width().round() as i32, display.height().round() as i32);
if size.width < 1 || size.height < 1 || self.raster.as_ref().is_some_and(|(have, _)| *have == size) {
return;
}
let Some(mut surface) = surfaces::raster(&ImageInfo::new_n32_premul(size, None), None, None) else { return };
let Some(dom) = self.dom.as_mut() else { return };
dom.set_container_size(Size::new(size.width as f32, size.height as f32));
dom.render(surface.canvas());
self.raster = Some((size, surface.image_snapshot()));
}
fn paint(&self, cx: &mut PaintCx) {
let Some((_, raster)) = &self.raster else { return };
let display = self.display_rect(cx.rect);
let clip = !cx.rect.contains(display);
if clip {
cx.canvas.save();
cx.canvas.clip_rect(cx.rect, ClipOp::Intersect, true);
}
let (w, h) = (raster.width() as f32, raster.height() as f32);
if (display.width() - w).abs() < 0.5 && (display.height() - h).abs() < 0.5 {
cx.canvas.draw_image(raster, (display.left.round(), display.top.round()), Some(&self.paint));
} else {
let sampling = SamplingOptions::new(FilterMode::Linear, MipmapMode::None);
cx.canvas.draw_image_rect_with_sampling_options(raster, None, display, sampling, &self.paint);
}
if clip {
cx.canvas.restore();
}
}
}