use super::context::RenderContext;
pub fn draw_svg_icon(ctx: &mut dyn RenderContext, svg: &str, x: f64, y: f64, width: f64, height: f64, color: &str) {
let (vb_width, vb_height) = parse_viewbox(svg).unwrap_or((24.0, 24.0));
let scale_x = width / vb_width;
let scale_y = height / vb_height;
let scale = scale_x.min(scale_y);
let offset_x = (x + (width - vb_width * scale) / 2.0).floor();
let offset_y = (y + (height - vb_height * scale) / 2.0).floor();
let has_fill_none = svg_root_has_fill_none(svg);
let default_filled = !has_fill_none;
let stroke_width = (1.5 * scale * 2.0).round() / 2.0;
ctx.set_stroke_color(color);
ctx.set_stroke_width(stroke_width);
ctx.set_line_cap("round");
ctx.set_line_join("round");
ctx.set_line_dash(&[]);
for path_info in parse_svg_paths(svg, default_filled) {
ctx.begin_path();
render_path_data(ctx, &path_info.d, offset_x, offset_y, scale);
if path_info.filled {
ctx.set_fill_color(color);
ctx.fill();
}
if path_info.stroked {
if let Some(ref dash) = path_info.dash_array {
let scaled_dash: Vec<f64> = dash.iter().map(|d| d * scale).collect();
ctx.set_line_dash(&scaled_dash);
}
ctx.stroke();
if path_info.dash_array.is_some() {
ctx.set_line_dash(&[]);
}
}
}
for (cx, cy, r, filled) in parse_svg_circles(svg, default_filled) {
let tx = offset_x + cx * scale;
let ty = offset_y + cy * scale;
let tr = r * scale;
ctx.begin_path();
ctx.arc(tx, ty, tr, 0.0, std::f64::consts::PI * 2.0);
if filled {
ctx.set_fill_color(color);
ctx.fill();
} else {
ctx.stroke();
}
}
for (rx, ry, rw, rh, rounding, filled) in parse_svg_rects(svg, default_filled) {
let tx = offset_x + rx * scale;
let ty = offset_y + ry * scale;
let tw = rw * scale;
let th = rh * scale;
let tr = rounding * scale;
if filled {
ctx.set_fill_color(color);
if tr > 0.0 {
ctx.fill_rounded_rect(tx, ty, tw, th, tr);
} else {
ctx.fill_rect(tx, ty, tw, th);
}
} else if tr > 0.0 {
ctx.stroke_rounded_rect(tx, ty, tw, th, tr);
} else {
ctx.stroke_rect(tx, ty, tw, th);
}
}
for (x1, y1, x2, y2) in parse_svg_lines(svg) {
let tx1 = offset_x + x1 * scale;
let ty1 = offset_y + y1 * scale;
let tx2 = offset_x + x2 * scale;
let ty2 = offset_y + y2 * scale;
ctx.begin_path();
ctx.move_to(tx1, ty1);
ctx.line_to(tx2, ty2);
ctx.stroke();
}
for (points, closed) in parse_svg_polylines(svg) {
if points.len() >= 2 {
ctx.begin_path();
let (px, py) = points[0];
ctx.move_to(offset_x + px * scale, offset_y + py * scale);
for &(px, py) in &points[1..] {
ctx.line_to(offset_x + px * scale, offset_y + py * scale);
}
if closed {
ctx.close_path();
}
ctx.stroke();
}
}
}
fn parse_viewbox(svg: &str) -> Option<(f64, f64)> {
let vb_start = svg.find("viewBox=\"")?;
let vb_content_start = vb_start + 9;
let vb_end = svg[vb_content_start..].find('"')?;
let vb_str = &svg[vb_content_start..vb_content_start + vb_end];
let parts: Vec<&str> = vb_str.split_whitespace().collect();
if parts.len() >= 4 {
let w = parts[2].parse::<f64>().ok()?;
let h = parts[3].parse::<f64>().ok()?;
Some((w, h))
} else {
None
}
}
struct PathInfo {
d: String,
filled: bool,
stroked: bool,
dash_array: Option<Vec<f64>>,
fill_color: Option<String>, stroke_color: Option<String>, stroke_width: Option<f64>, }
fn parse_svg_paths(svg: &str, default_filled: bool) -> Vec<PathInfo> {
let mut paths = Vec::new();
let mut search_from = 0;
while let Some(start) = svg[search_from..].find("<path") {
let abs_start = search_from + start;
let tag_end = if let Some(end) = svg[abs_start..].find("/>") {
abs_start + end + 2
} else if let Some(end) = svg[abs_start..].find('>') {
abs_start + end + 1
} else {
break;
};
let tag_content = &svg[abs_start..tag_end];
if let Some(d_start) = tag_content.find(" d=\"") {
let d_content_start = d_start + 4;
if let Some(d_end) = tag_content[d_content_start..].find('"') {
let d = tag_content[d_content_start..d_content_start + d_end].to_string();
let (filled, fill_color) = if let Some(fill_start) = tag_content.find("fill=\"") {
let fill_content_start = fill_start + 6;
if let Some(fill_end) = tag_content[fill_content_start..].find('"') {
let fill_value = &tag_content[fill_content_start..fill_content_start + fill_end];
if fill_value != "none" {
(true, Some(fill_value.to_string()))
} else {
(false, None)
}
} else {
(false, None)
}
} else {
(default_filled, None) };
let (stroked, stroke_color) = if let Some(stroke_start) = tag_content.find("stroke=\"") {
let stroke_content_start = stroke_start + 8;
if let Some(stroke_end) = tag_content[stroke_content_start..].find('"') {
let stroke_value = &tag_content[stroke_content_start..stroke_content_start + stroke_end];
if stroke_value != "none" {
(true, Some(stroke_value.to_string()))
} else {
(false, None)
}
} else {
(true, None)
}
} else {
(!filled, None) };
let stroke_width = if let Some(sw_start) = tag_content.find("stroke-width=\"") {
let sw_content_start = sw_start + 14;
if let Some(sw_end) = tag_content[sw_content_start..].find('"') {
tag_content[sw_content_start..sw_content_start + sw_end].parse::<f64>().ok()
} else {
None
}
} else {
None
};
let dash_array = if let Some(dash_start) = tag_content.find("stroke-dasharray=\"") {
let dash_content_start = dash_start + 18;
if let Some(dash_end) = tag_content[dash_content_start..].find('"') {
let dash_value = &tag_content[dash_content_start..dash_content_start + dash_end];
let values: Vec<f64> = dash_value
.split([' ', ','])
.filter_map(|s| s.trim().parse::<f64>().ok())
.collect();
if !values.is_empty() {
Some(values)
} else {
None
}
} else {
None
}
} else {
None
};
paths.push(PathInfo { d, filled, stroked, dash_array, fill_color, stroke_color, stroke_width });
}
}
search_from = tag_end;
}
paths
}
const ARC_SEGMENTS: usize = 16;
#[allow(clippy::too_many_arguments)]
fn arc_to_points(
start_x: f64,
start_y: f64,
mut rx: f64,
mut ry: f64,
x_rotation: f64,
large_arc: bool,
sweep: bool,
end_x: f64,
end_y: f64,
) -> Vec<(f64, f64)> {
let mut points = Vec::new();
if (start_x - end_x).abs() < 0.001 && (start_y - end_y).abs() < 0.001 {
return points;
}
rx = rx.abs();
ry = ry.abs();
if rx < 0.001 || ry < 0.001 {
points.push((end_x, end_y));
return points;
}
let phi = x_rotation.to_radians();
let cos_phi = phi.cos();
let sin_phi = phi.sin();
let dx = (start_x - end_x) / 2.0;
let dy = (start_y - end_y) / 2.0;
let x1p = cos_phi * dx + sin_phi * dy;
let y1p = -sin_phi * dx + cos_phi * dy;
let x1p2 = x1p * x1p;
let y1p2 = y1p * y1p;
let rx2 = rx * rx;
let ry2 = ry * ry;
let lambda = x1p2 / rx2 + y1p2 / ry2;
if lambda > 1.0 {
let sqrt_lambda = lambda.sqrt();
rx *= sqrt_lambda;
ry *= sqrt_lambda;
}
let rx2 = rx * rx;
let ry2 = ry * ry;
let num = rx2 * ry2 - rx2 * y1p2 - ry2 * x1p2;
let denom = rx2 * y1p2 + ry2 * x1p2;
let factor = if denom > 0.0 && num > 0.0 {
let mut f = (num / denom).sqrt();
if large_arc == sweep {
f = -f;
}
f
} else {
0.0
};
let cxp = factor * rx * y1p / ry;
let cyp = -factor * ry * x1p / rx;
let cx = cos_phi * cxp - sin_phi * cyp + (start_x + end_x) / 2.0;
let cy = sin_phi * cxp + cos_phi * cyp + (start_y + end_y) / 2.0;
let ux = (x1p - cxp) / rx;
let uy = (y1p - cyp) / ry;
let vx = (-x1p - cxp) / rx;
let vy = (-y1p - cyp) / ry;
let n = (ux * ux + uy * uy).sqrt();
let theta1 = if uy < 0.0 { -1.0 } else { 1.0 } * (ux / n).clamp(-1.0, 1.0).acos();
let n = ((ux * ux + uy * uy) * (vx * vx + vy * vy)).sqrt();
let dot = ux * vx + uy * vy;
let mut dtheta = if ux * vy - uy * vx < 0.0 { -1.0 } else { 1.0 } * (dot / n).clamp(-1.0, 1.0).acos();
if !sweep && dtheta > 0.0 {
dtheta -= 2.0 * std::f64::consts::PI;
} else if sweep && dtheta < 0.0 {
dtheta += 2.0 * std::f64::consts::PI;
}
for i in 1..=ARC_SEGMENTS {
let t = i as f64 / ARC_SEGMENTS as f64;
let theta = theta1 + dtheta * t;
let cos_theta = theta.cos();
let sin_theta = theta.sin();
let px = rx * cos_theta;
let py = ry * sin_theta;
let x = cos_phi * px - sin_phi * py + cx;
let y = sin_phi * px + cos_phi * py + cy;
points.push((x, y));
}
points
}
fn render_path_data(ctx: &mut dyn RenderContext, path_data: &str, offset_x: f64, offset_y: f64, scale: f64) {
let mut current_x = 0.0;
let mut current_y = 0.0;
let mut start_x = 0.0;
let mut start_y = 0.0;
let mut last_control: Option<(f64, f64)> = None;
let mut chars = path_data.chars().peekable();
let mut current_cmd = 'M';
while chars.peek().is_some() {
while chars.peek().map(|c| c.is_whitespace() || *c == ',').unwrap_or(false) {
chars.next();
}
if let Some(&c) = chars.peek() {
if c.is_alphabetic() {
current_cmd = c;
chars.next();
while chars.peek().map(|c| c.is_whitespace() || *c == ',').unwrap_or(false) {
chars.next();
}
}
}
match current_cmd {
'M' => {
if let Some((x, y)) = parse_two_numbers(&mut chars) {
current_x = x;
current_y = y;
start_x = x;
start_y = y;
ctx.move_to(offset_x + x * scale, offset_y + y * scale);
current_cmd = 'L'; last_control = None;
}
}
'm' => {
if let Some((dx, dy)) = parse_two_numbers(&mut chars) {
current_x += dx;
current_y += dy;
start_x = current_x;
start_y = current_y;
ctx.move_to(offset_x + current_x * scale, offset_y + current_y * scale);
current_cmd = 'l'; last_control = None;
}
}
'L' => {
if let Some((x, y)) = parse_two_numbers(&mut chars) {
current_x = x;
current_y = y;
ctx.line_to(offset_x + x * scale, offset_y + y * scale);
last_control = None;
}
}
'l' => {
if let Some((dx, dy)) = parse_two_numbers(&mut chars) {
current_x += dx;
current_y += dy;
ctx.line_to(offset_x + current_x * scale, offset_y + current_y * scale);
last_control = None;
}
}
'H' => {
if let Some(x) = parse_number(&mut chars) {
current_x = x;
ctx.line_to(offset_x + x * scale, offset_y + current_y * scale);
last_control = None;
}
}
'h' => {
if let Some(dx) = parse_number(&mut chars) {
current_x += dx;
ctx.line_to(offset_x + current_x * scale, offset_y + current_y * scale);
last_control = None;
}
}
'V' => {
if let Some(y) = parse_number(&mut chars) {
current_y = y;
ctx.line_to(offset_x + current_x * scale, offset_y + y * scale);
last_control = None;
}
}
'v' => {
if let Some(dy) = parse_number(&mut chars) {
current_y += dy;
ctx.line_to(offset_x + current_x * scale, offset_y + current_y * scale);
last_control = None;
}
}
'C' => {
if let Some((c1x, c1y, c2x, c2y, x, y)) = parse_six_numbers(&mut chars) {
ctx.bezier_curve_to(
offset_x + c1x * scale,
offset_y + c1y * scale,
offset_x + c2x * scale,
offset_y + c2y * scale,
offset_x + x * scale,
offset_y + y * scale,
);
current_x = x;
current_y = y;
last_control = Some((c2x, c2y));
}
}
'c' => {
if let Some((dc1x, dc1y, dc2x, dc2y, dx, dy)) = parse_six_numbers(&mut chars) {
let c1x = current_x + dc1x;
let c1y = current_y + dc1y;
let c2x = current_x + dc2x;
let c2y = current_y + dc2y;
let x = current_x + dx;
let y = current_y + dy;
ctx.bezier_curve_to(
offset_x + c1x * scale,
offset_y + c1y * scale,
offset_x + c2x * scale,
offset_y + c2y * scale,
offset_x + x * scale,
offset_y + y * scale,
);
current_x = x;
current_y = y;
last_control = Some((c2x, c2y));
}
}
'S' => {
if let Some((c2x, c2y, x, y)) = parse_four_numbers(&mut chars) {
let (c1x, c1y) = match last_control {
Some((lx, ly)) => (2.0 * current_x - lx, 2.0 * current_y - ly),
None => (current_x, current_y),
};
ctx.bezier_curve_to(
offset_x + c1x * scale,
offset_y + c1y * scale,
offset_x + c2x * scale,
offset_y + c2y * scale,
offset_x + x * scale,
offset_y + y * scale,
);
current_x = x;
current_y = y;
last_control = Some((c2x, c2y));
}
}
's' => {
if let Some((dc2x, dc2y, dx, dy)) = parse_four_numbers(&mut chars) {
let (c1x, c1y) = match last_control {
Some((lx, ly)) => (2.0 * current_x - lx, 2.0 * current_y - ly),
None => (current_x, current_y),
};
let c2x = current_x + dc2x;
let c2y = current_y + dc2y;
let x = current_x + dx;
let y = current_y + dy;
ctx.bezier_curve_to(
offset_x + c1x * scale,
offset_y + c1y * scale,
offset_x + c2x * scale,
offset_y + c2y * scale,
offset_x + x * scale,
offset_y + y * scale,
);
current_x = x;
current_y = y;
last_control = Some((c2x, c2y));
}
}
'Q' => {
if let Some((cx, cy, x, y)) = parse_four_numbers(&mut chars) {
ctx.quadratic_curve_to(
offset_x + cx * scale,
offset_y + cy * scale,
offset_x + x * scale,
offset_y + y * scale,
);
current_x = x;
current_y = y;
last_control = Some((cx, cy));
}
}
'q' => {
if let Some((dcx, dcy, dx, dy)) = parse_four_numbers(&mut chars) {
let cx = current_x + dcx;
let cy = current_y + dcy;
let x = current_x + dx;
let y = current_y + dy;
ctx.quadratic_curve_to(
offset_x + cx * scale,
offset_y + cy * scale,
offset_x + x * scale,
offset_y + y * scale,
);
current_x = x;
current_y = y;
last_control = Some((cx, cy));
}
}
'T' => {
if let Some((x, y)) = parse_two_numbers(&mut chars) {
let (cx, cy) = match last_control {
Some((lx, ly)) => (2.0 * current_x - lx, 2.0 * current_y - ly),
None => (current_x, current_y),
};
ctx.quadratic_curve_to(
offset_x + cx * scale,
offset_y + cy * scale,
offset_x + x * scale,
offset_y + y * scale,
);
current_x = x;
current_y = y;
last_control = Some((cx, cy));
}
}
't' => {
if let Some((dx, dy)) = parse_two_numbers(&mut chars) {
let (cx, cy) = match last_control {
Some((lx, ly)) => (2.0 * current_x - lx, 2.0 * current_y - ly),
None => (current_x, current_y),
};
let x = current_x + dx;
let y = current_y + dy;
ctx.quadratic_curve_to(
offset_x + cx * scale,
offset_y + cy * scale,
offset_x + x * scale,
offset_y + y * scale,
);
current_x = x;
current_y = y;
last_control = Some((cx, cy));
}
}
'A' | 'a' => {
let is_relative = current_cmd == 'a';
if let Some((rx, ry, rotation, large, sweep, x, y)) = parse_arc_params(&mut chars) {
let (end_x, end_y) = if is_relative {
(current_x + x, current_y + y)
} else {
(x, y)
};
let arc_points = arc_to_points(
current_x, current_y,
rx, ry,
rotation,
large != 0.0,
sweep != 0.0,
end_x, end_y,
);
for (px, py) in arc_points {
ctx.line_to(offset_x + px * scale, offset_y + py * scale);
}
current_x = end_x;
current_y = end_y;
last_control = None;
}
}
'Z' | 'z' => {
ctx.close_path();
current_x = start_x;
current_y = start_y;
last_control = None;
}
_ => {
chars.next();
}
}
}
}
fn parse_number(chars: &mut std::iter::Peekable<std::str::Chars>) -> Option<f64> {
loop {
match chars.peek() {
Some(&c) if c.is_whitespace() || c == ',' => { chars.next(); }
Some(&'&') => {
while let Some(&c) = chars.peek() {
chars.next();
if c == ';' { break; }
}
}
_ => break,
}
}
let mut num_str = String::new();
if let Some(&c) = chars.peek() {
if c == '-' || c == '+' {
num_str.push(chars.next().unwrap());
}
}
while let Some(&c) = chars.peek() {
if c.is_ascii_digit() || c == '.' {
num_str.push(chars.next().unwrap());
} else {
break;
}
}
num_str.parse::<f64>().ok()
}
fn parse_two_numbers(chars: &mut std::iter::Peekable<std::str::Chars>) -> Option<(f64, f64)> {
let x = parse_number(chars)?;
let y = parse_number(chars)?;
Some((x, y))
}
fn parse_four_numbers(chars: &mut std::iter::Peekable<std::str::Chars>) -> Option<(f64, f64, f64, f64)> {
let a = parse_number(chars)?;
let b = parse_number(chars)?;
let c = parse_number(chars)?;
let d = parse_number(chars)?;
Some((a, b, c, d))
}
fn parse_six_numbers(chars: &mut std::iter::Peekable<std::str::Chars>) -> Option<(f64, f64, f64, f64, f64, f64)> {
let a = parse_number(chars)?;
let b = parse_number(chars)?;
let c = parse_number(chars)?;
let d = parse_number(chars)?;
let e = parse_number(chars)?;
let f = parse_number(chars)?;
Some((a, b, c, d, e, f))
}
fn parse_arc_params(chars: &mut std::iter::Peekable<std::str::Chars>) -> Option<(f64, f64, f64, f64, f64, f64, f64)> {
let rx = parse_number(chars)?;
let ry = parse_number(chars)?;
let rotation = parse_number(chars)?;
let large_arc = parse_number(chars)?;
let sweep = parse_number(chars)?;
let x = parse_number(chars)?;
let y = parse_number(chars)?;
Some((rx, ry, rotation, large_arc, sweep, x, y))
}
fn extract_svg_attr(content: &str, attr: &str) -> Option<f64> {
let pattern = format!("{}=\"", attr);
if let Some(start) = content.find(&pattern) {
let value_start = start + pattern.len();
if let Some(end) = content[value_start..].find('"') {
return content[value_start..value_start + end].parse().ok();
}
}
None
}
fn is_svg_filled_with_default(content: &str, default_filled: bool) -> bool {
if let Some(start) = content.find("fill=\"") {
let value_start = start + 6;
if let Some(end) = content[value_start..].find('"') {
let fill_value = &content[value_start..value_start + end];
return fill_value != "none";
}
}
default_filled
}
fn svg_root_has_fill_none(svg: &str) -> bool {
if let Some(start) = svg.find("<svg") {
if let Some(end) = svg[start..].find('>') {
let svg_tag = &svg[start..start + end + 1];
if let Some(fill_start) = svg_tag.find("fill=\"") {
let value_start = fill_start + 6;
if let Some(fill_end) = svg_tag[value_start..].find('"') {
let fill_value = &svg_tag[value_start..value_start + fill_end];
return fill_value == "none";
}
}
}
}
false
}
fn parse_svg_circles(svg: &str, default_filled: bool) -> Vec<(f64, f64, f64, bool)> {
let mut circles = Vec::new();
let mut search_from = 0;
while let Some(start) = svg[search_from..].find("<circle") {
let abs_start = search_from + start;
if let Some(end) = svg[abs_start..].find("/>") {
let tag_content = &svg[abs_start..abs_start + end + 2];
let cx = extract_svg_attr(tag_content, "cx").unwrap_or(0.0);
let cy = extract_svg_attr(tag_content, "cy").unwrap_or(0.0);
let r = extract_svg_attr(tag_content, "r").unwrap_or(0.0);
let filled = is_svg_filled_with_default(tag_content, default_filled);
if r > 0.0 {
circles.push((cx, cy, r, filled));
}
search_from = abs_start + end + 2;
} else if let Some(end) = svg[abs_start..].find('>') {
let tag_content = &svg[abs_start..abs_start + end + 1];
let cx = extract_svg_attr(tag_content, "cx").unwrap_or(0.0);
let cy = extract_svg_attr(tag_content, "cy").unwrap_or(0.0);
let r = extract_svg_attr(tag_content, "r").unwrap_or(0.0);
let filled = is_svg_filled_with_default(tag_content, default_filled);
if r > 0.0 {
circles.push((cx, cy, r, filled));
}
search_from = abs_start + end + 1;
} else {
break;
}
}
circles
}
fn parse_svg_rects(svg: &str, default_filled: bool) -> Vec<(f64, f64, f64, f64, f64, bool)> {
let mut rects = Vec::new();
let mut search_from = 0;
while let Some(start) = svg[search_from..].find("<rect") {
let abs_start = search_from + start;
if let Some(end) = svg[abs_start..].find("/>") {
let tag_content = &svg[abs_start..abs_start + end + 2];
let x = extract_svg_attr(tag_content, "x").unwrap_or(0.0);
let y = extract_svg_attr(tag_content, "y").unwrap_or(0.0);
let w = extract_svg_attr(tag_content, "width").unwrap_or(0.0);
let h = extract_svg_attr(tag_content, "height").unwrap_or(0.0);
let rx = extract_svg_attr(tag_content, "rx").unwrap_or(0.0);
let filled = is_svg_filled_with_default(tag_content, default_filled);
if w > 0.0 && h > 0.0 {
rects.push((x, y, w, h, rx, filled));
}
search_from = abs_start + end + 2;
} else if let Some(end) = svg[abs_start..].find('>') {
let tag_content = &svg[abs_start..abs_start + end + 1];
let x = extract_svg_attr(tag_content, "x").unwrap_or(0.0);
let y = extract_svg_attr(tag_content, "y").unwrap_or(0.0);
let w = extract_svg_attr(tag_content, "width").unwrap_or(0.0);
let h = extract_svg_attr(tag_content, "height").unwrap_or(0.0);
let rx = extract_svg_attr(tag_content, "rx").unwrap_or(0.0);
let filled = is_svg_filled_with_default(tag_content, default_filled);
if w > 0.0 && h > 0.0 {
rects.push((x, y, w, h, rx, filled));
}
search_from = abs_start + end + 1;
} else {
break;
}
}
rects
}
fn parse_svg_lines(svg: &str) -> Vec<(f64, f64, f64, f64)> {
let mut lines = Vec::new();
let mut search_from = 0;
while let Some(start) = svg[search_from..].find("<line") {
let abs_start = search_from + start;
if let Some(end) = svg[abs_start..].find("/>") {
let tag_content = &svg[abs_start..abs_start + end + 2];
let x1 = extract_svg_attr(tag_content, "x1").unwrap_or(0.0);
let y1 = extract_svg_attr(tag_content, "y1").unwrap_or(0.0);
let x2 = extract_svg_attr(tag_content, "x2").unwrap_or(0.0);
let y2 = extract_svg_attr(tag_content, "y2").unwrap_or(0.0);
lines.push((x1, y1, x2, y2));
search_from = abs_start + end + 2;
} else if let Some(end) = svg[abs_start..].find('>') {
let tag_content = &svg[abs_start..abs_start + end + 1];
let x1 = extract_svg_attr(tag_content, "x1").unwrap_or(0.0);
let y1 = extract_svg_attr(tag_content, "y1").unwrap_or(0.0);
let x2 = extract_svg_attr(tag_content, "x2").unwrap_or(0.0);
let y2 = extract_svg_attr(tag_content, "y2").unwrap_or(0.0);
lines.push((x1, y1, x2, y2));
search_from = abs_start + end + 1;
} else {
break;
}
}
lines
}
fn extract_svg_points(content: &str) -> Vec<(f64, f64)> {
let mut points = Vec::new();
if let Some(start) = content.find("points=\"") {
let value_start = start + 8;
if let Some(end) = content[value_start..].find('"') {
let points_str = &content[value_start..value_start + end];
let mut chars = points_str.chars().peekable();
loop {
while chars.peek().map(|c| c.is_whitespace()).unwrap_or(false) {
chars.next();
}
if chars.peek().is_none() {
break;
}
let mut num_str = String::new();
if chars.peek() == Some(&'-') || chars.peek() == Some(&'+') {
num_str.push(chars.next().unwrap());
}
while chars.peek().map(|c| c.is_ascii_digit() || *c == '.').unwrap_or(false) {
num_str.push(chars.next().unwrap());
}
let x: f64 = match num_str.parse() {
Ok(v) => v,
Err(_) => break,
};
while chars.peek().map(|c| c.is_whitespace() || *c == ',').unwrap_or(false) {
chars.next();
}
let mut num_str = String::new();
if chars.peek() == Some(&'-') || chars.peek() == Some(&'+') {
num_str.push(chars.next().unwrap());
}
while chars.peek().map(|c| c.is_ascii_digit() || *c == '.').unwrap_or(false) {
num_str.push(chars.next().unwrap());
}
let y: f64 = match num_str.parse() {
Ok(v) => v,
Err(_) => break,
};
points.push((x, y));
while chars.peek().map(|c| c.is_whitespace() || *c == ',').unwrap_or(false) {
chars.next();
}
}
}
}
points
}
fn parse_svg_polylines(svg: &str) -> Vec<(Vec<(f64, f64)>, bool)> {
let mut polylines = Vec::new();
let mut search_from = 0;
while let Some(start) = svg[search_from..].find("<polyline") {
let abs_start = search_from + start;
if let Some(end) = svg[abs_start..].find("/>") {
let tag_content = &svg[abs_start..abs_start + end + 2];
let points = extract_svg_points(tag_content);
if !points.is_empty() {
polylines.push((points, false));
}
search_from = abs_start + end + 2;
} else if let Some(end) = svg[abs_start..].find('>') {
let tag_content = &svg[abs_start..abs_start + end + 1];
let points = extract_svg_points(tag_content);
if !points.is_empty() {
polylines.push((points, false));
}
search_from = abs_start + end + 1;
} else {
break;
}
}
search_from = 0;
while let Some(start) = svg[search_from..].find("<polygon") {
let abs_start = search_from + start;
if let Some(end) = svg[abs_start..].find("/>") {
let tag_content = &svg[abs_start..abs_start + end + 2];
let points = extract_svg_points(tag_content);
if !points.is_empty() {
polylines.push((points, true));
}
search_from = abs_start + end + 2;
} else if let Some(end) = svg[abs_start..].find('>') {
let tag_content = &svg[abs_start..abs_start + end + 1];
let points = extract_svg_points(tag_content);
if !points.is_empty() {
polylines.push((points, true));
}
search_from = abs_start + end + 1;
} else {
break;
}
}
polylines
}
#[inline]
fn rotate_pt(px: f64, py: f64, cx: f64, cy: f64, sin_a: f64, cos_a: f64) -> (f64, f64) {
let dx = px - cx;
let dy = py - cy;
(cx + dx * cos_a - dy * sin_a, cy + dx * sin_a + dy * cos_a)
}
#[allow(clippy::too_many_arguments)]
fn render_path_data_rotated(
ctx: &mut dyn RenderContext, path_data: &str,
offset_x: f64, offset_y: f64, scale: f64,
cx: f64, cy: f64, sin_a: f64, cos_a: f64,
) {
let mut current_x = 0.0;
let mut current_y = 0.0;
let mut start_x = 0.0;
let mut start_y = 0.0;
let mut last_control: Option<(f64, f64)> = None;
let mut chars = path_data.chars().peekable();
let mut current_cmd = 'M';
while chars.peek().is_some() {
while chars.peek().map(|c| c.is_whitespace() || *c == ',').unwrap_or(false) {
chars.next();
}
if let Some(&c) = chars.peek() {
if c.is_alphabetic() {
current_cmd = c;
chars.next();
while chars.peek().map(|c| c.is_whitespace() || *c == ',').unwrap_or(false) {
chars.next();
}
}
}
match current_cmd {
'M' => {
if let Some((x, y)) = parse_two_numbers(&mut chars) {
current_x = x;
current_y = y;
start_x = x;
start_y = y;
let (rx, ry) = rotate_pt(offset_x + x * scale, offset_y + y * scale, cx, cy, sin_a, cos_a);
ctx.move_to(rx, ry);
current_cmd = 'L';
last_control = None;
}
}
'm' => {
if let Some((dx, dy)) = parse_two_numbers(&mut chars) {
current_x += dx;
current_y += dy;
start_x = current_x;
start_y = current_y;
let (rx, ry) = rotate_pt(offset_x + current_x * scale, offset_y + current_y * scale, cx, cy, sin_a, cos_a);
ctx.move_to(rx, ry);
current_cmd = 'l';
last_control = None;
}
}
'L' => {
if let Some((x, y)) = parse_two_numbers(&mut chars) {
current_x = x;
current_y = y;
let (rx, ry) = rotate_pt(offset_x + x * scale, offset_y + y * scale, cx, cy, sin_a, cos_a);
ctx.line_to(rx, ry);
last_control = None;
}
}
'l' => {
if let Some((dx, dy)) = parse_two_numbers(&mut chars) {
current_x += dx;
current_y += dy;
let (rx, ry) = rotate_pt(offset_x + current_x * scale, offset_y + current_y * scale, cx, cy, sin_a, cos_a);
ctx.line_to(rx, ry);
last_control = None;
}
}
'H' => {
if let Some(x) = parse_number(&mut chars) {
current_x = x;
let (rx, ry) = rotate_pt(offset_x + x * scale, offset_y + current_y * scale, cx, cy, sin_a, cos_a);
ctx.line_to(rx, ry);
last_control = None;
}
}
'h' => {
if let Some(dx) = parse_number(&mut chars) {
current_x += dx;
let (rx, ry) = rotate_pt(offset_x + current_x * scale, offset_y + current_y * scale, cx, cy, sin_a, cos_a);
ctx.line_to(rx, ry);
last_control = None;
}
}
'V' => {
if let Some(y) = parse_number(&mut chars) {
current_y = y;
let (rx, ry) = rotate_pt(offset_x + current_x * scale, offset_y + y * scale, cx, cy, sin_a, cos_a);
ctx.line_to(rx, ry);
last_control = None;
}
}
'v' => {
if let Some(dy) = parse_number(&mut chars) {
current_y += dy;
let (rx, ry) = rotate_pt(offset_x + current_x * scale, offset_y + current_y * scale, cx, cy, sin_a, cos_a);
ctx.line_to(rx, ry);
last_control = None;
}
}
'C' => {
if let Some((c1x, c1y, c2x, c2y, x, y)) = parse_six_numbers(&mut chars) {
let (r1x, r1y) = rotate_pt(offset_x + c1x * scale, offset_y + c1y * scale, cx, cy, sin_a, cos_a);
let (r2x, r2y) = rotate_pt(offset_x + c2x * scale, offset_y + c2y * scale, cx, cy, sin_a, cos_a);
let (rx, ry) = rotate_pt(offset_x + x * scale, offset_y + y * scale, cx, cy, sin_a, cos_a);
ctx.bezier_curve_to(r1x, r1y, r2x, r2y, rx, ry);
current_x = x;
current_y = y;
last_control = Some((c2x, c2y));
}
}
'c' => {
if let Some((dc1x, dc1y, dc2x, dc2y, dx, dy)) = parse_six_numbers(&mut chars) {
let c1x = current_x + dc1x;
let c1y = current_y + dc1y;
let c2x = current_x + dc2x;
let c2y = current_y + dc2y;
let x = current_x + dx;
let y = current_y + dy;
let (r1x, r1y) = rotate_pt(offset_x + c1x * scale, offset_y + c1y * scale, cx, cy, sin_a, cos_a);
let (r2x, r2y) = rotate_pt(offset_x + c2x * scale, offset_y + c2y * scale, cx, cy, sin_a, cos_a);
let (rx, ry) = rotate_pt(offset_x + x * scale, offset_y + y * scale, cx, cy, sin_a, cos_a);
ctx.bezier_curve_to(r1x, r1y, r2x, r2y, rx, ry);
current_x = x;
current_y = y;
last_control = Some((c2x, c2y));
}
}
'S' => {
if let Some((c2x, c2y, x, y)) = parse_four_numbers(&mut chars) {
let (c1x, c1y) = match last_control {
Some((lx, ly)) => (2.0 * current_x - lx, 2.0 * current_y - ly),
None => (current_x, current_y),
};
let (r1x, r1y) = rotate_pt(offset_x + c1x * scale, offset_y + c1y * scale, cx, cy, sin_a, cos_a);
let (r2x, r2y) = rotate_pt(offset_x + c2x * scale, offset_y + c2y * scale, cx, cy, sin_a, cos_a);
let (rx, ry) = rotate_pt(offset_x + x * scale, offset_y + y * scale, cx, cy, sin_a, cos_a);
ctx.bezier_curve_to(r1x, r1y, r2x, r2y, rx, ry);
current_x = x;
current_y = y;
last_control = Some((c2x, c2y));
}
}
's' => {
if let Some((dc2x, dc2y, dx, dy)) = parse_four_numbers(&mut chars) {
let (c1x, c1y) = match last_control {
Some((lx, ly)) => (2.0 * current_x - lx, 2.0 * current_y - ly),
None => (current_x, current_y),
};
let c2x = current_x + dc2x;
let c2y = current_y + dc2y;
let x = current_x + dx;
let y = current_y + dy;
let (r1x, r1y) = rotate_pt(offset_x + c1x * scale, offset_y + c1y * scale, cx, cy, sin_a, cos_a);
let (r2x, r2y) = rotate_pt(offset_x + c2x * scale, offset_y + c2y * scale, cx, cy, sin_a, cos_a);
let (rx, ry) = rotate_pt(offset_x + x * scale, offset_y + y * scale, cx, cy, sin_a, cos_a);
ctx.bezier_curve_to(r1x, r1y, r2x, r2y, rx, ry);
current_x = x;
current_y = y;
last_control = Some((c2x, c2y));
}
}
'Q' => {
if let Some((qcx, qcy, x, y)) = parse_four_numbers(&mut chars) {
let (rcx, rcy) = rotate_pt(offset_x + qcx * scale, offset_y + qcy * scale, cx, cy, sin_a, cos_a);
let (rx, ry) = rotate_pt(offset_x + x * scale, offset_y + y * scale, cx, cy, sin_a, cos_a);
ctx.quadratic_curve_to(rcx, rcy, rx, ry);
current_x = x;
current_y = y;
last_control = Some((qcx, qcy));
}
}
'q' => {
if let Some((dcx, dcy, dx, dy)) = parse_four_numbers(&mut chars) {
let qcx = current_x + dcx;
let qcy = current_y + dcy;
let x = current_x + dx;
let y = current_y + dy;
let (rcx, rcy) = rotate_pt(offset_x + qcx * scale, offset_y + qcy * scale, cx, cy, sin_a, cos_a);
let (rx, ry) = rotate_pt(offset_x + x * scale, offset_y + y * scale, cx, cy, sin_a, cos_a);
ctx.quadratic_curve_to(rcx, rcy, rx, ry);
current_x = x;
current_y = y;
last_control = Some((qcx, qcy));
}
}
'T' => {
if let Some((x, y)) = parse_two_numbers(&mut chars) {
let (qcx, qcy) = match last_control {
Some((lx, ly)) => (2.0 * current_x - lx, 2.0 * current_y - ly),
None => (current_x, current_y),
};
let (rcx, rcy) = rotate_pt(offset_x + qcx * scale, offset_y + qcy * scale, cx, cy, sin_a, cos_a);
let (rx, ry) = rotate_pt(offset_x + x * scale, offset_y + y * scale, cx, cy, sin_a, cos_a);
ctx.quadratic_curve_to(rcx, rcy, rx, ry);
current_x = x;
current_y = y;
last_control = Some((qcx, qcy));
}
}
't' => {
if let Some((dx, dy)) = parse_two_numbers(&mut chars) {
let (qcx, qcy) = match last_control {
Some((lx, ly)) => (2.0 * current_x - lx, 2.0 * current_y - ly),
None => (current_x, current_y),
};
let x = current_x + dx;
let y = current_y + dy;
let (rcx, rcy) = rotate_pt(offset_x + qcx * scale, offset_y + qcy * scale, cx, cy, sin_a, cos_a);
let (rx, ry) = rotate_pt(offset_x + x * scale, offset_y + y * scale, cx, cy, sin_a, cos_a);
ctx.quadratic_curve_to(rcx, rcy, rx, ry);
current_x = x;
current_y = y;
last_control = Some((qcx, qcy));
}
}
'A' | 'a' => {
let is_relative = current_cmd == 'a';
if let Some((rx, ry, rotation, large, sweep, x, y)) = parse_arc_params(&mut chars) {
let (end_x, end_y) = if is_relative {
(current_x + x, current_y + y)
} else {
(x, y)
};
let arc_points = arc_to_points(
current_x, current_y,
rx, ry,
rotation,
large != 0.0,
sweep != 0.0,
end_x, end_y,
);
for (px, py) in arc_points {
let (rpx, rpy) = rotate_pt(offset_x + px * scale, offset_y + py * scale, cx, cy, sin_a, cos_a);
ctx.line_to(rpx, rpy);
}
current_x = end_x;
current_y = end_y;
last_control = None;
}
}
'Z' | 'z' => {
ctx.close_path();
current_x = start_x;
current_y = start_y;
last_control = None;
}
_ => {
chars.next();
}
}
}
}
#[allow(clippy::too_many_arguments)]
pub fn draw_svg_icon_rotated(
ctx: &mut dyn RenderContext, svg: &str,
x: f64, y: f64, width: f64, height: f64,
color: &str, angle: f64,
) {
let (vb_width, vb_height) = parse_viewbox(svg).unwrap_or((24.0, 24.0));
let scale_x = width / vb_width;
let scale_y = height / vb_height;
let scale = scale_x.min(scale_y);
let offset_x = (x + (width - vb_width * scale) / 2.0).floor();
let offset_y = (y + (height - vb_height * scale) / 2.0).floor();
let cx = x + width / 2.0;
let cy = y + height / 2.0;
let sin_a = angle.sin();
let cos_a = angle.cos();
let has_fill_none = svg_root_has_fill_none(svg);
let default_filled = !has_fill_none;
let stroke_width = 1.5 * scale;
ctx.set_stroke_color(color);
ctx.set_stroke_width(stroke_width);
ctx.set_line_cap("round");
ctx.set_line_join("round");
ctx.set_line_dash(&[]);
for path_info in parse_svg_paths(svg, default_filled) {
ctx.begin_path();
render_path_data_rotated(ctx, &path_info.d, offset_x, offset_y, scale, cx, cy, sin_a, cos_a);
if path_info.filled {
ctx.set_fill_color(color);
ctx.fill();
}
if path_info.stroked {
if let Some(ref dash) = path_info.dash_array {
let scaled_dash: Vec<f64> = dash.iter().map(|d| d * scale).collect();
ctx.set_line_dash(&scaled_dash);
}
ctx.stroke();
if path_info.dash_array.is_some() {
ctx.set_line_dash(&[]);
}
}
}
for (ccx, ccy, r, filled) in parse_svg_circles(svg, default_filled) {
let tx = offset_x + ccx * scale;
let ty = offset_y + ccy * scale;
let (rtx, rty) = rotate_pt(tx, ty, cx, cy, sin_a, cos_a);
let tr = r * scale;
ctx.begin_path();
ctx.arc(rtx, rty, tr, 0.0, std::f64::consts::PI * 2.0);
if filled {
ctx.set_fill_color(color);
ctx.fill();
} else {
ctx.stroke();
}
}
for (rect_x, rect_y, rw, rh, _rounding, filled) in parse_svg_rects(svg, default_filled) {
let tx = offset_x + rect_x * scale;
let ty = offset_y + rect_y * scale;
let tw = rw * scale;
let th = rh * scale;
let corners = [
(tx, ty),
(tx + tw, ty),
(tx + tw, ty + th),
(tx, ty + th),
];
let rotated: Vec<(f64, f64)> = corners.iter()
.map(|&(px, py)| rotate_pt(px, py, cx, cy, sin_a, cos_a))
.collect();
ctx.begin_path();
ctx.move_to(rotated[0].0, rotated[0].1);
for &(rx, ry) in &rotated[1..] {
ctx.line_to(rx, ry);
}
ctx.close_path();
if filled {
ctx.set_fill_color(color);
ctx.fill();
} else {
ctx.stroke();
}
}
for (x1, y1, x2, y2) in parse_svg_lines(svg) {
let tx1 = offset_x + x1 * scale;
let ty1 = offset_y + y1 * scale;
let tx2 = offset_x + x2 * scale;
let ty2 = offset_y + y2 * scale;
let (rtx1, rty1) = rotate_pt(tx1, ty1, cx, cy, sin_a, cos_a);
let (rtx2, rty2) = rotate_pt(tx2, ty2, cx, cy, sin_a, cos_a);
ctx.begin_path();
ctx.move_to(rtx1, rty1);
ctx.line_to(rtx2, rty2);
ctx.stroke();
}
for (points, closed) in parse_svg_polylines(svg) {
if points.len() >= 2 {
ctx.begin_path();
let (px, py) = points[0];
let tx = offset_x + px * scale;
let ty = offset_y + py * scale;
let (rtx, rty) = rotate_pt(tx, ty, cx, cy, sin_a, cos_a);
ctx.move_to(rtx, rty);
for &(px, py) in &points[1..] {
let tx = offset_x + px * scale;
let ty = offset_y + py * scale;
let (rtx, rty) = rotate_pt(tx, ty, cx, cy, sin_a, cos_a);
ctx.line_to(rtx, rty);
}
if closed {
ctx.close_path();
}
ctx.stroke();
}
}
}
struct GradientInfo {
x1: f64,
y1: f64,
x2: f64,
y2: f64,
stops: Vec<(f32, String)>,
}
fn extract_attr(text: &str, attr_prefix: &str) -> Option<String> {
let start = text.find(attr_prefix)? + attr_prefix.len();
let end = text[start..].find('"')?;
Some(text[start..start + end].to_string())
}
#[allow(dead_code)]
fn resolve_gradient_color(svg: &str, url_ref: &str) -> Option<String> {
let id = url_ref.strip_prefix("url(#")?.strip_suffix(')')?;
let search = format!("id=\"{}\"", id);
let grad_pos = svg.find(&search)?;
let after_grad = &svg[grad_pos..];
let stop_pos = after_grad.find("stop-color=\"")?;
let color_start = stop_pos + 12; let color_end = after_grad[color_start..].find('"')?;
Some(after_grad[color_start..color_start + color_end].to_string())
}
fn parse_gradient(svg: &str, gradient_id: &str) -> Option<GradientInfo> {
let search = format!("id=\"{}\"", gradient_id);
let grad_pos = svg.find(&search)?;
let after = &svg[grad_pos..];
let grad_end = after.find("</linearGradient>")?;
let grad_text = &after[..grad_end];
let x1 = extract_attr(grad_text, "x1=\"")
.and_then(|s| s.trim_end_matches('%').parse::<f64>().ok())
.unwrap_or(0.0);
let y1 = extract_attr(grad_text, "y1=\"")
.and_then(|s| s.trim_end_matches('%').parse::<f64>().ok())
.unwrap_or(0.0);
let x2 = extract_attr(grad_text, "x2=\"")
.and_then(|s| s.trim_end_matches('%').parse::<f64>().ok())
.unwrap_or(0.0);
let y2 = extract_attr(grad_text, "y2=\"")
.and_then(|s| s.trim_end_matches('%').parse::<f64>().ok())
.unwrap_or(1.0);
let mut stops: Vec<(f32, String)> = Vec::new();
let mut search_from = 0usize;
while let Some(stop_rel) = grad_text[search_from..].find("<stop") {
let abs = search_from + stop_rel;
let remaining = &grad_text[abs..];
let stop_end = match remaining.find("/>") {
Some(e) => e,
None => break,
};
let stop_tag = &remaining[..stop_end + 2];
let offset = extract_attr(stop_tag, "offset=\"")
.and_then(|s| {
let s = s.trim_end_matches('%');
s.parse::<f32>().ok().map(|v| if v > 1.0 { v / 100.0 } else { v })
})
.unwrap_or(0.0);
let color = extract_attr(stop_tag, "stop-color=\"")
.or_else(|| {
extract_attr(stop_tag, "style=\"").and_then(|style| {
let sc_pos = style.find("stop-color:")?;
let after_sc = style[sc_pos + 11..].trim_start();
let end = after_sc.find(|c: char| c == ';' || c == '"').unwrap_or(after_sc.len());
Some(after_sc[..end].trim().to_string())
})
})
.unwrap_or_else(|| "black".to_string());
stops.push((offset, color));
search_from = abs + stop_end + 2;
}
if stops.is_empty() {
return None;
}
Some(GradientInfo { x1, y1, x2, y2, stops })
}
pub fn draw_svg_multicolor(ctx: &mut dyn RenderContext, svg: &str, x: f64, y: f64, width: f64, height: f64) {
let (vb_width, vb_height) = parse_viewbox(svg).unwrap_or((24.0, 24.0));
let scale_x = width / vb_width;
let scale_y = height / vb_height;
let scale = scale_x.min(scale_y);
let offset_x = (x + (width - vb_width * scale) / 2.0).floor();
let offset_y = (y + (height - vb_height * scale) / 2.0).floor();
let has_fill_none = svg_root_has_fill_none(svg);
let default_filled = !has_fill_none;
for path_info in parse_svg_paths(svg, default_filled) {
if path_info.filled {
ctx.begin_path();
render_path_data(ctx, &path_info.d, offset_x, offset_y, scale);
if let Some(ref color) = path_info.fill_color {
if color.starts_with("url(#") {
let grad_id = color.strip_prefix("url(#").and_then(|s| s.strip_suffix(')'));
if let Some(id) = grad_id {
if let Some(grad) = parse_gradient(svg, id) {
let gx1 = offset_x + grad.x1 * scale;
let gy1 = offset_y + grad.y1 * scale;
let gx2 = offset_x + grad.x2 * scale;
let gy2 = offset_y + grad.y2 * scale;
let stops_refs: Vec<(f32, &str)> = grad
.stops
.iter()
.map(|(o, c)| (*o, c.as_str()))
.collect();
ctx.fill_linear_gradient(&stops_refs, gx1, gy1, gx2, gy2);
} else {
ctx.set_fill_color("black");
ctx.fill();
}
} else {
ctx.set_fill_color("black");
ctx.fill();
}
} else {
ctx.set_fill_color(color);
ctx.fill();
}
} else {
ctx.set_fill_color("black");
ctx.fill();
}
}
if path_info.stroked {
ctx.begin_path();
render_path_data(ctx, &path_info.d, offset_x, offset_y, scale);
let sc = path_info.stroke_color.as_deref().unwrap_or("black");
let sw = path_info.stroke_width.unwrap_or(1.0) * scale;
ctx.set_stroke_color(sc);
ctx.set_stroke_width(sw);
ctx.set_line_cap("round");
ctx.set_line_join("round");
if let Some(ref dash) = path_info.dash_array {
let scaled_dash: Vec<f64> = dash.iter().map(|d| d * scale).collect();
ctx.set_line_dash(&scaled_dash);
} else {
ctx.set_line_dash(&[]);
}
ctx.stroke();
if path_info.dash_array.is_some() {
ctx.set_line_dash(&[]);
}
}
}
for (rx, ry, rw, rh, rounding, filled) in parse_svg_rects(svg, default_filled) {
if filled {
if rw >= vb_width * 0.95 && rh >= vb_height * 0.95 {
continue;
}
let tx = offset_x + rx * scale;
let ty = offset_y + ry * scale;
let tw = rw * scale;
let th = rh * scale;
ctx.set_fill_color("black");
if rounding > 0.0 {
ctx.fill_rounded_rect(tx, ty, tw, th, rounding * scale);
} else {
ctx.fill_rect(tx, ty, tw, th);
}
}
}
for (cx_val, cy_val, r, filled) in parse_svg_circles(svg, default_filled) {
if filled {
let tx = offset_x + cx_val * scale;
let ty = offset_y + cy_val * scale;
let tr = r * scale;
ctx.begin_path();
ctx.arc(tx, ty, tr, 0.0, std::f64::consts::PI * 2.0);
ctx.set_fill_color("black");
ctx.fill();
}
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_viewbox_64x64() {
let svg = r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path d="m 32,1 2,1z"/></svg>"#;
let (w, h) = parse_viewbox(svg).unwrap();
assert_eq!(w, 64.0);
assert_eq!(h, 64.0);
}
#[test]
fn test_parse_viewbox_24x24() {
let svg = r#"<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none"><path d="M1 1L23 23"/></svg>"#;
let (w, h) = parse_viewbox(svg).unwrap();
assert_eq!(w, 24.0);
assert_eq!(h, 24.0);
}
#[test]
fn test_svg_root_fill_none() {
let stroke_svg = r#"<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor"><path d="M1 1"/></svg>"#;
assert!(svg_root_has_fill_none(stroke_svg));
let fill_svg = r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path d="m 32,1z"/></svg>"#;
assert!(!svg_root_has_fill_none(fill_svg));
}
#[test]
fn test_parse_paths_fill_based() {
let svg = r#"<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64"><path d="m 32,0 32,64 -64,0z"/></svg>"#;
let paths = parse_svg_paths(svg, true); assert_eq!(paths.len(), 1);
assert!(paths[0].filled, "Fill-based SVG path should be filled");
assert!(!paths[0].stroked, "Fill-based SVG path should NOT be stroked");
assert_eq!(paths[0].d, "m 32,0 32,64 -64,0z");
}
#[test]
fn test_parse_paths_stroke_based() {
let svg = r#"<svg xmlns="http://www.w3.org/2000/svg" fill="none" stroke="currentColor"><path d="M1 1L23 23" /></svg>"#;
let paths = parse_svg_paths(svg, false); assert_eq!(paths.len(), 1);
assert!(!paths[0].filled, "Stroke-based SVG path should NOT be filled");
assert!(paths[0].stroked, "Stroke-based SVG path should be stroked");
}
#[test]
fn test_jet_svg_parses() {
let jet_svg = crate::render::icons::aviation::JET;
let has_fill_none = svg_root_has_fill_none(jet_svg);
assert!(!has_fill_none, "JET SVG should NOT have fill=none on root");
let paths = parse_svg_paths(jet_svg, !has_fill_none);
assert_eq!(paths.len(), 1, "JET SVG should have exactly one path");
assert!(paths[0].filled, "JET path should be filled");
assert!(!paths[0].d.is_empty(), "JET path data should not be empty");
}
#[test]
fn test_military_jet_svg_parses() {
let mil_svg = crate::render::icons::aviation::MILITARY_JET;
let paths = parse_svg_paths(mil_svg, true);
assert_eq!(paths.len(), 1);
assert!(paths[0].filled);
assert_eq!(paths[0].d, "m 32,0 32,64 -64,0z");
}
#[test]
fn test_maki_airport_with_xml_entities() {
let airport_svg = crate::render::icons::infrastructure::AIRPORT;
let (w, h) = parse_viewbox(airport_svg).unwrap();
assert_eq!(w, 15.0);
assert_eq!(h, 15.0);
let has_fill_none = svg_root_has_fill_none(airport_svg);
assert!(!has_fill_none, "AIRPORT should not have fill=none");
let paths = parse_svg_paths(airport_svg, true);
assert_eq!(paths.len(), 1, "AIRPORT should have one path");
println!("AIRPORT path d: {:?}", &paths[0].d);
}
#[test]
fn test_parse_number_basic() {
let input = "32,1 2,3";
let mut chars = input.chars().peekable();
assert_eq!(parse_number(&mut chars), Some(32.0));
assert_eq!(parse_number(&mut chars), Some(1.0));
assert_eq!(parse_number(&mut chars), Some(2.0));
assert_eq!(parse_number(&mut chars), Some(3.0));
}
#[test]
fn test_parse_number_negative() {
let input = "-15,-2 -9,0";
let mut chars = input.chars().peekable();
assert_eq!(parse_number(&mut chars), Some(-15.0));
assert_eq!(parse_number(&mut chars), Some(-2.0));
assert_eq!(parse_number(&mut chars), Some(-9.0));
assert_eq!(parse_number(&mut chars), Some(0.0));
}
#[test]
fn test_parse_number_decimal_no_leading_zero() {
let input = ".2761 -.5";
let mut chars = input.chars().peekable();
assert_eq!(parse_number(&mut chars), Some(0.2761));
assert_eq!(parse_number(&mut chars), Some(-0.5));
}
#[test]
fn test_parse_number_with_xml_entities() {
let input = "6.5-1
	l-0.3182,4.7727";
let mut chars = input.chars().peekable();
assert_eq!(parse_number(&mut chars), Some(6.5), "Should parse 6.5");
assert_eq!(parse_number(&mut chars), Some(-1.0), "Should parse -1.0");
assert_eq!(chars.next(), Some('&'), "Should be at first entity");
assert_eq!(parse_number(&mut chars), None, "Should return None when encountering 'l' after entities");
let input2 = "15,8.5
	l-6.5-1";
let mut chars2 = input2.chars().peekable();
assert_eq!(parse_number(&mut chars2), Some(15.0), "Should parse 15");
assert_eq!(parse_number(&mut chars2), Some(8.5), "Should parse 8.5");
assert_eq!(chars2.peek(), Some(&'&'), "Should be at entity after 8.5");
let input3 = "l-6.5
	-1";
let mut chars3 = input3.chars().peekable();
assert_eq!(chars3.next(), Some('l'), "Skip command");
assert_eq!(parse_number(&mut chars3), Some(-6.5), "Should parse -6.5");
assert_eq!(parse_number(&mut chars3), Some(-1.0), "Should skip entities and parse -1.0");
}
struct MockContext {
ops: Vec<String>,
}
impl MockContext {
fn new() -> Self {
Self { ops: Vec::new() }
}
}
impl crate::render::context::RenderContext for MockContext {
fn dpr(&self) -> f64 { 1.0 }
fn begin_path(&mut self) { self.ops.push("begin_path".to_string()); }
fn move_to(&mut self, x: f64, y: f64) { self.ops.push(format!("move_to({:.1},{:.1})", x, y)); }
fn line_to(&mut self, x: f64, y: f64) { self.ops.push(format!("line_to({:.1},{:.1})", x, y)); }
fn close_path(&mut self) { self.ops.push("close_path".to_string()); }
fn fill(&mut self) { self.ops.push("fill".to_string()); }
fn stroke(&mut self) { self.ops.push("stroke".to_string()); }
fn set_fill_color(&mut self, _color: &str) { self.ops.push("set_fill_color".to_string()); }
fn set_stroke_color(&mut self, _color: &str) { self.ops.push("set_stroke_color".to_string()); }
fn set_stroke_width(&mut self, _w: f64) { self.ops.push("set_stroke_width".to_string()); }
fn set_line_cap(&mut self, _cap: &str) { self.ops.push("set_line_cap".to_string()); }
fn set_line_join(&mut self, _join: &str) { self.ops.push("set_line_join".to_string()); }
fn set_line_dash(&mut self, _pattern: &[f64]) { self.ops.push("set_line_dash".to_string()); }
fn set_global_alpha(&mut self, _alpha: f64) { self.ops.push("set_global_alpha".to_string()); }
fn rect(&mut self, _x: f64, _y: f64, _w: f64, _h: f64) { self.ops.push("rect".to_string()); }
fn fill_rect(&mut self, _x: f64, _y: f64, _w: f64, _h: f64) { self.ops.push("fill_rect".to_string()); }
fn stroke_rect(&mut self, _x: f64, _y: f64, _w: f64, _h: f64) { self.ops.push("stroke_rect".to_string()); }
fn fill_rounded_rect(&mut self, _x: f64, _y: f64, _w: f64, _h: f64, _r: f64) { self.ops.push("fill_rounded_rect".to_string()); }
fn stroke_rounded_rect(&mut self, _x: f64, _y: f64, _w: f64, _h: f64, _r: f64) { self.ops.push("stroke_rounded_rect".to_string()); }
fn arc(&mut self, _x: f64, _y: f64, _r: f64, _start: f64, _end: f64) { self.ops.push("arc".to_string()); }
fn ellipse(&mut self, _cx: f64, _cy: f64, _rx: f64, _ry: f64, _rot: f64, _start: f64, _end: f64) { self.ops.push("ellipse".to_string()); }
fn bezier_curve_to(&mut self, _c1x: f64, _c1y: f64, _c2x: f64, _c2y: f64, _x: f64, _y: f64) { self.ops.push("bezier_curve_to".to_string()); }
fn quadratic_curve_to(&mut self, _cx: f64, _cy: f64, _x: f64, _y: f64) { self.ops.push("quadratic_curve_to".to_string()); }
fn clip(&mut self) { self.ops.push("clip".to_string()); }
fn set_font(&mut self, _font: &str) { self.ops.push("set_font".to_string()); }
fn set_text_align(&mut self, _align: crate::render::types::TextAlign) { self.ops.push("set_text_align".to_string()); }
fn set_text_baseline(&mut self, _baseline: crate::render::types::TextBaseline) { self.ops.push("set_text_baseline".to_string()); }
fn fill_text(&mut self, _text: &str, _x: f64, _y: f64) { self.ops.push("fill_text".to_string()); }
fn stroke_text(&mut self, _text: &str, _x: f64, _y: f64) { self.ops.push("stroke_text".to_string()); }
fn measure_text(&self, _text: &str) -> f64 { 0.0 }
fn save(&mut self) { self.ops.push("save".to_string()); }
fn restore(&mut self) { self.ops.push("restore".to_string()); }
fn translate(&mut self, _x: f64, _y: f64) { self.ops.push("translate".to_string()); }
fn rotate(&mut self, _angle: f64) { self.ops.push("rotate".to_string()); }
fn scale(&mut self, _x: f64, _y: f64) { self.ops.push("scale".to_string()); }
}
#[test]
fn test_draw_jet_icon_produces_fill_ops() {
let mut ctx = MockContext::new();
let jet_svg = crate::render::icons::aviation::JET;
draw_svg_icon(&mut ctx, jet_svg, 100.0, 100.0, 22.0, 22.0, "#4fc3f7");
println!("JET ops count: {}", ctx.ops.len());
for (i, op) in ctx.ops.iter().enumerate() {
println!(" [{:3}] {}", i, op);
}
assert!(ctx.ops.contains(&"begin_path".to_string()), "Should call begin_path");
assert!(ctx.ops.iter().any(|op| op.starts_with("move_to")), "Should call move_to");
assert!(ctx.ops.iter().any(|op| op.starts_with("line_to")), "Should call line_to");
assert!(ctx.ops.contains(&"close_path".to_string()), "Should call close_path");
assert!(ctx.ops.contains(&"set_fill_color".to_string()), "Should call set_fill_color");
assert!(ctx.ops.contains(&"fill".to_string()), "Should call fill");
assert!(!ctx.ops.contains(&"stroke".to_string()), "Should NOT call stroke for fill-based icon");
let line_count = ctx.ops.iter().filter(|op| op.starts_with("line_to")).count();
println!("JET line_to count: {}", line_count);
assert!(line_count > 20, "JET path should have 30+ line segments, got {}", line_count);
}
#[test]
fn test_draw_cloud_icon_produces_stroke_ops() {
let mut ctx = MockContext::new();
let cloud_svg = crate::render::icons::weather::CLOUD;
draw_svg_icon(&mut ctx, cloud_svg, 100.0, 100.0, 22.0, 22.0, "#ffa726");
println!("CLOUD ops count: {}", ctx.ops.len());
for (i, op) in ctx.ops.iter().enumerate() {
println!(" [{:3}] {}", i, op);
}
assert!(ctx.ops.contains(&"begin_path".to_string()), "Should call begin_path");
assert!(ctx.ops.contains(&"stroke".to_string()), "Should call stroke");
assert!(!ctx.ops.contains(&"fill".to_string()), "Should NOT call fill for stroke-based icon");
}
#[test]
fn test_draw_military_jet_simple_triangle() {
let mut ctx = MockContext::new();
let mil_svg = crate::render::icons::aviation::MILITARY_JET;
draw_svg_icon(&mut ctx, mil_svg, 100.0, 100.0, 22.0, 22.0, "#ff0000");
println!("MILITARY_JET ops:");
for (i, op) in ctx.ops.iter().enumerate() {
println!(" [{:3}] {}", i, op);
}
assert!(ctx.ops.contains(&"fill".to_string()), "Triangle should be filled");
let line_count = ctx.ops.iter().filter(|op| op.starts_with("line_to")).count();
assert_eq!(line_count, 2, "Triangle should have 2 line_to ops (3 points: move + 2 lines + close)");
}
#[test]
fn test_draw_airport_icon_with_xml_entities() {
let mut ctx = MockContext::new();
let airport_svg = crate::render::icons::infrastructure::AIRPORT;
draw_svg_icon(&mut ctx, airport_svg, 100.0, 100.0, 16.0, 16.0, "#2196f3");
println!("AIRPORT ops count: {}", ctx.ops.len());
println!("First 10 ops:");
for (i, op) in ctx.ops.iter().take(10).enumerate() {
println!(" [{:3}] {}", i, op);
}
assert!(ctx.ops.contains(&"begin_path".to_string()), "Should call begin_path");
assert!(ctx.ops.iter().any(|op| op.starts_with("move_to")), "Should call move_to");
assert!(ctx.ops.iter().any(|op| op.starts_with("line_to")), "Should call line_to");
assert!(ctx.ops.contains(&"fill".to_string()), "AIRPORT should be filled");
let line_count = ctx.ops.iter().filter(|op| op.starts_with("line_to")).count();
println!("AIRPORT line_to count: {}", line_count);
assert!(line_count > 5, "AIRPORT should have several line segments");
assert!(line_count < 100, "AIRPORT should not have excessive line segments (would indicate parsing issue)");
}
#[test]
fn test_all_maki_icons_with_xml_entities() {
let icons = vec![
("AIRPORT", crate::render::icons::infrastructure::AIRPORT),
("HELIPORT", crate::render::icons::infrastructure::HELIPORT),
("FUEL", crate::render::icons::infrastructure::FUEL),
("HOSPITAL", crate::render::icons::infrastructure::HOSPITAL),
];
for (name, svg) in icons {
println!("Testing {} icon...", name);
let mut ctx = MockContext::new();
draw_svg_icon(&mut ctx, svg, 100.0, 100.0, 16.0, 16.0, "#2196f3");
let line_count = ctx.ops.iter().filter(|op| op.starts_with("line_to")).count();
println!(" {} line_to ops: {}", name, line_count);
assert!(ctx.ops.contains(&"begin_path".to_string()), "{} should call begin_path", name);
assert!(line_count > 0, "{} should have line segments", name);
assert!(line_count < 200, "{} should not have excessive line segments", name);
}
println!("All Maki icons with XML entities render successfully!");
}
}