kinda works now...

This commit is contained in:
griffi-gh 2024-02-25 04:02:10 +01:00
parent ae26a4d933
commit cdba2fedd8
12 changed files with 183 additions and 195 deletions

View file

@ -6,7 +6,7 @@ use winit::{
event_loop::{EventLoopBuilder, ControlFlow} event_loop::{EventLoopBuilder, ControlFlow}
}; };
use hui::{ use hui::{
UiInstance, elements, UiInstance,
layout::{Alignment, UiDirection, UiSize}, layout::{Alignment, UiDirection, UiSize},
rectangle::{Corners, Sides}, rectangle::{Corners, Sides},
element::{ element::{
@ -17,6 +17,12 @@ use hui::{
}; };
use hui_glium::GliumUiRenderer; use hui_glium::GliumUiRenderer;
fn elements(mut f: impl FnMut(&mut Vec<Box<dyn hui::element::UiElement>>)) -> Vec<Box<dyn hui::element::UiElement>> {
let mut e = vec![];
f(&mut e);
e
}
fn main() { fn main() {
kubi_logging::init(); kubi_logging::init();
@ -27,7 +33,7 @@ fn main() {
let mut hui = UiInstance::new(); let mut hui = UiInstance::new();
let mut backend = GliumUiRenderer::new(&display); let mut backend = GliumUiRenderer::new(&display);
let font_handle = hui.add_font_from_bytes(include_bytes!("../assets/roboto/Roboto-Regular.ttf")); let font_handle = hui.add_font(include_bytes!("../assets/roboto/Roboto-Regular.ttf"));
let instant = Instant::now(); let instant = Instant::now();
@ -59,18 +65,18 @@ fn main() {
corner_radius: Corners::all(8.), corner_radius: Corners::all(8.),
elements: elements(|el| { elements: elements(|el| {
if instant.elapsed().as_secs_f32() < 5. { if instant.elapsed().as_secs_f32() < 5. {
el.add(Text { el.push(Box::new(Text {
text: "Downloading your mom...".into(), text: "Downloading your mom...".into(),
font: font_handle, font: font_handle,
text_size: 24, text_size: 24,
..Default::default() ..Default::default()
}); }));
el.add(ProgressBar { el.push(Box::new(ProgressBar {
value: mom_ratio, value: mom_ratio,
corner_radius: Corners::all(0.125 * ProgressBar::DEFAULT_HEIGHT), corner_radius: Corners::all(0.125 * ProgressBar::DEFAULT_HEIGHT),
..Default::default() ..Default::default()
}); }));
el.add(Container { el.push(Box::new(Container {
direction: UiDirection::Horizontal, direction: UiDirection::Horizontal,
align: (Alignment::End, Alignment::Center).into(), align: (Alignment::End, Alignment::Center).into(),
size: (UiSize::Fraction(1.), UiSize::Auto), size: (UiSize::Fraction(1.), UiSize::Auto),
@ -81,21 +87,21 @@ fn main() {
..Default::default() ..Default::default()
})], })],
..Default::default() ..Default::default()
}); }));
} else if instant.elapsed().as_secs() < 10 { } else if instant.elapsed().as_secs() < 10 {
el.add(Text { el.push(Box::new(Text {
text: "Error 413: Request Entity Too Large".into(), text: "Error 413: Request Entity Too Large".into(),
font: font_handle, font: font_handle,
color: vec4(1., 0.125, 0.125, 1.), color: vec4(1., 0.125, 0.125, 1.),
text_size: 20, text_size: 20,
..Default::default() ..Default::default()
}); }));
el.add(Text { el.push(Box::new(Text {
text: format!("Exiting in {}...", 10 - instant.elapsed().as_secs()).into(), text: format!("Exiting in {}...", 10 - instant.elapsed().as_secs()).into(),
font: font_handle, font: font_handle,
text_size: 16, text_size: 16,
..Default::default() ..Default::default()
}); }));
} else { } else {
window_target.exit(); window_target.exit();
} }

View file

@ -6,7 +6,7 @@ use winit::{
event_loop::{EventLoopBuilder, ControlFlow} event_loop::{EventLoopBuilder, ControlFlow}
}; };
use hui::{ use hui::{
UiInstance, elements, UiInstance,
layout::UiSize, layout::UiSize,
element::{ element::{
container::Container, container::Container,
@ -15,6 +15,12 @@ use hui::{
}; };
use hui_glium::GliumUiRenderer; use hui_glium::GliumUiRenderer;
fn elements(mut f: impl FnMut(&mut Vec<Box<dyn hui::element::UiElement>>)) -> Vec<Box<dyn hui::element::UiElement>> {
let mut e = vec![];
f(&mut e);
e
}
fn main() { fn main() {
kubi_logging::init(); kubi_logging::init();
@ -25,7 +31,7 @@ fn main() {
let mut hui = UiInstance::new(); let mut hui = UiInstance::new();
let mut backend = GliumUiRenderer::new(&display); let mut backend = GliumUiRenderer::new(&display);
let font_handle = hui.add_font_from_bytes(include_bytes!("../assets/roboto/Roboto-Regular.ttf")); let font_handle = hui.add_font(include_bytes!("../assets/roboto/Roboto-Regular.ttf"));
let instant = Instant::now(); let instant = Instant::now();
event_loop.run(|event, window_target| { event_loop.run(|event, window_target| {
@ -46,56 +52,56 @@ fn main() {
size: (UiSize::Fraction(1.), UiSize::Fraction(1.)), size: (UiSize::Fraction(1.), UiSize::Fraction(1.)),
background: vec4(0.1, 0.1, 0.1, 1.).into(), background: vec4(0.1, 0.1, 0.1, 1.).into(),
elements: elements(|elem| { elements: elements(|elem| {
elem.add(Text { elem.push(Box::new(Text {
text: "THIS LINE SHOULD BE SHARP!".into(), text: "THIS LINE SHOULD BE SHARP!".into(),
..Default::default() ..Default::default()
}); }));
elem.add(Text { elem.push(Box::new(Text {
text: "THIS LINE SHOULD BE SHARP!".into(), text: "THIS LINE SHOULD BE SHARP!".into(),
text_size: 32, text_size: 32,
..Default::default() ..Default::default()
}); }));
elem.add(Text { elem.push(Box::new(Text {
text: "All lines except 3 and 6 below will be blurry:".into(), text: "All lines except 3 and 6 below will be blurry:".into(),
..Default::default() ..Default::default()
}); }));
for size in [9, 12, 16, 18, 24, 32] { for size in [9, 12, 16, 18, 24, 32] {
elem.add(Text { elem.push(Box::new(Text {
text: "Testing default font, Proggy Tiny".into(), text: "Testing default font, Proggy Tiny".into(),
text_size: size, text_size: size,
..Default::default() ..Default::default()
}); }));
} }
elem.add(Rect { elem.push(Box::new(Rect {
size: (UiSize::Fraction(1.), UiSize::Static(10.)), size: (UiSize::Fraction(1.), UiSize::Static(10.)),
color: vec4(0., 0., 1., 1.).into(), color: vec4(0., 0., 1., 1.).into(),
}); }));
elem.add(Rect { elem.push(Box::new(Rect {
size: (UiSize::Fraction(1.), UiSize::Static(10.)), size: (UiSize::Fraction(1.), UiSize::Static(10.)),
color: vec4(1., 1., 0., 1.).into(), color: vec4(1., 1., 0., 1.).into(),
}); }));
elem.add(Text { elem.push(Box::new(Text {
text: "Hello, world!\nżółty liść. życie nie ma sensu i wszyscy zginemy;\nтест кирилиці їїїїїїїїїїї\njapanese text: テスト".into(), text: "Hello, world!\nżółty liść. życie nie ma sensu i wszyscy zginemy;\nтест кирилиці їїїїїїїїїїї\njapanese text: テスト".into(),
font: font_handle, font: font_handle,
text_size: 32, text_size: 32,
..Default::default() ..Default::default()
}); }));
if instant.elapsed().as_secs() & 1 != 0 { if instant.elapsed().as_secs() & 1 != 0 {
elem.add(Rect { elem.push(Box::new(Rect {
size: (UiSize::Fraction(1.), UiSize::Static(10.)), size: (UiSize::Fraction(1.), UiSize::Static(10.)),
color: vec4(1., 0., 0., 1.).into(), color: vec4(1., 0., 0., 1.).into(),
}); }));
elem.add(Rect { elem.push(Box::new(Rect {
size: (UiSize::Fraction(1.), UiSize::Static(10.)), size: (UiSize::Fraction(1.), UiSize::Static(10.)),
color: vec4(0., 0., 0., 1.).into(), color: vec4(0., 0., 0., 1.).into(),
}); }));
elem.add(Spacer(100.)); elem.push(Box::new(Spacer(100.)));
elem.add(Text { elem.push(Box::new(Text {
text: "FLAG SHOULD NOT OVERLAP WITH TEXT".into(), text: "FLAG SHOULD NOT OVERLAP WITH TEXT".into(),
text_size: 64, text_size: 64,
color: vec4(1., 0., 1., 1.), color: vec4(1., 0., 1., 1.),
..Default::default() ..Default::default()
}); }));
} }
}), }),
..Default::default() ..Default::default()

View file

@ -6,7 +6,8 @@ precision highp sampler2D;
out vec4 out_color; out vec4 out_color;
in vec4 vtx_color; in vec4 vtx_color;
in vec2 vtx_uv; in vec2 vtx_uv;
uniform sampler2D tex;
void main() { void main() {
out_color = vtx_color; out_color = texture(tex, vtx_uv) * vtx_color;
} }

View file

@ -1,13 +0,0 @@
#version 300 es
precision highp float;
precision highp sampler2D;
out vec4 out_color;
in vec4 vtx_color;
in vec2 vtx_uv;
uniform sampler2D tex;
void main() {
out_color = texture(tex, vtx_uv) * vtx_color;
}

View file

@ -10,14 +10,11 @@ use glium::{
uniform, uniforms::{Sampler, SamplerBehavior, SamplerWrapFunction}, uniform, uniforms::{Sampler, SamplerBehavior, SamplerWrapFunction},
}; };
use hui::{ use hui::{
UiInstance, draw::{TextureAtlasMeta, UiDrawCall, UiVertex}, UiInstance
draw::{UiDrawCall, UiVertex, BindTexture},
text::FontTextureInfo, IfModified,
}; };
const VERTEX_SHADER: &str = include_str!("../shaders/vertex.vert"); const VERTEX_SHADER: &str = include_str!("../shaders/vertex.vert");
const FRAGMENT_SHADER: &str = include_str!("../shaders/fragment.frag"); const FRAGMENT_SHADER: &str = include_str!("../shaders/fragment.frag");
const FRAGMENT_SHADER_TEX: &str = include_str!("../shaders/fragment_tex.frag");
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
#[repr(C)] #[repr(C)]
@ -119,7 +116,6 @@ impl BufferPair {
pub struct GliumUiRenderer { pub struct GliumUiRenderer {
context: Rc<Context>, context: Rc<Context>,
program: glium::Program, program: glium::Program,
program_tex: glium::Program,
ui_texture: Option<Rc<SrgbTexture2d>>, ui_texture: Option<Rc<SrgbTexture2d>>,
buffer_pair: Option<BufferPair>, buffer_pair: Option<BufferPair>,
} }
@ -129,7 +125,6 @@ impl GliumUiRenderer {
log::info!("initializing hui glium backend"); log::info!("initializing hui glium backend");
Self { Self {
program: Program::from_source(facade, VERTEX_SHADER, FRAGMENT_SHADER, None).unwrap(), program: Program::from_source(facade, VERTEX_SHADER, FRAGMENT_SHADER, None).unwrap(),
program_tex: Program::from_source(facade, VERTEX_SHADER, FRAGMENT_SHADER_TEX, None).unwrap(),
context: Rc::clone(facade.get_context()), context: Rc::clone(facade.get_context()),
ui_texture: None, ui_texture: None,
buffer_pair: None, buffer_pair: None,
@ -144,34 +139,25 @@ impl GliumUiRenderer {
} else if !call.indices.is_empty() { } else if !call.indices.is_empty() {
self.buffer_pair = Some(BufferPair::new_with_data(&self.context, data_vtx, data_idx)); self.buffer_pair = Some(BufferPair::new_with_data(&self.context, data_vtx, data_idx));
} }
// self.plan[0].bind_texture = match call.bind_texture {
// Some(BindTexture::FontTexture) => {
// const NO_FNT_TEX: &str = "Font texture exists in draw plan but not yet inited. Make sure to call update_font_texture() *before* update_draw_plan()";
// Some(Rc::clone(self.font_texture.as_ref().expect(NO_FNT_TEX)))
// },
// Some(BindTexture::UserDefined(_)) => todo!("user defined textures are not implemented yet"),
// None => None,
// }
} }
pub fn update_ui_texture(&mut self, font_texture: &FontTextureInfo) { pub fn update_texture_atlas(&mut self, atlas: &TextureAtlasMeta) {
log::debug!("updating font texture"); log::trace!("updating ui atlas texture");
self.ui_texture = Some(Rc::new(SrgbTexture2d::new( self.ui_texture = Some(Rc::new(SrgbTexture2d::new(
&self.context, &self.context,
RawImage2d::from_raw_rgba( RawImage2d::from_raw_rgba(
font_texture.data.to_owned(), atlas.data.to_owned(),
(font_texture.size.x, font_texture.size.y) (atlas.size.x, atlas.size.y)
) )
).unwrap())); ).unwrap()));
} }
pub fn update(&mut self, hui: &UiInstance) { pub fn update(&mut self, hui: &UiInstance) {
if let Some(texture) = hui.font_texture().if_modified() { if hui.atlas().modified {
self.update_ui_texture(texture); self.update_texture_atlas(&hui.atlas());
} }
if let Some(plan) = hui.draw_call().if_modified() { if hui.draw_call().0 {
self.update_draw_plan(plan); self.update_draw_plan(hui.draw_call().1);
} }
} }
@ -192,7 +178,7 @@ impl GliumUiRenderer {
frame.draw( frame.draw(
vtx_buffer, vtx_buffer,
idx_buffer, idx_buffer,
&self.program_tex, &self.program,
&uniform! { &uniform! {
resolution: resolution.to_array(), resolution: resolution.to_array(),
tex: Sampler(self.ui_texture.as_ref().unwrap().as_ref(), SamplerBehavior { tex: Sampler(self.ui_texture.as_ref().unwrap().as_ref(), SamplerBehavior {
@ -202,20 +188,6 @@ impl GliumUiRenderer {
}, },
&params, &params,
).unwrap(); ).unwrap();
// if let Some(bind_texture) = call.bind_texture.as_ref() {
// } else {
// frame.draw(
// vtx_buffer,
// idx_buffer,
// &self.program,
// &uniform! {
// resolution: resolution.to_array(),
// },
// &params,
// ).unwrap();
// }
} }
} }
} }

View file

@ -2,13 +2,12 @@
use crate::{ use crate::{
rectangle::Corners, rectangle::Corners,
text::{FontHandle, TextRenderer}, text::{FontHandle, TextRenderer}
IfModified
}; };
pub(crate) mod atlas; pub(crate) mod atlas;
//pub(crate) use atlas::{TextureAllocation, TextureAtlasManager}; use atlas::TextureAtlasManager;
//pub use atlas::TextureHandle; pub use atlas::{TextureHandle, TextureAtlasMeta};
mod corner_radius; mod corner_radius;
pub use corner_radius::RoundedCorners; pub use corner_radius::RoundedCorners;
@ -48,7 +47,7 @@ pub enum UiDrawCommand {
///Position in pixels ///Position in pixels
position: Vec2, position: Vec2,
///Font size ///Font size
size: u8, size: u16,
///Color (RGBA) ///Color (RGBA)
color: Vec4, color: Vec4,
///Text to draw ///Text to draw
@ -78,19 +77,6 @@ impl UiDrawCommandList {
// } // }
// } // }
/// Texture to bind for a draw call
///
/// - FontTexture: The internally managed font texture
/// - UserDefined: User-defined texture, value is user-defined and usually depends on the render backend
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum BindTexture {
/// The internally managed font texture
FontTexture,
/// User-defined texture, value is user-defined and usually depends on the render backend
UserDefined(usize),
}
/// A vertex for UI rendering /// A vertex for UI rendering
#[derive(Clone, Copy, Debug, PartialEq, Default)] #[derive(Clone, Copy, Debug, PartialEq, Default)]
pub struct UiVertex { pub struct UiVertex {
@ -108,7 +94,7 @@ pub struct UiDrawCall {
impl UiDrawCall { impl UiDrawCall {
/// Tesselate the UI and build a complete draw plan from a list of draw commands /// Tesselate the UI and build a complete draw plan from a list of draw commands
pub fn build(draw_commands: &UiDrawCommandList, tr: &mut TextRenderer) -> Self { pub(crate) fn build(draw_commands: &UiDrawCommandList, atlas: &mut TextureAtlasManager, text_renderer: &mut TextRenderer) -> Self {
let mut draw_call = UiDrawCall::default(); let mut draw_call = UiDrawCall::default();
for command in &draw_commands.commands { for command in &draw_commands.commands {
match command { match command {
@ -209,17 +195,17 @@ impl UiDrawCall {
UiVertex { UiVertex {
position: *position + vec2(size.x, 0.0), position: *position + vec2(size.x, 0.0),
color: color.top_right, color: color.top_right,
uv: vec2(1.0, 0.0), uv: vec2(0.0, 0.0), // vec2(1.0, 0.0),
}, },
UiVertex { UiVertex {
position: *position + *size, position: *position + *size,
color: color.bottom_right, color: color.bottom_right,
uv: vec2(1.0, 1.0), uv: vec2(0.0, 0.0), // vec2(1.0, 1.0),
}, },
UiVertex { UiVertex {
position: *position + vec2(0.0, size.y), position: *position + vec2(0.0, size.y),
color: color.bottom_left, color: color.bottom_left,
uv: vec2(0.0, 1.0), uv: vec2(0.0, 0.0), // vec2(0.0, 1.0),
}, },
]); ]);
} }
@ -227,7 +213,7 @@ impl UiDrawCall {
UiDrawCommand::Circle { .. } => { UiDrawCommand::Circle { .. } => {
todo!("circle draw command not implemented yet") todo!("circle draw command not implemented yet")
}, },
UiDrawCommand::Text { position, size, color, text, font } => { UiDrawCommand::Text { position, size, color, text, font: font_handle } => {
if text.is_empty() { if text.is_empty() {
continue continue
} }
@ -235,7 +221,7 @@ impl UiDrawCall {
//XXX: should we be doing this every time? //XXX: should we be doing this every time?
let mut layout = Layout::new(CoordinateSystem::PositiveYDown); let mut layout = Layout::new(CoordinateSystem::PositiveYDown);
layout.append( layout.append(
&[tr.internal_font(*font)], &[text_renderer.internal_font(*font_handle)],
&TextStyle::new(text, *size as f32, 0) &TextStyle::new(text, *size as f32, 0)
); );
let glyphs = layout.glyphs(); let glyphs = layout.glyphs();
@ -245,38 +231,32 @@ impl UiDrawCall {
if !layout_glyph.char_data.rasterize() { if !layout_glyph.char_data.rasterize() {
continue continue
} }
let font_texture_size = ( let atlas_size = atlas.meta().size.as_vec2();
tr.font_texture().size.x as f32,
tr.font_texture().size.y as f32
);
let vidx = draw_call.vertices.len() as u32; let vidx = draw_call.vertices.len() as u32;
let glyph = tr.glyph(*font, layout_glyph.parent, layout_glyph.key.px as u8); let glyph = text_renderer.glyph(atlas, *font_handle, layout_glyph.parent, layout_glyph.key.px as u8);
let uv = atlas.get_uv(glyph.texture);
//rpos_x += glyph.metrics.advance_width;//glyph.metrics.advance_width; //rpos_x += glyph.metrics.advance_width;//glyph.metrics.advance_width;
draw_call.indices.extend([vidx, vidx + 1, vidx + 2, vidx, vidx + 2, vidx + 3]); draw_call.indices.extend([vidx, vidx + 1, vidx + 2, vidx, vidx + 2, vidx + 3]);
let p0x = glyph.position.x as f32 / font_texture_size.0;
let p1x = (glyph.position.x + glyph.size.x as i32) as f32 / font_texture_size.0;
let p0y = glyph.position.y as f32 / font_texture_size.1;
let p1y = (glyph.position.y + glyph.size.y as i32) as f32 / font_texture_size.1;
draw_call.vertices.extend([ draw_call.vertices.extend([
UiVertex { UiVertex {
position: *position + vec2(layout_glyph.x, layout_glyph.y), position: *position + vec2(layout_glyph.x, layout_glyph.y),
color: *color, color: *color,
uv: vec2(p0x, p0y), uv: uv.top_left,
}, },
UiVertex { UiVertex {
position: *position + vec2(layout_glyph.x + glyph.metrics.width as f32, layout_glyph.y), position: *position + vec2(layout_glyph.x + glyph.metrics.width as f32, layout_glyph.y),
color: *color, color: *color,
uv: vec2(p1x, p0y), uv: uv.top_right,
}, },
UiVertex { UiVertex {
position: *position + vec2(layout_glyph.x + glyph.metrics.width as f32, layout_glyph.y + glyph.metrics.height as f32), position: *position + vec2(layout_glyph.x + glyph.metrics.width as f32, layout_glyph.y + glyph.metrics.height as f32),
color: *color, color: *color,
uv: vec2(p1x, p1y), uv: uv.bottom_right,
}, },
UiVertex { UiVertex {
position: *position + vec2(layout_glyph.x, layout_glyph.y + glyph.metrics.height as f32), position: *position + vec2(layout_glyph.x, layout_glyph.y + glyph.metrics.height as f32),
color: *color, color: *color,
uv: vec2(p0x, p1y), uv: uv.bottom_left,
}, },
]); ]);
#[cfg(all( #[cfg(all(
@ -298,13 +278,3 @@ impl UiDrawCall {
draw_call draw_call
} }
} }
#[allow(deprecated)]
impl IfModified<UiDrawCall> for (bool, &UiDrawCall) {
fn if_modified(&self) -> Option<&UiDrawCall> {
match self.0 {
true => Some(self.1),
false => None,
}
}
}

View file

@ -1,14 +1,19 @@
use glam::{uvec2, UVec2}; use glam::{uvec2, vec2, UVec2, Vec2};
use hashbrown::HashMap; use hashbrown::HashMap;
use nohash_hasher::BuildNoHashHasher; use nohash_hasher::BuildNoHashHasher;
use rect_packer::DensePacker; use rect_packer::DensePacker;
use crate::rectangle::Corners;
use crate::IfModified;
const CHANNEL_COUNT: u32 = 4; const CHANNEL_COUNT: u32 = 4;
//TODO: make this work //TODO: make this work
const ALLOW_ROTATION: bool = false; const ALLOW_ROTATION: bool = false;
pub struct TextureAtlasMeta<'a> {
pub data: &'a [u8],
pub size: UVec2,
pub modified: bool,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub struct TextureHandle { pub struct TextureHandle {
//TODO automatic cleanup when handle is dropped //TODO automatic cleanup when handle is dropped
@ -22,13 +27,13 @@ pub(crate) struct TextureAllocation {
pub index: u32, pub index: u32,
/// Position in the texture atlas /// Position in the texture atlas
pub position: UVec2, pub(crate) position: UVec2,
/// Requested texture size /// Requested texture size
pub size: UVec2, pub size: UVec2,
/// True if the texture was rotated by 90 degrees /// True if the texture was rotated by 90 degrees
pub rotated: bool, pub(crate) rotated: bool,
} }
/// Manages a texture atlas and the allocation of space within it\ /// Manages a texture atlas and the allocation of space within it\
@ -39,25 +44,35 @@ pub(crate) struct TextureAtlasManager {
size: UVec2, size: UVec2,
data: Vec<u8>, data: Vec<u8>,
allocations: HashMap<u32, TextureAllocation, BuildNoHashHasher<u32>>, allocations: HashMap<u32, TextureAllocation, BuildNoHashHasher<u32>>,
/// True if the atlas has been modified in a way which requires a texture reupload
/// since the beginning of the current frame
modified: bool, modified: bool,
} }
impl TextureAtlasManager { impl TextureAtlasManager {
/// Create a new texture atlas with the specified size\ /// 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 /// 512x512 is a good default size for most applications, and the texture atlas can grow dynamically as needed
/// By default, the texture atlas gets initialized with a single white pixel texture
pub fn new(size: UVec2) -> Self { pub fn new(size: UVec2) -> Self {
Self { let mut tmp = Self {
packer: DensePacker::new(size.x as i32, size.y as i32), packer: DensePacker::new(size.x as i32, size.y as i32),
count: 0, count: 0,
size: UVec2::new(0, 0), size,
data: vec![0; (size.x * size.y * CHANNEL_COUNT) as usize], data: vec![0; (size.x * size.y * CHANNEL_COUNT) as usize],
allocations: HashMap::default(), allocations: HashMap::default(),
modified: true, modified: true,
} };
tmp.add(1, &[255,255,255,255]);
tmp
} }
/// Resize the texture atlas to the new size in-place, preserving the existing data /// Resize the texture atlas to the new size in-place, preserving the existing data
pub fn resize(&mut self, new_size: UVec2) { pub fn resize(&mut self, new_size: UVec2) {
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{ 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); self.packer.resize(new_size.x as i32, new_size.y as i32);
//Resize the data array in-place //Resize the data array in-place
@ -84,6 +99,7 @@ impl TextureAtlasManager {
/// Use `allocate` to allocate a texture and resize the atlas if necessary\ /// Use `allocate` to allocate a texture and resize the atlas if necessary\
/// Does not modify the texture data /// Does not modify the texture data
fn try_allocate(&mut self, size: UVec2) -> Option<TextureHandle> { fn try_allocate(&mut self, size: UVec2) -> Option<TextureHandle> {
log::trace!("Allocating texture of size {:?}", size);
let result = self.packer.pack(size.x as i32, size.y as i32, ALLOW_ROTATION)?; let result = self.packer.pack(size.x as i32, size.y as i32, ALLOW_ROTATION)?;
let index = self.count; let index = self.count;
self.count += 1; self.count += 1;
@ -107,7 +123,9 @@ impl TextureAtlasManager {
new_size *= 2; new_size *= 2;
self.packer.resize(new_size.x as i32, new_size.y as i32); self.packer.resize(new_size.x as i32, new_size.y as i32);
} }
if new_size != self.size {
self.resize(new_size); self.resize(new_size);
}
self.try_allocate(size).unwrap() self.try_allocate(size).unwrap()
} }
@ -115,8 +133,8 @@ impl TextureAtlasManager {
/// This function may resize the atlas as needed, and should never fail under normal circumstances. /// This function may resize the atlas as needed, and should never fail under normal circumstances.
pub fn add(&mut self, width: usize, data: &[u8]) -> TextureHandle { pub fn add(&mut self, width: usize, data: &[u8]) -> TextureHandle {
let size = uvec2(width as u32, (data.len() / (width * CHANNEL_COUNT as usize)) as u32); let size = uvec2(width as u32, (data.len() / (width * CHANNEL_COUNT as usize)) as u32);
let handle = self.allocate(size); let handle: TextureHandle = self.allocate(size);
let allocation = self.allocations.get_mut(&handle.index).unwrap(); let allocation = self.allocations.get(&handle.index).unwrap();
assert!(!allocation.rotated, "Rotated textures are not implemented yet"); assert!(!allocation.rotated, "Rotated textures are not implemented yet");
for y in 0..size.y { for y in 0..size.y {
for x in 0..size.x { for x in 0..size.x {
@ -127,6 +145,26 @@ impl TextureAtlasManager {
} }
} }
} }
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 fn add_grayscale(&mut self, width: usize, data: &[u8]) -> TextureHandle {
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) * CHANNEL_COUNT) as usize;
self.data[dst_idx..(dst_idx + CHANNEL_COUNT as usize)].copy_from_slice(&[255, 255, 255, data[src_idx]]);
}
}
self.modified = true;
handle handle
} }
@ -138,25 +176,43 @@ impl TextureAtlasManager {
todo!() todo!()
} }
pub fn atlas_size(&self) -> UVec2 {
self.size
}
pub fn get(&self, handle: TextureHandle) -> Option<&TextureAllocation> { pub fn get(&self, handle: TextureHandle) -> Option<&TextureAllocation> {
self.allocations.get(&handle.index) self.allocations.get(&handle.index)
} }
pub(crate) fn get_uv(&self, handle: TextureHandle) -> Corners<Vec2> {
let info = self.get(handle).unwrap();
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;
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 /// Reset the `is_modified` flag
pub fn reset_modified(&mut self) { pub(crate) fn reset_modified(&mut self) {
self.modified = false; self.modified = false;
} }
/// Returns true if the atlas has been modified since the last call to `reset_modified`\ /// Returns 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\ /// If this function returns true, the texture atlas should be re-uploaded to the GPU before rendering\
/// This function is mostly useful for developers of graphics backends
pub fn is_modified(&self) -> bool { pub fn is_modified(&self) -> bool {
self.modified self.modified
} }
pub fn meta(&self) -> TextureAtlasMeta {
TextureAtlasMeta {
data: &self.data,
size: self.size,
modified: self.modified,
}
}
} }
impl Default for TextureAtlasManager { impl Default for TextureAtlasManager {
@ -165,13 +221,3 @@ impl Default for TextureAtlasManager {
Self::new(UVec2::new(512, 512)) Self::new(UVec2::new(512, 512))
} }
} }
#[allow(deprecated)]
impl<'a> IfModified<TextureAtlasManager> for TextureAtlasManager {
fn if_modified(&self) -> Option<&Self> {
match self.modified {
true => Some(self),
false => None,
}
}
}

View file

@ -17,7 +17,7 @@ pub struct Text {
pub size: (UiSize, UiSize), pub size: (UiSize, UiSize),
pub color: Vec4, pub color: Vec4,
pub font: FontHandle, pub font: FontHandle,
pub text_size: u8, pub text_size: u16,
} }
impl Default for Text { impl Default for Text {

View file

@ -1,12 +1,15 @@
use std::collections::VecDeque; use std::collections::VecDeque;
use glam::Vec2; use glam::Vec2;
use crate::{ use crate::{
layout::{UiDirection, LayoutInfo}, draw::{
atlas::{TextureAtlasManager, TextureAtlasMeta},
UiDrawCall, UiDrawCommandList,
},
element::{MeasureContext, ProcessContext, UiElement}, element::{MeasureContext, ProcessContext, UiElement},
event::UiEvent, event::UiEvent,
layout::{LayoutInfo, UiDirection},
state::StateRepo, state::StateRepo,
draw::{UiDrawCommandList, UiDrawCall}, text::{FontHandle, TextRenderer}
text::{TextRenderer, FontTextureInfo, FontHandle},
}; };
/// The main instance of the UI system.\ /// The main instance of the UI system.\
@ -20,6 +23,7 @@ pub struct UiInstance {
draw_call: UiDrawCall, draw_call: UiDrawCall,
draw_call_modified: bool, draw_call_modified: bool,
text_renderer: TextRenderer, text_renderer: TextRenderer,
atlas: TextureAtlasManager,
events: VecDeque<UiEvent>, events: VecDeque<UiEvent>,
} }
@ -39,13 +43,19 @@ impl UiInstance {
draw_call_modified: false, draw_call_modified: false,
// ftm: FontTextureManager::default(), // ftm: FontTextureManager::default(),
text_renderer: TextRenderer::new(), text_renderer: TextRenderer::new(),
atlas: {
let mut atlas = TextureAtlasManager::default();
//HACK: Ensure that vec(0, 0) uv is white square
atlas.add_grayscale(1, &[255]);
atlas
},
events: VecDeque::new(), events: VecDeque::new(),
} }
} }
/// Parse and add a font from a raw byte slice to the UI\ /// Parse and add a font from a raw byte slice to the UI\
/// Returns a font handle. /// Returns a font handle.
pub fn add_font_from_bytes(&mut self, font: &[u8]) -> FontHandle { pub fn add_font(&mut self, font: &[u8]) -> FontHandle {
self.text_renderer.add_font_from_bytes(font) self.text_renderer.add_font_from_bytes(font)
} }
@ -80,7 +90,7 @@ impl UiInstance {
std::mem::swap(&mut self.prev_draw_commands, &mut self.draw_commands); std::mem::swap(&mut self.prev_draw_commands, &mut self.draw_commands);
self.draw_call_modified = false; self.draw_call_modified = false;
self.draw_commands.commands.clear(); self.draw_commands.commands.clear();
self.text_renderer.reset_frame(); self.atlas.reset_modified();
} }
/// End the frame and prepare the UI for rendering /// End the frame and prepare the UI for rendering
@ -90,29 +100,29 @@ impl UiInstance {
if self.draw_commands.commands == self.prev_draw_commands.commands { if self.draw_commands.commands == self.prev_draw_commands.commands {
return return
} }
self.draw_call = UiDrawCall::build(&self.draw_commands, &mut self.text_renderer); self.draw_call = UiDrawCall::build(&self.draw_commands, &mut self.atlas, &mut self.text_renderer);
self.draw_call_modified = true; self.draw_call_modified = true;
} }
/// Get the draw call for the current frame /// Get the draw call information for the current frame
/// ///
/// This function should only be used by the render backend.\ /// This function should only be used by the render backend.\
/// You should not call this directly unless you're implementing a custom 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 draw plan was modified since the last frame /// Returns a tuple with a boolean indicating if the buffers have been modified since the last frame
pub fn draw_call(&self) -> (bool, &UiDrawCall) { pub fn draw_call(&self) -> (bool, &UiDrawCall) {
(self.draw_call_modified, &self.draw_call) (self.draw_call_modified, &self.draw_call)
} }
/// Get the font texture for the current frame /// Get the texture atlas size and data for the current frame
/// ///
/// This function should only be used by the render backend.\ /// This function should only be used by the render backend.\
/// You should not call this directly unless you're implementing a custom render backend /// You should not call this directly unless you're implementing a custom render backend
/// ///
/// Make sure to check `FontTextureInfo::modified` to see if the texture was modified /// Make sure to check [`TextureAtlasMeta::modified`] to see if the texture has been modified
/// since the last frame before uploading it to the GPU /// since the beginning of the current frame before uploading it to the GPU
pub fn font_texture(&self) -> FontTextureInfo { pub fn atlas(&self) -> TextureAtlasMeta {
self.text_renderer.font_texture() self.atlas.meta()
} }
/// Push a platform event to the UI event queue /// Push a platform event to the UI event queue

View file

@ -22,9 +22,3 @@ pub mod state;
pub mod text; pub mod text;
pub use instance::UiInstance; pub use instance::UiInstance;
#[deprecated(since = "0.1.0-alpha.4", note = "will be removed in the next release")]
pub trait IfModified<T> {
#[deprecated(since = "0.1.0-alpha.4", note = "will be removed in the next release")]
fn if_modified(&self) -> Option<&T>;
}

View file

@ -10,7 +10,7 @@ pub use font::BUILTIN_FONT;
pub(crate) use font::DEFAULT_FONT; pub(crate) use font::DEFAULT_FONT;
use fontdue::{Font, FontSettings}; use fontdue::{Font, FontSettings};
use ftm::FontTextureManager; use ftm::FontTextureManager;
pub use ftm::{FontTextureInfo, GlyphCacheEntry}; pub use ftm::GlyphCacheEntry;
use crate::draw::atlas::TextureAtlasManager; use crate::draw::atlas::TextureAtlasManager;
@ -32,7 +32,7 @@ impl TextRenderer {
} }
pub fn glyph(&mut self, atlas: &mut TextureAtlasManager, font_handle: FontHandle, character: char, size: u8) -> Arc<GlyphCacheEntry> { pub fn glyph(&mut self, atlas: &mut TextureAtlasManager, font_handle: FontHandle, character: char, size: u8) -> Arc<GlyphCacheEntry> {
self.ftm.glyph(&self.fm, atlas, font_handle, character, size) self.ftm.glyph(atlas, &self.fm, font_handle, character, size)
} }
pub(crate) fn internal_font(&self, handle: FontHandle) -> &Font { pub(crate) fn internal_font(&self, handle: FontHandle) -> &Font {
@ -55,7 +55,7 @@ pub struct TextMeasureResponse {
pub struct TextMeasure<'a>(&'a TextRenderer); pub struct TextMeasure<'a>(&'a TextRenderer);
impl<'a> TextMeasure<'a> { impl<'a> TextMeasure<'a> {
pub fn measure(&self, font: FontHandle, size: u8, text: &str) -> TextMeasureResponse { pub fn measure(&self, font: FontHandle, size: u16, text: &str) -> TextMeasureResponse {
use fontdue::layout::{Layout, CoordinateSystem, TextStyle}; use fontdue::layout::{Layout, CoordinateSystem, TextStyle};
let mut layout = Layout::new(CoordinateSystem::PositiveYDown); let mut layout = Layout::new(CoordinateSystem::PositiveYDown);
layout.append( layout.append(
@ -79,7 +79,7 @@ impl TextRenderer {
TextMeasure(self) TextMeasure(self)
} }
pub fn measure(&self, font: FontHandle, size: u8, text: &str) -> TextMeasureResponse { pub fn measure(&self, font: FontHandle, size: u16, text: &str) -> TextMeasureResponse {
TextMeasure(self).measure(font, size, text) TextMeasure(self).measure(font, size, text)
} }
} }

View file

@ -1,11 +1,7 @@
use std::sync::Arc; use std::sync::Arc;
use fontdue::Metrics; use fontdue::Metrics;
use glam::UVec2;
use hashbrown::HashMap; use hashbrown::HashMap;
use crate::{ use crate::draw::atlas::{TextureAtlasManager, TextureHandle};
draw::atlas::{TextureAtlasManager, TextureHandle},
IfModified
};
use super::font::{FontHandle, FontManager}; use super::font::{FontHandle, FontManager};
@ -51,8 +47,8 @@ impl FontTextureManager {
} }
let font = font_manager.get(font_handle).unwrap(); let font = font_manager.get(font_handle).unwrap();
let (metrics, bitmap) = font.rasterize(character, size as f32); let (metrics, bitmap) = font.rasterize(character, size as f32);
log::debug!("rasterized glyph: {}, {:?}, {:?}", character, metrics, bitmap); log::trace!("rasterized glyph: {}, {:?}, {:?}", character, metrics, bitmap);
let texture = atlas.add(metrics.width, &bitmap); let texture = atlas.add_grayscale(metrics.width, &bitmap);
let entry = Arc::new(GlyphCacheEntry { let entry = Arc::new(GlyphCacheEntry {
metrics, metrics,
texture texture