use std::ops::Deref;
use oxframe_core::{Color, Signal};
use crate::builder::{BaseProps, NodeBuilder, SvgBuilder};
use crate::graph::{NodeId, SceneGraph};
use crate::handles::{Node, Path, Svg};
use crate::theme::{Ink, IntoPaint};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum IconStyle {
Stroke,
Fill,
}
#[derive(Debug)]
#[must_use = "a builder does nothing until it is passed to `add`"]
pub struct IconBuilder {
svg: SvgBuilder,
style: IconStyle,
size: f64,
color: Option<Color>,
stroke_width: f64,
}
impl IconBuilder {
pub fn size(mut self, v: f64) -> Self {
self.size = v;
self
}
pub fn color(mut self, v: impl IntoPaint) -> Self {
self.color = Some(v.into_paint());
self
}
pub fn stroke_width(mut self, v: f64) -> Self {
self.stroke_width = v;
self
}
}
impl NodeBuilder for IconBuilder {
type Handle = Icon;
fn base(&mut self) -> &mut BaseProps {
self.svg.base()
}
fn build(self, g: &SceneGraph, parent: NodeId) -> Icon {
let color = Signal::new(self.color.unwrap_or_else(|| Ink::Text.into_paint()));
let stroke_width = Signal::new(self.stroke_width);
let style = self.style;
let svg = self.svg.width(self.size).height(self.size).build(g, parent);
for path in g.descendant_paths(svg.id()) {
let n = g.node(path);
if n.stroke.peek().to_rgba8()[3] > 0 {
let c = color.clone();
n.stroke.bind(move || c.get());
let base = n.line_width.peek() / 2.0;
let w = stroke_width.clone();
n.line_width.bind(move || base * w.get());
}
if n.fill.peek().to_rgba8()[3] > 0 {
let c = color.clone();
n.fill.bind(move || c.get());
}
}
Icon {
svg,
style,
color,
stroke_width,
}
}
}
#[derive(Debug, Clone)]
pub struct Icon {
svg: Svg,
style: IconStyle,
color: Signal<Color>,
stroke_width: Signal<f64>,
}
impl Icon {
pub fn from_svg(svg: &[u8], style: IconStyle) -> IconBuilder {
IconBuilder {
svg: Svg::new().src(svg),
style,
size: 48.0,
color: None,
stroke_width: 2.0,
}
}
pub fn node(&self) -> Node {
*self.svg
}
pub fn color(&self) -> Signal<Color> {
self.color.clone()
}
pub fn stroke_width(&self) -> Signal<f64> {
self.stroke_width.clone()
}
pub fn paths(&self) -> Vec<Path> {
self.svg.paths()
}
pub fn style(&self) -> IconStyle {
self.style
}
}
impl Deref for Icon {
type Target = Node;
fn deref(&self) -> &Node {
&self.svg
}
}
impl From<Icon> for Node {
fn from(h: Icon) -> Node {
*h.svg
}
}
impl From<&Icon> for Node {
fn from(h: &Icon) -> Node {
*h.svg
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::View;
use crate::graph::SceneGraph;
const ARROW: &str = r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="none" stroke="#000" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"><path d="M5 12h14"/><path d="m12 5 7 7-7 7"/></svg>"##;
const DOT: &str = r##"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" fill="#000"><circle cx="12" cy="12" r="8"/></svg>"##;
#[test]
fn stroke_icon_follows_its_signals() {
SceneGraph::new(200, 200).enter(|| {
let icon = View::current().add(
Icon::from_svg(ARROW.as_bytes(), IconStyle::Stroke)
.size(96.0)
.color(Color::rgb(1.0, 0.0, 0.0)),
);
assert_eq!(icon.size(), (96.0, 96.0));
let paths = Svg::from_id(icon.id()).paths();
assert_eq!(paths.len(), 2);
assert_eq!(paths[0].stroke().get(), Color::rgb(1.0, 0.0, 0.0));
assert!((paths[0].line_width().get() - 8.0).abs() < 1e-6);
icon.color().set(Color::WHITE);
icon.stroke_width().set(1.0);
assert_eq!(paths[1].stroke().get(), Color::WHITE);
assert!((paths[1].line_width().get() - 4.0).abs() < 1e-6);
assert_eq!(paths[1].fill().get().to_rgba8()[3], 0);
assert_eq!(icon.style(), IconStyle::Stroke);
});
}
#[test]
fn fill_icon_colors_its_fill() {
SceneGraph::new(200, 200).enter(|| {
let icon = View::current()
.add(Icon::from_svg(DOT.as_bytes(), IconStyle::Fill).color(Color::WHITE));
let paths = Svg::from_id(icon.id()).paths();
assert_eq!(paths[0].fill().get(), Color::WHITE);
assert_eq!(icon.style(), IconStyle::Fill);
});
}
}