1
1
Fork 0
mirror of https://github.com/griffi-gh/hUI.git synced 2025-04-01 21:46:29 -05:00

wip move stuff to hui-painter

This commit is contained in:
griffi-gh 2025-03-04 20:55:43 +01:00
parent d1cb6cf5f4
commit 72ce1a5fa5
35 changed files with 770 additions and 1673 deletions

View file

@ -153,11 +153,11 @@ impl GliumUiRenderer {
}
pub fn update(&mut self, instance: &UiInstance) {
if self.ui_texture.is_none() || instance.atlas().modified {
self.update_texture_atlas(&instance.atlas());
if self.ui_texture.is_none() || instance.backend_atlas().modified {
self.update_texture_atlas(&instance.backend_atlas());
}
if self.buffer_pair.is_none() || instance.draw_call().0 {
self.update_buffers(instance.draw_call().1);
if self.buffer_pair.is_none() || instance.backend_paint_buffer().0 {
self.update_buffers(instance.backend_paint_buffer().1);
}
}

View file

@ -0,0 +1 @@
// TODO

View file

@ -2,6 +2,7 @@ pub mod paint;
pub mod texture;
pub mod text;
pub mod util;
pub mod backend;
use text::FontManager;
use texture::TextureAtlas;
@ -9,8 +10,8 @@ use texture::TextureAtlas;
/// Painter instance, stores textures and fonts needed for rendering
#[derive(Default)]
pub struct PainterInstance {
pub atlas: TextureAtlas,
pub fonts: FontManager,
pub(crate) textures: TextureAtlas,
pub(crate) fonts: FontManager,
}
impl PainterInstance {
@ -18,6 +19,26 @@ impl PainterInstance {
Self::default()
}
/// Get an immutable reference to the texture atlas
pub fn textures(&self) -> &TextureAtlas {
&self.textures
}
/// Get a mutable reference to the texture atlas
pub fn textures_mut(&mut self) -> &mut TextureAtlas {
&mut self.textures
}
/// Get an immutable reference to the font manager
pub fn fonts(&self) -> &FontManager {
&self.fonts
}
/// Get a mutable reference to the font manager
pub fn fonts_mut(&mut self) -> &mut FontManager {
&mut self.fonts
}
// pub fn atlas(&self) -> &TextureAtlas {
// &self.atlas
// }

View file

@ -18,6 +18,11 @@ impl PaintBuffer {
indices: Vec::new(),
}
}
pub fn clear(&mut self) {
self.vertices.clear();
self.indices.clear();
}
}
impl Default for PaintBuffer {

View file

@ -1,6 +1,3 @@
use std::hash::Hash;
use glam::Vec2;
use hui_shared::rect::Rect;
use crate::{paint::buffer::PaintBuffer, PainterInstance};
@ -16,8 +13,7 @@ pub use transform::PaintTransform;
mod rectangle;
pub use rectangle::PaintRectangle;
mod text;
pub use text::PaintText;
pub mod text;
pub trait PaintCommand {
/// Called before actual paint command is executed\

View file

@ -23,6 +23,10 @@ impl PaintList {
pub fn add(&mut self, command: impl PaintCommand + 'static) {
self.commands.push(Box::new(command));
}
pub fn clear(&mut self) {
self.commands.clear();
}
}
impl Default for PaintList {

View file

@ -105,7 +105,7 @@ impl PaintCommand for PaintRectangle {
// Otherwise, if texture handle is not set or invalid, use the bottom left
// corner of the texture which contains a white pixel.
let uvs = self.texture
.and_then(|handle| ctx.atlas.get_uv(handle))
.and_then(|handle| ctx.textures.get_uv(handle))
.map(|global_uv| {
let texture_uv = self.texture_uv;
let texture_uv_is_default =

View file

@ -66,7 +66,7 @@ impl PaintCommand for PaintText {
let layout = self.build_layout(&font_array);
for glyph in layout.glyphs() {
ctx.fonts.render_glyph(&mut ctx.atlas, self.text.font, glyph.key);
ctx.fonts.render_glyph(&mut ctx.textures, self.text.font, glyph.key);
}
}
@ -89,28 +89,28 @@ impl PaintCommand for PaintText {
let font_handle = self.text.font; // TODO use font_index here
let vidx = into.vertices.len() as u32;
let glyph_texture = ctx.fonts.render_glyph(&mut ctx.atlas, font_handle, glyph.key);
let uv = ctx.atlas.get_uv(glyph_texture).unwrap();
let glyph_texture = ctx.fonts.render_glyph(&mut ctx.textures, font_handle, glyph.key);
let uv = ctx.textures.get_uv(glyph_texture).unwrap();
into.indices.extend([vidx, vidx + 1, vidx + 2, vidx, vidx + 2, vidx + 3]);
into.vertices.extend([
Vertex {
position: vec2(glyph.x, glyph.y),
position: vec2(glyph.x, glyph.y).round(),
color: self.text.color,
uv: uv.top_left,
},
Vertex {
position: vec2(glyph.x + glyph_texture.size().x as f32, glyph.y),
position: vec2(glyph.x + glyph_texture.size().x as f32, glyph.y).round().round(),
color: self.text.color,
uv: uv.top_right,
},
Vertex {
position: vec2(glyph.x + glyph_texture.size().x as f32, glyph.y + glyph_texture.size().y as f32),
position: vec2(glyph.x + glyph_texture.size().x as f32, glyph.y + glyph_texture.size().y as f32).round(),
color: self.text.color,
uv: uv.bottom_right,
},
Vertex {
position: vec2(glyph.x, glyph.y + glyph_texture.size().y as f32),
position: vec2(glyph.x, glyph.y + glyph_texture.size().y as f32).round(),
color: self.text.color,
uv: uv.bottom_left,
},

View file

@ -19,10 +19,10 @@ impl FontManager {
}
}
/// Add a font to the manager.
/// Add a font to the manager from raw font file data.
///
/// Panics:
/// - If the font data is invalid.
/// - If the font data is invalid or corrupted
pub fn add(&mut self, data: &[u8]) -> FontHandle {
let font = self.fonts.add_font(data);
self.ftm.init_font(font);

View file

@ -1,2 +1,417 @@
mod atlas;
pub use atlas::*;
use glam::{ivec2, uvec2, vec2, UVec2, Vec2};
use hui_shared::rect::Corners;
use rect_packer::DensePacker;
use hashbrown::HashMap;
use nohash_hasher::BuildNoHashHasher;
//TODO support rotation
const DEFAULT_ATLAS_SIZE: UVec2 = uvec2(128, 128);
// const ALLOW_ROTATION: bool = false;
// Destination format is always RGBA
const RGBA_BYTES_PER_PIXEL: usize = 4;
/// Assert that the passed texture size is valid, panicking if it's not.
///
/// - The size must be greater than 0.
/// - The size must be less than `i32::MAX`.
fn assert_size(size: UVec2) {
assert!(
size.x > 0 &&
size.y > 0,
"size must be greater than 0"
);
assert!(
size.x <= i32::MAX as u32 &&
size.y <= i32::MAX as u32,
"size must be less than i32::MAX"
);
}
/// The format of the source texture data to use when updating a texture in the atlas.
#[derive(Clone, Copy, Debug, Default)]
pub enum SourceTextureFormat {
/// RGBA, 8-bit per channel
#[default]
RGBA8,
//TODO native-endian RGBA32 format
/// ARGB, 8-bit per channel
ARGB8,
/// BGRA, 8-bit per channel
BGRA8,
/// ABGR, 8-bit per channel
ABGR8,
/// RGB, 8-bit per channel (Alpha = 255)
RGB8,
/// BGR, 8-bit per channel (Alpha = 255)
BGR8,
/// Alpha only, 8-bit per channel (RGB = #ffffff)
A8,
}
impl SourceTextureFormat {
pub const fn bytes_per_pixel(&self) -> usize {
match self {
SourceTextureFormat::RGBA8 |
SourceTextureFormat::ARGB8 |
SourceTextureFormat::BGRA8 |
SourceTextureFormat::ABGR8 => 4,
SourceTextureFormat::RGB8 |
SourceTextureFormat::BGR8 => 3,
SourceTextureFormat::A8 => 1,
}
}
}
type TextureId = u32;
/// A handle to a texture in the texture atlas.
///
/// Can be cheaply copied and passed around.\
/// The handle is only valid for the texture atlas it was created from.
#[derive(Clone, Copy, Hash, PartialEq, Eq, Debug)]
pub struct TextureHandle {
pub(crate) id: TextureId,
pub(crate) size: UVec2,
}
impl TextureHandle {
/// Create a new broken texture handle.
pub fn new_broken() -> Self {
Self {
id: u32::MAX,
size: uvec2(0, 0),
}
}
pub fn size(&self) -> UVec2 {
self.size
}
}
/// Represents an area allocated to a specific texture handle in the texture atlas.
struct TextureAllocation {
/// Corresponding copyable texture handle
handle: TextureHandle,
/// The offset of the allocation in the atlas, in pixels
offset: UVec2,
/// The requested size of the allocation, in pixels
size: UVec2,
/// The maximum size of the allocation, used for reusing deallocated allocations
///
/// Usually equal to `size`, but may be larger than the requested size
/// if the allocation was reused by a smaller texture at some point
max_size: UVec2,
}
impl TextureAllocation {
/// Create a new texture allocation with the specified parameters.
///
/// The `max_size` parameter will be set equal to `size`.
pub fn new(handle: TextureHandle, offset: UVec2, size: UVec2) -> Self {
Self {
handle,
offset,
size,
max_size: size,
}
}
}
pub struct TextureAtlasBackendData<'a> {
pub data: &'a [u8],
pub size: UVec2,
pub version: u64,
}
/// A texture atlas that can be used to pack multiple textures into a single texture.
pub struct TextureAtlas {
/// The size of the atlas, in pixels
size: UVec2,
/// The texture data of the atlas, ALWAYS in RGBA8 format
data: Vec<u8>,
/// The packer used to allocate space for textures in the atlas
packer: DensePacker,
/// The next id to be used for a texture handle\
/// Gets incremented every time a new texture is allocated
next_id: TextureId,
/// Active allocated textures, indexed by id of their handle
allocations: HashMap<TextureId, TextureAllocation, BuildNoHashHasher<TextureId>>,
/// Deallocated allocations that can be reused, sorted by size
//TODO: use binary heap or btreeset for reuse_allocations instead, but this works for now
reuse_allocations: Vec<TextureAllocation>,
/// Version of the texture atlas, incremented every time the atlas is modified
version: u64,
}
impl TextureAtlas {
/// Create a new texture atlas with the specified size.
pub(crate) fn new(size: UVec2) -> Self {
assert_size(size);
let data_bytes = (size.x * size.y) as usize * RGBA_BYTES_PER_PIXEL;
Self {
size,
data: vec![0; data_bytes],
packer: DensePacker::new(
size.x as i32,
size.y as i32,
),
next_id: 0,
allocations: HashMap::default(),
reuse_allocations: Vec::new(),
version: 0,
}
}
/// The version of the atlas, incremented every time the atlas is modified.
pub fn version(&self) -> u64 {
self.version
}
/// The underlying texture data of the atlas, in RGBA8 format.
pub fn data_rgba(&self) -> &[u8] {
&self.data
}
/// Get data needed by the backend implementation.
pub fn backend_data(&self) -> TextureAtlasBackendData {
TextureAtlasBackendData {
data: &self.data,
size: self.size,
version: self.version,
}
}
/// Increment the version of the atlas
fn increment_version(&mut self) {
// XXX: wrapping_add? will this *ever* overflow?
self.version = self.version.wrapping_add(1);
}
/// Get the next handle
///
/// Does not allocate a texture associated with it
/// This handle will be invalid until it's associated with a texture.
///
/// Used internally in `allocate` and `allocate_with_data`.
fn next_handle(&mut self, size: UVec2) -> TextureHandle {
let handle = TextureHandle {
id: self.next_id,
size,
};
self.next_id += 1;
handle
}
/// Allocate a texture in the atlas, returning a handle to it.\
/// The data present in the texture is undefined, and may include garbage data.
///
/// # Panics
/// - If any of the dimensions of the texture are zero or exceed `i32::MAX`.
pub fn allocate(&mut self, size: UVec2) -> TextureHandle {
assert_size(size);
// Check if any deallocated allocations can be reused
// Find the smallest allocation that fits the requested size
// (The list is already sorted by size)
for (idx, allocation) in self.reuse_allocations.iter().enumerate() {
if allocation.max_size.x >= size.x && allocation.max_size.y >= size.y {
let allocation = self.reuse_allocations.remove(idx);
let handle = self.next_handle(size);
unsafe {
self.allocations.insert_unique_unchecked(handle.id, TextureAllocation {
handle,
offset: allocation.offset,
size,
max_size: allocation.max_size,
});
}
return handle;
}
}
// Pack the texture
let pack = self.packer.pack(
size.x as i32,
size.y as i32,
false
);
//TODO: handle pack failure by either resizing the atlas or returning an error
let pack = pack.unwrap();
let offset = ivec2(pack.x, pack.y).as_uvec2();
// Allocate the texture
let handle = self.next_handle(size);
let allocation = TextureAllocation::new(handle, offset, size);
unsafe {
self.allocations.insert_unique_unchecked(handle.id, allocation);
}
handle
}
/// Deallocate a texture in the atlas, allowing its space to be reused by future allocations.
///
/// # Panics
/// - If the texture handle is invalid for this atlas.
pub fn deallocate(&mut self, handle: TextureHandle) {
// Remove the allocation from the active allocations
let allocation = self.allocations
.remove(&handle.id)
.expect("invalid texture handle");
// TODO: this is not the most efficient way to do this:
// And put it in the reuse allocations queue
self.reuse_allocations.push(allocation);
self.reuse_allocations.sort_unstable_by_key(|a| a.size.x * a.size.y);
}
/// Update the data of a texture in the atlas.\
/// The texture must have been previously allocated with `allocate` or `allocate_with_data`.
///
/// The source data must be in the format specified by the `format` parameter.\
/// (Please note that the internal format of the texture is always RGBA8, regardless of the source format.)
///
/// The function will silently ignore any data that doesn't fit in the texture.
///
/// # Panics
/// - If the texture handle is invalid for this atlas.
/// - The length of the data array is less than the size of the texture.
pub fn update(&mut self, handle: TextureHandle, format: SourceTextureFormat, data: &[u8]) {
assert!(
data.len() >= handle.size.x as usize * handle.size.y as usize * format.bytes_per_pixel(),
"data length must be at least the size of the texture"
);
let bpp = format.bytes_per_pixel();
let TextureAllocation { size, offset, ..} = self.allocations
.get(&handle.id)
.expect("invalid texture handle");
for y in 0..size.y {
for x in 0..size.x {
let src_idx = (y * size.x + x) as usize * bpp;
let dst_idx: usize = (
(offset.y + y) * size.x +
(offset.x + x)
) as usize * RGBA_BYTES_PER_PIXEL;
let src = &data[src_idx..src_idx + bpp];
let dst = &mut self.data[dst_idx..dst_idx + RGBA_BYTES_PER_PIXEL];
match format {
SourceTextureFormat::RGBA8 => {
dst.copy_from_slice(src);
},
SourceTextureFormat::ARGB8 => {
dst[..3].copy_from_slice(&src[1..]);
dst[3] = src[0];
},
SourceTextureFormat::BGRA8 => {
dst.copy_from_slice(src);
dst.rotate_right(1);
dst.reverse();
},
SourceTextureFormat::ABGR8 => {
dst.copy_from_slice(src);
dst.reverse();
},
SourceTextureFormat::RGB8 => {
dst[..3].copy_from_slice(src);
dst[3] = 0xff;
},
SourceTextureFormat::BGR8 => {
dst[..3].copy_from_slice(src);
dst[..3].reverse();
dst[3] = 0xff;
},
SourceTextureFormat::A8 => {
dst[..3].fill(0xff);
dst[3] = src[0];
},
}
}
}
self.increment_version();
}
/// Allocate a texture in the atlas, returning a handle to it.\
/// The texture is initialized with the provided data.
///
/// The source data must be in the format specified by the `format` parameter.\
/// (Please note that the internal format of the texture is always RGBA8, regardless of the source format.)
///
/// # Panics
/// - If any of the dimensions of the texture are zero or exceed `i32::MAX`.
/// - The length of the data array is zero or not a multiple of the stride (stride = width * bytes per pixel).
pub fn allocate_with_data(&mut self, format: SourceTextureFormat, data: &[u8], width: usize) -> TextureHandle {
assert!(
!data.is_empty(),
"texture data must not be empty"
);
// Calculate the stride of the texture
let bytes_per_pixel = format.bytes_per_pixel();
let stride = bytes_per_pixel * width;
assert_eq!(
data.len() % stride, 0,
"texture data must be a multiple of the stride",
);
// Calculate the size of the texture
let size = uvec2(
width as u32,
(data.len() / stride) as u32,
);
assert_size(size);
// Allocate the texture
let handle = self.allocate(size);
// Write the data to the texture
self.update(handle, format, data);
handle
}
/// Get uv coordinates for the texture handle.
pub(crate) fn get_uv(&self, handle: TextureHandle) -> Option<Corners<Vec2>> {
let TextureAllocation { offset, size, .. } = self.allocations
.get(&handle.id)?;
let p0x = offset.x as f32 / self.size.x as f32;
let p1x = (offset.x as f32 + size.x as f32) / self.size.x as f32;
let p0y = offset.y as f32 / self.size.y as f32;
let p1y = (offset.y as f32 + size.y as f32) / self.size.y as f32;
Some(Corners {
top_left: vec2(p0x, p0y),
top_right: vec2(p1x, p0y),
bottom_left: vec2(p0x, p1y),
bottom_right: vec2(p1x, p1y),
})
}
}
impl Default for TextureAtlas {
fn default() -> Self {
Self::new(DEFAULT_ATLAS_SIZE)
}
}

View file

@ -1,387 +0,0 @@
use glam::{ivec2, uvec2, vec2, UVec2, Vec2};
use hui_shared::rect::Corners;
use rect_packer::DensePacker;
use hashbrown::HashMap;
use nohash_hasher::BuildNoHashHasher;
//TODO support rotation
const DEFAULT_ATLAS_SIZE: UVec2 = uvec2(128, 128);
// const ALLOW_ROTATION: bool = false;
// Destination format is always RGBA
const RGBA_BYTES_PER_PIXEL: usize = 4;
/// Assert that the passed texture size is valid, panicking if it's not.
///
/// - The size must be greater than 0.
/// - The size must be less than `i32::MAX`.
fn assert_size(size: UVec2) {
assert!(
size.x > 0 &&
size.y > 0,
"size must be greater than 0"
);
assert!(
size.x <= i32::MAX as u32 &&
size.y <= i32::MAX as u32,
"size must be less than i32::MAX"
);
}
/// The format of the source texture data to use when updating a texture in the atlas.
#[derive(Clone, Copy, Debug, Default)]
pub enum SourceTextureFormat {
/// RGBA, 8-bit per channel
#[default]
RGBA8,
//TODO native-endian RGBA32 format
/// ARGB, 8-bit per channel
ARGB8,
/// BGRA, 8-bit per channel
BGRA8,
/// ABGR, 8-bit per channel
ABGR8,
/// RGB, 8-bit per channel (Alpha = 255)
RGB8,
/// BGR, 8-bit per channel (Alpha = 255)
BGR8,
/// Alpha only, 8-bit per channel (RGB = #ffffff)
A8,
}
impl SourceTextureFormat {
pub const fn bytes_per_pixel(&self) -> usize {
match self {
SourceTextureFormat::RGBA8 |
SourceTextureFormat::ARGB8 |
SourceTextureFormat::BGRA8 |
SourceTextureFormat::ABGR8 => 4,
SourceTextureFormat::RGB8 |
SourceTextureFormat::BGR8 => 3,
SourceTextureFormat::A8 => 1,
}
}
}
type TextureId = u32;
/// A handle to a texture in the texture atlas.
///
/// Can be cheaply copied and passed around.\
/// The handle is only valid for the texture atlas it was created from.
#[derive(Clone, Copy, Hash, PartialEq, Eq, Debug)]
pub struct TextureHandle {
pub(crate) id: TextureId,
pub(crate) size: UVec2,
}
impl TextureHandle {
pub fn size(&self) -> UVec2 {
self.size
}
}
/// Represents an area allocated to a specific texture handle in the texture atlas.
struct TextureAllocation {
/// Corresponding copyable texture handle
handle: TextureHandle,
/// The offset of the allocation in the atlas, in pixels
offset: UVec2,
/// The requested size of the allocation, in pixels
size: UVec2,
/// The maximum size of the allocation, used for reusing deallocated allocations
///
/// Usually equal to `size`, but may be larger than the requested size
/// if the allocation was reused by a smaller texture at some point
max_size: UVec2,
}
impl TextureAllocation {
/// Create a new texture allocation with the specified parameters.
///
/// The `max_size` parameter will be set equal to `size`.
pub fn new(handle: TextureHandle, offset: UVec2, size: UVec2) -> Self {
Self {
handle,
offset,
size,
max_size: size,
}
}
}
/// A texture atlas that can be used to pack multiple textures into a single texture.
pub struct TextureAtlas {
/// The size of the atlas, in pixels
size: UVec2,
/// The texture data of the atlas, ALWAYS in RGBA8 format
data: Vec<u8>,
/// The packer used to allocate space for textures in the atlas
packer: DensePacker,
/// The next id to be used for a texture handle\
/// Gets incremented every time a new texture is allocated
next_id: TextureId,
/// Active allocated textures, indexed by id of their handle
allocations: HashMap<TextureId, TextureAllocation, BuildNoHashHasher<TextureId>>,
/// Deallocated allocations that can be reused, sorted by size
//TODO: use binary heap or btreeset for reuse_allocations instead, but this works for now
reuse_allocations: Vec<TextureAllocation>,
/// Version of the texture atlas, incremented every time the atlas is modified
version: u64,
}
impl TextureAtlas {
/// Create a new texture atlas with the specified size.
pub(crate) fn new(size: UVec2) -> Self {
assert_size(size);
let data_bytes = (size.x * size.y) as usize * RGBA_BYTES_PER_PIXEL;
Self {
size,
data: vec![0; data_bytes],
packer: DensePacker::new(
size.x as i32,
size.y as i32,
),
next_id: 0,
allocations: HashMap::default(),
reuse_allocations: Vec::new(),
version: 0,
}
}
pub fn version(&self) -> u64 {
self.version
}
fn mark_modified(&mut self) {
// XXX: wrapping_add? will this *ever* overflow?
self.version = self.version.wrapping_add(1);
}
/// Get the next handle
///
/// Does not allocate a texture associated with it
/// This handle will be invalid until it's associated with a texture.
///
/// Used internally in `allocate` and `allocate_with_data`.
fn next_handle(&mut self, size: UVec2) -> TextureHandle {
let handle = TextureHandle {
id: self.next_id,
size,
};
self.next_id += 1;
handle
}
/// Allocate a texture in the atlas, returning a handle to it.\
/// The data present in the texture is undefined, and may include garbage data.
///
/// # Panics
/// - If any of the dimensions of the texture are zero or exceed `i32::MAX`.
pub fn allocate(&mut self, size: UVec2) -> TextureHandle {
assert_size(size);
// Check if any deallocated allocations can be reused
// Find the smallest allocation that fits the requested size
// (The list is already sorted by size)
for (idx, allocation) in self.reuse_allocations.iter().enumerate() {
if allocation.max_size.x >= size.x && allocation.max_size.y >= size.y {
let allocation = self.reuse_allocations.remove(idx);
let handle = self.next_handle(size);
unsafe {
self.allocations.insert_unique_unchecked(handle.id, TextureAllocation {
handle,
offset: allocation.offset,
size,
max_size: allocation.max_size,
});
}
return handle;
}
}
// Pack the texture
let pack = self.packer.pack(
size.x as i32,
size.y as i32,
false
);
//TODO: handle pack failure by either resizing the atlas or returning an error
let pack = pack.unwrap();
let offset = ivec2(pack.x, pack.y).as_uvec2();
// Allocate the texture
let handle = self.next_handle(size);
let allocation = TextureAllocation::new(handle, offset, size);
unsafe {
self.allocations.insert_unique_unchecked(handle.id, allocation);
}
handle
}
/// Deallocate a texture in the atlas, allowing its space to be reused by future allocations.
///
/// # Panics
/// - If the texture handle is invalid for this atlas.
pub fn deallocate(&mut self, handle: TextureHandle) {
// Remove the allocation from the active allocations
let allocation = self.allocations
.remove(&handle.id)
.expect("invalid texture handle");
// TODO: this is not the most efficient way to do this:
// And put it in the reuse allocations queue
self.reuse_allocations.push(allocation);
self.reuse_allocations.sort_unstable_by_key(|a| a.size.x * a.size.y);
}
/// Update the data of a texture in the atlas.\
/// The texture must have been previously allocated with `allocate` or `allocate_with_data`.
///
/// The source data must be in the format specified by the `format` parameter.\
/// (Please note that the internal format of the texture is always RGBA8, regardless of the source format.)
///
/// The function will silently ignore any data that doesn't fit in the texture.
///
/// # Panics
/// - If the texture handle is invalid for this atlas.
/// - The length of the data array is less than the size of the texture.
pub fn update(&mut self, handle: TextureHandle, format: SourceTextureFormat, data: &[u8]) {
assert!(
data.len() >= handle.size.x as usize * handle.size.y as usize * format.bytes_per_pixel(),
"data length must be at least the size of the texture"
);
let bpp = format.bytes_per_pixel();
let TextureAllocation { size, offset, ..} = self.allocations
.get(&handle.id)
.expect("invalid texture handle");
for y in 0..size.y {
for x in 0..size.x {
let src_idx = (y * size.x + x) as usize * bpp;
let dst_idx: usize = (
(offset.y + y) * size.x +
(offset.x + x)
) as usize * RGBA_BYTES_PER_PIXEL;
let src = &data[src_idx..src_idx + bpp];
let dst = &mut self.data[dst_idx..dst_idx + RGBA_BYTES_PER_PIXEL];
match format {
SourceTextureFormat::RGBA8 => {
dst.copy_from_slice(src);
},
SourceTextureFormat::ARGB8 => {
dst[..3].copy_from_slice(&src[1..]);
dst[3] = src[0];
},
SourceTextureFormat::BGRA8 => {
dst.copy_from_slice(src);
dst.rotate_right(1);
dst.reverse();
},
SourceTextureFormat::ABGR8 => {
dst.copy_from_slice(src);
dst.reverse();
},
SourceTextureFormat::RGB8 => {
dst[..3].copy_from_slice(src);
dst[3] = 0xff;
},
SourceTextureFormat::BGR8 => {
dst[..3].copy_from_slice(src);
dst[..3].reverse();
dst[3] = 0xff;
},
SourceTextureFormat::A8 => {
dst[..3].fill(0xff);
dst[3] = src[0];
},
}
}
}
self.mark_modified();
}
/// Allocate a texture in the atlas, returning a handle to it.\
/// The texture is initialized with the provided data.
///
/// The source data must be in the format specified by the `format` parameter.\
/// (Please note that the internal format of the texture is always RGBA8, regardless of the source format.)
///
/// # Panics
/// - If any of the dimensions of the texture are zero or exceed `i32::MAX`.
/// - The length of the data array is zero or not a multiple of the stride (stride = width * bytes per pixel).
pub fn allocate_with_data(&mut self, format: SourceTextureFormat, data: &[u8], width: usize) -> TextureHandle {
assert!(
!data.is_empty(),
"texture data must not be empty"
);
// Calculate the stride of the texture
let bytes_per_pixel = format.bytes_per_pixel();
let stride = bytes_per_pixel * width;
assert_eq!(
data.len() % stride, 0,
"texture data must be a multiple of the stride",
);
// Calculate the size of the texture
let size = uvec2(
width as u32,
(data.len() / stride) as u32,
);
assert_size(size);
// Allocate the texture
let handle = self.allocate(size);
// Write the data to the texture
self.update(handle, format, data);
handle
}
/// Get uv coordinates for the texture handle.
pub(crate) fn get_uv(&self, handle: TextureHandle) -> Option<Corners<Vec2>> {
let TextureAllocation { offset, size, .. } = self.allocations
.get(&handle.id)?;
let p0x = offset.x as f32 / self.size.x as f32;
let p1x = (offset.x as f32 + size.x as f32) / self.size.x as f32;
let p0y = offset.y as f32 / self.size.y as f32;
let p1y = (offset.y as f32 + size.y as f32) / self.size.y as f32;
Some(Corners {
top_left: vec2(p0x, p0y),
top_right: vec2(p1x, p0y),
bottom_left: vec2(p0x, p1y),
bottom_right: vec2(p1x, p1y),
})
}
}
impl Default for TextureAtlas {
fn default() -> Self {
Self::new(DEFAULT_ATLAS_SIZE)
}
}

View file

@ -290,12 +290,12 @@ impl WgpuUiRenderer {
device: &wgpu::Device,
resolution: Vec2,
) {
let (modified, call) = instance.draw_call();
let (modified, call) = instance.backend_paint_buffer();
if self.modified || modified {
self.update_buffers(call, queue, device, resolution);
}
let meta = instance.atlas();
let meta = instance.backend_atlas();
if self.modified || meta.modified {
self.update_texture(meta, queue, device);
}

View file

@ -18,7 +18,7 @@ include = [
[dependencies]
hui-derive = { version = "0.1.0-alpha.6", path = "../hui-derive", optional = true }
hui-shared = { version = "0.1.0-alpha.6", path = "../hui-shared" }
# hui-painter = { version = "0.1.0-alpha.6", path = "../hui-painter" }
hui-painter = { version = "0.1.0-alpha.6", path = "../hui-painter" }
hashbrown = "0.15"
nohash-hasher = "0.2"
glam = "0.30"
@ -33,7 +33,7 @@ image = { version = "0.25", default-features = false, optional = true }
rustc-hash = "2.0"
[features]
default = ["el_all", "image", "builtin_font", "pixel_perfect_text", "derive"]
default = ["el_all", "image", "builtin_font", "derive"]
## Enable derive macros
derive = ["dep:hui-derive"]
@ -44,17 +44,6 @@ image = ["dep:image"]
## Enable the built-in font (ProggyTiny, adds *35kb* to the executable)
builtin_font = []
#! #### Pixel-perfect rendering:
## Round all vertex positions to nearest integer coordinates (not recommended)
pixel_perfect = ["pixel_perfect_text"]
## Apply pixel-perfect rendering hack to text (fixes blurry text rendering)
pixel_perfect_text = []
#! Make sure to disable both features if you are not rendering UI "as-is" at 1:1 scale\
#! For exmaple, you should disable them if using DPI (or any other form of) scaling while passing the virtual resolution to the ui or rendering it in 3d space
#! #### Built-in elements:
## Enable all built-in elements

View file

@ -1,396 +0,0 @@
//! draw commands, tesselation and UI rendering.
use crate::{
rect::Corners,
text::{FontHandle, TextRenderer}
};
pub(crate) mod atlas;
use atlas::TextureAtlasManager;
pub use atlas::{ImageHandle, TextureAtlasMeta, TextureFormat, ImageCtx};
mod corner_radius;
pub use corner_radius::RoundedCorners;
use std::borrow::Cow;
use fontdue::layout::{Layout, CoordinateSystem, TextStyle};
use glam::{vec2, Vec2, Affine2, Vec4};
//TODO: circle draw command
/// Available draw commands
/// - Rectangle: Filled, colored rectangle, with optional rounded corners and texture
/// - Text: Draw text using the specified font, size, color, and position
#[derive(Clone, Debug, PartialEq)]
pub enum UiDrawCommand {
///Filled, colored rectangle
Rectangle {
///Position in pixels
position: Vec2,
///Size in pixels
size: Vec2,
///Color (RGBA)
color: Corners<Vec4>,
///Texture
texture: Option<ImageHandle>,
///Sub-UV coordinates for the texture
texture_uv: Option<Corners<Vec2>>,
///Rounded corners
rounded_corners: Option<RoundedCorners>,
},
/// Draw text using the specified font, size, color, and position
Text {
///Position in pixels
position: Vec2,
///Font size
size: u16,
///Color (RGBA)
color: Vec4,
///Text to draw
text: Cow<'static, str>,
///Font handle to use
font: FontHandle,
},
/// Push a transformation matrix to the stack
PushTransform(Affine2),
/// Pop a transformation matrix from the stack
PopTransform,
//TODO PushClip PopClip
}
/// List of draw commands
#[derive(Default)]
pub struct UiDrawCommandList {
pub commands: Vec<UiDrawCommand>,
}
impl UiDrawCommandList {
/// Add a draw command to the list
pub fn add(&mut self, command: UiDrawCommand) {
self.commands.push(command);
}
}
// impl UiDrawCommands {
// pub fn compare(&self, other: &Self) -> bool {
// // if self.commands.len() != other.commands.len() { return false }
// // self.commands.iter().zip(other.commands.iter()).all(|(a, b)| a == b)
// }
// }
/// A vertex for UI rendering
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub struct UiVertex {
pub position: Vec2,
pub color: Vec4,
pub uv: Vec2,
}
/// Represents a single draw call (vertices + indices), should be handled by the render backend
#[derive(Default)]
pub struct UiDrawCall {
pub vertices: Vec<UiVertex>,
pub indices: Vec<u32>,
}
impl UiDrawCall {
/// Tesselate the UI and build a complete draw plan from a list of draw commands
pub(crate) fn build(draw_commands: &UiDrawCommandList, atlas: &mut TextureAtlasManager, text_renderer: &mut TextRenderer) -> Self {
let mut trans_stack = Vec::new();
let mut draw_call = UiDrawCall::default();
//HACK: atlas may get resized while creating new glyphs,
//which invalidates all uvs, causing corrupted-looking texture
//so we need to pregenerate font textures before generating any vertices
//we are doing *a lot* of double work here, but it's the easiest way to avoid the issue
for comamnd in &draw_commands.commands {
if let UiDrawCommand::Text { text, font: font_handle, size, .. } = comamnd {
let mut layout = Layout::new(CoordinateSystem::PositiveYDown);
layout.append(
&[text_renderer.internal_font(*font_handle)],
&TextStyle::new(text, *size as f32, 0)
);
let glyphs = layout.glyphs();
for layout_glyph in glyphs {
if !layout_glyph.char_data.rasterize() { continue }
text_renderer.glyph(atlas, *font_handle, layout_glyph.parent, layout_glyph.key.px as u8);
}
}
}
//note to future self:
//RESIZING OR ADDING STUFF TO ATLAS AFTER THIS POINT IS A BIG NO-NO,
//DON'T DO IT EVER AGAIN UNLESS YOU WANT TO SPEND HOURS DEBUGGING
atlas.lock_atlas = true;
for command in &draw_commands.commands {
match command {
UiDrawCommand::PushTransform(trans) => {
//Take note of the current index, and the transformation matrix\
//We will actually apply the transformation matrix when we pop it,
//to all vertices between the current index and the index we pushed
trans_stack.push((trans, draw_call.vertices.len() as u32));
},
UiDrawCommand::PopTransform => {
//Pop the transformation matrix and apply it to all vertices between the current index and the index we pushed
let (&trans, idx) = trans_stack.pop().expect("Unbalanced push/pop transform");
//If Push is immediately followed by a pop (which is dumb but possible), we don't need to do anything
//(this can also happen if push and pop are separated by a draw command that doesn't add any vertices, like a text command with an empty string)
if idx == draw_call.vertices.len() as u32 {
continue
}
//Kinda a hack:
//We want to apply the transform aronnd the center, so we need to compute the center of the vertices
//We won't actually do that, we will compute the center of the bounding box of the vertices
let mut min = Vec2::splat(f32::INFINITY);
let mut max = Vec2::splat(f32::NEG_INFINITY);
for v in &draw_call.vertices[idx as usize..] {
min = min.min(v.position);
max = max.max(v.position);
}
//TODO: make the point of transform configurable
let center = (min + max) / 2.;
//Apply trans mtx to all vertices between idx and the current index
for v in &mut draw_call.vertices[idx as usize..] {
v.position -= center;
v.position = trans.transform_point2(v.position);
v.position += center;
}
},
UiDrawCommand::Rectangle { position, size, color, texture, texture_uv, rounded_corners } => {
let uvs = texture
.map(|x| atlas.get_uv(x))
.flatten()
.map(|guv| {
if let Some(texture_uv) = texture_uv {
//XXX: this may not work if the texture is rotated
//also is this slow?
let top = guv.top_left.lerp(guv.top_right, texture_uv.top_left.x);
let bottom = guv.bottom_left.lerp(guv.bottom_right, texture_uv.top_left.x);
let top_left = top.lerp(bottom, texture_uv.top_left.y);
let top = guv.top_left.lerp(guv.top_right, texture_uv.top_right.x);
let bottom = guv.bottom_left.lerp(guv.bottom_right, texture_uv.top_right.x);
let top_right = top.lerp(bottom, texture_uv.top_right.y);
let top = guv.top_left.lerp(guv.top_right, texture_uv.bottom_left.x);
let bottom = guv.bottom_left.lerp(guv.bottom_right, texture_uv.bottom_left.x);
let bottom_left = top.lerp(bottom, texture_uv.bottom_left.y);
let top = guv.top_left.lerp(guv.top_right, texture_uv.bottom_right.x);
let bottom = guv.bottom_left.lerp(guv.bottom_right, texture_uv.bottom_right.x);
let bottom_right = top.lerp(bottom, texture_uv.bottom_right.y);
Corners { top_left, top_right, bottom_left, bottom_right }
} else {
guv
}
})
.unwrap_or(Corners::all(Vec2::ZERO));
let vidx = draw_call.vertices.len() as u32;
if let Some(corner) = rounded_corners.filter(|x| x.radius.max_f32() > 0.0) {
//this code is stupid as fuck
//but it works... i think?
//maybe some verts end up missing, but it's close enough...
//Random vert in the center for no reason
//lol
draw_call.vertices.push(UiVertex {
position: *position + *size * vec2(0.5, 0.5),
color: (color.bottom_left + color.bottom_right + color.top_left + color.top_right) / 4.,
//TODO: fix this uv
uv: vec2(0., 0.),
});
//TODO: fix some corners tris being invisible (but it's already close enough lol)
let rounded_corner_verts = corner.point_count.get() as u32;
for i in 0..rounded_corner_verts {
let cratio = i as f32 / (rounded_corner_verts - 1) as f32;
let angle = cratio * std::f32::consts::PI * 0.5;
let x = angle.sin();
let y = angle.cos();
let mut corner_impl = |rp: Vec2, color: &Corners<Vec4>| {
let rrp = rp / *size;
let color_at_point =
color.bottom_right * rrp.x * rrp.y +
color.top_right * rrp.x * (1. - rrp.y) +
color.bottom_left * (1. - rrp.x) * rrp.y +
color.top_left * (1. - rrp.x) * (1. - rrp.y);
let uv_at_point =
uvs.bottom_right * rrp.x * rrp.y +
uvs.top_right * rrp.x * (1. - rrp.y) +
uvs.bottom_left * (1. - rrp.x) * rrp.y +
uvs.top_left * (1. - rrp.x) * (1. - rrp.y);
draw_call.vertices.push(UiVertex {
position: *position + rp,
color: color_at_point,
uv: uv_at_point,
});
};
//Top-right corner
corner_impl(
vec2(x, 1. - y) * corner.radius.top_right + vec2(size.x - corner.radius.top_right, 0.),
color,
);
//Bottom-right corner
corner_impl(
vec2(x - 1., y) * corner.radius.bottom_right + vec2(size.x, size.y - corner.radius.bottom_right),
color,
);
//Bottom-left corner
corner_impl(
vec2(1. - x, y) * corner.radius.bottom_left + vec2(0., size.y - corner.radius.bottom_left),
color,
);
//Top-left corner
corner_impl(
vec2(1. - x, 1. - y) * corner.radius.top_left,
color,
);
// mental illness:
if i > 0 {
draw_call.indices.extend([
//Top-right corner
vidx,
vidx + 1 + (i - 1) * 4,
vidx + 1 + i * 4,
//Bottom-right corner
vidx,
vidx + 1 + (i - 1) * 4 + 1,
vidx + 1 + i * 4 + 1,
//Bottom-left corner
vidx,
vidx + 1 + (i - 1) * 4 + 2,
vidx + 1 + i * 4 + 2,
//Top-left corner
vidx,
vidx + 1 + (i - 1) * 4 + 3,
vidx + 1 + i * 4 + 3,
]);
}
}
//Fill in the rest
//mental illness 2:
draw_call.indices.extend([
//Top
vidx,
vidx + 4,
vidx + 1,
//Right?, i think
vidx,
vidx + 1 + (rounded_corner_verts - 1) * 4,
vidx + 1 + (rounded_corner_verts - 1) * 4 + 1,
//Left???
vidx,
vidx + 1 + (rounded_corner_verts - 1) * 4 + 2,
vidx + 1 + (rounded_corner_verts - 1) * 4 + 3,
//Bottom???
vidx,
vidx + 3,
vidx + 2,
]);
} else {
//...Normal rectangle
draw_call.indices.extend([vidx, vidx + 1, vidx + 2, vidx, vidx + 2, vidx + 3]);
draw_call.vertices.extend([
UiVertex {
position: *position,
color: color.top_left,
uv: uvs.top_left,
},
UiVertex {
position: *position + vec2(size.x, 0.0),
color: color.top_right,
uv: uvs.top_right,
},
UiVertex {
position: *position + *size,
color: color.bottom_right,
uv: uvs.bottom_right,
},
UiVertex {
position: *position + vec2(0.0, size.y),
color: color.bottom_left,
uv: uvs.bottom_left,
},
]);
}
},
UiDrawCommand::Text { position, size, color, text, font: font_handle } => {
if text.is_empty() {
continue
}
//XXX: should we be doing this every time?
let mut layout = Layout::new(CoordinateSystem::PositiveYDown);
layout.append(
&[text_renderer.internal_font(*font_handle)],
&TextStyle::new(text, *size as f32, 0)
);
let glyphs = layout.glyphs();
for layout_glyph in glyphs {
if !layout_glyph.char_data.rasterize() {
continue
}
let vidx = draw_call.vertices.len() as u32;
let glyph = text_renderer.glyph(atlas, *font_handle, layout_glyph.parent, layout_glyph.key.px as u8);
let uv = atlas.get_uv(glyph.texture).unwrap();
draw_call.indices.extend([vidx, vidx + 1, vidx + 2, vidx, vidx + 2, vidx + 3]);
draw_call.vertices.extend([
UiVertex {
position: *position + vec2(layout_glyph.x, layout_glyph.y),
color: *color,
uv: uv.top_left,
},
UiVertex {
position: *position + vec2(layout_glyph.x + glyph.metrics.width as f32, layout_glyph.y),
color: *color,
uv: uv.top_right,
},
UiVertex {
position: *position + vec2(layout_glyph.x + glyph.metrics.width as f32, layout_glyph.y + glyph.metrics.height as f32),
color: *color,
uv: uv.bottom_right,
},
UiVertex {
position: *position + vec2(layout_glyph.x, layout_glyph.y + glyph.metrics.height as f32),
color: *color,
uv: uv.bottom_left,
},
]);
#[cfg(all(
feature = "pixel_perfect_text",
not(feature = "pixel_perfect")
))] {
//Round the position of the vertices to the nearest pixel, unless any transformations are active
if trans_stack.is_empty() {
for vtx in &mut draw_call.vertices[(vidx as usize)..] {
vtx.position = vtx.position.round()
}
}
}
}
}
}
}
atlas.lock_atlas = false;
#[cfg(feature = "pixel_perfect")]
draw_call.vertices.iter_mut().for_each(|v| {
v.position = v.position.round()
});
draw_call
}
}

View file

@ -1,299 +0,0 @@
use glam::{uvec2, vec2, UVec2, Vec2};
use hashbrown::HashMap;
use nohash_hasher::BuildNoHashHasher;
use rect_packer::DensePacker;
use crate::rect::Corners;
const RGBA_CHANNEL_COUNT: u32 = 4;
//TODO make this work
const ALLOW_ROTATION: bool = false;
/// Texture format of the source texture data
#[derive(Default, Clone, Copy, PartialEq, Eq)]
pub enum TextureFormat {
/// The data is stored in RGBA format, with 1 byte (8 bits) per channel
#[default]
Rgba,
/// The data is copied into the Alpha channel, with 1 byte (8 bits) per channel\
/// Remaining channels are set to 255 (which can be easily shaded to any color)
///
/// This format is useful for storing grayscale textures such as icons\
/// (Please note that the internal representation is still RGBA, this is just a convenience feature)
Grayscale,
}
/// Contains a reference to the texture data, and metadata associated with it
pub struct TextureAtlasMeta<'a> {
/// Texture data\
/// The data is stored in RGBA format, with 1 byte (8 bits) per channel
pub data: &'a [u8],
/// Current size of the texture atlas\
/// Please note that this value might change
pub size: UVec2,
/// True if the atlas has been modified since the beginning of the current frame\
/// If this function returns true, the texture atlas should be re-uploaded to the GPU before rendering\
pub modified: bool,
}
/// Texture handle, stores the internal index of a texture within the texture atlas and can be cheaply copied.
///
/// Please note that dropping a handle does not deallocate the texture from the atlas, you must do it manually.
///
/// Only valid for the `UiInstance` that created it.\
/// Using it with other instances may result in panics or unexpected behavior.
///
/// Handle values are not guaranteed to be valid.\
/// Creating or transmuting an invalid handle is allowed and is *not* UB.
///
/// Internal value is an implementation detail and should not be relied upon.
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Hash)]
pub struct ImageHandle {
pub(crate) index: u32,
}
#[derive(Clone, Copy, Debug)]
pub(crate) struct TextureAllocation {
/// Position in the texture atlas\
/// (This is an implementation detail and should not be exposed to the user)
pub(crate) position: UVec2,
/// Requested texture size
pub size: UVec2,
/// True if the texture was rotated by 90 degrees\
/// (This is an implementation detail and should not be exposed to the user)
pub(crate) rotated: bool,
}
/// Manages a texture atlas and the allocation of space within it\
/// The atlas is alllowed to grow and resize dynamically, as needed
pub(crate) struct TextureAtlasManager {
packer: DensePacker,
count: u32,
size: UVec2,
data: Vec<u8>,
allocations: HashMap<u32, TextureAllocation, BuildNoHashHasher<u32>>,
/// Items that have been removed from the allocation list, but still affect
remove_queue: Vec<TextureAllocation>,
/// True if the atlas has been modified in a way which requires a texture reupload
/// since the beginning of the current frame
modified: bool,
/// If true, attempting to modify the atlas in a way which invalidates UVs will cause a panic\
/// Used internally to ensure that the UVs do not become invalidated mid-render
pub(crate) lock_atlas: bool,
}
impl TextureAtlasManager {
/// Create a new texture atlas with the specified size\
/// 512x512 is a good default size for most applications, and the texture atlas can grow dynamically as needed
pub fn new(size: UVec2) -> Self {
Self {
packer: DensePacker::new(size.x as i32, size.y as i32),
count: 0,
size,
data: vec![0; (size.x * size.y * RGBA_CHANNEL_COUNT) as usize],
allocations: HashMap::default(),
remove_queue: Vec::new(),
modified: true,
lock_atlas: false,
}
}
/// Resize the texture atlas to the new size in-place, preserving the existing data
pub fn resize(&mut self, new_size: UVec2) {
if self.lock_atlas {
panic!("Attempted to resize the texture atlas while the atlas is locked");
}
log::trace!("resizing texture atlas to {:?}", new_size);
if self.size == new_size {
log::warn!("Texture atlas is already the requested size");
return
}
if new_size.x > self.size.x && new_size.y > self.size.y {
self.packer.resize(new_size.x as i32, new_size.y as i32);
//Resize the data array in-place
self.data.resize((new_size.x * new_size.y * RGBA_CHANNEL_COUNT) as usize, 0);
for y in (0..self.size.y).rev() {
for x in (1..self.size.x).rev() {
let idx = ((y * self.size.x + x) * RGBA_CHANNEL_COUNT) as usize;
let new_idx = ((y * new_size.x + x) * RGBA_CHANNEL_COUNT) as usize;
for c in 0..(RGBA_CHANNEL_COUNT as usize) {
self.data[new_idx + c] = self.data[idx + c];
}
}
}
} else {
//If scaling down, just recreate the atlas from scratch (since we need to re-pack everything anyway)
todo!("Atlas downscaling is not implemented yet");
}
self.size = new_size;
self.modified = true;
}
/// Ensure that a texture with specified size would fit without resizing on the next allocation attempt\
pub fn ensure_fits(&mut self, size: UVec2) {
// Plan A: try if any of the existing items in the remove queue would fit the texture
// Plan B: purge the remove queue, recreate the packer and try again (might be expensive...!)
// TODO: implement these
// Plan C: resize the atlas
let mut new_size = self.size;
while !self.packer.can_pack(size.x as i32, size.y as i32, ALLOW_ROTATION) {
new_size *= 2;
self.packer.resize(new_size.x as i32, new_size.y as i32);
}
if new_size != self.size {
self.resize(new_size);
}
}
/// Allocate a new texture region in the atlas and return a handle to it\
/// Returns None if the texture could not be allocated due to lack of space\
/// Use `allocate` to allocate a texture and resize the atlas if necessary\
/// Does not modify the texture data
fn try_allocate(&mut self, size: UVec2) -> Option<ImageHandle> {
log::trace!("Allocating texture of size {:?}", size);
let result = self.packer.pack(size.x as i32, size.y as i32, ALLOW_ROTATION)?;
let index = self.count;
self.count += 1;
let allocation = TextureAllocation {
position: UVec2::new(result.x as u32, result.y as u32),
size,
//If the size does not match the requested size, the texture was rotated
rotated: ALLOW_ROTATION && (result.width != size.x as i32),
};
unsafe {
self.allocations.insert_unique_unchecked(index, allocation);
}
Some(ImageHandle { index })
}
/// Allocate a new texture region in the atlas and resize the atlas if necessary\
/// This function should never fail under normal circumstances.\
/// May modify the texture data if the atlas is resized
pub fn allocate(&mut self, size: UVec2) -> ImageHandle {
self.ensure_fits(size);
self.try_allocate(size).unwrap()
}
/// Allocate a new texture region in the atlas and copy the data into it\
/// This function may resize the atlas as needed, and should never fail under normal circumstances.
pub(crate) fn add_rgba(&mut self, width: usize, data: &[u8]) -> ImageHandle {
let size = uvec2(width as u32, (data.len() / (width * RGBA_CHANNEL_COUNT as usize)) as u32);
let handle: ImageHandle = self.allocate(size);
let allocation = self.allocations.get(&handle.index).unwrap();
assert!(!allocation.rotated, "Rotated textures are not implemented yet");
for y in 0..size.y {
for x in 0..size.x {
let src_idx = (y * size.x + x) * RGBA_CHANNEL_COUNT;
let dst_idx = ((allocation.position.y + y) * self.size.x + allocation.position.x + x) * RGBA_CHANNEL_COUNT;
for c in 0..RGBA_CHANNEL_COUNT as usize {
self.data[dst_idx as usize + c] = data[src_idx as usize + c];
}
}
}
self.modified = true;
handle
}
/// Works the same way as [`TextureAtlasManager::add`], but the input data is assumed to be grayscale (1 channel per pixel)\
/// The data is copied into the alpha channel of the texture, while all the other channels are set to 255\
/// May resize the atlas as needed, and should never fail under normal circumstances.
pub(crate) fn add_grayscale(&mut self, width: usize, data: &[u8]) -> ImageHandle {
let size = uvec2(width as u32, (data.len() / width) as u32);
let handle = self.allocate(size);
let allocation = self.allocations.get(&handle.index).unwrap();
assert!(!allocation.rotated, "Rotated textures are not implemented yet");
for y in 0..size.y {
for x in 0..size.x {
let src_idx = (y * size.x + x) as usize;
let dst_idx = (((allocation.position.y + y) * self.size.x + allocation.position.x + x) * RGBA_CHANNEL_COUNT) as usize;
self.data[dst_idx..(dst_idx + RGBA_CHANNEL_COUNT as usize)].copy_from_slice(&[255, 255, 255, data[src_idx]]);
}
}
self.modified = true;
handle
}
pub fn add(&mut self, width: usize, data: &[u8], format: TextureFormat) -> ImageHandle {
match format {
TextureFormat::Rgba => self.add_rgba(width, data),
TextureFormat::Grayscale => self.add_grayscale(width, data),
}
}
pub(crate) fn add_dummy(&mut self) {
let handle = self.allocate((1, 1).into());
assert!(handle.index == 0, "Dummy texture handle is not 0");
assert!(self.get(handle).unwrap().position == (0, 0).into(), "Dummy texture position is not (0, 0)");
self.data[0..4].copy_from_slice(&[255, 255, 255, 255]);
self.modified = true;
}
pub fn modify(&mut self, handle: ImageHandle) {
todo!()
}
pub fn remove(&mut self, handle: ImageHandle) {
todo!()
}
pub fn get(&self, handle: ImageHandle) -> Option<&TextureAllocation> {
self.allocations.get(&handle.index)
}
pub(crate) fn get_uv(&self, handle: ImageHandle) -> Option<Corners<Vec2>> {
let info = self.get(handle)?;
let atlas_size = self.meta().size.as_vec2();
let p0x = info.position.x as f32 / atlas_size.x;
let p1x = (info.position.x as f32 + info.size.x as f32) / atlas_size.x;
let p0y = info.position.y as f32 / atlas_size.y;
let p1y = (info.position.y as f32 + info.size.y as f32) / atlas_size.y;
Some(Corners {
top_left: vec2(p0x, p0y),
top_right: vec2(p1x, p0y),
bottom_left: vec2(p0x, p1y),
bottom_right: vec2(p1x, p1y),
})
}
/// Reset the `is_modified` flag
pub(crate) fn reset_modified(&mut self) {
self.modified = false;
}
pub fn meta(&self) -> TextureAtlasMeta {
TextureAtlasMeta {
data: &self.data,
size: self.size,
modified: self.modified,
}
}
pub fn context(&self) -> ImageCtx {
ImageCtx { atlas: self }
}
}
impl Default for TextureAtlasManager {
/// Create a new texture atlas with a default size of 512x512
fn default() -> Self {
Self::new(UVec2::new(512, 512))
}
}
/// Context that allows read-only accss to image metadata
#[derive(Clone, Copy)]
pub struct ImageCtx<'a> {
pub(crate) atlas: &'a TextureAtlasManager,
}
impl ImageCtx<'_> {
/// Get size of the image with the specified handle
///
/// Returns None if the handle is invalid for the current context
pub fn get_size(&self, handle: ImageHandle) -> Option<UVec2> {
self.atlas.get(handle).map(|a| a.size)
}
}

View file

@ -1,55 +0,0 @@
use std::num::NonZeroU16;
use crate::rect::Corners;
//TODO uneven corners (separate width/height for each corner)
/// Calculate the number of points based on the maximum corner radius
fn point_count(corners: Corners<f32>) -> NonZeroU16 {
//Increase for higher quality
const VTX_PER_CORER_RADIUS_PIXEL: f32 = 0.5;
NonZeroU16::new(
(corners.max_f32() * VTX_PER_CORER_RADIUS_PIXEL).round() as u16 + 2
).unwrap()
}
/// Low-level options for rendering rounded corners
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct RoundedCorners {
/// Corner radius of each corner
pub radius: Corners<f32>,
/// Number of points to use for each corner
///
/// This value affects all corners, regardless of their individual radius
pub point_count: NonZeroU16,
}
impl From<Corners<f32>> for RoundedCorners {
/// Create a new `RoundedCorners` from [`Corners<f32>`](crate::rect::Corners)
///
/// Point count will be calculated automatically based on the maximum radius
fn from(radius: Corners<f32>) -> Self {
Self::from_radius(radius)
}
}
impl RoundedCorners {
/// Create a new `RoundedCorners` from [`Corners<f32>`](crate::rect::Corners)
///
/// Point count will be calculated automatically based on the maximum radius
pub fn from_radius(radius: Corners<f32>) -> Self {
Self {
radius,
point_count: point_count(radius),
}
}
}
impl Default for RoundedCorners {
fn default() -> Self {
Self {
radius: Corners::default(),
point_count: NonZeroU16::new(8).unwrap(),
}
}
}

View file

@ -1,40 +1,37 @@
//! element API and built-in elements like `Container`, `Button`, `Text`, etc.
use crate::{
draw::{atlas::ImageCtx, UiDrawCommandList},
input::InputCtx,
layout::{LayoutInfo, Size2d},
measure::Response,
rect::Rect,
signal::SignalStore,
state::StateRepo,
text::{FontHandle, TextMeasure},
UiInstance,
};
mod builtin;
pub use builtin::*;
use hui_painter::{paint::command::PaintList, text::FontHandle, PainterInstance};
/// Context for the `Element::measure` function
pub struct MeasureContext<'a> {
pub painter: &'a PainterInstance,
pub current_font: FontHandle,
pub layout: &'a LayoutInfo,
pub state: &'a StateRepo,
pub text_measure: TextMeasure<'a>,
pub current_font: FontHandle,
pub images: ImageCtx<'a>,
//XXX: should measure have a reference to input?
//pub input: InputCtx<'a>,
}
/// Context for the `Element::process` function
pub struct ProcessContext<'a> {
pub painter: &'a mut PainterInstance,
pub paint_target: &'a mut PaintList,
pub measure: &'a Response,
pub layout: &'a LayoutInfo,
pub draw: &'a mut UiDrawCommandList,
pub state: &'a mut StateRepo,
pub text_measure: TextMeasure<'a>,
pub current_font: FontHandle,
pub images: ImageCtx<'a>,
pub input: InputCtx<'a>,
pub signal: &'a mut SignalStore,
}

View file

@ -187,7 +187,8 @@ impl UiElement for Container {
}
}
let measure = element.measure(MeasureContext{
let measure = element.measure(MeasureContext {
painter: ctx.painter,
state: ctx.state,
layout: &LayoutInfo {
//XXX: if the element gets wrapped, this will be inaccurate.
@ -201,9 +202,7 @@ impl UiElement for Container {
direction: self.direction,
remaining_space: None,
},
text_measure: ctx.text_measure,
current_font: ctx.current_font,
images: ctx.images,
});
//Check the position of the side of element closest to the end on the primary axis
@ -375,7 +374,7 @@ impl UiElement for Container {
// });
// }
self.background_frame.draw(ctx.draw, (ctx.layout.position, ctx.measure.size).into());
self.background_frame.draw(ctx.paint_target, (ctx.layout.position, ctx.measure.size).into());
//padding
position += vec2(self.padding.left, self.padding.top);
@ -444,11 +443,10 @@ impl UiElement for Container {
//measure
let el_measure = element.measure(MeasureContext {
painter: ctx.painter,
layout: &el_layout,
state: ctx.state,
text_measure: ctx.text_measure,
current_font: ctx.current_font,
images: ctx.images,
});
//align (on sec. axis)
@ -485,13 +483,12 @@ impl UiElement for Container {
//process
element.process(ProcessContext {
painter: ctx.painter,
measure: &el_measure,
layout: &el_layout,
draw: ctx.draw,
paint_target: ctx.paint_target,
state: ctx.state,
text_measure: ctx.text_measure,
current_font: ctx.current_font,
images: ctx.images,
input: ctx.input,
signal: ctx.signal,
});

View file

@ -63,6 +63,6 @@ impl UiElement for FrameView {
}
fn process(&self, ctx: ProcessContext) {
self.frame.draw(ctx.draw, (ctx.layout.position, ctx.measure.size).into());
self.frame.draw(ctx.paint_target, (ctx.layout.position, ctx.measure.size).into());
}
}

View file

@ -1,7 +1,7 @@
use derive_setters::Setters;
use glam::vec2;
use glam::{vec2, Affine2};
use hui_painter::{paint::command::{PaintRectangle, PaintTransform}, texture::TextureHandle};
use crate::{
draw::{ImageHandle, RoundedCorners, UiDrawCommand},
element::{MeasureContext, ProcessContext, UiElement},
layout::{compute_size, Size, Size2d},
measure::Response,
@ -13,7 +13,7 @@ use crate::{
pub struct Image {
/// Image handle to draw
#[setters(skip)]
pub image: ImageHandle,
pub image: TextureHandle,
/// Size of the image.
///
@ -36,7 +36,7 @@ pub struct Image {
}
impl Image {
pub fn new(handle: ImageHandle) -> Self {
pub fn new(handle: TextureHandle) -> Self {
Self {
image: handle,
size: Size2d {
@ -59,7 +59,7 @@ impl UiElement for Image {
}
fn measure(&self, ctx: MeasureContext) -> Response {
let dim = ctx.images.get_size(self.image).expect("invalid image handle");
let dim = self.image.size();
let pre_size = compute_size(ctx.layout, self.size, dim.as_vec2());
Response {
size: compute_size(ctx.layout, self.size, vec2(
@ -78,16 +78,17 @@ impl UiElement for Image {
fn process(&self, ctx: ProcessContext) {
if !self.color.is_transparent() {
ctx.draw.add(UiDrawCommand::Rectangle {
position: ctx.layout.position,
size: ctx.measure.size,
color: self.color.corners(),
texture: Some(self.image),
texture_uv: None,
rounded_corners: (self.corner_radius.max_f32() > 0.).then_some({
RoundedCorners::from_radius(self.corner_radius)
}),
});
ctx.paint_target.add(
PaintTransform {
transform: Affine2::from_translation(ctx.layout.position),
child: PaintRectangle {
size: ctx.measure.size,
color: self.color,
texture: Some(self.image),
..Default::default()
},
}
);
}
}
}

View file

@ -75,10 +75,10 @@ impl UiElement for ProgressBar {
//FIXME: these optimizations may not be valid
if value < 1. || !self.foreground.covers_opaque() {
self.background.draw(ctx.draw, (ctx.layout.position, ctx.measure.size).into());
self.background.draw(ctx.paint_target, (ctx.layout.position, ctx.measure.size).into());
}
if value > 0. {
self.foreground.draw(ctx.draw, (ctx.layout.position, ctx.measure.size * vec2(value, 1.)).into());
self.foreground.draw(ctx.paint_target, (ctx.layout.position, ctx.measure.size * vec2(value, 1.)).into());
}
// let rounded_corners =

View file

@ -158,7 +158,7 @@ impl UiElement for Slider {
// if !(self.track_color.is_transparent() || (self.track_active_color.is_opaque() && self.handle_color.is_opaque() && self.value >= 1.)) {
if !(self.track_active.covers_opaque() && self.handle.covers_opaque() && (self.handle_size.1 >= self.track_height) && self.value >= 1.) {
self.track.draw(
ctx.draw,
ctx.paint_target,
(
ctx.layout.position + ctx.measure.size * vec2(0., 0.5 - self.track_height / 2.),
ctx.measure.size * vec2(1., self.track_height),
@ -172,7 +172,7 @@ impl UiElement for Slider {
// if !(self.track_active_color.is_transparent() || (self.value <= 0. && self.handle_color.is_opaque())) {
if !(self.handle.covers_opaque() && (self.handle_size.1 >= self.track_height) && self.value <= 0.) {
self.track_active.draw(
ctx.draw,
ctx.paint_target,
(
ctx.layout.position + ctx.measure.size * vec2(0., 0.5 - self.track_height / 2.),
(ctx.measure.size - handle_size * Vec2::X) * vec2(self.value, self.track_height) + handle_size * Vec2::X / 2.,
@ -193,7 +193,7 @@ impl UiElement for Slider {
// }
if (self.handle_size.0 > 0. && self.handle_size.1 > 0.) {
self.handle.draw(
ctx.draw,
ctx.paint_target,
(
ctx.layout.position +
((ctx.measure.size.x - handle_size.x) * self.value) * Vec2::X +

View file

@ -2,16 +2,17 @@
use std::borrow::Cow;
use derive_setters::Setters;
use glam::Vec4;
use glam::{Affine2, Vec4};
use hui_painter::{
paint::command::{text::{PaintText, TextChunk}, PaintCommand, PaintTransform},
text::FontHandle,
};
use crate::{
draw::UiDrawCommand,
element::{MeasureContext, ProcessContext, UiElement},
layout::{compute_size, Size, Size2d},
measure::Response,
text::FontHandle,
};
//TODO: text fit
// pub enum TextSize {
// FitToWidthRatio(f32),
@ -41,7 +42,7 @@ pub struct Text {
pub font: Option<FontHandle>,
/// Size of the text, in points (these are not pixels)
pub text_size: u16,
pub text_size: f32,
}
impl Default for Text {
@ -51,7 +52,7 @@ impl Default for Text {
size: (Size::Auto, Size::Auto).into(),
color: Vec4::new(1., 1., 1., 1.),
font: None,
text_size: 16,
text_size: 16.,
}
}
}
@ -69,6 +70,19 @@ impl Text {
}
}
impl Text {
fn paint_cmd(&self, current_font: FontHandle) -> PaintText {
PaintText {
text: TextChunk {
text: self.text.clone(),
font: self.font.unwrap_or(current_font),
size: self.text_size as f32,
color: self.color.into(),
}
}
}
}
impl UiElement for Text {
fn name(&self) -> &'static str {
"text"
@ -82,9 +96,13 @@ impl UiElement for Text {
let mut size = (0., 0.);
if matches!(self.size.width, Size::Auto) || matches!(self.size.height, Size::Auto) {
//TODO optimized measure if only one of the sizes is auto
let res = ctx.text_measure.measure(self.font(ctx.current_font), self.text_size, &self.text);
size.0 = res.max_width;
size.1 = res.height;
// let res = ctx.text_measure.measure(self.font(ctx.current_font), self.text_size, &self.text);
// size.0 = res.max_width;
// size.1 = res.height;
let cmd = self.paint_cmd(ctx.current_font);
let cmd_size = cmd.bounds(ctx.painter).size;
size.0 = cmd_size.x;
size.1 = cmd_size.y;
}
Response {
size: compute_size(ctx.layout, self.size, size.into()),
@ -96,12 +114,9 @@ impl UiElement for Text {
if self.text.is_empty() || self.color.w == 0. {
return
}
ctx.draw.add(UiDrawCommand::Text {
text: self.text.clone(),
position: ctx.layout.position,
size: self.text_size,
color: self.color,
font: self.font(ctx.current_font),
ctx.paint_target.add(PaintTransform {
transform: Affine2::from_translation(ctx.layout.position),
child: self.paint_cmd(ctx.current_font),
});
}
}

View file

@ -1,8 +1,10 @@
//! wrapper that allows applying various transformations to an element, such as translation, rotation, or scaling
use glam::{Affine2, Vec2};
use hui_painter::paint::command::{PaintList, PaintTransform};
use crate::{
draw::UiDrawCommand, element::{MeasureContext, ProcessContext, UiElement}, measure::Response
element::{MeasureContext, ProcessContext, UiElement},
measure::Response,
};
pub struct Transformer {
@ -46,20 +48,27 @@ impl UiElement for Transformer {
}
fn process(&self, ctx: ProcessContext) {
ctx.draw.add(UiDrawCommand::PushTransform(self.transform));
//This is stupid:
if self.transform == Affine2::IDENTITY {
self.element.process(ctx);
return;
}
let mut sub_list = PaintList::new_empty();
self.element.process(ProcessContext {
painter: ctx.painter,
measure: ctx.measure,
state: ctx.state,
layout: ctx.layout,
draw: ctx.draw,
text_measure: ctx.text_measure,
paint_target: &mut sub_list,
current_font: ctx.current_font,
images: ctx.images,
input: ctx.input,
signal: ctx.signal,
});
ctx.draw.add(UiDrawCommand::PopTransform);
ctx.paint_target.add(PaintTransform {
transform: self.transform,
child: sub_list,
});
}
}

View file

@ -1,4 +1,5 @@
use super::FontHandle;
use hui_painter::text::FontHandle;
pub struct FontStack {
fonts: Vec<FontHandle>,
@ -7,10 +8,12 @@ pub struct FontStack {
impl FontStack {
pub fn new() -> Self {
Self {
#[cfg(not(feature = "builtin_font"))]
fonts: Vec::new(),
#[cfg(feature = "builtin_font")]
fonts: vec![super::BUILTIN_FONT],
// TODO builtin_font
// #[cfg(not(feature = "builtin_font"))]
// fonts: Vec::new(),
// #[cfg(feature = "builtin_font")]
// fonts: vec![super::BUILTIN_FONT],
}
}
@ -26,7 +29,7 @@ impl FontStack {
self.fonts.last().copied()
}
pub fn current_or_default(&self) -> FontHandle {
self.current().unwrap_or_default()
}
// pub fn current_or_default(&self) -> FontHandle {
// self.current().unwrap_or_default()
// }
}

View file

@ -1,6 +1,7 @@
//! modular procedural background system
use crate::{draw::UiDrawCommandList, rect::Rect};
use crate::rect::Rect;
use hui_painter::paint::command::PaintList;
pub mod point;
mod rect;
@ -9,11 +10,10 @@ pub mod nine_patch;
mod impls;
pub use rect::RectFrame;
/// Trait for a drawable frame
pub trait Frame {
/// Draw the frame at the given rect's position and size
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect);
fn draw(&self, draw: &mut PaintList, rect: Rect);
/// Check if the frame is guaranteed to be fully opaque and fully cover the parent frame regardless of it's size
///

View file

@ -1,21 +1,22 @@
use glam::{Vec3, Vec4};
use glam::{Affine2, Vec3, Vec4};
use hui_painter::{paint::command::{PaintList, PaintRectangle, PaintTransform}, texture::TextureHandle};
use super::Frame;
use crate::{
color,
draw::{ImageHandle, UiDrawCommand, UiDrawCommandList},
rect::{Rect, Corners, FillColor},
};
impl Frame for ImageHandle {
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
draw.add(UiDrawCommand::Rectangle {
position: rect.position,
size: rect.size,
color: color::WHITE.into(),
texture: Some(*self),
texture_uv: None,
rounded_corners: None,
})
impl Frame for TextureHandle {
fn draw(&self, draw: &mut PaintList, rect: Rect) {
draw.add(PaintTransform {
transform: Affine2::from_translation(rect.position),
child: PaintRectangle {
size: rect.size.into(),
color: color::WHITE.into(),
texture: Some(*self),
..Default::default()
},
});
}
fn covers_opaque(&self) -> bool {
@ -24,17 +25,17 @@ impl Frame for ImageHandle {
}
impl Frame for FillColor {
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
fn draw(&self, draw: &mut PaintList, rect: Rect) {
if self.is_transparent() {
return
}
draw.add(UiDrawCommand::Rectangle {
position: rect.position,
size: rect.size,
color: self.corners(),
texture: None,
texture_uv: None,
rounded_corners: None,
draw.add(PaintTransform {
transform: Affine2::from_translation(rect.position),
child: PaintRectangle {
size: rect.size,
color: *self,
..Default::default()
},
})
}
@ -48,7 +49,7 @@ impl Frame for FillColor {
// Corners (RGBA):
impl Frame for Corners<Vec4> {
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
fn draw(&self, draw: &mut PaintList, rect: Rect) {
FillColor::from(*self).draw(draw, rect)
}
fn covers_opaque(&self) -> bool {
@ -57,7 +58,7 @@ impl Frame for Corners<Vec4> {
}
impl Frame for (Vec4, Vec4, Vec4, Vec4) {
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
fn draw(&self, draw: &mut PaintList, rect: Rect) {
FillColor::from(*self).draw(draw, rect)
}
fn covers_opaque(&self) -> bool {
@ -66,7 +67,7 @@ impl Frame for (Vec4, Vec4, Vec4, Vec4) {
}
impl Frame for ((f32, f32, f32, f32), (f32, f32, f32, f32), (f32, f32, f32, f32), (f32, f32, f32, f32)) {
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
fn draw(&self, draw: &mut PaintList, rect: Rect) {
FillColor::from(*self).draw(draw, rect)
}
fn covers_opaque(&self) -> bool {
@ -75,7 +76,7 @@ impl Frame for ((f32, f32, f32, f32), (f32, f32, f32, f32), (f32, f32, f32, f32)
}
impl Frame for [[f32; 4]; 4] {
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
fn draw(&self, draw: &mut PaintList, rect: Rect) {
FillColor::from(*self).draw(draw, rect)
}
fn covers_opaque(&self) -> bool {
@ -86,7 +87,7 @@ impl Frame for [[f32; 4]; 4] {
// Corners (RGB):
impl Frame for Corners<Vec3> {
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
fn draw(&self, draw: &mut PaintList, rect: Rect) {
FillColor::from(*self).draw(draw, rect)
}
fn covers_opaque(&self) -> bool {
@ -95,7 +96,7 @@ impl Frame for Corners<Vec3> {
}
impl Frame for (Vec3, Vec3, Vec3, Vec3) {
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
fn draw(&self, draw: &mut PaintList, rect: Rect) {
FillColor::from(*self).draw(draw, rect)
}
fn covers_opaque(&self) -> bool {
@ -104,7 +105,7 @@ impl Frame for (Vec3, Vec3, Vec3, Vec3) {
}
impl Frame for ((f32, f32, f32), (f32, f32, f32), (f32, f32, f32), (f32, f32, f32)) {
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
fn draw(&self, draw: &mut PaintList, rect: Rect) {
FillColor::from(*self).draw(draw, rect)
}
fn covers_opaque(&self) -> bool {
@ -113,7 +114,7 @@ impl Frame for ((f32, f32, f32), (f32, f32, f32), (f32, f32, f32), (f32, f32, f3
}
impl Frame for [[f32; 3]; 4] {
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
fn draw(&self, draw: &mut PaintList, rect: Rect) {
FillColor::from(*self).draw(draw, rect)
}
fn covers_opaque(&self) -> bool {
@ -124,7 +125,7 @@ impl Frame for [[f32; 3]; 4] {
// RGBA:
impl Frame for Vec4 {
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
fn draw(&self, draw: &mut PaintList, rect: Rect) {
FillColor::from(*self).draw(draw, rect)
}
fn covers_opaque(&self) -> bool {
@ -133,7 +134,7 @@ impl Frame for Vec4 {
}
impl Frame for (f32, f32, f32, f32) {
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
fn draw(&self, draw: &mut PaintList, rect: Rect) {
FillColor::from(*self).draw(draw, rect)
}
fn covers_opaque(&self) -> bool {
@ -142,7 +143,7 @@ impl Frame for (f32, f32, f32, f32) {
}
impl Frame for [f32; 4] {
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
fn draw(&self, draw: &mut PaintList, rect: Rect) {
FillColor::from(*self).draw(draw, rect)
}
fn covers_opaque(&self) -> bool {
@ -153,7 +154,7 @@ impl Frame for [f32; 4] {
// RGB:
impl Frame for Vec3 {
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
fn draw(&self, draw: &mut PaintList, rect: Rect) {
FillColor::from(*self).draw(draw, rect)
}
fn covers_opaque(&self) -> bool {
@ -162,7 +163,7 @@ impl Frame for Vec3 {
}
impl Frame for (f32, f32, f32) {
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
fn draw(&self, draw: &mut PaintList, rect: Rect) {
FillColor::from(*self).draw(draw, rect)
}
fn covers_opaque(&self) -> bool {
@ -171,7 +172,7 @@ impl Frame for (f32, f32, f32) {
}
impl Frame for [f32; 3] {
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
fn draw(&self, draw: &mut PaintList, rect: Rect) {
FillColor::from(*self).draw(draw, rect)
}
fn covers_opaque(&self) -> bool {

View file

@ -3,10 +3,10 @@
//! A 9-patch image is an image that can be scaled in a way that preserves the corners and edges of the image while scaling the center.
//! This is useful for creating scalable UI elements like buttons, windows, etc.
use glam::{vec2, UVec2, Vec2};
use glam::{vec2, Affine2, UVec2, Vec2};
use hui_painter::{paint::command::{PaintList, PaintRectangle, PaintTransform}, texture::TextureHandle};
use crate::{
color,
draw::{ImageHandle, UiDrawCommand, UiDrawCommandList},
rect::{Rect, Corners, FillColor}
};
use super::Frame;
@ -14,7 +14,7 @@ use super::Frame;
/// Represents a 9-patch image asset
#[derive(Clone, Copy, Debug)]
pub struct NinePatchAsset {
pub image: ImageHandle,
pub image: TextureHandle,
//TODO: remove this:
pub size: (u32, u32),
pub scalable_region: Rect,
@ -46,14 +46,14 @@ impl Default for NinePatchFrame {
fn default() -> Self {
Self {
//This is not supposed to be left out as the default, so just set it to whatever :p
asset: NinePatchAsset { image: ImageHandle::default(), size: (0, 0), scalable_region: Rect::default() },
asset: NinePatchAsset { image: TextureHandle::new_broken(), size: (0, 0), scalable_region: Rect::default() },
color: color::WHITE.into(),
}
}
}
impl Frame for NinePatchFrame {
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
fn draw(&self, draw: &mut PaintList, rect: Rect) {
// without this, shїt gets messed up when the position is not a whole number
//XXX: should we round the size as well?
let position = rect.position.round();
@ -100,13 +100,15 @@ impl Frame for NinePatchFrame {
bottom_left: vec2(0., region_uv.top_left.y),
bottom_right: region_uv.top_left,
};
draw.add(UiDrawCommand::Rectangle {
position,
size: vec2(size_h.0, size_v.0),
color: interpolate_color_rect(top_left_patch_uv),
texture: Some(self.asset.image),
texture_uv: Some(top_left_patch_uv),
rounded_corners: None
draw.add(PaintTransform {
transform: Affine2::from_translation(position),
child: PaintRectangle {
size: vec2(size_h.0, size_v.0),
color: interpolate_color_rect(top_left_patch_uv).into(),
texture: Some(self.asset.image),
texture_uv: top_left_patch_uv,
..Default::default()
},
});
//Top patch
@ -116,13 +118,15 @@ impl Frame for NinePatchFrame {
bottom_left: region_uv.top_left,
bottom_right: region_uv.top_right,
};
draw.add(UiDrawCommand::Rectangle {
position: position + vec2(size_h.0, 0.),
size: vec2(size_h.1, size_v.0),
color: interpolate_color_rect(top_patch_uv),
texture: Some(self.asset.image),
texture_uv: Some(top_patch_uv),
rounded_corners: None
draw.add(PaintTransform {
transform: Affine2::from_translation(position + vec2(size_h.0, 0.)),
child: PaintRectangle {
size: vec2(size_h.1, size_v.0),
color: interpolate_color_rect(top_patch_uv).into(),
texture: Some(self.asset.image),
texture_uv: top_patch_uv,
..Default::default()
},
});
//Top-right patch
@ -132,13 +136,15 @@ impl Frame for NinePatchFrame {
bottom_left: region_uv.top_right,
bottom_right: vec2(1., region_uv.top_right.y),
};
draw.add(UiDrawCommand::Rectangle {
position: position + vec2(size_h.0 + size_h.1, 0.),
size: vec2(size_h.2, size_v.0),
color: interpolate_color_rect(top_right_patch_uv),
texture: Some(self.asset.image),
texture_uv: Some(top_right_patch_uv),
rounded_corners: None
draw.add(PaintTransform {
transform: Affine2::from_translation(position + vec2(size_h.0 + size_h.1, 0.)),
child: PaintRectangle {
size: vec2(size_h.2, size_v.0),
color: interpolate_color_rect(top_right_patch_uv).into(),
texture: Some(self.asset.image),
texture_uv: top_right_patch_uv,
..Default::default()
},
});
//Left patch
@ -148,23 +154,27 @@ impl Frame for NinePatchFrame {
bottom_left: vec2(0., region_uv.bottom_left.y),
bottom_right: region_uv.bottom_left,
};
draw.add(UiDrawCommand::Rectangle {
position: position + vec2(0., size_v.0),
size: vec2(size_h.0, size_v.1),
color: interpolate_color_rect(left_patch_uv),
texture: Some(self.asset.image),
texture_uv: Some(left_patch_uv),
rounded_corners: None
draw.add(PaintTransform {
transform: Affine2::from_translation(position + vec2(0., size_v.0)),
child: PaintRectangle {
size: vec2(size_h.0, size_v.1),
color: interpolate_color_rect(left_patch_uv).into(),
texture: Some(self.asset.image),
texture_uv: left_patch_uv,
..Default::default()
},
});
// Center patch
draw.add(UiDrawCommand::Rectangle {
position: position + vec2(size_h.0, size_v.0),
size: vec2(size_h.1, size_v.1),
color: interpolate_color_rect(region_uv),
texture: Some(self.asset.image),
texture_uv: Some(region_uv),
rounded_corners: None
draw.add(PaintTransform {
transform: Affine2::from_translation(position + vec2(size_h.0, size_v.0)),
child: PaintRectangle {
size: vec2(size_h.1, size_v.1),
color: interpolate_color_rect(region_uv).into(),
texture: Some(self.asset.image),
texture_uv: region_uv,
..Default::default()
},
});
//Right patch
@ -174,13 +184,15 @@ impl Frame for NinePatchFrame {
bottom_left: region_uv.bottom_right,
bottom_right: vec2(1., region_uv.bottom_right.y),
};
draw.add(UiDrawCommand::Rectangle {
position: position + vec2(size_h.0 + size_h.1, size_v.0),
size: vec2(size_h.2, size_v.1),
color: interpolate_color_rect(right_patch_uv),
texture: Some(self.asset.image),
texture_uv: Some(right_patch_uv),
rounded_corners: None
draw.add(PaintTransform {
transform: Affine2::from_translation(position + vec2(size_h.0 + size_h.1, size_v.0)),
child: PaintRectangle {
size: vec2(size_h.2, size_v.1),
color: interpolate_color_rect(right_patch_uv).into(),
texture: Some(self.asset.image),
texture_uv: right_patch_uv,
..Default::default()
},
});
//Bottom-left patch
@ -190,13 +202,15 @@ impl Frame for NinePatchFrame {
bottom_left: vec2(0., 1.),
bottom_right: vec2(region_uv.bottom_left.x, 1.),
};
draw.add(UiDrawCommand::Rectangle {
position: position + vec2(0., size_v.0 + size_v.1),
size: vec2(size_h.0, size_v.2),
color: interpolate_color_rect(bottom_left_patch_uv),
texture: Some(self.asset.image),
texture_uv: Some(bottom_left_patch_uv),
rounded_corners: None
draw.add(PaintTransform {
transform: Affine2::from_translation(position + vec2(0., size_v.0 + size_v.1)),
child: PaintRectangle {
size: vec2(size_h.0, size_v.2),
color: interpolate_color_rect(bottom_left_patch_uv).into(),
texture: Some(self.asset.image),
texture_uv: bottom_left_patch_uv,
..Default::default()
},
});
//Bottom patch
@ -206,13 +220,15 @@ impl Frame for NinePatchFrame {
bottom_left: vec2(region_uv.bottom_left.x, 1.),
bottom_right: vec2(region_uv.bottom_right.x, 1.),
};
draw.add(UiDrawCommand::Rectangle {
position: position + vec2(size_h.0, size_v.0 + size_v.1),
size: vec2(size_h.1, size_v.2),
color: interpolate_color_rect(bottom_patch_uv),
texture: Some(self.asset.image),
texture_uv: Some(bottom_patch_uv),
rounded_corners: None
draw.add(PaintTransform {
transform: Affine2::from_translation(position + vec2(size_h.0, size_v.0 + size_v.1)),
child: PaintRectangle {
size: vec2(size_h.1, size_v.2),
color: interpolate_color_rect(bottom_patch_uv).into(),
texture: Some(self.asset.image),
texture_uv: bottom_patch_uv,
..Default::default()
},
});
//Bottom-right patch
@ -222,13 +238,15 @@ impl Frame for NinePatchFrame {
bottom_left: vec2(region_uv.bottom_right.x, 1.),
bottom_right: vec2(1., 1.),
};
draw.add(UiDrawCommand::Rectangle {
position: position + vec2(size_h.0 + size_h.1, size_v.0 + size_v.1),
size: vec2(size_h.2, size_v.2),
color: interpolate_color_rect(bottom_right_patch_uv),
texture: Some(self.asset.image),
texture_uv: Some(bottom_right_patch_uv),
rounded_corners: None
draw.add(PaintTransform {
transform: Affine2::from_translation(position + vec2(size_h.0 + size_h.1, size_v.0 + size_v.1)),
child: PaintRectangle {
size: vec2(size_h.2, size_v.2),
color: interpolate_color_rect(bottom_right_patch_uv).into(),
texture: Some(self.asset.image),
texture_uv: bottom_right_patch_uv,
..Default::default()
},
});
}

View file

@ -1,7 +1,7 @@
use glam::Vec2;
use glam::{Affine2, Vec2};
use hui_painter::{paint::command::{PaintList, PaintRectangle, PaintTransform}, texture::TextureHandle};
use crate::{
color,
draw::{ImageHandle, RoundedCorners, UiDrawCommand, UiDrawCommandList},
rect::{Rect, Corners, FillColor},
};
use super::{Frame, point::FramePoint2d};
@ -23,7 +23,7 @@ pub struct RectFrame {
///
/// Please note that if the background color is NOT set (or set to transparent), the texture will NOT be visible\
/// This is because the texture is multiplied by the color, and if the color is transparent, the texture will be too\
pub image: Option<ImageHandle>,
pub image: Option<TextureHandle>,
/// Top left corner of the rectangle
pub top_left: FramePoint2d,
@ -47,8 +47,8 @@ impl From<FillColor> for RectFrame {
}
}
impl From<ImageHandle> for RectFrame {
fn from(image: ImageHandle) -> Self {
impl From<TextureHandle> for RectFrame {
fn from(image: TextureHandle) -> Self {
Self::image(image)
}
}
@ -65,7 +65,7 @@ impl RectFrame {
/// Create a new [`RectFrame`] with the given image\
///
/// Color will be set to [`WHITE`](crate::color::WHITE) to ensure the image is visible
pub fn image(image: ImageHandle) -> Self {
pub fn image(image: TextureHandle) -> Self {
Self {
color: color::WHITE.into(),
image: Some(image),
@ -74,7 +74,7 @@ impl RectFrame {
}
/// Create a new [`RectFrame`] with the given color and image
pub fn color_image(color: impl Into<FillColor>, image: ImageHandle) -> Self {
pub fn color_image(color: impl Into<FillColor>, image: TextureHandle) -> Self {
Self {
color: color.into(),
image: Some(image),
@ -115,22 +115,21 @@ impl Default for RectFrame {
}
impl Frame for RectFrame {
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
fn draw(&self, draw: &mut PaintList, rect: Rect) {
if self.color.is_transparent() {
return
}
//TODO: handle bottom_right < top_left
let top_left = self.top_left.resolve(rect.size);
let bottom_right = self.bottom_right.resolve(rect.size);
draw.add(UiDrawCommand::Rectangle {
position: rect.position + top_left,
size: bottom_right - top_left,
color: self.color.corners(),
texture: self.image,
texture_uv: None,
rounded_corners: (self.corner_radius.max_f32() > 0.).then_some(
RoundedCorners::from_radius(self.corner_radius)
),
draw.add(PaintTransform{
transform: Affine2::from_translation(rect.position + top_left),
child: PaintRectangle {
size: bottom_right - top_left,
color: self.color,
texture: self.image,
..Default::default()
},
});
}

View file

@ -1,13 +1,14 @@
//! allows stacking two frames on top of each other
use crate::{draw::UiDrawCommandList, rect::Rect};
use hui_painter::paint::command::PaintList;
use crate::rect::Rect;
use super::Frame;
/// A frame that draws two frames on top of each other
pub struct FrameStack(pub Box<dyn Frame>, pub Box<dyn Frame>);
impl Frame for FrameStack {
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect) {
fn draw(&self, draw: &mut PaintList, rect: Rect) {
self.0.draw(draw, rect);
self.1.draw(draw, rect);
}

View file

@ -1,18 +1,17 @@
use hui_painter::{
paint::{buffer::PaintBuffer, command::{PaintCommand, PaintList, PaintRoot}},
text::FontHandle,
texture::{SourceTextureFormat, TextureHandle},
PainterInstance,
};
use crate::{
element::{MeasureContext, ProcessContext, UiElement},
layout::{Direction, LayoutInfo},
text::{FontHandle, TextRenderer},
draw::{
ImageHandle,
TextureFormat,
UiDrawCall,
UiDrawCommandList,
atlas::{TextureAtlasManager, TextureAtlasMeta},
},
signal::{Signal, SignalStore},
event::{EventQueue, UiEvent},
font::FontStack,
input::UiInputState,
layout::{Direction, LayoutInfo},
rect::Rect,
signal::{Signal, SignalStore},
state::StateRepo,
};
@ -21,16 +20,16 @@ use crate::{
/// In most cases, you should only have one instance of this struct, but multiple instances are allowed\
/// (Please note that it's possible to render multiple UI "roots" using a single instance)
pub struct UiInstance {
painter: PainterInstance,
prev_draw_command_hash: Option<u64>,
cur_draw_command_hash: Option<u64>,
draw_commands: PaintList,
paint_buffer: PaintBuffer,
stateful_state: StateRepo,
prev_draw_commands: UiDrawCommandList,
draw_commands: UiDrawCommandList,
draw_call: UiDrawCall,
draw_call_modified: bool,
text_renderer: TextRenderer,
atlas: TextureAtlasManager,
events: EventQueue,
input: UiInputState,
signal: SignalStore,
font_stack: FontStack,
/// True if in the middle of a laying out a frame
state: bool,
}
@ -41,21 +40,13 @@ impl UiInstance {
/// In most cases, you should only do this *once*, during the initialization of your application
pub fn new() -> Self {
UiInstance {
//mouse_position: Vec2::ZERO,
painter: PainterInstance::new(),
prev_draw_command_hash: None,
cur_draw_command_hash: None,
draw_commands: PaintList::default(),
paint_buffer: PaintBuffer::new(),
font_stack: FontStack::new(),
stateful_state: StateRepo::new(),
//event_queue: VecDeque::new(),
// root_elements: Vec::new(),
prev_draw_commands: UiDrawCommandList::default(),
draw_commands: UiDrawCommandList::default(),
draw_call: UiDrawCall::default(),
draw_call_modified: false,
// ftm: FontTextureManager::default(),
text_renderer: TextRenderer::new(),
atlas: {
let mut atlas = TextureAtlasManager::default();
atlas.add_dummy();
atlas
},
events: EventQueue::new(),
input: UiInputState::new(),
signal: SignalStore::new(),
@ -70,8 +61,9 @@ impl UiInstance {
///
/// ## Panics:
/// If the font data is invalid or corrupt
#[deprecated(note = "use painter.fonts_mut().add instead")]
pub fn add_font(&mut self, font: &[u8]) -> FontHandle {
self.text_renderer.add_font_from_bytes(font)
self.painter.fonts_mut().add(font)
}
/// Add an image to the texture atlas\
@ -80,8 +72,10 @@ impl UiInstance {
/// Returns an image handle ([`ImageHandle`])\
/// This handle can be used to reference the texture in draw commands\
/// It's a light reference and can be cloned/copied freely, but will not be cleaned up even when dropped
pub fn add_image(&mut self, format: TextureFormat, data: &[u8], width: usize) -> ImageHandle {
self.atlas.add(width, data, format)
#[deprecated(note = "use painter.textures_mut().atlas_mut().allocate_with_data instead")]
pub fn add_image(&mut self, format: SourceTextureFormat, data: &[u8], width: usize) -> TextureHandle {
// self.atlas().add(width, data, format)
self.painter.textures_mut().allocate_with_data(format, data, width)
}
//TODO better error handling
@ -95,7 +89,8 @@ impl UiInstance {
/// - If the file exists but contains invalid image data\
/// (this will change to a soft error in the future)
#[cfg(feature = "image")]
pub fn add_image_file_path(&mut self, path: impl AsRef<std::path::Path>) -> Result<ImageHandle, std::io::Error> {
#[deprecated]
pub fn add_image_file_path(&mut self, path: impl AsRef<std::path::Path>) -> Result<TextureHandle, std::io::Error> {
use std::io::{Read, Seek};
// Open the file (and wrap it in a bufreader)
@ -115,7 +110,7 @@ impl UiInstance {
//Add the image to the atlas
let handle = self.add_image(
TextureFormat::Rgba,
SourceTextureFormat::RGBA8,
image_rgba,
image.width() as usize
);
@ -129,7 +124,7 @@ impl UiInstance {
/// This function is useful for replacing the default font, use sparingly\
/// (This library attempts to be stateless, however passing the font to every text element is not very practical)
pub fn push_font(&mut self, font: FontHandle) {
self.text_renderer.push_font(font);
self.font_stack.push(font);
}
/// Pop a font from the font stack\
@ -137,12 +132,12 @@ impl UiInstance {
/// ## Panics:
/// If the font stack is empty
pub fn pop_font(&mut self) {
self.text_renderer.pop_font();
self.font_stack.pop();
}
/// Get the current default font
pub fn current_font(&self) -> FontHandle {
self.text_renderer.current_font()
pub fn current_font(&self) -> Option<FontHandle> {
self.font_stack.current()
}
/// Add an element or an element tree to the UI
@ -161,23 +156,23 @@ impl UiInstance {
direction: Direction::Vertical,
remaining_space: None,
};
// TODO handle font_stack.current() font being None
let current_font = self.font_stack.current().expect("No current font");
let measure = element.measure(MeasureContext {
painter: &self.painter,
state: &self.stateful_state,
layout: &layout,
text_measure: self.text_renderer.to_measure(),
current_font: self.text_renderer.current_font(),
images: self.atlas.context(),
current_font,
});
element.process(ProcessContext {
painter: &mut self.painter,
measure: &measure,
state: &mut self.stateful_state,
layout: &layout,
draw: &mut self.draw_commands,
text_measure: self.text_renderer.to_measure(),
current_font: self.text_renderer.current_font(),
images: self.atlas.context(),
paint_target: &mut self.draw_commands,
input: self.input.ctx(),
signal: &mut self.signal,
current_font,
});
}
@ -198,13 +193,19 @@ impl UiInstance {
//then, reset the (remaining) signals
self.signal.clear();
// Compute the hash of the current commands
self.prev_draw_command_hash = Some(self.draw_commands.cache_hash());
// Clear the draw commands
self.draw_commands.clear();
//then, reset the draw commands
std::mem::swap(&mut self.prev_draw_commands, &mut self.draw_commands);
self.draw_commands.commands.clear();
self.draw_call_modified = false;
// std::mem::swap(&mut self.prev_draw_commands, &mut self.draw_commands);
// self.draw_commands.commands.clear();
// self.draw_call_modified = false;
//reset atlas modification flag
self.atlas.reset_modified();
// self.atlas.reset_modified();
}
/// End the frame and prepare the UI for rendering\
@ -219,13 +220,17 @@ impl UiInstance {
self.state = false;
//check if the draw commands have been modified
if self.draw_commands.commands == self.prev_draw_commands.commands {
return
if let Some(prev_hash) = self.prev_draw_command_hash {
let cur_hash = self.draw_commands.cache_hash();
self.cur_draw_command_hash = Some(cur_hash);
if cur_hash == prev_hash {
return
}
}
//if they have, rebuild the draw call and set the modified flag
self.draw_call = UiDrawCall::build(&self.draw_commands, &mut self.atlas, &mut self.text_renderer);
self.draw_call_modified = true;
self.paint_buffer.clear();
self.draw_commands.paint_root(&mut self.painter, &mut self.paint_buffer);
}
/// Get the draw call information for the current frame
@ -233,18 +238,18 @@ impl UiInstance {
/// This function should only be used by the render backend.\
/// You should not call this directly unless you're implementing a custom render backend
///
/// Returns a tuple with a boolean indicating if the buffers have been modified since the last frame
/// Returns a tuple with a unique hash of the draw commands and the draw call data\
///
/// You should only call this function *after* [`UiInstance::end`]\
/// Calling it in the middle of a frame will result in a warning but will not cause a panic\
/// (please note that doing so is probably a mistake and should be fixed in your code)\
/// Doing so anyway will return draw call data for the previous frame, but the `modified` flag will *always* be incorrect until [`UiInstance::end`] is called
/// Doing so anyway will return draw call data for the previous frame
///
pub fn draw_call(&self) -> (bool, &UiDrawCall) {
pub fn backend_paint_buffer(&self) -> (u64, &PaintBuffer) {
if self.state {
log::warn!("UiInstance::draw_call called while in the middle of a frame, this is probably a mistake");
}
(self.draw_call_modified, &self.draw_call)
(self.cur_draw_command_hash.unwrap_or_default(), &self.paint_buffer)
}
/// Get the texture atlas size and data for the current frame
@ -260,11 +265,13 @@ impl UiInstance {
///
/// Make sure to check [`TextureAtlasMeta::modified`] to see if the texture has been modified
/// since the beginning of the current frame before uploading it to the GPU
pub fn atlas(&self) -> TextureAtlasMeta {
pub fn backend_atlas(&self) -> TextureAtlasMeta {
if self.state {
log::warn!("UiInstance::atlas called while in the middle of a frame, this is probably a mistake");
}
self.atlas.meta()
// unimplemented!()
// self.painter.textures_mut().
// self.backend_atlas.meta()
}
/// Push a platform event to the UI event queue

View file

@ -12,6 +12,7 @@
#![allow(unused_parens)]
pub use hui_shared::*;
pub use hui_painter as painter;
mod instance;
mod macros;
@ -19,11 +20,10 @@ pub mod layout;
pub mod element;
pub mod event;
pub mod input;
pub mod draw;
pub mod measure;
pub mod state;
pub mod text;
pub mod signal;
pub mod frame;
pub mod font;
pub use instance::UiInstance;

View file

@ -1,106 +0,0 @@
//! text rendering, styling, measuring
use std::sync::Arc;
use fontdue::{Font, FontSettings};
use crate::draw::atlas::TextureAtlasManager;
mod font;
mod ftm;
mod stack;
/// Built-in font handle
#[cfg(feature="builtin_font")]
pub use font::BUILTIN_FONT;
pub use font::FontHandle;
use font::FontManager;
use ftm::FontTextureManager;
use ftm::GlyphCacheEntry;
use stack::FontStack;
pub(crate) struct TextRenderer {
manager: FontManager,
ftm: FontTextureManager,
stack: FontStack,
}
impl TextRenderer {
pub fn new() -> Self {
Self {
manager: FontManager::new(),
ftm: FontTextureManager::default(),
stack: FontStack::new(),
}
}
pub fn add_font_from_bytes(&mut self, font: &[u8]) -> FontHandle {
self.manager.add_font(Font::from_bytes(font, FontSettings::default()).unwrap())
}
pub fn glyph(&mut self, atlas: &mut TextureAtlasManager, font_handle: FontHandle, character: char, size: u8) -> Arc<GlyphCacheEntry> {
self.ftm.glyph(atlas, &self.manager, font_handle, character, size)
}
pub fn push_font(&mut self, font: FontHandle) {
self.stack.push(font);
}
pub fn pop_font(&mut self) {
self.stack.pop();
}
pub fn current_font(&self) -> FontHandle {
self.stack.current_or_default()
}
pub(crate) fn internal_font(&self, handle: FontHandle) -> &Font {
self.manager.get(handle).unwrap()
}
}
impl Default for TextRenderer {
fn default() -> Self {
Self::new()
}
}
/// Size of measured text
pub struct TextMeasureResponse {
pub max_width: f32,
pub height: f32,
}
/// Context for measuring text
#[derive(Clone, Copy)]
pub struct TextMeasure<'a>(&'a TextRenderer);
impl TextMeasure<'_> {
/// Measure the given string of text with the given font and size
pub fn measure(&self, font: FontHandle, size: u16, text: &str) -> TextMeasureResponse {
use fontdue::layout::{Layout, CoordinateSystem, TextStyle};
let mut layout = Layout::new(CoordinateSystem::PositiveYDown);
layout.append(
&[self.0.internal_font(font)],
&TextStyle::new(text, size as f32, 0)
);
TextMeasureResponse {
max_width: layout.lines().map(|lines| {
lines.iter().fold(0.0_f32, |acc, x| {
let glyph = layout.glyphs().get(x.glyph_end).unwrap();
acc.max(glyph.x + glyph.width as f32)
})
}).unwrap_or(0.),
height: layout.height(),
}
}
}
impl TextRenderer {
pub fn to_measure(&self) -> TextMeasure {
TextMeasure(self)
}
pub fn measure(&self, font: FontHandle, size: u16, text: &str) -> TextMeasureResponse {
TextMeasure(self).measure(font, size, text)
}
}

View file

@ -1,65 +0,0 @@
use fontdue::Font;
/// Font handle, stores the internal font id and can be cheaply copied.
///
/// Only valid for the `UiInstance` that created it.\
/// Using it with other instances may result in panics or unexpected behavior.
///
/// Handle values are not guaranteed to be valid.\
/// Creating or transmuting an invalid handle is allowed and is *not* UB.
///
/// Internal value is an implementation detail and should not be relied upon.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct FontHandle(pub(crate) usize);
#[cfg(feature = "builtin_font")]
pub const BUILTIN_FONT: FontHandle = FontHandle(0);
impl Default for FontHandle {
/// Default font handle is the builtin font, if the feature is enabled;\
/// Otherwise returns an invalid handle.
fn default() -> Self {
#[cfg(feature = "builtin_font")] { BUILTIN_FONT }
#[cfg(not(feature = "builtin_font"))] { Self(usize::MAX) }
}
}
#[cfg(feature = "builtin_font")]
const BUILTIN_FONT_DATA: &[u8] = include_bytes!("../../assets/font/ProggyTiny.ttf");
pub struct FontManager {
fonts: Vec<Font>,
}
impl FontManager {
pub fn new() -> Self {
let mut this = Self {
fonts: Vec::new(),
};
#[cfg(feature = "builtin_font")]
{
let font = Font::from_bytes(
BUILTIN_FONT_DATA,
fontdue::FontSettings::default()
).unwrap();
this.add_font(font);
};
this
}
/// Add a (fontdue) font to the renderer.
pub fn add_font(&mut self, font: Font) -> FontHandle {
self.fonts.push(font);
FontHandle(self.fonts.len() - 1)
}
pub fn get(&self, handle: FontHandle) -> Option<&Font> {
self.fonts.get(handle.0)
}
}
impl Default for FontManager {
fn default() -> Self {
Self::new()
}
}

View file

@ -1,74 +0,0 @@
use std::sync::Arc;
use fontdue::Metrics;
use hashbrown::HashMap;
use crate::draw::atlas::{TextureAtlasManager, ImageHandle};
use super::font::{FontHandle, FontManager};
#[derive(PartialEq, Eq, Hash)]
struct GlyphCacheKey {
font_index: usize,
character: char,
size: u8,
}
pub struct GlyphCacheEntry {
pub metrics: Metrics,
pub texture: ImageHandle,
}
pub struct FontTextureManager {
glyph_cache: HashMap<GlyphCacheKey, Arc<GlyphCacheEntry>>
}
impl FontTextureManager {
pub fn new() -> Self {
FontTextureManager {
glyph_cache: HashMap::new(),
}
}
/// Either looks up the glyph in the cache or renders it and adds it to the cache.
pub fn glyph(
&mut self,
atlas: &mut TextureAtlasManager,
font_manager: &FontManager,
font_handle: FontHandle,
character: char,
size: u8
) -> Arc<GlyphCacheEntry> {
let key = GlyphCacheKey {
font_index: font_handle.0,
character,
size,
};
if let Some(entry) = self.glyph_cache.get(&key) {
return Arc::clone(entry);
}
let font = font_manager.get(font_handle).unwrap();
let (metrics, bitmap) = font.rasterize(character, size as f32);
log::trace!("rasterized glyph: {}, {:?}, {:?}", character, metrics, bitmap);
let texture = atlas.add_grayscale(metrics.width, &bitmap);
let entry = Arc::new(GlyphCacheEntry {
metrics,
texture
});
unsafe {
self.glyph_cache.insert_unique_unchecked(key, Arc::clone(&entry));
}
entry
}
// pub fn glyph(&mut self, font_manager: &FontManager, font_handle: FontHandle, character: char, size: u8) -> Arc<GlyphCacheEntry> {
// let (is_new, glyph) = self.glyph_allocate(font_manager, font_handle, character, size);
// if is_new {
// self.glyph_place(&glyph);
// self.modified = true;
// }
// glyph
// }
}
impl Default for FontTextureManager {
fn default() -> Self { Self::new() }
}