new "context" system and text measuring

This commit is contained in:
griffi-gh 2024-02-18 04:04:02 +01:00
parent ea6623f143
commit b65e540f0e
11 changed files with 226 additions and 90 deletions

View file

@ -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();
}

View file

@ -4,7 +4,7 @@ description = "Simple UI library for games and other interactive applications"
repository = "https://github.com/griffi-gh/hui" repository = "https://github.com/griffi-gh/hui"
authors = ["griffi-gh <prasol258@gmail.com>"] authors = ["griffi-gh <prasol258@gmail.com>"]
rust-version = "1.75" rust-version = "1.75"
version = "0.0.2" version = "0.0.3"
edition = "2021" edition = "2021"
license = "GPL-3.0-or-later" license = "GPL-3.0-or-later"
publish = true publish = true

View file

@ -1,9 +1,10 @@
use std::any::Any; use std::any::Any;
use crate::{ use crate::{
LayoutInfo,
draw::UiDrawCommands, draw::UiDrawCommands,
measure::Response, measure::Response,
state::StateRepo state::StateRepo,
text::TextMeasure,
LayoutInfo
}; };
#[cfg(feature = "builtin_elements")] #[cfg(feature = "builtin_elements")]
@ -18,12 +19,26 @@ mod builtin {
#[cfg(feature = "builtin_elements")] #[cfg(feature = "builtin_elements")]
pub use builtin::*; 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 { pub trait UiElement {
fn name(&self) -> &'static str { "UiElement" } fn name(&self) -> &'static str { "UiElement" }
fn state_id(&self) -> Option<u64> { None } fn state_id(&self) -> Option<u64> { None }
fn is_stateful(&self) -> bool { self.state_id().is_some() } fn is_stateful(&self) -> bool { self.state_id().is_some() }
fn is_stateless(&self) -> bool { self.state_id().is_none() } fn is_stateless(&self) -> bool { self.state_id().is_none() }
fn init_state(&self) -> Option<Box<dyn Any>> { None } fn init_state(&self) -> Option<Box<dyn Any>> { None }
fn measure(&self, state: &StateRepo, layout: &LayoutInfo) -> Response; fn measure(&self, ctx: MeasureContext) -> Response;
fn process(&self, measure: &Response, state: &mut StateRepo, layout: &LayoutInfo, draw: &mut UiDrawCommands); fn process(&self, ctx: ProcessContext);
} }

View file

@ -1,12 +1,6 @@
use glam::{Vec2, vec2, Vec4}; use glam::{Vec2, vec2, Vec4};
use crate::{ use crate::{
UiDirection, draw::{UiDrawCommand, UiDrawCommands}, element::{MeasureContext, ProcessContext, UiElement}, measure::{Hints, Response}, state::StateRepo, LayoutInfo, UiDirection, UiSize
UiSize,
LayoutInfo,
draw::{UiDrawCommand, UiDrawCommands},
measure::{Response, Hints},
state::StateRepo,
element::UiElement
}; };
#[derive(Clone, Copy, PartialEq, Eq, Debug)] #[derive(Clone, Copy, PartialEq, Eq, Debug)]
@ -106,15 +100,19 @@ impl Container {
} }
impl UiElement for Container { impl UiElement for Container {
fn measure(&self, state: &StateRepo, layout: &LayoutInfo) -> Response { fn measure(&self, ctx: MeasureContext) -> Response {
let mut size = Vec2::ZERO; let mut size = Vec2::ZERO;
//if matches!(self.size.0, UiSize::Auto) || matches!(self.size.1, UiSize::Auto) { //if matches!(self.size.0, UiSize::Auto) || matches!(self.size.1, UiSize::Auto) {
let mut leftover_gap = Vec2::ZERO; let mut leftover_gap = Vec2::ZERO;
for element in &self.elements { for element in &self.elements {
let measure = element.measure(state, &LayoutInfo { let measure = element.measure(MeasureContext{
position: layout.position + size, state: ctx.state,
max_size: self.measure_max_inner_size(layout), // - size TODO layout: &LayoutInfo {
direction: self.direction, 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 { match self.direction {
UiDirection::Horizontal => { UiDirection::Horizontal => {
@ -140,12 +138,12 @@ impl UiElement for Container {
match self.size.0 { match self.size.0 {
UiSize::Auto => (), 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, UiSize::Pixels(pixels) => size.x = pixels,
} }
match self.size.1 { match self.size.1 {
UiSize::Auto => (), 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, 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) { fn process(&self, ctx: ProcessContext) {
let mut position = layout.position; let mut position = ctx.layout.position;
//background //background
if let Some(color) = self.background { if let Some(color) = self.background {
draw.add(UiDrawCommand::Rectangle { ctx.draw.add(UiDrawCommand::Rectangle {
position, position,
size: measure.size, size: ctx.measure.size,
color color
}); });
} }
@ -178,16 +176,16 @@ impl UiElement for Container {
match (self.align.0, self.direction) { match (self.align.0, self.direction) {
(Alignment::Begin, _) => (), (Alignment::Begin, _) => (),
(Alignment::Center, UiDirection::Horizontal) => { (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) => { (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) => { (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) => { (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 { let mut el_layout = LayoutInfo {
position, position,
max_size: self.measure_max_inner_size(layout), max_size: self.measure_max_inner_size(ctx.layout),
direction: self.direction, direction: self.direction,
}; };
//measure //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) //align (on sec. axis)
match (self.align.1, self.direction) { match (self.align.1, self.direction) {
(Alignment::Begin, _) => (), (Alignment::Begin, _) => (),
(Alignment::Center, UiDirection::Horizontal) => { (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) => { (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) => { (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) => { (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 //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 //layout
match self.direction { match self.direction {

View file

@ -1,10 +1,11 @@
use glam::{vec2, Vec4, vec4}; use glam::{vec2, Vec4, vec4};
use crate::{ use crate::{
UiSize, LayoutInfo,
draw::{UiDrawCommand, UiDrawCommands}, draw::{UiDrawCommand, UiDrawCommands},
element::{MeasureContext, ProcessContext, UiElement},
measure::Response, measure::Response,
state::StateRepo, state::StateRepo,
element::UiElement LayoutInfo,
UiSize
}; };
#[derive(Debug, Clone, Copy)] #[derive(Debug, Clone, Copy)]
@ -31,17 +32,17 @@ const BAR_HEIGHT: f32 = 20.0;
impl UiElement for ProgressBar { impl UiElement for ProgressBar {
fn name(&self) -> &'static str { "Progress bar" } fn name(&self) -> &'static str { "Progress bar" }
fn measure(&self, _: &StateRepo, layout: &LayoutInfo) -> Response { fn measure(&self, ctx: MeasureContext) -> Response {
Response { Response {
size: vec2( size: vec2(
match self.size.0 { match self.size.0 {
UiSize::Auto => layout.max_size.x.max(300.), UiSize::Auto => ctx.layout.max_size.x.max(300.),
UiSize::Percentage(p) => layout.max_size.x * p, UiSize::Percentage(p) => ctx.layout.max_size.x * p,
UiSize::Pixels(p) => p, UiSize::Pixels(p) => p,
}, },
match self.size.1 { match self.size.1 {
UiSize::Auto => BAR_HEIGHT, 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, 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.); let value = self.value.clamp(0., 1.);
if value < 1. { if value < 1. {
draw.add(UiDrawCommand::Rectangle { ctx.draw.add(UiDrawCommand::Rectangle {
position: layout.position, position: ctx.layout.position,
size: measure.size, size: ctx.measure.size,
color: self.color_background color: self.color_background
}); });
} }
if value > 0. { if value > 0. {
draw.add(UiDrawCommand::Rectangle { ctx.draw.add(UiDrawCommand::Rectangle {
position: layout.position, position: ctx.layout.position,
size: measure.size * vec2(value, 1.0), size: ctx.measure.size * vec2(value, 1.0),
color: self.color_foreground color: self.color_foreground
}); });
} }

View file

@ -1,11 +1,10 @@
use glam::{vec2, Vec4}; use glam::{vec2, Vec4};
use crate::{ use crate::{
LayoutInfo, draw::{UiDrawCommand, UiDrawCommands},
UiSize, element::{MeasureContext, ProcessContext, UiElement},
element::UiElement,
state::StateRepo,
measure::Response, measure::Response,
draw::{UiDrawCommand, UiDrawCommands} state::StateRepo,
LayoutInfo, UiSize
}; };
pub struct Rect { pub struct Rect {
@ -23,17 +22,17 @@ impl Default for Rect {
} }
impl UiElement for Rect { impl UiElement for Rect {
fn measure(&self, _state: &StateRepo, layout: &LayoutInfo) -> Response { fn measure(&self, ctx: MeasureContext) -> Response {
Response { Response {
size: vec2( size: vec2(
match self.size.0 { match self.size.0 {
UiSize::Auto => layout.max_size.x, UiSize::Auto => ctx.layout.max_size.x,
UiSize::Percentage(percentage) => layout.max_size.x * percentage, UiSize::Percentage(percentage) => ctx.layout.max_size.x * percentage,
UiSize::Pixels(pixels) => pixels, UiSize::Pixels(pixels) => pixels,
}, },
match self.size.1 { match self.size.1 {
UiSize::Auto => layout.max_size.y, UiSize::Auto => ctx.layout.max_size.y,
UiSize::Percentage(percentage) => layout.max_size.y * percentage, UiSize::Percentage(percentage) => ctx.layout.max_size.y * percentage,
UiSize::Pixels(pixels) => pixels, 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 { if let Some(color) = self.color {
draw.add(UiDrawCommand::Rectangle { ctx.draw.add(UiDrawCommand::Rectangle {
position: layout.position, position: ctx.layout.position,
size: measure.size, size: ctx.measure.size,
color, color,
}); });
} }

View file

@ -1,11 +1,8 @@
use glam::vec2; use glam::vec2;
use crate::{ use crate::{
LayoutInfo, element::{MeasureContext, ProcessContext, UiElement},
UiDirection,
element::UiElement,
state::StateRepo,
measure::Response, measure::Response,
draw::{UiDrawCommand, UiDrawCommands} UiDirection
}; };
pub struct Spacer(pub f32); pub struct Spacer(pub f32);
@ -17,9 +14,9 @@ impl Default for Spacer {
} }
impl UiElement for Spacer { impl UiElement for Spacer {
fn measure(&self, state: &StateRepo, layout: &LayoutInfo) -> Response { fn measure(&self, ctx: MeasureContext) -> Response {
Response { Response {
size: match layout.direction { size: match ctx.layout.direction {
UiDirection::Horizontal => vec2(self.0, 0.), UiDirection::Horizontal => vec2(self.0, 0.),
UiDirection::Vertical => vec2(0., self.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) {}
} }

View file

@ -1,12 +1,12 @@
use std::borrow::Cow; use std::borrow::Cow;
use glam::{vec2, Vec4}; use glam::{vec2, Vec4};
use crate::{ use crate::{
LayoutInfo, draw::{UiDrawCommand, UiDrawCommands},
UiSize, element::{MeasureContext, ProcessContext, UiElement},
element::UiElement,
state::StateRepo,
measure::Response, measure::Response,
draw::{UiDrawCommand, UiDrawCommands}, text::FontHandle state::StateRepo,
text::FontHandle,
LayoutInfo, UiSize
}; };
pub struct Text { pub struct Text {
@ -30,17 +30,23 @@ impl Default for Text {
} }
impl UiElement 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 { Response {
size: vec2( size: vec2(
match self.size.0 { match self.size.0 {
UiSize::Auto => layout.max_size.x, UiSize::Auto => size.0,
UiSize::Percentage(percentage) => layout.max_size.x * percentage, UiSize::Percentage(percentage) => ctx.layout.max_size.x * percentage,
UiSize::Pixels(pixels) => pixels, UiSize::Pixels(pixels) => pixels,
}, },
match self.size.1 { match self.size.1 {
UiSize::Auto => self.text_size as f32, UiSize::Auto => size.1,
UiSize::Percentage(percentage) => layout.max_size.y * percentage, UiSize::Percentage(percentage) => ctx.layout.max_size.y * percentage,
UiSize::Pixels(pixels) => pixels, 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) { fn process(&self, ctx: ProcessContext) {
draw.add(UiDrawCommand::Text { ctx.draw.add(UiDrawCommand::Text {
text: self.text.clone(), text: self.text.clone(),
position: layout.position, position: ctx.layout.position,
size: self.text_size, size: self.text_size,
color: self.color, color: self.color,
font: self.font font: self.font

View file

@ -1,4 +1,4 @@
use crate::{element::UiElement, draw::UiDrawCommands}; use crate::element::{UiElement, MeasureContext, ProcessContext};
pub struct Interactable<T: UiElement> { pub struct Interactable<T: UiElement> {
pub element: T, pub element: T,
@ -31,12 +31,12 @@ impl<T: UiElement> Interactable<T> {
} }
impl<T: UiElement> UiElement for Interactable<T> { impl<T: UiElement> UiElement for Interactable<T> {
fn measure(&self, state: &crate::state::StateRepo, layout: &crate::LayoutInfo) -> crate::measure::Response { fn measure(&self, ctx: MeasureContext) -> crate::measure::Response {
self.element.measure(state, layout) self.element.measure(ctx)
} }
fn process(&self, measure: &crate::measure::Response, state: &mut crate::state::StateRepo, layout: &crate::LayoutInfo, draw: &mut UiDrawCommands) { fn process(&self, ctx: ProcessContext) {
self.element.process(measure, state, layout, draw) self.element.process(ctx)
} }
} }

View file

@ -13,7 +13,7 @@ pub mod state;
pub mod text; pub mod text;
pub mod interaction; pub mod interaction;
use element::UiElement; use element::{MeasureContext, ProcessContext, UiElement};
use state::StateRepo; use state::StateRepo;
use draw::{UiDrawCommands, UiDrawPlan}; use draw::{UiDrawCommands, UiDrawPlan};
use text::{TextRenderer, FontTextureInfo, FontHandle}; use text::{TextRenderer, FontTextureInfo, FontHandle};
@ -65,8 +65,18 @@ impl UiInstance {
max_size, max_size,
direction: UiDirection::Vertical, direction: UiDirection::Vertical,
}; };
let measure = element.measure(&self.stateful_state, &layout); let measure = element.measure(MeasureContext {
element.process(&measure, &mut self.stateful_state, &layout, &mut self.draw_commands); 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) { pub fn begin(&mut self) {

View file

@ -48,3 +48,41 @@ impl Default for TextRenderer {
Self::new() 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)
}
}