From e0ee7c9aa6aaace97f376f747122d1ad7f8968b5 Mon Sep 17 00:00:00 2001 From: griffi-gh Date: Sun, 18 Feb 2024 04:04:02 +0100 Subject: [PATCH] new "context" system and text measuring --- hui-examples/examples/test2.rs | 62 +++++++++++++++++++++++ hui/Cargo.toml | 2 +- hui/src/element.rs | 23 +++++++-- hui/src/element/builtin/container.rs | 66 ++++++++++++++----------- hui/src/element/builtin/progress_bar.rs | 27 +++++----- hui/src/element/builtin/rect.rs | 27 +++++----- hui/src/element/builtin/spacer.rs | 13 ++--- hui/src/element/builtin/text.rs | 32 +++++++----- hui/src/interaction.rs | 10 ++-- hui/src/lib.rs | 16 ++++-- hui/src/text.rs | 38 ++++++++++++++ 11 files changed, 226 insertions(+), 90 deletions(-) create mode 100644 hui-examples/examples/test2.rs diff --git a/hui-examples/examples/test2.rs b/hui-examples/examples/test2.rs new file mode 100644 index 0000000..a7e4e71 --- /dev/null +++ b/hui-examples/examples/test2.rs @@ -0,0 +1,62 @@ +use std::time::Instant; +use glam::{UVec2, vec4}; +use glium::{backend::glutin::SimpleWindowBuilder, Surface}; +use winit::{ + event::{Event, WindowEvent}, + event_loop::{EventLoopBuilder, ControlFlow} +}; +use hui::{ + element::{ + container::{Alignment, Container, Sides}, progress_bar::ProgressBar, rect::Rect, text::Text, UiElement + }, interaction::IntoInteractable, IfModified, UiDirection, UiInstance, UiSize +}; +use hui_glium::GliumUiRenderer; + +fn main() { + kubi_logging::init(); + + let event_loop = EventLoopBuilder::new().build().unwrap(); + let (_window, display) = SimpleWindowBuilder::new().build(&event_loop); + + let mut hui = UiInstance::new(); + let mut backend = GliumUiRenderer::new(&display); + 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(); + + hui.begin(); + + hui.add(Container { + gap: 5., + padding: Sides::all(5.), + align: (Alignment::Center, Alignment::Center), + size: (UiSize::Percentage(1.), UiSize::Percentage(1.)), + elements: vec![ + Box::new(Text { + text: "Hello, world!\nGoodbye, world!\nowo\nuwu".into(), + text_size: 120, + ..Default::default() + }), + ], + ..Default::default() + }, resolution); + + hui.end(); + + backend.update(&hui); + backend.draw(&mut frame, resolution); + + frame.finish().unwrap(); + } + _ => (), + } + }).unwrap(); +} diff --git a/hui/Cargo.toml b/hui/Cargo.toml index 000489a..c0388bc 100644 --- a/hui/Cargo.toml +++ b/hui/Cargo.toml @@ -4,7 +4,7 @@ description = "Simple UI library for games and other interactive applications" repository = "https://github.com/griffi-gh/hui" authors = ["griffi-gh "] rust-version = "1.75" -version = "0.0.2" +version = "0.0.3" edition = "2021" license = "GPL-3.0-or-later" publish = true diff --git a/hui/src/element.rs b/hui/src/element.rs index eb6d762..71a692f 100644 --- a/hui/src/element.rs +++ b/hui/src/element.rs @@ -1,9 +1,10 @@ use std::any::Any; use crate::{ - LayoutInfo, draw::UiDrawCommands, measure::Response, - state::StateRepo + state::StateRepo, + text::TextMeasure, + LayoutInfo }; #[cfg(feature = "builtin_elements")] @@ -18,12 +19,26 @@ mod builtin { #[cfg(feature = "builtin_elements")] pub use builtin::*; +pub struct MeasureContext<'a> { + pub state: &'a StateRepo, + pub layout: &'a LayoutInfo, + pub text_measure: TextMeasure<'a>, +} + +pub struct ProcessContext<'a> { + pub measure: &'a Response, + pub state: &'a mut StateRepo, + pub layout: &'a LayoutInfo, + pub draw: &'a mut UiDrawCommands, + pub text_measure: TextMeasure<'a>, +} + pub trait UiElement { fn name(&self) -> &'static str { "UiElement" } fn state_id(&self) -> Option { None } fn is_stateful(&self) -> bool { self.state_id().is_some() } fn is_stateless(&self) -> bool { self.state_id().is_none() } fn init_state(&self) -> Option> { None } - fn measure(&self, state: &StateRepo, layout: &LayoutInfo) -> Response; - fn process(&self, measure: &Response, state: &mut StateRepo, layout: &LayoutInfo, draw: &mut UiDrawCommands); + fn measure(&self, ctx: MeasureContext) -> Response; + fn process(&self, ctx: ProcessContext); } diff --git a/hui/src/element/builtin/container.rs b/hui/src/element/builtin/container.rs index 955180f..19a007c 100644 --- a/hui/src/element/builtin/container.rs +++ b/hui/src/element/builtin/container.rs @@ -1,12 +1,6 @@ use glam::{Vec2, vec2, Vec4}; use crate::{ - UiDirection, - UiSize, - LayoutInfo, - draw::{UiDrawCommand, UiDrawCommands}, - measure::{Response, Hints}, - state::StateRepo, - element::UiElement + draw::{UiDrawCommand, UiDrawCommands}, element::{MeasureContext, ProcessContext, UiElement}, measure::{Hints, Response}, state::StateRepo, LayoutInfo, UiDirection, UiSize }; #[derive(Clone, Copy, PartialEq, Eq, Debug)] @@ -106,15 +100,19 @@ impl Container { } impl UiElement for Container { - fn measure(&self, state: &StateRepo, layout: &LayoutInfo) -> Response { + fn measure(&self, ctx: MeasureContext) -> Response { let mut size = Vec2::ZERO; //if matches!(self.size.0, UiSize::Auto) || matches!(self.size.1, UiSize::Auto) { let mut leftover_gap = Vec2::ZERO; for element in &self.elements { - let measure = element.measure(state, &LayoutInfo { - position: layout.position + size, - max_size: self.measure_max_inner_size(layout), // - size TODO - direction: self.direction, + let measure = element.measure(MeasureContext{ + state: ctx.state, + layout: &LayoutInfo { + position: ctx.layout.position + size, + max_size: self.measure_max_inner_size(ctx.layout), // - size TODO + direction: self.direction, + }, + text_measure: ctx.text_measure, }); match self.direction { UiDirection::Horizontal => { @@ -140,12 +138,12 @@ impl UiElement for Container { match self.size.0 { UiSize::Auto => (), - UiSize::Percentage(percentage) => size.x = layout.max_size.x * percentage, + UiSize::Percentage(percentage) => size.x = ctx.layout.max_size.x * percentage, UiSize::Pixels(pixels) => size.x = pixels, } match self.size.1 { UiSize::Auto => (), - UiSize::Percentage(percentage) => size.y = layout.max_size.y * percentage, + UiSize::Percentage(percentage) => size.y = ctx.layout.max_size.y * percentage, UiSize::Pixels(pixels) => size.y = pixels, } @@ -159,14 +157,14 @@ impl UiElement for Container { } } - fn process(&self, measure: &Response, state: &mut StateRepo, layout: &LayoutInfo, draw: &mut UiDrawCommands) { - let mut position = layout.position; + fn process(&self, ctx: ProcessContext) { + let mut position = ctx.layout.position; //background if let Some(color) = self.background { - draw.add(UiDrawCommand::Rectangle { + ctx.draw.add(UiDrawCommand::Rectangle { position, - size: measure.size, + size: ctx.measure.size, color }); } @@ -178,16 +176,16 @@ impl UiElement for Container { match (self.align.0, self.direction) { (Alignment::Begin, _) => (), (Alignment::Center, UiDirection::Horizontal) => { - position.x += (measure.size.x - measure.hints.inner_content_size.unwrap().x) / 2.; + position.x += (ctx.measure.size.x - ctx.measure.hints.inner_content_size.unwrap().x) / 2.; }, (Alignment::Center, UiDirection::Vertical) => { - position.y += (measure.size.y - measure.hints.inner_content_size.unwrap().y) / 2.; + position.y += (ctx.measure.size.y - ctx.measure.hints.inner_content_size.unwrap().y) / 2.; }, (Alignment::End, UiDirection::Horizontal) => { - position.x += measure.size.x - measure.hints.inner_content_size.unwrap().x - self.padding.right - self.padding.left; + position.x += ctx.measure.size.x - ctx.measure.hints.inner_content_size.unwrap().x - self.padding.right - self.padding.left; }, (Alignment::End, UiDirection::Vertical) => { - position.y += measure.size.y - measure.hints.inner_content_size.unwrap().y - self.padding.bottom - self.padding.top; + position.y += ctx.measure.size.y - ctx.measure.hints.inner_content_size.unwrap().y - self.padding.bottom - self.padding.top; } } @@ -196,32 +194,42 @@ impl UiElement for Container { let mut el_layout = LayoutInfo { position, - max_size: self.measure_max_inner_size(layout), + max_size: self.measure_max_inner_size(ctx.layout), direction: self.direction, }; //measure - let el_measure = element.measure(state, &el_layout); + let el_measure = element.measure(MeasureContext { + state: ctx.state, + layout: &el_layout, + text_measure: ctx.text_measure, + }); //align (on sec. axis) match (self.align.1, self.direction) { (Alignment::Begin, _) => (), (Alignment::Center, UiDirection::Horizontal) => { - el_layout.position.y += (measure.size.y - self.padding.bottom - self.padding.top - el_measure.size.y) / 2.; + el_layout.position.y += (ctx.measure.size.y - self.padding.bottom - self.padding.top - el_measure.size.y) / 2.; }, (Alignment::Center, UiDirection::Vertical) => { - el_layout.position.x += (measure.size.x - self.padding.left - self.padding.right - el_measure.size.x) / 2.; + el_layout.position.x += (ctx.measure.size.x - self.padding.left - self.padding.right - el_measure.size.x) / 2.; }, (Alignment::End, UiDirection::Horizontal) => { - el_layout.position.y += measure.size.y - el_measure.size.y - self.padding.bottom; + el_layout.position.y += ctx.measure.size.y - el_measure.size.y - self.padding.bottom; }, (Alignment::End, UiDirection::Vertical) => { - el_layout.position.x += measure.size.x - el_measure.size.x - self.padding.right; + el_layout.position.x += ctx.measure.size.x - el_measure.size.x - self.padding.right; } } //process - element.process(&el_measure, state, &el_layout, draw); + element.process(ProcessContext { + measure: &el_measure, + state: ctx.state, + layout: &el_layout, + draw: ctx.draw, + text_measure: ctx.text_measure, + }); //layout match self.direction { diff --git a/hui/src/element/builtin/progress_bar.rs b/hui/src/element/builtin/progress_bar.rs index d7f9ffa..d949a4c 100644 --- a/hui/src/element/builtin/progress_bar.rs +++ b/hui/src/element/builtin/progress_bar.rs @@ -1,10 +1,11 @@ use glam::{vec2, Vec4, vec4}; use crate::{ - UiSize, LayoutInfo, draw::{UiDrawCommand, UiDrawCommands}, + element::{MeasureContext, ProcessContext, UiElement}, measure::Response, state::StateRepo, - element::UiElement + LayoutInfo, + UiSize }; #[derive(Debug, Clone, Copy)] @@ -31,17 +32,17 @@ const BAR_HEIGHT: f32 = 20.0; impl UiElement for ProgressBar { fn name(&self) -> &'static str { "Progress bar" } - fn measure(&self, _: &StateRepo, layout: &LayoutInfo) -> Response { + fn measure(&self, ctx: MeasureContext) -> Response { Response { size: vec2( match self.size.0 { - UiSize::Auto => layout.max_size.x.max(300.), - UiSize::Percentage(p) => layout.max_size.x * p, + UiSize::Auto => ctx.layout.max_size.x.max(300.), + UiSize::Percentage(p) => ctx.layout.max_size.x * p, UiSize::Pixels(p) => p, }, match self.size.1 { UiSize::Auto => BAR_HEIGHT, - UiSize::Percentage(p) => layout.max_size.y * p, + UiSize::Percentage(p) => ctx.layout.max_size.y * p, UiSize::Pixels(p) => p, } ), @@ -50,19 +51,19 @@ impl UiElement for ProgressBar { } } - fn process(&self, measure: &Response, state: &mut StateRepo, layout: &LayoutInfo, draw: &mut UiDrawCommands) { + fn process(&self, ctx: ProcessContext) { let value = self.value.clamp(0., 1.); if value < 1. { - draw.add(UiDrawCommand::Rectangle { - position: layout.position, - size: measure.size, + ctx.draw.add(UiDrawCommand::Rectangle { + position: ctx.layout.position, + size: ctx.measure.size, color: self.color_background }); } if value > 0. { - draw.add(UiDrawCommand::Rectangle { - position: layout.position, - size: measure.size * vec2(value, 1.0), + ctx.draw.add(UiDrawCommand::Rectangle { + position: ctx.layout.position, + size: ctx.measure.size * vec2(value, 1.0), color: self.color_foreground }); } diff --git a/hui/src/element/builtin/rect.rs b/hui/src/element/builtin/rect.rs index d4bcf8d..0118e65 100644 --- a/hui/src/element/builtin/rect.rs +++ b/hui/src/element/builtin/rect.rs @@ -1,11 +1,10 @@ use glam::{vec2, Vec4}; use crate::{ - LayoutInfo, - UiSize, - element::UiElement, - state::StateRepo, + draw::{UiDrawCommand, UiDrawCommands}, + element::{MeasureContext, ProcessContext, UiElement}, measure::Response, - draw::{UiDrawCommand, UiDrawCommands} + state::StateRepo, + LayoutInfo, UiSize }; pub struct Rect { @@ -23,17 +22,17 @@ impl Default for Rect { } impl UiElement for Rect { - fn measure(&self, _state: &StateRepo, layout: &LayoutInfo) -> Response { + fn measure(&self, ctx: MeasureContext) -> Response { Response { size: vec2( match self.size.0 { - UiSize::Auto => layout.max_size.x, - UiSize::Percentage(percentage) => layout.max_size.x * percentage, + UiSize::Auto => ctx.layout.max_size.x, + UiSize::Percentage(percentage) => ctx.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::Auto => ctx.layout.max_size.y, + UiSize::Percentage(percentage) => ctx.layout.max_size.y * percentage, UiSize::Pixels(pixels) => pixels, }, ), @@ -42,11 +41,11 @@ impl UiElement for Rect { } } - fn process(&self, measure: &Response, _state: &mut StateRepo, layout: &LayoutInfo, draw: &mut UiDrawCommands) { + fn process(&self, ctx: ProcessContext) { if let Some(color) = self.color { - draw.add(UiDrawCommand::Rectangle { - position: layout.position, - size: measure.size, + ctx.draw.add(UiDrawCommand::Rectangle { + position: ctx.layout.position, + size: ctx.measure.size, color, }); } diff --git a/hui/src/element/builtin/spacer.rs b/hui/src/element/builtin/spacer.rs index e951061..1c81537 100644 --- a/hui/src/element/builtin/spacer.rs +++ b/hui/src/element/builtin/spacer.rs @@ -1,11 +1,8 @@ use glam::vec2; use crate::{ - LayoutInfo, - UiDirection, - element::UiElement, - state::StateRepo, + element::{MeasureContext, ProcessContext, UiElement}, measure::Response, - draw::{UiDrawCommand, UiDrawCommands} + UiDirection }; pub struct Spacer(pub f32); @@ -17,9 +14,9 @@ impl Default for Spacer { } impl UiElement for Spacer { - fn measure(&self, state: &StateRepo, layout: &LayoutInfo) -> Response { + fn measure(&self, ctx: MeasureContext) -> Response { Response { - size: match layout.direction { + size: match ctx.layout.direction { UiDirection::Horizontal => vec2(self.0, 0.), UiDirection::Vertical => vec2(0., self.0), }, @@ -28,5 +25,5 @@ impl UiElement for Spacer { } } - fn process(&self, _measure: &Response, _state: &mut StateRepo, _layout: &LayoutInfo, _draw: &mut UiDrawCommands) {} + fn process(&self, _ctx: ProcessContext) {} } diff --git a/hui/src/element/builtin/text.rs b/hui/src/element/builtin/text.rs index e5fed6f..29044c8 100644 --- a/hui/src/element/builtin/text.rs +++ b/hui/src/element/builtin/text.rs @@ -1,12 +1,12 @@ use std::borrow::Cow; use glam::{vec2, Vec4}; use crate::{ - LayoutInfo, - UiSize, - element::UiElement, - state::StateRepo, + draw::{UiDrawCommand, UiDrawCommands}, + element::{MeasureContext, ProcessContext, UiElement}, measure::Response, - draw::{UiDrawCommand, UiDrawCommands}, text::FontHandle + state::StateRepo, + text::FontHandle, + LayoutInfo, UiSize }; pub struct Text { @@ -30,17 +30,23 @@ impl Default for Text { } impl UiElement for Text { - fn measure(&self, _state: &StateRepo, layout: &LayoutInfo) -> Response { + fn measure(&self, ctx: MeasureContext) -> Response { + let mut size = (0., 0.); + if matches!(self.size.0, UiSize::Auto) || matches!(self.size.1, UiSize::Auto) { + let res = ctx.text_measure.measure(self.font, self.text_size, &self.text); + size.0 = res.max_width; + size.1 = res.height; + } Response { size: vec2( match self.size.0 { - UiSize::Auto => layout.max_size.x, - UiSize::Percentage(percentage) => layout.max_size.x * percentage, + UiSize::Auto => size.0, + UiSize::Percentage(percentage) => ctx.layout.max_size.x * percentage, UiSize::Pixels(pixels) => pixels, }, match self.size.1 { - UiSize::Auto => self.text_size as f32, - UiSize::Percentage(percentage) => layout.max_size.y * percentage, + UiSize::Auto => size.1, + UiSize::Percentage(percentage) => ctx.layout.max_size.y * percentage, UiSize::Pixels(pixels) => pixels, }, ), @@ -49,10 +55,10 @@ impl UiElement for Text { } } - fn process(&self, _measure: &Response, _state: &mut StateRepo, layout: &LayoutInfo, draw: &mut UiDrawCommands) { - draw.add(UiDrawCommand::Text { + fn process(&self, ctx: ProcessContext) { + ctx.draw.add(UiDrawCommand::Text { text: self.text.clone(), - position: layout.position, + position: ctx.layout.position, size: self.text_size, color: self.color, font: self.font diff --git a/hui/src/interaction.rs b/hui/src/interaction.rs index b7d97a1..ee5412a 100644 --- a/hui/src/interaction.rs +++ b/hui/src/interaction.rs @@ -1,4 +1,4 @@ -use crate::{element::UiElement, draw::UiDrawCommands}; +use crate::element::{UiElement, MeasureContext, ProcessContext}; pub struct Interactable { pub element: T, @@ -31,12 +31,12 @@ impl Interactable { } impl UiElement for Interactable { - fn measure(&self, state: &crate::state::StateRepo, layout: &crate::LayoutInfo) -> crate::measure::Response { - self.element.measure(state, layout) + fn measure(&self, ctx: MeasureContext) -> crate::measure::Response { + self.element.measure(ctx) } - fn process(&self, measure: &crate::measure::Response, state: &mut crate::state::StateRepo, layout: &crate::LayoutInfo, draw: &mut UiDrawCommands) { - self.element.process(measure, state, layout, draw) + fn process(&self, ctx: ProcessContext) { + self.element.process(ctx) } } diff --git a/hui/src/lib.rs b/hui/src/lib.rs index a374f47..f1d1a32 100644 --- a/hui/src/lib.rs +++ b/hui/src/lib.rs @@ -13,7 +13,7 @@ pub mod state; pub mod text; pub mod interaction; -use element::UiElement; +use element::{MeasureContext, ProcessContext, UiElement}; use state::StateRepo; use draw::{UiDrawCommands, UiDrawPlan}; use text::{TextRenderer, FontTextureInfo, FontHandle}; @@ -65,8 +65,18 @@ impl UiInstance { max_size, direction: UiDirection::Vertical, }; - let measure = element.measure(&self.stateful_state, &layout); - element.process(&measure, &mut self.stateful_state, &layout, &mut self.draw_commands); + let measure = element.measure(MeasureContext { + state: &self.stateful_state, + layout: &layout, + text_measure: self.text_renderer.to_measure(), + }); + element.process(ProcessContext { + measure: &measure, + state: &mut self.stateful_state, + layout: &layout, + draw: &mut self.draw_commands, + text_measure: self.text_renderer.to_measure(), + }); } pub fn begin(&mut self) { diff --git a/hui/src/text.rs b/hui/src/text.rs index 5da286a..6cc8c03 100644 --- a/hui/src/text.rs +++ b/hui/src/text.rs @@ -48,3 +48,41 @@ impl Default for TextRenderer { Self::new() } } + +pub struct TextMeasureResponse { + pub max_width: f32, + pub height: f32, +} + +#[derive(Clone, Copy)] +pub struct TextMeasure<'a>(&'a TextRenderer); + +impl<'a> TextMeasure<'a> { + pub fn measure(&self, font: FontHandle, size: u8, 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() as f32, + } + } +} + +impl TextRenderer { + pub fn to_measure(&self) -> TextMeasure { + TextMeasure(self) + } + + pub fn measure(&self, font: FontHandle, size: u8, text: &str) -> TextMeasureResponse { + TextMeasure(self).measure(font, size, text) + } +}