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"
authors = ["griffi-gh <prasol258@gmail.com>"]
rust-version = "1.75"
version = "0.0.2"
version = "0.0.3"
edition = "2021"
license = "GPL-3.0-or-later"
publish = true

View file

@ -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<u64> { 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<Box<dyn Any>> { 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);
}

View file

@ -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 {

View file

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

View file

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

View file

@ -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) {}
}

View file

@ -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

View file

@ -1,4 +1,4 @@
use crate::{element::UiElement, draw::UiDrawCommands};
use crate::element::{UiElement, MeasureContext, ProcessContext};
pub struct Interactable<T: UiElement> {
pub element: T,
@ -31,12 +31,12 @@ impl<T: UiElement> Interactable<T> {
}
impl<T: UiElement> UiElement for Interactable<T> {
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)
}
}

View file

@ -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) {

View file

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