diff --git a/kubi-ui-examples/examples/text.rs b/kubi-ui-examples/examples/text.rs new file mode 100644 index 0000000..84f836f --- /dev/null +++ b/kubi-ui-examples/examples/text.rs @@ -0,0 +1,70 @@ +use std::time::Instant; +use glam::{UVec2, vec4}; +use glium::{backend::glutin::SimpleWindowBuilder, Surface}; +use winit::{ + event::{Event, WindowEvent}, + event_loop::{EventLoopBuilder, ControlFlow} +}; +use kubi_ui::{ + KubiUi, + element::{ + UiElement, + progress_bar::ProgressBar, + container::{Container, Sides, Alignment}, + rect::Rect, text::Text + }, + interaction::IntoInteractable, + UiSize, + UiDirection, IfModified, +}; +use kubi_ui_glium::GliumUiRenderer; + +fn main() { + kubi_logging::init(); + + let event_loop = EventLoopBuilder::new().build().unwrap(); + let (window, display) = SimpleWindowBuilder::new().build(&event_loop); + + let mut kui = KubiUi::new(); + let mut backend = GliumUiRenderer::new(&display); + + let instant = Instant::now(); + event_loop.run(|event, window_target| { + window_target.set_control_flow(ControlFlow::Poll); + match event { + Event::WindowEvent { event: WindowEvent::CloseRequested, .. } => { + window_target.exit(); + }, + Event::AboutToWait => { + let mut frame = display.draw(); + frame.clear_color_srgb(0.5, 0.5, 0.5, 0.); + + let resolution = UVec2::from(display.get_framebuffer_dimensions()).as_vec2(); + + kui.begin(); + + kui.add(Container { + gap: 5., + padding: Sides::all(5.), + align: (Alignment::Begin, Alignment::Begin), + size: (UiSize::Percentage(1.), UiSize::Percentage(1.)), + elements: vec![ + Box::new(Text { + text: "Heloworld!Loremipsumsimdoloramet".into(), + ..Default::default() + }), + ], + ..Default::default() + }, resolution); + + kui.end(); + + backend.update(&kui); + backend.draw(&mut frame, resolution); + + frame.finish().unwrap(); + } + _ => (), + } + }).unwrap(); +} diff --git a/kubi-ui-glium/shaders/fragment.frag b/kubi-ui-glium/shaders/fragment.frag index f450433..796d94e 100644 --- a/kubi-ui-glium/shaders/fragment.frag +++ b/kubi-ui-glium/shaders/fragment.frag @@ -1,11 +1,21 @@ #version 300 es precision highp float; +precision highp sampler2D; out vec4 out_color; in vec4 vtx_color; +in vec2 vtx_uv; +uniform bool use_tex; +uniform sampler2D tex; void main() { - if (vtx_color.w <= 0.) discard; - out_color = vtx_color; + //if (vtx_color.w <= 0.) discard; + vec4 tex_color; + if (use_tex) { + tex_color = texture(tex, vtx_uv); + } else { + tex_color = vec4(1.); + } + out_color = tex_color * vtx_color; } diff --git a/kubi-ui-glium/shaders/vertex.vert b/kubi-ui-glium/shaders/vertex.vert index 60edda6..6df175f 100644 --- a/kubi-ui-glium/shaders/vertex.vert +++ b/kubi-ui-glium/shaders/vertex.vert @@ -3,12 +3,15 @@ precision highp float; uniform vec2 resolution; +in vec2 uv; in vec4 color; in vec2 position; out vec4 vtx_color; +out vec2 vtx_uv; void main() { vtx_color = color; + vtx_uv = uv; vec2 pos2d = (vec2(2., -2.) * (position / resolution)) + vec2(-1, 1); gl_Position = vec4(pos2d, 0., 1.); } diff --git a/kubi-ui-glium/src/lib.rs b/kubi-ui-glium/src/lib.rs index 9bd4be6..5765137 100644 --- a/kubi-ui-glium/src/lib.rs +++ b/kubi-ui-glium/src/lib.rs @@ -1,14 +1,17 @@ +use std::rc::Rc; use glam::Vec2; use glium::{ Surface, DrawParameters, Blend, Program, VertexBuffer, IndexBuffer, - backend::Facade, + backend::{Facade, Context}, + texture::{SrgbTexture2d, RawImage2d}, index::PrimitiveType, - implement_vertex, uniform, texture::{SrgbTexture2d, RawImage2d}, + implement_vertex, + uniform, uniforms::DynamicUniforms, }; use kubi_ui::{ KubiUi, - draw::{UiDrawPlan, UiVertex}, + draw::{UiDrawPlan, UiVertex, BindTexture}, text::FontTextureInfo, IfModified, }; @@ -36,10 +39,10 @@ impl From for Vertex { implement_vertex!(Vertex, position, color, uv); struct BufferPair { - vertex_buffer: glium::VertexBuffer, - index_buffer: glium::IndexBuffer, - vertex_count: usize, - index_count: usize, + pub vertex_buffer: glium::VertexBuffer, + pub index_buffer: glium::IndexBuffer, + pub vertex_count: usize, + pub index_count: usize, } impl BufferPair { @@ -102,65 +105,121 @@ impl BufferPair { } } -pub struct GliumUiRenderer { - program: glium::Program, +struct GlDrawCall { + active: bool, buffer: BufferPair, + bind_texture: Option>, +} + +pub struct GliumUiRenderer { + context: Rc, + program: glium::Program, + font_texture: Option>, + plan: Vec, } impl GliumUiRenderer { pub fn new(facade: &F) -> Self { - log::info!("init glium backend for ui"); - log::debug!("init program"); - let program = Program::from_source(facade, VERTEX_SHADER, FRAGMENT_SHADER, None).unwrap(); + log::info!("init glium backend for kui"); Self { - program, - buffer: BufferPair::new(facade) + program: Program::from_source(facade, VERTEX_SHADER, FRAGMENT_SHADER, None).unwrap(), + context: Rc::clone(facade.get_context()), + font_texture: None, + plan: vec![] } } pub fn update_draw_plan(&mut self, plan: &UiDrawPlan) { - assert!(plan.calls.len() == 1, "multiple draw calls not supported yet"); - let data_vtx = &plan.calls[0].vertices.iter().copied().map(Vertex::from).collect::>(); - let data_idx = &plan.calls[0].indices; - self.buffer.write_data(data_vtx, data_idx); + if plan.calls.len() > self.plan.len() { + self.plan.resize_with(plan.calls.len(), || { + GlDrawCall { + buffer: BufferPair::new(&self.context), + bind_texture: None, + active: false, + } + }); + } else { + for step in &mut self.plan[plan.calls.len()..] { + step.active = false; + } + } + for (idx, call) in plan.calls.iter().enumerate() { + let data_vtx = &call.vertices.iter().copied().map(Vertex::from).collect::>()[..]; + let data_idx = &call.indices[..]; + self.plan[idx].active = true; + self.plan[idx].buffer.write_data(data_vtx, data_idx); + self.plan[idx].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))) + }, + None => None, + } + } } pub fn update_font_texture(&mut self, font_texture: &FontTextureInfo) { - //HACK: get context from buffer - let ctx = self.buffer.index_buffer.get_context(); - SrgbTexture2d::new(ctx, RawImage2d::from_raw_rgb( - font_texture.data.to_owned(), - (font_texture.size.x, font_texture.size.y) - )).unwrap(); + log::debug!("updating font texture"); + self.font_texture = Some(Rc::new(SrgbTexture2d::new( + &self.context, + RawImage2d::from_raw_rgba( + font_texture.data.to_owned(), + (font_texture.size.x, font_texture.size.y) + ) + ).unwrap())); } pub fn update(&mut self, kui: &KubiUi) { - if let Some(plan) = kui.draw_plan().if_modified() { - self.update_draw_plan(plan); - } if let Some(texture) = kui.font_texture().if_modified() { self.update_font_texture(texture); } + if let Some(plan) = kui.draw_plan().if_modified() { + self.update_draw_plan(plan); + } } pub fn draw(&self, frame: &mut glium::Frame, resolution: Vec2) { - if self.buffer.is_empty() { - return - } - let params = DrawParameters { blend: Blend::alpha_blending(), ..Default::default() }; - frame.draw( - self.buffer.vertex_buffer.slice(0..self.buffer.vertex_count).unwrap(), - self.buffer.index_buffer.slice(0..self.buffer.index_count).unwrap(), - &self.program, - &uniform! { - resolution: resolution.to_array(), - }, - ¶ms, - ).unwrap(); + for step in &self.plan { + if !step.active { + continue + } + + if step.buffer.is_empty() { + continue + } + + let vtx_buffer = step.buffer.vertex_buffer.slice(0..step.buffer.vertex_count).unwrap(); + let idx_buffer = step.buffer.index_buffer.slice(0..step.buffer.index_count).unwrap(); + + if let Some(bind_texture) = step.bind_texture.as_ref() { + frame.draw( + vtx_buffer, + idx_buffer, + &self.program, + &uniform! { + resolution: resolution.to_array(), + tex: bind_texture.as_ref(), + use_tex: true, + }, + ¶ms, + ).unwrap(); + } else { + frame.draw( + vtx_buffer, + idx_buffer, + &self.program, + &uniform! { + resolution: resolution.to_array(), + use_tex: false, + }, + ¶ms, + ).unwrap(); + } + } } } diff --git a/kubi-ui/src/draw.rs b/kubi-ui/src/draw.rs index 68d62ca..9efe93f 100644 --- a/kubi-ui/src/draw.rs +++ b/kubi-ui/src/draw.rs @@ -1,4 +1,4 @@ -use crate::{IfModified, text::TextRenderer}; +use crate::{IfModified, text::{TextRenderer, FontHandle}}; use std::borrow::Cow; use glam::{Vec2, Vec4, vec2}; @@ -63,22 +63,75 @@ pub struct UiDrawPlan { pub calls: Vec } +struct CallSwapper { + calls: Vec, + call: UiDrawCall, +} + +impl CallSwapper { + pub fn new() -> Self { + Self { + calls: vec![], + call: UiDrawCall::default(), + } + } + + pub fn current(&self) -> &UiDrawCall { + &self.call + } + + pub fn current_mut(&mut self) -> &mut UiDrawCall { + &mut self.call + } + + pub fn swap(&mut self) { + self.calls.push(std::mem::replace(&mut self.call, UiDrawCall::default())); + } + + pub fn finish(mut self) -> Vec { + self.calls.push(self.call); + self.calls + } +} + impl UiDrawPlan { - pub fn build(calls: &UiDrawCommands, tr: &mut TextRenderer) -> Self { - let mut call = UiDrawCall::default(); - for command in &calls.commands { + pub fn build(draw_commands: &UiDrawCommands, tr: &mut TextRenderer) -> Self { + let mut swapper = CallSwapper::new(); + let mut prev_command = None; + for command in &draw_commands.commands { + + let do_swap = if let Some(prev_command) = prev_command { + std::mem::discriminant(prev_command) != std::mem::discriminant(command) + } else { + false + }; + + if do_swap { + swapper.swap(); + } + + if do_swap || prev_command.is_none() { + match command { + UiDrawCommand::Rectangle { .. } => (), + UiDrawCommand::Text { .. } => { + swapper.current_mut().bind_texture = Some(BindTexture::FontTexture); + } + } + } + + let vidx = swapper.current().vertices.len() as u32; + match command { UiDrawCommand::Rectangle { position, size, color } => { - let idx = call.vertices.len() as u32; - call.indices.extend([idx, idx + 1, idx + 2, idx, idx + 2, idx + 3]); - call.vertices.extend([ + swapper.current_mut().indices.extend([vidx, vidx + 1, vidx + 2, vidx, vidx + 2, vidx + 3]); + swapper.current_mut().vertices.extend([ UiVertex { position: *position, color: *color, uv: vec2(0.0, 0.0), }, UiVertex { - position: *position + Vec2::new(size.x, 0.0), + position: *position + vec2(size.x, 0.0), color: *color, uv: vec2(1.0, 0.0), }, @@ -88,19 +141,45 @@ impl UiDrawPlan { uv: vec2(1.0, 1.0), }, UiVertex { - position: *position + Vec2::new(0.0, size.y), + position: *position + vec2(0.0, size.y), color: *color, uv: vec2(0.0, 1.0), }, ]); }, UiDrawCommand::Text { position, size, color, text } => { - todo!() + for char in text.chars() { + tr.glyph(FontHandle(0), char, *size); + } + swapper.current_mut().indices.extend([vidx, vidx + 1, vidx + 2, vidx, vidx + 2, vidx + 3]); + swapper.current_mut().vertices.extend([ + UiVertex { + position: *position, + color: *color, + uv: vec2(0.0, 0.0), + }, + UiVertex { + position: *position + vec2(32., 0.0), + color: *color, + uv: vec2(1.0, 0.0), + }, + UiVertex { + position: *position + vec2(32., 32.), + color: *color, + uv: vec2(1.0, 1.0), + }, + UiVertex { + position: *position + vec2(0.0, 32.), + color: *color, + uv: vec2(0.0, 1.0), + }, + ]); } } + prev_command = Some(command); } Self { - calls: vec![call] + calls: swapper.finish() } } } diff --git a/kubi-ui/src/element.rs b/kubi-ui/src/element.rs index b5a9d87..5ba7ea0 100644 --- a/kubi-ui/src/element.rs +++ b/kubi-ui/src/element.rs @@ -12,6 +12,7 @@ mod builtin { pub mod container; pub mod spacer; pub mod progress_bar; + pub mod text; } #[cfg(feature = "builtin_elements")] diff --git a/kubi-ui/src/element/builtin/text.rs b/kubi-ui/src/element/builtin/text.rs new file mode 100644 index 0000000..2c423df --- /dev/null +++ b/kubi-ui/src/element/builtin/text.rs @@ -0,0 +1,56 @@ +use std::borrow::Cow; +use glam::{vec2, Vec4}; +use crate::{ + LayoutInfo, + UiSize, + element::UiElement, + state::StateRepo, + measure::Response, + draw::UiDrawCommand +}; + +pub struct Text { + pub text: Cow<'static, str>, + pub size: (UiSize, UiSize), + pub color: Vec4, +} + +impl Default for Text { + fn default() -> Self { + Self { + text: "".into(), + size: (UiSize::Percentage(1.), UiSize::Pixels(32.)), + color: Vec4::new(1., 1., 1., 1.), + } + } +} + +impl UiElement for Text { + fn measure(&self, _state: &StateRepo, layout: &LayoutInfo) -> Response { + Response { + size: vec2( + match self.size.0 { + UiSize::Auto => layout.max_size.x, + UiSize::Percentage(percentage) => layout.max_size.x * percentage, + UiSize::Pixels(pixels) => pixels, + }, + match self.size.1 { + UiSize::Auto => layout.max_size.y, + UiSize::Percentage(percentage) => layout.max_size.y * percentage, + UiSize::Pixels(pixels) => pixels, + }, + ), + hints: Default::default(), + user_data: None + } + } + + fn process(&self, _measure: &Response, _state: &mut StateRepo, layout: &LayoutInfo, draw: &mut Vec) { + draw.push(UiDrawCommand::Text { + text: self.text.clone(), + position: layout.position, + size: 32, + color: self.color, + }); + } +} diff --git a/kubi-ui/src/text/texman.rs b/kubi-ui/src/text/texman.rs index 6624970..8e9b684 100644 --- a/kubi-ui/src/text/texman.rs +++ b/kubi-ui/src/text/texman.rs @@ -60,7 +60,7 @@ impl FontTextureManager { FontTextureManager { glyph_cache: HashMap::new(), packer: DensePacker::new(size.x as i32, size.y as i32), - font_texture: vec![0; (size.x * size.y) as usize], + font_texture: vec![0; (size.x * size.y * 4) as usize], font_texture_size: size, modified: false, } @@ -79,17 +79,18 @@ impl FontTextureManager { } /// Either looks up the glyph in the cache or renders it and adds it to the cache. - fn glyph_allocate(&mut self, font_manager: &FontManager, font_handle: FontHandle, character: char, size: u8) -> (bool, Arc) { + pub fn glyph(&mut self, font_manager: &FontManager, font_handle: FontHandle, character: char, size: u8) -> Arc { let key = GlyphCacheKey { font_index: font_handle.0, character, size, }; if let Some(entry) = self.glyph_cache.get(&key) { - return (false, Arc::clone(entry)); + return Arc::clone(entry); } let font = font_manager.get(font_handle).unwrap(); let (metrics, bitmap) = font.rasterize(character, size as f32); + log::debug!("rasterized glyph: {:?}, {:?}", metrics, bitmap); let texture_position = self.packer.pack(metrics.width as i32, metrics.height as i32, false).unwrap(); let texture_size = uvec2(metrics.width as u32, metrics.height as u32); let entry = Arc::new(GlyphCacheEntry { @@ -99,30 +100,37 @@ impl FontTextureManager { size: texture_size, }); self.glyph_cache.insert_unique_unchecked(key, Arc::clone(&entry)); - (true, entry) + self.glyph_place(&entry); + self.modified = true; + entry } /// Place glyph onto the font texture. fn glyph_place(&mut self, entry: &GlyphCacheEntry) { let tex_size = self.font_texture_size; - let GlyphCacheEntry { size, position, .. } = entry; + let GlyphCacheEntry { size, position, data, .. } = entry; + //println!("{size:?} {position:?}"); for y in 0..size.y { for x in 0..size.x { let src = (size.x * y + x) as usize; - let dst = (tex_size.x * (y + position.y as u32) + (x + position.x as u32)) as usize; - self.font_texture[dst] = entry.data[src]; + let dst = (tex_size.x * (y + position.y as u32) + (x + position.x as u32)) as usize * 4; + self.font_texture[dst..=(dst + 3)].copy_from_slice(&[255, 0, 0, data[src]]); + self.font_texture[dst] = data[src]; + //print!("{} ", if data[src] > 0 {'#'} else {'.'}); + //print!("{src} {dst} / "); } + //println!(); } } - pub fn glyph(&mut self, font_manager: &FontManager, font_handle: FontHandle, character: char, size: u8) -> Arc { - let (is_new, glyph) = self.glyph_allocate(font_manager, font_handle, character, size); - if is_new { - self.glyph_place(&glyph); - self.modified = true; - } - glyph - } + // pub fn glyph(&mut self, font_manager: &FontManager, font_handle: FontHandle, character: char, size: u8) -> Arc { + // 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 {