pub mod raster;
pub mod scene;
pub use raster::{RasterStats, Rasterizer, damage_batches, to_vello};
pub use scene::{Op, Scene, SceneBuilder, SceneOptions, SceneStats, TextCache, kind_name};
use lieui_geom::{Color, Rect, Size};
use vello_cpu::Pixmap;
use crate::track::Track;
#[derive(Clone, Copy, Debug, Default, PartialEq)]
pub struct RenderStats {
pub scene: SceneStats,
pub raster: RasterStats,
pub ops: usize,
}
pub struct Renderer {
scene: SceneBuilder,
raster: Rasterizer,
opts: SceneOptions,
}
impl Renderer {
pub fn new(size: Size, background: Color) -> Self {
Self {
scene: SceneBuilder::new(),
raster: Rasterizer::new(size),
opts: SceneOptions {
window: size,
background,
focus_ring: true,
theme: crate::theme::Theme::default(),
},
}
}
pub fn size(&self) -> Size {
self.opts.window
}
pub fn physical_size(&self) -> Size {
self.raster.size()
}
pub fn scale(&self) -> f32 {
self.raster.scale()
}
pub fn resize(&mut self, size: Size) {
self.opts.window = size;
self.raster.resize(size);
}
pub fn set_scale(&mut self, scale: f32) {
self.raster.set_scale(scale);
}
pub fn background(&self) -> Color {
self.opts.background
}
pub fn set_background(&mut self, c: Color) {
self.opts.background = c;
}
pub fn options(&self) -> &SceneOptions {
&self.opts
}
pub fn options_mut(&mut self) -> &mut SceneOptions {
&mut self.opts
}
pub fn pixmap(&self) -> &Pixmap {
self.raster.pixmap()
}
pub fn text_cache_len(&self) -> usize {
self.scene.text_cache().len()
}
pub fn render(&mut self, track: &Track, damage: &[Rect], damage_all: bool) -> RenderStats {
let all = damage_all || damage.is_empty();
let scale = self.raster.scale();
let physical = self.raster.size();
let scaled: Vec<Rect> = damage
.iter()
.map(|d| Rect::new(d.x * scale, d.y * scale, d.width * scale, d.height * scale))
.collect();
let batches = damage_batches(physical, &scaled, all);
if batches.is_empty() {
return RenderStats::default();
}
let full = Rect::new(0.0, 0.0, physical.width, physical.height);
let fallback = batches.len() == 1 && batches[0] == full;
let scene_all = all || fallback;
let cull: Vec<Rect> = batches
.iter()
.map(|b| Rect::new(b.x / scale, b.y / scale, b.width / scale, b.height / scale))
.collect();
let scene = self.scene.build(track, &self.opts, &cull, scene_all);
let ops = scene.len();
let raster = self.raster.rasterize(&scene, damage, scene_all);
RenderStats {
scene: scene.stats,
raster,
ops,
}
}
}
impl std::fmt::Debug for Renderer {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("Renderer")
.field("size", &self.opts.window)
.field("background", &self.opts.background)
.finish()
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::layout::layout;
use crate::track::{Kind, Layer, NodeId, Track};
use lieui_layout::FlexDirection;
use vello_cpu::color::PremulRgba8;
const W: f32 = 220.0;
const H: f32 = 120.0;
const BG: Color = Color::new(255, 255, 255);
const RED: Color = Color::new(255, 0, 0);
const BLUE: Color = Color::new(0, 80, 255);
fn px(r: &Renderer, x: u16, y: u16) -> PremulRgba8 {
let pix = r.pixmap();
pix.data()[usize::from(y) * usize::from(pix.width()) + usize::from(x)]
}
fn expect(p: PremulRgba8, c: Color) {
let a = u16::from(c.a);
let premul = |v: u8| ((u16::from(v) * a) / 255) as u8;
assert_eq!(
(p.r, p.g, p.b, p.a),
(premul(c.r), premul(c.g), premul(c.b), c.a),
"像素不匹配:{p:?}"
);
}
fn scrolled_window() -> (Track, NodeId, NodeId) {
let mut t = Track::new();
let root = t.create(Kind::Box, None);
{
let n = t.get_mut(root).unwrap();
n.layout.dim = [W, H];
n.layout.flex_direction = FlexDirection::Row;
}
t.add_root(Layer::Content, None, root);
let sc = t.create(Kind::Box, None);
{
let n = t.get_mut(sc).unwrap();
n.layout.dim = [100.0, H];
n.layout.flex_direction = FlexDirection::Column;
n.layout.overflow_scroll = true;
n.paint.background_color = Some(BG);
}
t.append_child(root, sc);
for _ in 0..10 {
let row = t.create(Kind::Box, None);
let n = t.get_mut(row).unwrap();
n.layout.dim = [80.0, 20.0];
n.paint.background_color = Some(BLUE);
t.append_child(sc, row);
}
let red = t.create(Kind::Box, None);
{
let n = t.get_mut(red).unwrap();
n.layout.dim = [100.0, H];
n.paint.background_color = Some(RED);
}
t.append_child(root, red);
(t, sc, red)
}
#[test]
fn full_window_fallback_does_not_erase_elements_outside_damage() {
let (mut t, sc, _red) = scrolled_window();
layout(&mut t, Size::new(W, H));
let _ = t.take_damage();
let mut r = Renderer::new(Size::new(W, H), BG);
r.render(&t, &[], true); expect(px(&r, 150, 60), RED);
expect(px(&r, 40, 10), BLUE);
assert!(t.set_scroll_offset(sc, (0.0, 20.0)));
layout(&mut t, Size::new(W, H));
let (damage, all) = t.take_damage();
assert!(!all);
assert!(damage.len() > 8, "应产生 >8 个碎片脏矩形:{}", damage.len());
r.render(&t, &damage, all);
expect(px(&r, 40, 10), BLUE);
expect(px(&r, 40, 30), BLUE);
expect(px(&r, 150, 60), RED);
}
}