1use std::ops::Deref;
6
7use oxframe_core::{Color, Signal};
8use oxframe_text::TextStyle;
9
10use crate::builder::{BaseProps, NodeBuilder, SvgBuilder};
11use crate::graph::{NodeId, SceneGraph};
12use crate::handles::{Node, Path, Svg};
13use crate::theme::{Ink, IntoPaint};
14
15#[derive(Debug)]
17#[must_use = "a builder does nothing until it is passed to `add`"]
18pub struct MathBuilder {
19 formula: String,
20 display: bool,
21 size: f64,
22 color: Option<Color>,
23 svg: SvgBuilder,
24}
25
26impl MathBuilder {
27 pub fn size(mut self, v: f64) -> Self {
30 self.size = v;
31 self
32 }
33
34 pub fn color(mut self, v: impl IntoPaint) -> Self {
36 self.color = Some(v.into_paint());
37 self
38 }
39
40 pub fn display(mut self) -> Self {
44 self.display = true;
45 self
46 }
47}
48
49impl NodeBuilder for MathBuilder {
50 type Handle = Math;
51
52 fn base(&mut self) -> &mut BaseProps {
53 self.svg.base()
54 }
55
56 fn build(self, g: &SceneGraph, parent: NodeId) -> Math {
57 let set = oxframe_math::to_svg(&self.formula, self.display)
58 .unwrap_or_else(|e| panic!("Math {:?}: {e}", self.formula));
59 let k = self.size / oxframe_math::LAYOUT_PT;
61 let svg = self
62 .svg
63 .src(set.svg.as_bytes())
64 .width(set.width * k)
65 .height(set.height * k)
66 .build(g, parent);
67 let color = Signal::new(self.color.unwrap_or_else(|| Ink::Text.into_paint()));
68 for path in g.descendant_paths(svg.id()) {
69 let n = g.node(path);
70 if n.fill.peek().to_rgba8()[3] > 0 {
71 let c = color.clone();
72 n.fill.bind(move || c.get());
73 }
74 if n.stroke.peek().to_rgba8()[3] > 0 {
75 let c = color.clone();
76 n.stroke.bind(move || c.get());
77 }
78 }
79 Math {
80 svg,
81 color,
82 formula: self.formula,
83 }
84 }
85}
86
87#[derive(Debug, Clone)]
90pub struct Math {
91 svg: Svg,
92 color: Signal<Color>,
93 formula: String,
94}
95
96impl Math {
97 #[allow(clippy::new_ret_no_self)]
106 pub fn new(formula: impl Into<String>) -> MathBuilder {
107 MathBuilder {
108 formula: formula.into(),
109 display: false,
110 size: TextStyle::default().size,
111 color: None,
112 svg: Svg::new(),
113 }
114 }
115
116 pub fn node(&self) -> Node {
118 *self.svg
119 }
120
121 pub fn color(&self) -> Signal<Color> {
123 self.color.clone()
124 }
125
126 pub fn paths(&self) -> Vec<Path> {
129 self.svg.paths()
130 }
131
132 pub fn formula(&self) -> &str {
134 &self.formula
135 }
136}
137
138impl Deref for Math {
139 type Target = Node;
140 fn deref(&self) -> &Node {
141 &self.svg
142 }
143}
144
145impl From<Math> for Node {
146 fn from(h: Math) -> Node {
147 *h.svg
148 }
149}
150
151impl From<&Math> for Node {
152 fn from(h: &Math) -> Node {
153 *h.svg
154 }
155}
156
157#[cfg(test)]
158mod tests {
159 use super::*;
160 use crate::View;
161
162 #[test]
163 fn a_formula_is_sized_by_its_font_size_and_colored_by_one_signal() {
164 SceneGraph::new(800, 400).enter(|| {
165 let small = View::current().add(Math::new("x^2 + y^2 = r^2").size(32.0));
166 let big = View::current().add(
167 Math::new("x^2 + y^2 = r^2")
168 .size(64.0)
169 .color(Color::rgb(1.0, 0.0, 0.0)),
170 );
171 let (sw, sh) = small.size();
172 let (bw, bh) = big.size();
173 assert!((bw / sw - 2.0).abs() < 1e-6 && (bh / sh - 2.0).abs() < 1e-6);
174 assert!(
176 (bh - 64.0).abs() < 1.0,
177 "one em, ascender to descender ({bh})"
178 );
179 let paths = big.paths();
180 assert!(paths.len() >= 8, "one path per glyph ({})", paths.len());
181 assert_eq!(paths[0].fill().get(), Color::rgb(1.0, 0.0, 0.0));
182 big.color().set(Color::WHITE);
183 assert!(paths.iter().all(|p| p.fill().get() == Color::WHITE));
184 assert_eq!(big.formula(), "x^2 + y^2 = r^2");
185 });
186 }
187
188 #[test]
189 fn display_math_sets_limits_above_and_below() {
190 SceneGraph::new(800, 400).enter(|| {
191 let inline = View::current().add(Math::new("sum_(k=1)^n k"));
192 let display = View::current().add(Math::new("sum_(k=1)^n k").display());
193 assert!(display.size().1 > inline.size().1);
194 });
195 }
196
197 #[test]
198 #[should_panic(expected = "Math \"#nosuch\"")]
199 fn a_bad_formula_names_itself() {
200 SceneGraph::new(200, 200).enter(|| {
201 let _ = View::current().add(Math::new("#nosuch"));
202 });
203 }
204}