use std::sync::OnceLock;
use lyon_tessellation::{
BuffersBuilder, FillOptions, FillTessellator, FillVertex, VertexBuffers,
StrokeTessellator, StrokeOptions, StrokeVertex, LineCap, LineJoin,
};
use lyon_path::math::point;
use skrifa::MetadataProvider;
use uzor::render::{RenderContext, RenderContextExt, TextAlign, TextBaseline};
use uzor::fonts::{self, FontFamily};
use crate::instances::{DrawCmd, QuadInstance, TriangleInstance};
use crate::text::TextAreaData;
static FONT_REGULAR: OnceLock<Option<skrifa::FontRef<'static>>> = OnceLock::new();
static FONT_BOLD: OnceLock<Option<skrifa::FontRef<'static>>> = OnceLock::new();
static FONT_ITALIC: OnceLock<Option<skrifa::FontRef<'static>>> = OnceLock::new();
static FONT_BOLD_ITALIC: OnceLock<Option<skrifa::FontRef<'static>>> = OnceLock::new();
static FONT_PT_ROOT_UI: OnceLock<Option<skrifa::FontRef<'static>>> = OnceLock::new();
static FONT_JB_MONO_REGULAR: OnceLock<Option<skrifa::FontRef<'static>>> = OnceLock::new();
static FONT_JB_MONO_BOLD: OnceLock<Option<skrifa::FontRef<'static>>> = OnceLock::new();
static FONT_NERD_FONT: OnceLock<Option<skrifa::FontRef<'static>>> = OnceLock::new();
static FONT_SYMBOLS: OnceLock<Option<skrifa::FontRef<'static>>> = OnceLock::new();
static FONT_COLOR_EMOJI: OnceLock<Option<skrifa::FontRef<'static>>> = OnceLock::new();
static FONT_EMOJI: OnceLock<Option<skrifa::FontRef<'static>>> = OnceLock::new();
fn make_font_ref(data: &'static [u8]) -> Option<skrifa::FontRef<'static>> {
use skrifa::raw::FileRef;
match FileRef::new(data).ok()? {
FileRef::Font(f) => Some(f),
FileRef::Collection(c) => c.get(0).ok(),
}
}
fn get_font_ref(family: FontFamily, bold: bool, italic: bool) -> Option<&'static skrifa::FontRef<'static>> {
match family {
FontFamily::PtRootUi => FONT_PT_ROOT_UI
.get_or_init(|| make_font_ref(fonts::font_bytes(family, bold, italic))).as_ref(),
FontFamily::JetBrainsMono => {
let _ = italic;
if bold {
FONT_JB_MONO_BOLD
.get_or_init(|| make_font_ref(fonts::font_bytes(family, true, false))).as_ref()
} else {
FONT_JB_MONO_REGULAR
.get_or_init(|| make_font_ref(fonts::font_bytes(family, false, false))).as_ref()
}
}
FontFamily::Roboto => match (bold, italic) {
(true, true) => FONT_BOLD_ITALIC
.get_or_init(|| make_font_ref(fonts::font_bytes(family, true, true))).as_ref(),
(true, false) => FONT_BOLD
.get_or_init(|| make_font_ref(fonts::font_bytes(family, true, false))).as_ref(),
(false, true) => FONT_ITALIC
.get_or_init(|| make_font_ref(fonts::font_bytes(family, false, true))).as_ref(),
(false, false) => FONT_REGULAR
.get_or_init(|| make_font_ref(fonts::font_bytes(family, false, false))).as_ref(),
},
}
}
fn get_fallback_font_refs() -> [Option<&'static skrifa::FontRef<'static>>; 4] {
[
FONT_NERD_FONT.get_or_init(|| make_font_ref(fonts::SYMBOLS_NERD_FONT_MONO)).as_ref(),
FONT_SYMBOLS.get_or_init(|| make_font_ref(fonts::NOTO_SANS_SYMBOLS2)).as_ref(),
FONT_COLOR_EMOJI.get_or_init(|| make_font_ref(fonts::NOTO_COLOR_EMOJI)).as_ref(),
FONT_EMOJI.get_or_init(|| make_font_ref(fonts::NOTO_EMOJI)).as_ref(),
]
}
fn parse_color(color: &str) -> [f32; 4] {
let (r, g, b, a) = uzor::render::parse_color(color);
[r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, a as f32 / 255.0]
}
#[inline]
fn approx_eq(a: f32, b: f32) -> bool {
(a - b).abs() < 0.5
}
#[inline]
fn apply(t: &[f32; 6], x: f32, y: f32) -> (f32, f32) {
(t[0] * x + t[1] * y + t[4], t[2] * x + t[3] * y + t[5])
}
#[inline]
fn mul(a: &[f32; 6], b: &[f32; 6]) -> [f32; 6] {
[
a[0] * b[0] + a[1] * b[2],
a[0] * b[1] + a[1] * b[3],
a[2] * b[0] + a[3] * b[2],
a[2] * b[1] + a[3] * b[3],
a[0] * b[4] + a[1] * b[5] + a[4],
a[2] * b[4] + a[3] * b[5] + a[5],
]
}
fn translate_transform(t: &[f32; 6], x: f32, y: f32) -> [f32; 6] {
mul(t, &[1.0, 0.0, 0.0, 1.0, x, y])
}
fn rotate_transform(t: &[f32; 6], angle: f32) -> [f32; 6] {
let (s, c) = angle.sin_cos();
mul(t, &[c, -s, s, c, 0.0, 0.0])
}
fn scale_transform(t: &[f32; 6], sx: f32, sy: f32) -> [f32; 6] {
mul(t, &[sx, 0.0, 0.0, sy, 0.0, 0.0])
}
#[derive(Debug, Clone)]
enum PathCmd {
MoveTo(f32, f32),
LineTo(f32, f32),
Close,
Rect(f32, f32, f32, f32),
Arc { cx: f32, cy: f32, r: f32, start: f32, end: f32 },
}
#[derive(Clone)]
struct SavedState {
transform: [f32; 6],
fill_color: [f32; 4],
stroke_color: [f32; 4],
stroke_width: f32,
global_alpha: f64,
font_size: f32,
font_bold: bool,
font_italic: bool,
font_family: FontFamily,
text_align: TextAlign,
text_baseline: TextBaseline,
clip_depth: usize,
line_cap: String,
line_join: String,
}
pub struct InstancedRenderContext {
pub draw_commands: Vec<DrawCmd>,
transform: [f32; 6],
fill_color: [f32; 4],
stroke_color: [f32; 4],
stroke_width: f32,
global_alpha: f64,
font_size: f32,
font_bold: bool,
font_italic: bool,
font_family: FontFamily,
text_align: TextAlign,
text_baseline: TextBaseline,
line_cap: String,
line_join: String,
clip_stack: Vec<[f32; 4]>,
state_stack: Vec<SavedState>,
path: Vec<PathCmd>,
screen_w: f32,
screen_h: f32,
}
impl InstancedRenderContext {
pub fn new(screen_w: f32, screen_h: f32, offset_x: f32, offset_y: f32) -> Self {
let transform = [1.0, 0.0, 0.0, 1.0, offset_x, offset_y];
let root_clip = [0.0, 0.0, screen_w, screen_h];
Self {
draw_commands: Vec::new(),
transform,
fill_color: [1.0, 1.0, 1.0, 1.0],
stroke_color: [1.0, 1.0, 1.0, 1.0],
stroke_width: 1.0,
global_alpha: 1.0,
font_size: 12.0,
font_bold: false,
font_italic: false,
font_family: FontFamily::Roboto,
text_align: TextAlign::Left,
text_baseline: TextBaseline::Middle,
line_cap: String::new(),
line_join: String::new(),
clip_stack: vec![root_clip],
state_stack: Vec::new(),
path: Vec::new(),
screen_w,
screen_h,
}
}
pub fn clear(&mut self) {
self.draw_commands.clear();
}
fn current_clip(&self) -> [f32; 4] {
self.clip_stack.last().copied().unwrap_or([0.0, 0.0, self.screen_w, self.screen_h])
}
fn last_path_point(&self) -> (f32, f32) {
for cmd in self.path.iter().rev() {
match *cmd {
PathCmd::MoveTo(x, y) | PathCmd::LineTo(x, y) => return (x, y),
PathCmd::Rect(x, y, w, h) => return (x + w, y + h),
PathCmd::Arc { cx, cy, r, end, .. } => return (cx + r * end.cos(), cy + r * end.sin()),
PathCmd::Close => continue,
}
}
(0.0, 0.0)
}
fn apply_alpha(&self, color: [f32; 4]) -> [f32; 4] {
let a = (color[3] * self.global_alpha as f32).clamp(0.0, 1.0);
[color[0], color[1], color[2], a]
}
fn tessellate_stroke(&mut self) {
let path = std::mem::take(&mut self.path);
let color = self.apply_alpha(self.stroke_color);
let clip = self.current_clip();
let cap = match self.line_cap.as_str() {
"round" => LineCap::Round,
"square" => LineCap::Square,
_ => LineCap::Butt,
};
let join = match self.line_join.as_str() {
"round" => LineJoin::Round,
"bevel" => LineJoin::Bevel,
_ => LineJoin::Miter,
};
let mut builder = lyon_path::Path::builder();
let transform = self.transform;
let mut in_subpath = false;
for cmd in &path {
match *cmd {
PathCmd::MoveTo(x, y) => {
if in_subpath {
builder.end(false);
}
let (px, py) = apply(&transform, x, y);
builder.begin(point(px, py));
in_subpath = true;
}
PathCmd::LineTo(x, y) => {
if !in_subpath {
let (px, py) = apply(&transform, x, y);
builder.begin(point(px, py));
in_subpath = true;
} else {
let (px, py) = apply(&transform, x, y);
builder.line_to(point(px, py));
}
}
PathCmd::Close => {
if in_subpath {
builder.close();
in_subpath = false;
}
}
PathCmd::Rect(x, y, w, h) => {
if in_subpath {
builder.end(false);
}
let tl = apply(&transform, x, y);
let tr = apply(&transform, x + w, y);
let br = apply(&transform, x + w, y + h);
let bl = apply(&transform, x, y + h);
builder.begin(point(tl.0, tl.1));
builder.line_to(point(tr.0, tr.1));
builder.line_to(point(br.0, br.1));
builder.line_to(point(bl.0, bl.1));
builder.close();
in_subpath = false;
}
PathCmd::Arc { cx, cy, r, start, end } => {
let scale = ((transform[0].abs() + transform[3].abs()) * 0.5).max(1.0);
let screen_r = r * scale;
if screen_r < 0.5 {
let center = apply(&transform, cx, cy);
if in_subpath {
builder.line_to(point(center.0, center.1));
} else {
builder.begin(point(center.0, center.1));
in_subpath = true;
}
continue;
}
let sweep = end - start;
let steps = ((sweep.abs() * screen_r).max(sweep.abs() * 5.0)).max(8.0) as usize;
let step_angle = sweep / steps as f32;
let first_pt = apply(&transform, cx + r * start.cos(), cy + r * start.sin());
if in_subpath {
builder.line_to(point(first_pt.0, first_pt.1));
} else {
builder.begin(point(first_pt.0, first_pt.1));
in_subpath = true;
}
for i in 1..=steps {
let angle = start + step_angle * i as f32;
let p = apply(&transform, cx + r * angle.cos(), cy + r * angle.sin());
builder.line_to(point(p.0, p.1));
}
}
}
}
if in_subpath {
builder.end(false);
}
let lyon_path = builder.build();
let mut geometry: VertexBuffers<[f32; 2], u16> = VertexBuffers::new();
let mut tessellator = StrokeTessellator::new();
let options = StrokeOptions::tolerance(0.5)
.with_line_width(self.stroke_width)
.with_line_cap(cap)
.with_line_join(join);
let result = tessellator.tessellate_path(
&lyon_path,
&options,
&mut BuffersBuilder::new(&mut geometry, |vertex: StrokeVertex| {
vertex.position().to_array()
}),
);
if result.is_ok() && !geometry.indices.is_empty() {
for tri in geometry.indices.chunks(3) {
if tri.len() == 3 {
let v0 = geometry.vertices[tri[0] as usize];
let v1 = geometry.vertices[tri[1] as usize];
let v2 = geometry.vertices[tri[2] as usize];
self.draw_commands.push(DrawCmd::Triangle(TriangleInstance {
v0,
v1,
v2,
_pad0: [0.0; 2],
color,
clip_rect: clip,
}));
}
}
}
}
fn tessellate_fill(&mut self) {
let path = std::mem::take(&mut self.path);
let color = self.apply_alpha(self.fill_color);
let clip = self.current_clip();
if path.len() == 1 {
if let PathCmd::Rect(x, y, w, h) = path[0] {
let (px, py) = apply(&self.transform, x, y);
let (px2, py2) = apply(&self.transform, x + w, y + h);
self.draw_commands.push(DrawCmd::Quad(QuadInstance {
pos: [px.min(px2), py.min(py2)],
size: [(px2 - px).abs(), (py2 - py).abs()],
color,
corner_radius: 0.0,
border_width: 0.0,
_pad0: [0.0; 2],
border_color: [0.0; 4],
clip_rect: clip,
}));
return;
}
}
if path.len() == 5 {
let is_move = matches!(path[0], PathCmd::MoveTo(..));
let is_close = matches!(path[4], PathCmd::Close);
let all_lines = matches!(path[1], PathCmd::LineTo(..))
&& matches!(path[2], PathCmd::LineTo(..))
&& matches!(path[3], PathCmd::LineTo(..));
if is_move && is_close && all_lines {
let (x0, y0) = if let PathCmd::MoveTo(a, b) = path[0] { (a, b) } else { unreachable!() };
let (x1, y1) = if let PathCmd::LineTo(a, b) = path[1] { (a, b) } else { unreachable!() };
let (x2, y2) = if let PathCmd::LineTo(a, b) = path[2] { (a, b) } else { unreachable!() };
let (x3, y3) = if let PathCmd::LineTo(a, b) = path[3] { (a, b) } else { unreachable!() };
let is_aabb = (approx_eq(x0, x3) && approx_eq(x1, x2) && approx_eq(y0, y1) && approx_eq(y2, y3))
|| (approx_eq(y0, y1) && approx_eq(y2, y3) && approx_eq(x0, x3) && approx_eq(x1, x2))
|| (approx_eq(x0, x1) && approx_eq(x2, x3) && approx_eq(y1, y2) && approx_eq(y0, y3))
|| (approx_eq(y0, y3) && approx_eq(y1, y2) && approx_eq(x0, x1) && approx_eq(x2, x3));
if is_aabb {
let min_x = x0.min(x1).min(x2).min(x3);
let min_y = y0.min(y1).min(y2).min(y3);
let max_x = x0.max(x1).max(x2).max(x3);
let max_y = y0.max(y1).max(y2).max(y3);
let (px1, py1) = apply(&self.transform, min_x, min_y);
let (px2, py2) = apply(&self.transform, max_x, max_y);
self.draw_commands.push(DrawCmd::Quad(QuadInstance {
pos: [px1.min(px2), py1.min(py2)],
size: [(px2 - px1).abs(), (py2 - py1).abs()],
color,
corner_radius: 0.0,
border_width: 0.0,
_pad0: [0.0; 2],
border_color: [0.0; 4],
clip_rect: clip,
}));
return;
}
}
}
let mut builder = lyon_path::Path::builder();
let transform = &self.transform;
let mut in_subpath = false;
for cmd in &path {
match *cmd {
PathCmd::MoveTo(x, y) => {
if in_subpath {
builder.end(false);
}
let (px, py) = apply(transform, x, y);
builder.begin(point(px, py));
in_subpath = true;
}
PathCmd::LineTo(x, y) => {
if !in_subpath {
let (px, py) = apply(transform, x, y);
builder.begin(point(px, py));
in_subpath = true;
} else {
let (px, py) = apply(transform, x, y);
builder.line_to(point(px, py));
}
}
PathCmd::Close => {
if in_subpath {
builder.close();
in_subpath = false;
}
}
PathCmd::Rect(x, y, w, h) => {
if in_subpath {
builder.end(false);
}
let tl = apply(transform, x, y);
let tr = apply(transform, x + w, y);
let br = apply(transform, x + w, y + h);
let bl = apply(transform, x, y + h);
builder.begin(point(tl.0, tl.1));
builder.line_to(point(tr.0, tr.1));
builder.line_to(point(br.0, br.1));
builder.line_to(point(bl.0, bl.1));
builder.close();
in_subpath = false;
}
PathCmd::Arc { cx, cy, r, start, end } => {
let sweep = end - start;
let scale = ((transform[0].abs() + transform[3].abs()) * 0.5).max(1.0);
let screen_r = r * scale;
if screen_r < 0.5 {
let center = apply(transform, cx, cy);
if in_subpath {
builder.line_to(point(center.0, center.1));
} else {
builder.begin(point(center.0, center.1));
in_subpath = true;
}
continue;
}
let steps = ((sweep.abs() * screen_r).max(sweep.abs() * 5.0)).max(8.0) as usize;
let step_angle = sweep / steps as f32;
let first_pt = apply(transform, cx + r * start.cos(), cy + r * start.sin());
if in_subpath {
builder.line_to(point(first_pt.0, first_pt.1));
} else {
builder.begin(point(first_pt.0, first_pt.1));
in_subpath = true;
}
for i in 1..=steps {
let angle = start + step_angle * i as f32;
let p = apply(transform, cx + r * angle.cos(), cy + r * angle.sin());
builder.line_to(point(p.0, p.1));
}
}
}
}
if in_subpath {
builder.close();
}
let lyon_path = builder.build();
let mut geometry: VertexBuffers<[f32; 2], u16> = VertexBuffers::new();
let mut tessellator = FillTessellator::new();
let result = tessellator.tessellate_path(
&lyon_path,
&FillOptions::tolerance(0.5),
&mut BuffersBuilder::new(&mut geometry, |vertex: FillVertex| {
vertex.position().to_array()
}),
);
if result.is_ok() && !geometry.indices.is_empty() {
for tri in geometry.indices.chunks(3) {
if tri.len() == 3 {
let v0 = geometry.vertices[tri[0] as usize];
let v1 = geometry.vertices[tri[1] as usize];
let v2 = geometry.vertices[tri[2] as usize];
self.draw_commands.push(DrawCmd::Triangle(TriangleInstance {
v0,
v1,
v2,
_pad0: [0.0; 2],
color,
clip_rect: clip,
}));
}
}
}
}
fn path_bounding_box(&self) -> [f32; 4] {
let mut min_x = f32::MAX;
let mut min_y = f32::MAX;
let mut max_x = f32::MIN;
let mut max_y = f32::MIN;
let mut update = |x: f32, y: f32| {
let (px, py) = apply(&self.transform, x, y);
min_x = min_x.min(px);
min_y = min_y.min(py);
max_x = max_x.max(px);
max_y = max_y.max(py);
};
for cmd in &self.path {
match *cmd {
PathCmd::MoveTo(x, y) | PathCmd::LineTo(x, y) => update(x, y),
PathCmd::Close => {}
PathCmd::Rect(x, y, w, h) => {
update(x, y);
update(x + w, y + h);
}
PathCmd::Arc { cx, cy, r, .. } => {
update(cx - r, cy - r);
update(cx + r, cy + r);
}
}
}
if min_x > max_x {
return self.current_clip();
}
[min_x, min_y, max_x - min_x, max_y - min_y]
}
fn intersect_clip(&self, r: [f32; 4]) -> [f32; 4] {
let c = self.current_clip();
let x1 = r[0].max(c[0]);
let y1 = r[1].max(c[1]);
let x2 = (r[0] + r[2]).min(c[0] + c[2]);
let y2 = (r[1] + r[3]).min(c[1] + c[3]);
if x2 <= x1 || y2 <= y1 {
[0.0; 4]
} else {
[x1, y1, x2 - x1, y2 - y1]
}
}
fn measure_text_internal(&self, text: &str) -> f32 {
let Some(font_ref) = get_font_ref(self.font_family, self.font_bold, self.font_italic) else {
return text.len() as f32 * self.font_size * 0.6;
};
let size = skrifa::instance::Size::new(self.font_size);
let var_loc = skrifa::instance::LocationRef::default();
let charmap = font_ref.charmap();
let glyph_metrics = font_ref.glyph_metrics(size, var_loc);
let fallbacks = get_fallback_font_refs();
text.chars()
.map(|ch| {
let gid = charmap.map(ch).unwrap_or_default();
if gid.to_u32() != 0 {
return glyph_metrics.advance_width(gid).unwrap_or_default();
}
for fb in &fallbacks {
if let Some(fb_ref) = fb {
let fb_gid = fb_ref.charmap().map(ch).unwrap_or_default();
if fb_gid.to_u32() != 0 {
return fb_ref
.glyph_metrics(size, var_loc)
.advance_width(fb_gid)
.unwrap_or_default();
}
}
}
glyph_metrics.advance_width(gid).unwrap_or_default()
})
.sum()
}
}
impl RenderContext for InstancedRenderContext {
fn dpr(&self) -> f64 { 1.0 }
fn set_stroke_color(&mut self, color: &str) {
self.stroke_color = parse_color(color);
}
fn set_stroke_width(&mut self, width: f64) {
self.stroke_width = width as f32;
}
fn set_line_dash(&mut self, _pattern: &[f64]) {
}
fn set_line_cap(&mut self, cap: &str) {
self.line_cap = cap.to_owned();
}
fn set_line_join(&mut self, join: &str) {
self.line_join = join.to_owned();
}
fn set_fill_color(&mut self, color: &str) {
self.fill_color = parse_color(color);
}
fn set_global_alpha(&mut self, alpha: f64) {
self.global_alpha = alpha.clamp(0.0, 1.0);
}
fn begin_path(&mut self) {
self.path.clear();
}
fn move_to(&mut self, x: f64, y: f64) {
self.path.push(PathCmd::MoveTo(x as f32, y as f32));
}
fn line_to(&mut self, x: f64, y: f64) {
self.path.push(PathCmd::LineTo(x as f32, y as f32));
}
fn close_path(&mut self) {
self.path.push(PathCmd::Close);
}
fn rect(&mut self, x: f64, y: f64, w: f64, h: f64) {
self.path.push(PathCmd::Rect(x as f32, y as f32, w as f32, h as f32));
}
fn arc(&mut self, cx: f64, cy: f64, radius: f64, start_angle: f64, end_angle: f64) {
self.path.push(PathCmd::Arc {
cx: cx as f32,
cy: cy as f32,
r: radius as f32,
start: start_angle as f32,
end: end_angle as f32,
});
}
fn ellipse(&mut self, cx: f64, cy: f64, rx: f64, ry: f64, rotation: f64, start: f64, end: f64) {
if (rx - ry).abs() < 0.001 && rotation.abs() < 0.001 {
self.arc(cx, cy, rx, start, end);
return;
}
let sweep = end - start;
let steps = ((sweep.abs() * rx.max(ry)).max(sweep.abs() * 5.0)).max(16.0) as usize;
let (sin_rot, cos_rot) = rotation.sin_cos();
for i in 0..=steps {
let t = start + sweep * (i as f64 / steps as f64);
let ex = rx * t.cos();
let ey = ry * t.sin();
let px = cx + ex * cos_rot - ey * sin_rot;
let py = cy + ex * sin_rot + ey * cos_rot;
if i == 0 {
if self.path.is_empty() || matches!(self.path.last(), Some(PathCmd::Close)) {
self.path.push(PathCmd::MoveTo(px as f32, py as f32));
} else {
self.path.push(PathCmd::LineTo(px as f32, py as f32));
}
} else {
self.path.push(PathCmd::LineTo(px as f32, py as f32));
}
}
}
fn quadratic_curve_to(&mut self, cpx: f64, cpy: f64, x: f64, y: f64) {
let (p0x, p0y) = self.last_path_point();
let (cpx, cpy, ex, ey) = (cpx as f32, cpy as f32, x as f32, y as f32);
const N: usize = 8;
for i in 1..=N {
let t = i as f32 / N as f32;
let u = 1.0 - t;
let px = u * u * p0x + 2.0 * u * t * cpx + t * t * ex;
let py = u * u * p0y + 2.0 * u * t * cpy + t * t * ey;
self.path.push(PathCmd::LineTo(px, py));
}
}
fn bezier_curve_to(
&mut self, cp1x: f64, cp1y: f64, cp2x: f64, cp2y: f64, x: f64, y: f64,
) {
let (p0x, p0y) = self.last_path_point();
let (c1x, c1y) = (cp1x as f32, cp1y as f32);
let (c2x, c2y) = (cp2x as f32, cp2y as f32);
let (ex, ey) = (x as f32, y as f32);
const N: usize = 8;
for i in 1..=N {
let t = i as f32 / N as f32;
let u = 1.0 - t;
let px = u*u*u*p0x + 3.0*u*u*t*c1x + 3.0*u*t*t*c2x + t*t*t*ex;
let py = u*u*u*p0y + 3.0*u*u*t*c1y + 3.0*u*t*t*c2y + t*t*t*ey;
self.path.push(PathCmd::LineTo(px, py));
}
}
fn stroke(&mut self) {
self.tessellate_stroke();
}
fn fill(&mut self) {
self.tessellate_fill();
}
fn fill_linear_gradient(
&mut self,
stops: &[(f32, &str)],
x1: f64,
y1: f64,
x2: f64,
y2: f64,
) {
if stops.is_empty() {
return;
}
let parsed: Vec<(f32, [f32; 4])> = stops
.iter()
.map(|(t, color)| (*t, parse_color(color)))
.collect();
self.tessellate_fill_gradient(
&parsed,
x1 as f32,
y1 as f32,
x2 as f32,
y2 as f32,
);
}
fn clip(&mut self) {
let bb = self.path_bounding_box();
let new_clip = self.intersect_clip(bb);
self.clip_stack.push(new_clip);
self.path.clear();
}
fn stroke_rect(&mut self, x: f64, y: f64, w: f64, h: f64) {
self.stroke_rounded_rect(x, y, w, h, 0.0);
}
fn fill_rect(&mut self, x: f64, y: f64, w: f64, h: f64) {
let (x, y, w, h) = (x as f32, y as f32, w as f32, h as f32);
let color = self.apply_alpha(self.fill_color);
let clip = self.current_clip();
let (px1, py1) = apply(&self.transform, x, y);
let (px2, py2) = apply(&self.transform, x + w, y + h);
self.draw_commands.push(DrawCmd::Quad(QuadInstance {
pos: [px1.min(px2), py1.min(py2)],
size: [(px2 - px1).abs(), (py2 - py1).abs()],
color,
corner_radius: 0.0,
border_width: 0.0,
_pad0: [0.0; 2],
border_color: [0.0; 4],
clip_rect: clip,
}));
}
fn set_font(&mut self, font: &str) {
let parsed = fonts::parse_css_font(font);
self.font_size = parsed.size;
self.font_bold = parsed.bold;
self.font_italic = parsed.italic;
self.font_family = parsed.family;
}
fn set_text_align(&mut self, align: TextAlign) {
self.text_align = align;
}
fn set_text_baseline(&mut self, baseline: TextBaseline) {
self.text_baseline = baseline;
}
fn fill_text(&mut self, text: &str, x: f64, y: f64) {
if text.is_empty() { return; }
let (px, py) = apply(&self.transform, x as f32, y as f32);
let estimated_width = self.measure_text_internal(text);
let estimated_height = self.font_size;
let clip = self.current_clip();
let color = self.apply_alpha(self.fill_color);
self.draw_commands.push(DrawCmd::Text(TextAreaData {
text: text.to_owned(),
x: px,
y: py,
font_size: self.font_size,
color,
family: self.font_family,
bold: self.font_bold,
italic: self.font_italic,
align: self.text_align,
baseline: self.text_baseline,
clip,
estimated_width,
estimated_height,
}));
}
fn stroke_text(&mut self, _text: &str, _x: f64, _y: f64) {
}
fn measure_text(&self, text: &str) -> f64 {
self.measure_text_internal(text) as f64
}
fn save(&mut self) {
let state = SavedState {
transform: self.transform,
fill_color: self.fill_color,
stroke_color: self.stroke_color,
stroke_width: self.stroke_width,
global_alpha: self.global_alpha,
font_size: self.font_size,
font_bold: self.font_bold,
font_italic: self.font_italic,
font_family: self.font_family,
text_align: self.text_align,
text_baseline: self.text_baseline,
clip_depth: self.clip_stack.len(),
line_cap: self.line_cap.clone(),
line_join: self.line_join.clone(),
};
self.state_stack.push(state);
}
fn restore(&mut self) {
if let Some(state) = self.state_stack.pop() {
self.transform = state.transform;
self.fill_color = state.fill_color;
self.stroke_color = state.stroke_color;
self.stroke_width = state.stroke_width;
self.global_alpha = state.global_alpha;
self.font_size = state.font_size;
self.font_bold = state.font_bold;
self.font_italic = state.font_italic;
self.font_family = state.font_family;
self.text_align = state.text_align;
self.text_baseline = state.text_baseline;
self.line_cap = state.line_cap;
self.line_join = state.line_join;
self.clip_stack.truncate(state.clip_depth);
}
}
fn translate(&mut self, x: f64, y: f64) {
self.transform = translate_transform(&self.transform, x as f32, y as f32);
}
fn rotate(&mut self, angle: f64) {
self.transform = rotate_transform(&self.transform, angle as f32);
}
fn scale(&mut self, x: f64, y: f64) {
self.transform = scale_transform(&self.transform, x as f32, y as f32);
}
fn fill_rounded_rect(&mut self, x: f64, y: f64, w: f64, h: f64, radius: f64) {
let (x, y, w, h) = (x as f32, y as f32, w as f32, h as f32);
let r = (radius as f32).min(w / 2.0).min(h / 2.0).max(0.0);
let color = self.apply_alpha(self.fill_color);
let clip = self.current_clip();
let (px1, py1) = apply(&self.transform, x, y);
let (px2, py2) = apply(&self.transform, x + w, y + h);
let scale = ((self.transform[0].abs() + self.transform[3].abs()) * 0.5).max(0.001);
self.draw_commands.push(DrawCmd::Quad(QuadInstance {
pos: [px1.min(px2), py1.min(py2)],
size: [(px2 - px1).abs(), (py2 - py1).abs()],
color,
corner_radius: r * scale,
border_width: 0.0,
_pad0: [0.0; 2],
border_color: [0.0; 4],
clip_rect: clip,
}));
}
fn stroke_rounded_rect(&mut self, x: f64, y: f64, w: f64, h: f64, radius: f64) {
let (x, y, w, h) = (x as f32, y as f32, w as f32, h as f32);
let r = (radius as f32).min(w / 2.0).min(h / 2.0).max(0.0);
let border_color = self.apply_alpha(self.stroke_color);
let clip = self.current_clip();
let (px1, py1) = apply(&self.transform, x, y);
let (px2, py2) = apply(&self.transform, x + w, y + h);
let scale = ((self.transform[0].abs() + self.transform[3].abs()) * 0.5).max(0.001);
self.draw_commands.push(DrawCmd::Quad(QuadInstance {
pos: [px1.min(px2), py1.min(py2)],
size: [(px2 - px1).abs(), (py2 - py1).abs()],
color: [0.0, 0.0, 0.0, 0.0],
corner_radius: r * scale,
border_width: self.stroke_width,
_pad0: [0.0; 2],
border_color,
clip_rect: clip,
}));
}
}
impl RenderContextExt for InstancedRenderContext {
type BlurImage = ();
}
fn sample_gradient(stops: &[(f32, [f32; 4])], t: f32) -> [f32; 4] {
if stops.is_empty() {
return [0.0; 4];
}
if stops.len() == 1 {
return stops[0].1;
}
let t = t.clamp(0.0, 1.0);
if t <= stops[0].0 {
return stops[0].1;
}
let last = stops.len() - 1;
if t >= stops[last].0 {
return stops[last].1;
}
for i in 0..last {
let (t0, c0) = stops[i];
let (t1, c1) = stops[i + 1];
if t >= t0 && t <= t1 {
let span = t1 - t0;
if span < 1e-6 {
return c1;
}
let f = (t - t0) / span;
return [
c0[0] + (c1[0] - c0[0]) * f,
c0[1] + (c1[1] - c0[1]) * f,
c0[2] + (c1[2] - c0[2]) * f,
c0[3] + (c1[3] - c0[3]) * f,
];
}
}
stops[last].1
}
fn project_onto_gradient(px: f32, py: f32, x1: f32, y1: f32, x2: f32, y2: f32) -> f32 {
let dx = x2 - x1;
let dy = y2 - y1;
let len_sq = dx * dx + dy * dy;
if len_sq < 1e-10 {
return 0.0;
}
let t = ((px - x1) * dx + (py - y1) * dy) / len_sq;
t.clamp(0.0, 1.0)
}
impl InstancedRenderContext {
fn tessellate_fill_gradient(
&mut self,
stops: &[(f32, [f32; 4])],
x1: f32,
y1: f32,
x2: f32,
y2: f32,
) {
let path = std::mem::take(&mut self.path);
let clip = self.current_clip();
let global_alpha = self.global_alpha as f32;
let (sx1, sy1) = apply(&self.transform, x1, y1);
let (sx2, sy2) = apply(&self.transform, x2, y2);
if path.len() == 1 {
if let PathCmd::Rect(rx, ry, rw, rh) = path[0] {
self.tessellate_gradient_rect(
rx, ry, rw, rh, stops, sx1, sy1, sx2, sy2, global_alpha, clip,
);
return;
}
}
if path.len() == 5 {
let is_move = matches!(path[0], PathCmd::MoveTo(..));
let is_close = matches!(path[4], PathCmd::Close);
let all_lines = matches!(path[1], PathCmd::LineTo(..))
&& matches!(path[2], PathCmd::LineTo(..))
&& matches!(path[3], PathCmd::LineTo(..));
if is_move && is_close && all_lines {
let (x0v, y0v) = if let PathCmd::MoveTo(a, b) = path[0] { (a, b) } else { unreachable!() };
let (x1v, y1v) = if let PathCmd::LineTo(a, b) = path[1] { (a, b) } else { unreachable!() };
let (x2v, y2v) = if let PathCmd::LineTo(a, b) = path[2] { (a, b) } else { unreachable!() };
let (x3v, y3v) = if let PathCmd::LineTo(a, b) = path[3] { (a, b) } else { unreachable!() };
let is_aabb = (approx_eq(x0v, x3v) && approx_eq(x1v, x2v) && approx_eq(y0v, y1v) && approx_eq(y2v, y3v))
|| (approx_eq(y0v, y1v) && approx_eq(y2v, y3v) && approx_eq(x0v, x3v) && approx_eq(x1v, x2v))
|| (approx_eq(x0v, x1v) && approx_eq(x2v, x3v) && approx_eq(y1v, y2v) && approx_eq(y0v, y3v))
|| (approx_eq(y0v, y3v) && approx_eq(y1v, y2v) && approx_eq(x0v, x1v) && approx_eq(x2v, x3v));
if is_aabb {
let min_x = x0v.min(x1v).min(x2v).min(x3v);
let min_y = y0v.min(y1v).min(y2v).min(y3v);
let max_x = x0v.max(x1v).max(x2v).max(x3v);
let max_y = y0v.max(y1v).max(y2v).max(y3v);
let rw = max_x - min_x;
let rh = max_y - min_y;
self.tessellate_gradient_rect(
min_x, min_y, rw, rh, stops, sx1, sy1, sx2, sy2, global_alpha, clip,
);
return;
}
}
}
let mut builder = lyon_path::Path::builder();
let transform = &self.transform;
let mut in_subpath = false;
for cmd in &path {
match *cmd {
PathCmd::MoveTo(x, y) => {
if in_subpath { builder.end(false); }
let (px, py) = apply(transform, x, y);
builder.begin(point(px, py));
in_subpath = true;
}
PathCmd::LineTo(x, y) => {
if !in_subpath {
let (px, py) = apply(transform, x, y);
builder.begin(point(px, py));
in_subpath = true;
} else {
let (px, py) = apply(transform, x, y);
builder.line_to(point(px, py));
}
}
PathCmd::Close => {
if in_subpath { builder.close(); in_subpath = false; }
}
PathCmd::Rect(x, y, w, h) => {
if in_subpath { builder.end(false); }
let tl = apply(transform, x, y);
let tr = apply(transform, x + w, y);
let br = apply(transform, x + w, y + h);
let bl = apply(transform, x, y + h);
builder.begin(point(tl.0, tl.1));
builder.line_to(point(tr.0, tr.1));
builder.line_to(point(br.0, br.1));
builder.line_to(point(bl.0, bl.1));
builder.close();
in_subpath = false;
}
PathCmd::Arc { cx, cy, r, start, end } => {
let sweep = end - start;
let scale = ((transform[0].abs() + transform[3].abs()) * 0.5).max(1.0);
let screen_r = r * scale;
if screen_r < 0.5 {
let center = apply(transform, cx, cy);
if in_subpath {
builder.line_to(point(center.0, center.1));
} else {
builder.begin(point(center.0, center.1));
in_subpath = true;
}
continue;
}
let steps = ((sweep.abs() * screen_r).max(sweep.abs() * 5.0)).max(8.0) as usize;
let step_angle = sweep / steps as f32;
let first_pt = apply(transform, cx + r * start.cos(), cy + r * start.sin());
if in_subpath {
builder.line_to(point(first_pt.0, first_pt.1));
} else {
builder.begin(point(first_pt.0, first_pt.1));
in_subpath = true;
}
for i in 1..=steps {
let angle = start + step_angle * i as f32;
let p = apply(transform, cx + r * angle.cos(), cy + r * angle.sin());
builder.line_to(point(p.0, p.1));
}
}
}
}
if in_subpath { builder.close(); }
let lyon_path = builder.build();
let mut geometry: VertexBuffers<[f32; 2], u16> = VertexBuffers::new();
let mut tessellator = FillTessellator::new();
let result = tessellator.tessellate_path(
&lyon_path,
&FillOptions::tolerance(0.5),
&mut BuffersBuilder::new(&mut geometry, |vertex: FillVertex| {
vertex.position().to_array()
}),
);
if result.is_ok() && !geometry.indices.is_empty() {
for tri in geometry.indices.chunks(3) {
if tri.len() == 3 {
let v0 = geometry.vertices[tri[0] as usize];
let v1 = geometry.vertices[tri[1] as usize];
let v2 = geometry.vertices[tri[2] as usize];
let cx = (v0[0] + v1[0] + v2[0]) / 3.0;
let cy = (v0[1] + v1[1] + v2[1]) / 3.0;
let t = project_onto_gradient(cx, cy, sx1, sy1, sx2, sy2);
let mut color = sample_gradient(stops, t);
color[3] = (color[3] * global_alpha).clamp(0.0, 1.0);
self.draw_commands.push(DrawCmd::Triangle(TriangleInstance {
v0,
v1,
v2,
_pad0: [0.0; 2],
color,
clip_rect: clip,
}));
}
}
}
}
fn tessellate_gradient_rect(
&mut self,
rx: f32,
ry: f32,
rw: f32,
rh: f32,
stops: &[(f32, [f32; 4])],
sx1: f32,
sy1: f32,
sx2: f32,
sy2: f32,
global_alpha: f32,
clip: [f32; 4],
) {
if stops.is_empty() { return; }
let (px1, py1) = apply(&self.transform, rx, ry);
let (px2, py2) = apply(&self.transform, rx + rw, ry + rh);
let (left, right) = (px1.min(px2), px1.max(px2));
let (top, bottom) = (py1.min(py2), py1.max(py2));
if stops.len() == 1 {
let mut color = stops[0].1;
color[3] = (color[3] * global_alpha).clamp(0.0, 1.0);
self.emit_rect_triangles(left, top, right, bottom, color, clip);
return;
}
let gdx = sx2 - sx1;
let gdy = sy2 - sy1;
let len_sq = gdx * gdx + gdy * gdy;
if len_sq < 1e-6 {
let mut color = stops[0].1;
color[3] = (color[3] * global_alpha).clamp(0.0, 1.0);
self.emit_rect_triangles(left, top, right, bottom, color, clip);
return;
}
const STRIPS: usize = 64;
for i in 0..STRIPS {
let t0 = i as f32 / STRIPS as f32;
let t1 = (i + 1) as f32 / STRIPS as f32;
let tc = (t0 + t1) * 0.5;
let (strip_left, strip_top, strip_right, strip_bottom) =
strip_bounds_for_t(t0, t1, left, top, right, bottom, sx1, sy1, sx2, sy2);
if strip_right <= strip_left || strip_bottom <= strip_top {
continue;
}
let mut color = sample_gradient(stops, tc);
color[3] = (color[3] * global_alpha).clamp(0.0, 1.0);
self.emit_rect_triangles(strip_left, strip_top, strip_right, strip_bottom, color, clip);
}
}
fn emit_rect_triangles(
&mut self,
left: f32,
top: f32,
right: f32,
bottom: f32,
color: [f32; 4],
clip: [f32; 4],
) {
let tl = [left, top];
let tr = [right, top];
let br = [right, bottom];
let bl = [left, bottom];
self.draw_commands.push(DrawCmd::Triangle(TriangleInstance {
v0: tl, v1: tr, v2: br,
_pad0: [0.0; 2],
color,
clip_rect: clip,
}));
self.draw_commands.push(DrawCmd::Triangle(TriangleInstance {
v0: tl, v1: br, v2: bl,
_pad0: [0.0; 2],
color,
clip_rect: clip,
}));
}
}
#[allow(clippy::too_many_arguments)]
fn strip_bounds_for_t(
t0: f32,
t1: f32,
left: f32,
top: f32,
right: f32,
bottom: f32,
sx1: f32,
sy1: f32,
sx2: f32,
sy2: f32,
) -> (f32, f32, f32, f32) {
let gdx = sx2 - sx1;
let gdy = sy2 - sy1;
if gdx.abs() >= gdy.abs() {
let x_at_t0 = (sx1 + t0 * gdx).clamp(left, right);
let x_at_t1 = (sx1 + t1 * gdx).clamp(left, right);
let (sl, sr) = if x_at_t0 <= x_at_t1 { (x_at_t0, x_at_t1) } else { (x_at_t1, x_at_t0) };
(sl.max(left), top, sr.min(right), bottom)
} else {
let y_at_t0 = (sy1 + t0 * gdy).clamp(top, bottom);
let y_at_t1 = (sy1 + t1 * gdy).clamp(top, bottom);
let (st, sb) = if y_at_t0 <= y_at_t1 { (y_at_t0, y_at_t1) } else { (y_at_t1, y_at_t0) };
(left, st.max(top), right, sb.min(bottom))
}
}