use glam::*;
use wgpu::util::DeviceExt;
mod atlas;
#[cfg(feature = "text")]
pub mod font;
#[cfg(feature = "text")]
mod text;
type Cache = std::collections::HashMap<u64, wgpu::Texture>;
pub type Color = rgb::Rgba<u8>;
#[cfg(feature = "text")]
pub use text::Label;
struct Sprite<'a> {
texture: &'a dyn Texture,
src_offset: IVec2,
src_size: UVec2,
src_layer: u32,
transform: Affine2,
tint: Color,
}
enum Command<'a> {
Sprite(Sprite<'a>),
#[cfg(feature = "text")]
Text(text::Section),
}
pub struct Canvas<'a> {
commands: Vec<Command<'a>>,
}
pub trait Drawable<'a>
where
Self: Sized + Clone,
{
fn draw(&self, canvas: &mut Canvas<'a>, tint: Color, transform: glam::Affine2);
fn tinted(&self, tint: Color) -> impl Drawable<'a> {
Tinted {
drawable: self.clone(),
tint,
}
}
}
#[cfg(feature = "text")]
impl<'a> Drawable<'a> for text::Label {
fn draw(&self, canvas: &mut Canvas<'a>, tint: Color, transform: glam::Affine2) {
canvas.commands.push(Command::Text(text::Section {
label: self.clone(),
transform,
tint,
}));
}
}
#[derive(Debug, Clone, Copy)]
struct Rect {
offset: IVec2,
size: UVec2,
}
impl Rect {
fn new(x: i32, y: i32, width: u32, height: u32) -> Self {
Self {
offset: IVec2::new(x, y),
size: UVec2::new(width, height),
}
}
const fn left(&self) -> i32 {
self.offset.x
}
const fn top(&self) -> i32 {
self.offset.y
}
const fn right(&self) -> i32 {
self.offset.x + self.size.x as i32
}
const fn bottom(&self) -> i32 {
self.offset.y + self.size.y as i32
}
}
pub trait Texture {
fn size(&self) -> wgpu::Extent3d;
fn upload_to_wgpu(&self, device: &wgpu::Device, queue: &wgpu::Queue, cache: &mut Cache);
fn get_wgpu_texture<'a>(&'a self, cache: &'a Cache) -> Option<&'a wgpu::Texture>;
}
pub struct Image {
id: u64,
pixels: Vec<u8>,
desc: wgpu::TextureDescriptor<'static>,
}
impl Image {
pub fn new(pixels: Vec<u8>, desc: wgpu::TextureDescriptor<'static>) -> Self {
static IMAGE_ID: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
Self {
id: IMAGE_ID.fetch_add(1, std::sync::atomic::Ordering::Relaxed),
pixels,
desc,
}
}
}
impl Texture for Image {
fn size(&self) -> wgpu::Extent3d {
self.desc.size
}
fn upload_to_wgpu(&self, device: &wgpu::Device, queue: &wgpu::Queue, cache: &mut Cache) {
cache.entry(self.id).or_insert_with(|| {
device.create_texture_with_data(
queue,
&self.desc,
wgpu::util::TextureDataOrder::default(),
&self.pixels,
)
});
}
fn get_wgpu_texture<'a>(&'a self, cache: &'a Cache) -> Option<&'a wgpu::Texture> {
cache.get(&self.id)
}
}
impl Texture for wgpu::Texture {
fn size(&self) -> wgpu::Extent3d {
self.size()
}
fn upload_to_wgpu(&self, _device: &wgpu::Device, _queue: &wgpu::Queue, _cache: &mut Cache) {}
fn get_wgpu_texture<'a>(&'a self, _cache: &'a Cache) -> Option<&'a wgpu::Texture> {
Some(self)
}
}
pub struct TextureSlice<'a, T> {
texture: &'a T,
layer: u32,
rect: Rect,
}
impl<'a, T> Clone for TextureSlice<'a, T> {
fn clone(&self) -> Self {
Self {
texture: self.texture,
layer: self.layer,
rect: self.rect,
}
}
}
impl<'a, T> Copy for TextureSlice<'a, T> {}
impl<'a, T> TextureSlice<'a, T>
where
T: Texture,
{
pub fn from_layer(texture: &'a T, layer: u32) -> Option<Self> {
let size = texture.size();
if layer >= size.depth_or_array_layers {
return None;
}
Some(Self {
texture,
layer,
rect: Rect::new(0, 0, size.width, size.height),
})
}
pub fn slice(&self, offset: glam::IVec2, size: glam::UVec2) -> Option<Self> {
let rect = Rect {
offset: self.rect.offset + offset,
size,
};
if rect.left() < self.rect.left()
|| rect.right() > self.rect.right()
|| rect.top() < self.rect.top()
|| rect.bottom() > self.rect.bottom()
{
return None;
}
Some(Self {
texture: self.texture,
layer: self.layer,
rect,
})
}
pub fn size(&self) -> glam::UVec2 {
self.rect.size
}
}
impl<'a, T> Drawable<'a> for TextureSlice<'a, T>
where
T: Texture,
{
fn draw(&self, canvas: &mut Canvas<'a>, tint: Color, transform: glam::Affine2) {
canvas.commands.push(Command::Sprite(Sprite {
transform,
tint,
texture: self.texture,
src_offset: self.rect.offset,
src_size: self.rect.size,
src_layer: self.layer,
}));
}
}
#[derive(Clone)]
struct Tinted<T> {
drawable: T,
tint: Color,
}
impl<'a, T> Drawable<'a> for Tinted<T>
where
T: Drawable<'a>,
{
fn draw(&self, canvas: &mut Canvas<'a>, tint: Color, transform: glam::Affine2) {
self.drawable.draw(
canvas,
Color::new(
((tint.r as u16 * self.tint.r as u16) / 0xff) as u8,
((tint.g as u16 * self.tint.g as u16) / 0xff) as u8,
((tint.b as u16 * self.tint.b as u16) / 0xff) as u8,
((tint.a as u16 * self.tint.a as u16) / 0xff) as u8,
),
transform,
);
}
}
impl<'a> Canvas<'a> {
pub fn new() -> Self {
Self { commands: vec![] }
}
#[inline]
pub fn draw(&mut self, drawable: impl Drawable<'a>, transform: glam::Affine2) {
drawable.draw(self, Color::new(0xff, 0xff, 0xff, 0xff), transform);
}
}
pub struct Renderer {
renderer: spright::Renderer,
cache: Cache,
#[cfg(feature = "text")]
text_sprite_maker: text::SpriteMaker,
}
#[derive(thiserror::Error, Debug)]
pub enum Error {
#[error("out of glylph atlas space")]
OutOfGlyphAtlasSpace,
}
impl Renderer {
pub fn new(device: &wgpu::Device, texture_format: wgpu::TextureFormat) -> Self {
Self {
renderer: spright::Renderer::new(device, texture_format),
cache: Cache::new(),
#[cfg(feature = "text")]
text_sprite_maker: text::SpriteMaker::new(device),
}
}
pub fn prepare(
&mut self,
device: &wgpu::Device,
queue: &wgpu::Queue,
font_system: &mut cosmic_text::FontSystem,
target_size: wgpu::Extent3d,
canvas: &Canvas,
) -> Result<(), Error> {
let mut staged = vec![];
enum Staged<'a> {
Sprite(spright::batch::Sprite<'a>),
TextSprite(text::TextSprite),
}
for cmd in canvas.commands.iter() {
if let Command::Sprite(sprite) = cmd {
sprite
.texture
.upload_to_wgpu(device, queue, &mut self.cache);
}
}
for cmd in canvas.commands.iter() {
match cmd {
Command::Sprite(sprite) => {
staged.push(Staged::Sprite(spright::batch::Sprite {
texture: sprite.texture.get_wgpu_texture(&self.cache).unwrap(),
src_offset: sprite.src_offset,
src_size: sprite.src_size,
src_layer: sprite.src_layer,
transform: sprite.transform,
tint: sprite.tint,
}));
}
Command::Text(section) => {
staged.extend(
self.text_sprite_maker
.make(device, queue, font_system, §ion.label, section.tint)
.ok_or(Error::OutOfGlyphAtlasSpace)?
.into_iter()
.map(|s| {
Staged::TextSprite(text::TextSprite {
transform: section.transform * s.transform,
..s
})
}),
);
}
}
}
self.renderer.prepare(
device,
queue,
target_size,
&spright::batch::batch(
&staged
.into_iter()
.map(|staged| match staged {
Staged::Sprite(sprite) => sprite,
Staged::TextSprite(text_sprite) => spright::batch::Sprite {
texture: if text_sprite.is_mask {
self.text_sprite_maker.mask_texture()
} else {
self.text_sprite_maker.color_texture()
},
src_offset: text_sprite.offset,
src_size: text_sprite.size,
src_layer: 0,
tint: text_sprite.tint,
transform: text_sprite.transform,
},
})
.collect::<Vec<_>>(),
),
);
#[cfg(feature = "text")]
self.text_sprite_maker.flush(queue);
Ok(())
}
pub fn render<'rpass>(&'rpass self, rpass: &'rpass mut wgpu::RenderPass<'rpass>) {
self.renderer.render(rpass);
}
}