use tiny_skia::{Paint, PathBuilder, Pixmap, PixmapPaint, Stroke, Transform};
use truce_core::cast::len_u32;
use crate::ColorExt;
use truce_gui_types::render::{ImageId, RenderBackend};
use truce_gui_types::theme::Color;
pub struct CpuBackend {
pixmap: Pixmap,
scale: f32,
images: Vec<Option<Pixmap>>,
}
impl CpuBackend {
#[must_use]
pub fn new(logical_w: u32, logical_h: u32, scale: f32) -> Option<Self> {
let scale = scale.max(0.0);
let phys_w = crate::platform::to_physical_px(logical_w, f64::from(scale));
let phys_h = crate::platform::to_physical_px(logical_h, f64::from(scale));
Pixmap::new(phys_w, phys_h).map(|pixmap| Self {
pixmap,
scale,
images: Vec::new(),
})
}
pub fn resize(&mut self, logical_w: u32, logical_h: u32, scale: f32) -> bool {
let scale = scale.max(0.0);
let phys_w = crate::platform::to_physical_px(logical_w, f64::from(scale));
let phys_h = crate::platform::to_physical_px(logical_h, f64::from(scale));
if phys_w == self.pixmap.width() && phys_h == self.pixmap.height() {
self.scale = scale;
return false;
}
match Pixmap::new(phys_w, phys_h) {
Some(pm) => {
self.pixmap = pm;
self.scale = scale;
true
}
None => false,
}
}
#[must_use]
pub fn data(&self) -> &[u8] {
self.pixmap.data()
}
#[must_use]
pub fn width(&self) -> u32 {
self.pixmap.width()
}
#[must_use]
pub fn height(&self) -> u32 {
self.pixmap.height()
}
#[must_use]
pub fn scale(&self) -> f32 {
self.scale
}
}
#[allow(clippy::many_single_char_names)]
impl RenderBackend for CpuBackend {
fn clear(&mut self, color: Color) {
self.pixmap.fill(color.to_skia());
}
fn fill_rect(&mut self, x: f32, y: f32, w: f32, h: f32, color: Color) {
let s = self.scale;
let Some(rect) = tiny_skia::Rect::from_xywh(x * s, y * s, w * s, h * s) else {
return;
};
let mut paint = Paint::default();
paint.set_color(color.to_skia());
paint.anti_alias = true;
self.pixmap
.fill_rect(rect, &paint, Transform::identity(), None);
}
fn fill_circle(&mut self, cx: f32, cy: f32, radius: f32, color: Color) {
let s = self.scale;
let mut pb = PathBuilder::new();
pb.push_circle(cx * s, cy * s, radius * s);
let Some(path) = pb.finish() else {
return;
};
let mut paint = Paint::default();
paint.set_color(color.to_skia());
paint.anti_alias = true;
self.pixmap.fill_path(
&path,
&paint,
tiny_skia::FillRule::Winding,
Transform::identity(),
None,
);
}
fn stroke_circle(&mut self, cx: f32, cy: f32, radius: f32, color: Color, width: f32) {
let s = self.scale;
let mut pb = PathBuilder::new();
pb.push_circle(cx * s, cy * s, radius * s);
let Some(path) = pb.finish() else {
return;
};
let mut paint = Paint::default();
paint.set_color(color.to_skia());
paint.anti_alias = true;
let stroke = Stroke {
width: width * s,
..Stroke::default()
};
self.pixmap
.stroke_path(&path, &paint, &stroke, Transform::identity(), None);
}
#[allow(clippy::cast_precision_loss)]
fn stroke_arc(
&mut self,
cx: f32,
cy: f32,
radius: f32,
start_angle: f32,
end_angle: f32,
color: Color,
width: f32,
) {
let s = self.scale;
let segments = 64;
let mut pb = PathBuilder::new();
let angle_range = end_angle - start_angle;
let step = angle_range / segments as f32;
for i in 0..=segments {
let angle = start_angle + step * i as f32;
let x = cx * s + radius * s * angle.cos();
let y = cy * s + radius * s * angle.sin();
if i == 0 {
pb.move_to(x, y);
} else {
pb.line_to(x, y);
}
}
let Some(path) = pb.finish() else {
return;
};
let mut paint = Paint::default();
paint.set_color(color.to_skia());
paint.anti_alias = true;
let stroke = Stroke {
width: width * s,
line_cap: tiny_skia::LineCap::Round,
..Stroke::default()
};
self.pixmap
.stroke_path(&path, &paint, &stroke, Transform::identity(), None);
}
fn draw_line(&mut self, x1: f32, y1: f32, x2: f32, y2: f32, color: Color, width: f32) {
let s = self.scale;
let mut pb = PathBuilder::new();
pb.move_to(x1 * s, y1 * s);
pb.line_to(x2 * s, y2 * s);
let Some(path) = pb.finish() else {
return;
};
let mut paint = Paint::default();
paint.set_color(color.to_skia());
paint.anti_alias = true;
let stroke = Stroke {
width: width * s,
line_cap: tiny_skia::LineCap::Round,
..Stroke::default()
};
self.pixmap
.stroke_path(&path, &paint, &stroke, Transform::identity(), None);
}
fn draw_text(&mut self, text: &str, x: f32, y: f32, size: f32, color: Color) {
let s = self.scale;
let w = self.pixmap.width();
let h = self.pixmap.height();
crate::font::draw_text_fontdue(
self.pixmap.data_mut(),
w,
h,
text,
x * s,
y * s,
size * s,
color.r,
color.g,
color.b,
color.a,
);
}
fn text_width(&self, text: &str, size: f32) -> f32 {
let s = self.scale;
crate::font::text_width_fontdue(text, size * s) / s
}
fn register_image(&mut self, rgba: &[u8], width: u32, height: u32) -> ImageId {
let Some(mut pm) = Pixmap::new(width, height) else {
return ImageId::INVALID;
};
let expected = (width as usize) * (height as usize) * 4;
if rgba.len() < expected {
return ImageId::INVALID;
}
pm.data_mut()[..expected].copy_from_slice(&rgba[..expected]);
if let Some(slot) = self
.images
.iter_mut()
.enumerate()
.find(|(_, s)| s.is_none())
{
*slot.1 = Some(pm);
return ImageId(len_u32(slot.0));
}
let id = len_u32(self.images.len());
self.images.push(Some(pm));
ImageId(id)
}
fn unregister_image(&mut self, id: ImageId) {
if let Some(slot) = self.images.get_mut(id.0 as usize) {
*slot = None;
}
}
#[allow(clippy::cast_precision_loss)]
fn draw_image(&mut self, id: ImageId, x: f32, y: f32, w: f32, h: f32) {
let s = self.scale;
let Some(pm) = self.images.get(id.0 as usize).and_then(|s| s.as_ref()) else {
return;
};
let sx = (w * s) / pm.width() as f32;
let sy = (h * s) / pm.height() as f32;
let transform = Transform::from_scale(sx, sy).post_translate(x * s, y * s);
let paint = PixmapPaint::default();
self.pixmap
.draw_pixmap(0, 0, pm.as_ref(), &paint, transform, None);
}
}