wip integrate hui-painter instead of hui::draw

This commit is contained in:
griffi-gh 2024-09-27 21:42:58 +02:00
parent 108deeab34
commit 3d01377eb2
25 changed files with 178 additions and 799 deletions

View file

@ -1,17 +1,19 @@
pub mod paint;
pub mod texture;
pub mod text;
pub mod util;
use text::FontManager;
use texture::TextureAtlas;
/// Painter instance, stores textures and fonts needed for rendering
#[derive(Default)]
pub struct Painter {
pub struct PainterInstance {
pub atlas: TextureAtlas,
pub fonts: FontManager,
}
impl Painter {
impl PainterInstance {
pub fn new() -> Self {
Self::default()
}

View file

@ -1,9 +1,12 @@
use glam::Vec2;
use crate::{paint::buffer::PaintBuffer, Painter};
use crate::{paint::buffer::PaintBuffer, PainterInstance};
// mod root;
// pub use root::RootCommand;
mod list;
pub use list::PaintList;
mod transform;
pub use transform::PaintTransform;
@ -19,15 +22,30 @@ pub trait PaintCommand {
///
/// Make sure to propagate this call to children!
#[allow(unused_variables)]
fn pre_paint(&self, ctx: &mut Painter) {}
fn pre_paint(&self, ctx: &mut PainterInstance) {}
/// Paint the command into the buffer
///
/// Do not allocate new textures or cache glyphs here, use `pre_paint` instead!\
/// (Doing this WILL lead to atlas corruption flicker for a single frame if it's forced to resize!)
fn paint(&self, ctx: &mut Painter, into: &mut PaintBuffer);
fn paint(&self, ctx: &mut PainterInstance, into: &mut PaintBuffer);
}
pub trait Measurable: PaintCommand {
fn size(&self, ctx: &Painter) -> Vec2;
fn size(&self, ctx: &PainterInstance) -> Vec2;
}
// TODO move paint_root to PaintCommand instead of separate trait?
pub trait PaintRoot: PaintCommand {
/// Paint the root command, calling `pre_paint` before painting
///
/// This is a convenience method for painting the root command
/// Do not use this inside the `paint` method of a command!
fn paint_root(&self, ctx: &mut PainterInstance, into: &mut PaintBuffer) {
self.pre_paint(ctx);
self.paint(ctx, into);
}
}
impl<T: PaintCommand> PaintRoot for T {}

View file

@ -0,0 +1,45 @@
use crate::PainterInstance;
use super::PaintCommand;
pub struct PaintList {
pub commands: Vec<Box<dyn PaintCommand>>,
}
impl PaintList {
pub fn new(commands: Vec<Box<dyn PaintCommand>>) -> Self {
Self {
commands
}
}
pub fn new_empty() -> Self {
Self {
commands: Vec::new(),
}
}
pub fn add(&mut self, command: impl PaintCommand + 'static) {
self.commands.push(Box::new(command));
}
}
impl Default for PaintList {
fn default() -> Self {
Self::new_empty()
}
}
impl PaintCommand for PaintList {
fn pre_paint(&self, ctx: &mut PainterInstance) {
for command in &self.commands {
command.pre_paint(ctx);
}
}
fn paint(&self, ctx: &mut crate::PainterInstance, into: &mut crate::paint::buffer::PaintBuffer) {
for command in &self.commands {
command.paint(ctx, into);
}
}
}

View file

@ -7,7 +7,7 @@ use crate::{
command::PaintCommand,
},
texture::TextureHandle,
Painter
PainterInstance
};
/// Calculate the number of points based on the maximum corner radius
@ -92,7 +92,7 @@ impl PaintRectangle {
}
impl PaintCommand for PaintRectangle {
fn paint(&self, ctx: &mut Painter, into: &mut PaintBuffer) {
fn paint(&self, ctx: &mut PainterInstance, into: &mut PaintBuffer) {
// If texture is set:
// - Get texture UV
// - Map local UVs to texture UV coords

View file

@ -1,11 +1,11 @@
use std::{borrow::Cow, sync::Arc};
use fontdue::layout::{self, CoordinateSystem, GlyphRasterConfig, Layout};
use std::borrow::Cow;
use fontdue::layout::{CoordinateSystem, Layout};
use glam::{vec2, Vec2};
use crate::{
paint::{
buffer::PaintBuffer,
command::PaintCommand,
}, text::FontHandle, Painter
}, text::FontHandle, PainterInstance
};
use super::Measurable;
@ -34,7 +34,7 @@ impl PaintText {
}
}
fn build_font_array<'a>(&self, ctx: &'a Painter) -> Vec<&'a fontdue::Font> {
fn build_font_array<'a>(&self, ctx: &'a PainterInstance) -> Vec<&'a fontdue::Font> {
let font = ctx.fonts.get_fontdue_font(self.text.font)
.expect("FontHandle is invalid");
vec![&font]
@ -55,7 +55,7 @@ impl PaintText {
}
impl PaintCommand for PaintText {
fn pre_paint(&self, ctx: &mut Painter) {
fn pre_paint(&self, ctx: &mut PainterInstance) {
let font_array = self.build_font_array(ctx);
let layout = self.build_layout(&font_array);
@ -64,7 +64,7 @@ impl PaintCommand for PaintText {
}
}
fn paint(&self, ctx: &mut Painter, into: &mut PaintBuffer) {
fn paint(&self, ctx: &mut PainterInstance, into: &mut PaintBuffer) {
// let font_array = self.build_font_array(ctx);
// let layout = self.build_layout(&font_array);
@ -80,7 +80,7 @@ impl PaintCommand for PaintText {
}
impl Measurable for PaintText {
fn size(&self, ctx: &Painter) -> Vec2 {
fn size(&self, ctx: &PainterInstance) -> Vec2 {
let font_array = self.build_font_array(ctx);
let layout = self.build_layout(&font_array);

View file

@ -1,31 +1,27 @@
use crate::{
Painter,
PainterInstance,
paint::{
buffer::PaintBuffer,
command::PaintCommand,
},
};
pub struct PaintTransform {
pub struct PaintTransform<T: PaintCommand + 'static> {
pub transform: glam::Affine2,
pub children: Vec<Box<dyn PaintCommand>>,
pub child: T,
}
impl PaintCommand for PaintTransform {
fn pre_paint(&self, ctx: &mut Painter) {
for child in &self.children {
child.pre_paint(ctx);
}
impl<T: PaintCommand + 'static> PaintCommand for PaintTransform<T> {
fn pre_paint(&self, ctx: &mut PainterInstance) {
self.child.pre_paint(ctx);
}
fn paint(&self, ctx: &mut Painter, into: &mut PaintBuffer) {
fn paint(&self, ctx: &mut PainterInstance, into: &mut PaintBuffer) {
// remember the starting index
let starting_index = into.vertices.len();
// paint children nodes
for child in &self.children {
child.paint(ctx, into);
}
// paint children node
self.child.paint(ctx, into);
let mut min_point = glam::Vec2::splat(f32::MAX);
let mut max_point = glam::Vec2::splat(f32::MIN);

View file

@ -23,7 +23,7 @@ impl FontManager {
///
/// Panics:
/// - If the font data is invalid.
pub fn add_font(&mut self, data: &[u8]) -> FontHandle {
pub fn add(&mut self, data: &[u8]) -> FontHandle {
let font = self.fonts.add_font(data);
self.ftm.init_font(font);
font
@ -33,7 +33,7 @@ impl FontManager {
///
/// Panics:
/// - If the font handle is invalid.
pub fn remove_font(&mut self, font: FontHandle, atlas: &mut TextureAtlas) {
pub fn remove(&mut self, font: FontHandle, atlas: &mut TextureAtlas) {
self.ftm.drop_font(font, atlas);
self.fonts.remove_font(font);
}

View file

@ -141,6 +141,9 @@ pub struct TextureAtlas {
/// Deallocated allocations that can be reused, sorted by size
//TODO: use binary heap or btreeset for reuse_allocations instead, but this works for now
reuse_allocations: Vec<TextureAllocation>,
/// Version of the texture atlas, incremented every time the atlas is modified
version: u64,
}
impl TextureAtlas {
@ -158,9 +161,19 @@ impl TextureAtlas {
next_id: 0,
allocations: HashMap::default(),
reuse_allocations: Vec::new(),
version: 0,
}
}
pub fn version(&self) -> u64 {
self.version
}
fn mark_modified(&mut self) {
// XXX: wrapping_add? will this *ever* overflow?
self.version = self.version.wrapping_add(1);
}
/// Get the next handle
///
/// Does not allocate a texture associated with it
@ -303,6 +316,8 @@ impl TextureAtlas {
}
}
}
self.mark_modified();
}
/// Allocate a texture in the atlas, returning a handle to it.\

0
hui-painter/src/util.rs Normal file
View file

View file

@ -1,396 +0,0 @@
//! draw commands, tesselation and UI rendering.
use crate::{
rect::Corners,
text::{FontHandle, TextRenderer}
};
pub(crate) mod atlas;
use atlas::TextureAtlasManager;
pub use atlas::{ImageHandle, TextureAtlasMeta, TextureFormat, ImageCtx};
mod corner_radius;
pub use corner_radius::RoundedCorners;
use std::borrow::Cow;
use fontdue::layout::{Layout, CoordinateSystem, TextStyle};
use glam::{vec2, Vec2, Affine2, Vec4};
//TODO: circle draw command
/// Available draw commands
/// - Rectangle: Filled, colored rectangle, with optional rounded corners and texture
/// - Text: Draw text using the specified font, size, color, and position
#[derive(Clone, Debug, PartialEq)]
pub enum UiDrawCommand {
///Filled, colored rectangle
Rectangle {
///Position in pixels
position: Vec2,
///Size in pixels
size: Vec2,
///Color (RGBA)
color: Corners<Vec4>,
///Texture
texture: Option<ImageHandle>,
///Sub-UV coordinates for the texture
texture_uv: Option<Corners<Vec2>>,
///Rounded corners
rounded_corners: Option<RoundedCorners>,
},
/// Draw text using the specified font, size, color, and position
Text {
///Position in pixels
position: Vec2,
///Font size
size: u16,
///Color (RGBA)
color: Vec4,
///Text to draw
text: Cow<'static, str>,
///Font handle to use
font: FontHandle,
},
/// Push a transformation matrix to the stack
PushTransform(Affine2),
/// Pop a transformation matrix from the stack
PopTransform,
//TODO PushClip PopClip
}
/// List of draw commands
#[derive(Default)]
pub struct UiDrawCommandList {
pub commands: Vec<UiDrawCommand>,
}
impl UiDrawCommandList {
/// Add a draw command to the list
pub fn add(&mut self, command: UiDrawCommand) {
self.commands.push(command);
}
}
// impl UiDrawCommands {
// pub fn compare(&self, other: &Self) -> bool {
// // if self.commands.len() != other.commands.len() { return false }
// // self.commands.iter().zip(other.commands.iter()).all(|(a, b)| a == b)
// }
// }
/// A vertex for UI rendering
#[derive(Clone, Copy, Debug, PartialEq, Default)]
pub struct UiVertex {
pub position: Vec2,
pub color: Vec4,
pub uv: Vec2,
}
/// Represents a single draw call (vertices + indices), should be handled by the render backend
#[derive(Default)]
pub struct UiDrawCall {
pub vertices: Vec<UiVertex>,
pub indices: Vec<u32>,
}
impl UiDrawCall {
/// Tesselate the UI and build a complete draw plan from a list of draw commands
pub(crate) fn build(draw_commands: &UiDrawCommandList, atlas: &mut TextureAtlasManager, text_renderer: &mut TextRenderer) -> Self {
let mut trans_stack = Vec::new();
let mut draw_call = UiDrawCall::default();
//HACK: atlas may get resized while creating new glyphs,
//which invalidates all uvs, causing corrupted-looking texture
//so we need to pregenerate font textures before generating any vertices
//we are doing *a lot* of double work here, but it's the easiest way to avoid the issue
for comamnd in &draw_commands.commands {
if let UiDrawCommand::Text { text, font: font_handle, size, .. } = comamnd {
let mut layout = Layout::new(CoordinateSystem::PositiveYDown);
layout.append(
&[text_renderer.internal_font(*font_handle)],
&TextStyle::new(text, *size as f32, 0)
);
let glyphs = layout.glyphs();
for layout_glyph in glyphs {
if !layout_glyph.char_data.rasterize() { continue }
text_renderer.glyph(atlas, *font_handle, layout_glyph.parent, layout_glyph.key.px as u8);
}
}
}
//note to future self:
//RESIZING OR ADDING STUFF TO ATLAS AFTER THIS POINT IS A BIG NO-NO,
//DON'T DO IT EVER AGAIN UNLESS YOU WANT TO SPEND HOURS DEBUGGING
atlas.lock_atlas = true;
for command in &draw_commands.commands {
match command {
UiDrawCommand::PushTransform(trans) => {
//Take note of the current index, and the transformation matrix\
//We will actually apply the transformation matrix when we pop it,
//to all vertices between the current index and the index we pushed
trans_stack.push((trans, draw_call.vertices.len() as u32));
},
UiDrawCommand::PopTransform => {
//Pop the transformation matrix and apply it to all vertices between the current index and the index we pushed
let (&trans, idx) = trans_stack.pop().expect("Unbalanced push/pop transform");
//If Push is immediately followed by a pop (which is dumb but possible), we don't need to do anything
//(this can also happen if push and pop are separated by a draw command that doesn't add any vertices, like a text command with an empty string)
if idx == draw_call.vertices.len() as u32 {
continue
}
//Kinda a hack:
//We want to apply the transform aronnd the center, so we need to compute the center of the vertices
//We won't actually do that, we will compute the center of the bounding box of the vertices
let mut min = Vec2::splat(std::f32::INFINITY);
let mut max = Vec2::splat(std::f32::NEG_INFINITY);
for v in &draw_call.vertices[idx as usize..] {
min = min.min(v.position);
max = max.max(v.position);
}
//TODO: make the point of transform configurable
let center = (min + max) / 2.;
//Apply trans mtx to all vertices between idx and the current index
for v in &mut draw_call.vertices[idx as usize..] {
v.position -= center;
v.position = trans.transform_point2(v.position);
v.position += center;
}
},
UiDrawCommand::Rectangle { position, size, color, texture, texture_uv, rounded_corners } => {
let uvs = texture
.map(|x| atlas.get_uv(x))
.flatten()
.map(|guv| {
if let Some(texture_uv) = texture_uv {
//XXX: this may not work if the texture is rotated
//also is this slow?
let top = guv.top_left.lerp(guv.top_right, texture_uv.top_left.x);
let bottom = guv.bottom_left.lerp(guv.bottom_right, texture_uv.top_left.x);
let top_left = top.lerp(bottom, texture_uv.top_left.y);
let top = guv.top_left.lerp(guv.top_right, texture_uv.top_right.x);
let bottom = guv.bottom_left.lerp(guv.bottom_right, texture_uv.top_right.x);
let top_right = top.lerp(bottom, texture_uv.top_right.y);
let top = guv.top_left.lerp(guv.top_right, texture_uv.bottom_left.x);
let bottom = guv.bottom_left.lerp(guv.bottom_right, texture_uv.bottom_left.x);
let bottom_left = top.lerp(bottom, texture_uv.bottom_left.y);
let top = guv.top_left.lerp(guv.top_right, texture_uv.bottom_right.x);
let bottom = guv.bottom_left.lerp(guv.bottom_right, texture_uv.bottom_right.x);
let bottom_right = top.lerp(bottom, texture_uv.bottom_right.y);
Corners { top_left, top_right, bottom_left, bottom_right }
} else {
guv
}
})
.unwrap_or(Corners::all(Vec2::ZERO));
let vidx = draw_call.vertices.len() as u32;
if let Some(corner) = rounded_corners.filter(|x| x.radius.max_f32() > 0.0) {
//this code is stupid as fuck
//but it works... i think?
//maybe some verts end up missing, but it's close enough...
//Random vert in the center for no reason
//lol
draw_call.vertices.push(UiVertex {
position: *position + *size * vec2(0.5, 0.5),
color: (color.bottom_left + color.bottom_right + color.top_left + color.top_right) / 4.,
//TODO: fix this uv
uv: vec2(0., 0.),
});
//TODO: fix some corners tris being invisible (but it's already close enough lol)
let rounded_corner_verts = corner.point_count.get() as u32;
for i in 0..rounded_corner_verts {
let cratio = i as f32 / (rounded_corner_verts - 1) as f32;
let angle = cratio * std::f32::consts::PI * 0.5;
let x = angle.sin();
let y = angle.cos();
let mut corner_impl = |rp: Vec2, color: &Corners<Vec4>| {
let rrp = rp / *size;
let color_at_point =
color.bottom_right * rrp.x * rrp.y +
color.top_right * rrp.x * (1. - rrp.y) +
color.bottom_left * (1. - rrp.x) * rrp.y +
color.top_left * (1. - rrp.x) * (1. - rrp.y);
let uv_at_point =
uvs.bottom_right * rrp.x * rrp.y +
uvs.top_right * rrp.x * (1. - rrp.y) +
uvs.bottom_left * (1. - rrp.x) * rrp.y +
uvs.top_left * (1. - rrp.x) * (1. - rrp.y);
draw_call.vertices.push(UiVertex {
position: *position + rp,
color: color_at_point,
uv: uv_at_point,
});
};
//Top-right corner
corner_impl(
vec2(x, 1. - y) * corner.radius.top_right + vec2(size.x - corner.radius.top_right, 0.),
color,
);
//Bottom-right corner
corner_impl(
vec2(x - 1., y) * corner.radius.bottom_right + vec2(size.x, size.y - corner.radius.bottom_right),
color,
);
//Bottom-left corner
corner_impl(
vec2(1. - x, y) * corner.radius.bottom_left + vec2(0., size.y - corner.radius.bottom_left),
color,
);
//Top-left corner
corner_impl(
vec2(1. - x, 1. - y) * corner.radius.top_left,
color,
);
// mental illness:
if i > 0 {
draw_call.indices.extend([
//Top-right corner
vidx,
vidx + 1 + (i - 1) * 4,
vidx + 1 + i * 4,
//Bottom-right corner
vidx,
vidx + 1 + (i - 1) * 4 + 1,
vidx + 1 + i * 4 + 1,
//Bottom-left corner
vidx,
vidx + 1 + (i - 1) * 4 + 2,
vidx + 1 + i * 4 + 2,
//Top-left corner
vidx,
vidx + 1 + (i - 1) * 4 + 3,
vidx + 1 + i * 4 + 3,
]);
}
}
//Fill in the rest
//mental illness 2:
draw_call.indices.extend([
//Top
vidx,
vidx + 4,
vidx + 1,
//Right?, i think
vidx,
vidx + 1 + (rounded_corner_verts - 1) * 4,
vidx + 1 + (rounded_corner_verts - 1) * 4 + 1,
//Left???
vidx,
vidx + 1 + (rounded_corner_verts - 1) * 4 + 2,
vidx + 1 + (rounded_corner_verts - 1) * 4 + 3,
//Bottom???
vidx,
vidx + 3,
vidx + 2,
]);
} else {
//...Normal rectangle
draw_call.indices.extend([vidx, vidx + 1, vidx + 2, vidx, vidx + 2, vidx + 3]);
draw_call.vertices.extend([
UiVertex {
position: *position,
color: color.top_left,
uv: uvs.top_left,
},
UiVertex {
position: *position + vec2(size.x, 0.0),
color: color.top_right,
uv: uvs.top_right,
},
UiVertex {
position: *position + *size,
color: color.bottom_right,
uv: uvs.bottom_right,
},
UiVertex {
position: *position + vec2(0.0, size.y),
color: color.bottom_left,
uv: uvs.bottom_left,
},
]);
}
},
UiDrawCommand::Text { position, size, color, text, font: font_handle } => {
if text.is_empty() {
continue
}
//XXX: should we be doing this every time?
let mut layout = Layout::new(CoordinateSystem::PositiveYDown);
layout.append(
&[text_renderer.internal_font(*font_handle)],
&TextStyle::new(text, *size as f32, 0)
);
let glyphs = layout.glyphs();
for layout_glyph in glyphs {
if !layout_glyph.char_data.rasterize() {
continue
}
let vidx = draw_call.vertices.len() as u32;
let glyph = text_renderer.glyph(atlas, *font_handle, layout_glyph.parent, layout_glyph.key.px as u8);
let uv = atlas.get_uv(glyph.texture).unwrap();
draw_call.indices.extend([vidx, vidx + 1, vidx + 2, vidx, vidx + 2, vidx + 3]);
draw_call.vertices.extend([
UiVertex {
position: *position + vec2(layout_glyph.x, layout_glyph.y),
color: *color,
uv: uv.top_left,
},
UiVertex {
position: *position + vec2(layout_glyph.x + glyph.metrics.width as f32, layout_glyph.y),
color: *color,
uv: uv.top_right,
},
UiVertex {
position: *position + vec2(layout_glyph.x + glyph.metrics.width as f32, layout_glyph.y + glyph.metrics.height as f32),
color: *color,
uv: uv.bottom_right,
},
UiVertex {
position: *position + vec2(layout_glyph.x, layout_glyph.y + glyph.metrics.height as f32),
color: *color,
uv: uv.bottom_left,
},
]);
#[cfg(all(
feature = "pixel_perfect_text",
not(feature = "pixel_perfect")
))] {
//Round the position of the vertices to the nearest pixel, unless any transformations are active
if trans_stack.is_empty() {
for vtx in &mut draw_call.vertices[(vidx as usize)..] {
vtx.position = vtx.position.round()
}
}
}
}
}
}
}
atlas.lock_atlas = false;
#[cfg(feature = "pixel_perfect")]
draw_call.vertices.iter_mut().for_each(|v| {
v.position = v.position.round()
});
draw_call
}
}

View file

@ -1,16 +1,15 @@
//! element API and built-in elements like `Container`, `Button`, `Text`, etc.
use crate::{
draw::{atlas::ImageCtx, UiDrawCommandList},
input::InputCtx,
layout::{LayoutInfo, Size2d},
measure::Response,
rect::Rect,
signal::SignalStore,
state::StateRepo,
text::{FontHandle, TextMeasure},
UiInstance,
};
use hui_painter::PainterInstance;
mod builtin;
pub use builtin::*;
@ -19,9 +18,10 @@ pub use builtin::*;
pub struct MeasureContext<'a> {
pub layout: &'a LayoutInfo,
pub state: &'a StateRepo,
pub text_measure: TextMeasure<'a>,
pub current_font: FontHandle,
pub images: ImageCtx<'a>,
pub painter: &'a PainterInstance,
// pub text_measure: TextMeasure<'a>,
// pub current_font: FontHandle,
// pub images: ImageCtx<'a>,
//XXX: should measure have a reference to input?
//pub input: InputCtx<'a>,
}
@ -30,11 +30,8 @@ pub struct MeasureContext<'a> {
pub struct ProcessContext<'a> {
pub measure: &'a Response,
pub layout: &'a LayoutInfo,
pub draw: &'a mut UiDrawCommandList,
pub painter: &'a mut PainterInstance,
pub state: &'a mut StateRepo,
pub text_measure: TextMeasure<'a>,
pub current_font: FontHandle,
pub images: ImageCtx<'a>,
pub input: InputCtx<'a>,
pub signal: &'a mut SignalStore,
}

View file

@ -375,7 +375,7 @@ impl UiElement for Container {
// });
// }
self.background_frame.draw(ctx.draw, (ctx.layout.position, ctx.measure.size).into());
self.background_frame.draw(ctx.paint, (ctx.layout.position, ctx.measure.size).into());
//padding
position += vec2(self.padding.left, self.padding.top);
@ -487,7 +487,7 @@ impl UiElement for Container {
element.process(ProcessContext {
measure: &el_measure,
layout: &el_layout,
draw: ctx.draw,
paint: ctx.paint,
state: ctx.state,
text_measure: ctx.text_measure,
current_font: ctx.current_font,

View file

@ -63,6 +63,6 @@ impl UiElement for FrameView {
}
fn process(&self, ctx: ProcessContext) {
self.frame.draw(ctx.draw, (ctx.layout.position, ctx.measure.size).into());
self.frame.draw(ctx.paint, (ctx.layout.position, ctx.measure.size).into());
}
}

View file

@ -78,7 +78,7 @@ impl UiElement for Image {
fn process(&self, ctx: ProcessContext) {
if !self.color.is_transparent() {
ctx.draw.add(UiDrawCommand::Rectangle {
ctx.paint.add(UiDrawCommand::Rectangle {
position: ctx.layout.position,
size: ctx.measure.size,
color: self.color.corners(),

View file

@ -75,10 +75,10 @@ impl UiElement for ProgressBar {
//FIXME: these optimizations may not be valid
if value < 1. || !self.foreground.covers_opaque() {
self.background.draw(ctx.draw, (ctx.layout.position, ctx.measure.size).into());
self.background.draw(ctx.paint, (ctx.layout.position, ctx.measure.size).into());
}
if value > 0. {
self.foreground.draw(ctx.draw, (ctx.layout.position, ctx.measure.size * vec2(value, 1.)).into());
self.foreground.draw(ctx.paint, (ctx.layout.position, ctx.measure.size * vec2(value, 1.)).into());
}
// let rounded_corners =

View file

@ -155,7 +155,7 @@ impl UiElement for Slider {
// if !(self.track_color.is_transparent() || (self.track_active_color.is_opaque() && self.handle_color.is_opaque() && self.value >= 1.)) {
if !(self.track_active.covers_opaque() && self.handle.covers_opaque() && (self.handle_size.1 >= self.track_height) && self.value >= 1.) {
self.track.draw(
ctx.draw,
ctx.paint,
(
ctx.layout.position + ctx.measure.size * vec2(0., 0.5 - self.track_height / 2.),
ctx.measure.size * vec2(1., self.track_height),
@ -169,7 +169,7 @@ impl UiElement for Slider {
// if !(self.track_active_color.is_transparent() || (self.value <= 0. && self.handle_color.is_opaque())) {
if !(self.handle.covers_opaque() && (self.handle_size.1 >= self.track_height) && self.value <= 0.) {
self.track_active.draw(
ctx.draw,
ctx.paint,
(
ctx.layout.position + ctx.measure.size * vec2(0., 0.5 - self.track_height / 2.),
(ctx.measure.size - handle_size * Vec2::X) * vec2(self.value, self.track_height) + handle_size * Vec2::X / 2.,
@ -190,7 +190,7 @@ impl UiElement for Slider {
// }
if (self.handle_size.0 > 0. && self.handle_size.1 > 0.) {
self.handle.draw(
ctx.draw,
ctx.paint,
(
ctx.layout.position +
((ctx.measure.size.x - handle_size.x) * self.value) * Vec2::X +

View file

@ -96,7 +96,7 @@ impl UiElement for Text {
if self.text.is_empty() || self.color.w == 0. {
return
}
ctx.draw.add(UiDrawCommand::Text {
ctx.paint.add(UiDrawCommand::Text {
text: self.text.clone(),
position: ctx.layout.position,
size: self.text_size,

View file

@ -46,20 +46,20 @@ impl UiElement for Transformer {
}
fn process(&self, ctx: ProcessContext) {
ctx.draw.add(UiDrawCommand::PushTransform(self.transform));
ctx.paint.add(UiDrawCommand::PushTransform(self.transform));
//This is stupid:
self.element.process(ProcessContext {
measure: ctx.measure,
state: ctx.state,
layout: ctx.layout,
draw: ctx.draw,
paint: ctx.paint,
text_measure: ctx.text_measure,
current_font: ctx.current_font,
images: ctx.images,
input: ctx.input,
signal: ctx.signal,
});
ctx.draw.add(UiDrawCommand::PopTransform);
ctx.paint.add(UiDrawCommand::PopTransform);
}
}

View file

@ -1,6 +1,7 @@
//! modular procedural background system
use crate::{draw::UiDrawCommandList, rect::Rect};
use crate::rect::Rect;
use hui_painter::PainterInstance;
pub mod point;
mod rect;
@ -13,7 +14,7 @@ pub use rect::RectFrame;
/// Trait for a drawable frame
pub trait Frame {
/// Draw the frame at the given rect's position and size
fn draw(&self, draw: &mut UiDrawCommandList, rect: Rect);
fn draw(&self, draw: &mut PainterInstance, rect: Rect);
/// Check if the frame is guaranteed to be fully opaque and fully cover the parent frame regardless of it's size
///

View file

@ -1,15 +1,12 @@
use glam::Vec2;
use hui_painter::{
PainterInstance,
paint::command::PaintList,
text::FontHandle,
texture::{SourceTextureFormat, TextureHandle},
};
use crate::{
element::{MeasureContext, ProcessContext, UiElement},
layout::{Direction, LayoutInfo},
text::{FontHandle, TextRenderer},
draw::{
ImageHandle,
TextureFormat,
UiDrawCall,
UiDrawCommandList,
atlas::{TextureAtlasManager, TextureAtlasMeta},
},
signal::{Signal, SignalStore},
event::{EventQueue, UiEvent},
input::UiInputState,
@ -17,21 +14,26 @@ use crate::{
state::StateRepo,
};
pub struct RenderInfo<'a> {
pub id: u64,
pub list: &'a PaintList,
}
/// The main instance of the UI system.
///
/// In most cases, you should only have one instance of this struct, but multiple instances are allowed\
/// (Please note that it's possible to render multiple UI "roots" using a single instance)
pub struct UiInstance {
stateful_state: StateRepo,
prev_draw_commands: UiDrawCommandList,
draw_commands: UiDrawCommandList,
draw_call: UiDrawCall,
draw_call_modified: bool,
text_renderer: TextRenderer,
atlas: TextureAtlasManager,
events: EventQueue,
input: UiInputState,
signal: SignalStore,
painter: PainterInstance,
draw_commands: PaintList,
prev_draw_commands: PaintList,
draw_call_id: u64,
/// True if in the middle of a laying out a frame
state: bool,
}
@ -44,19 +46,12 @@ impl UiInstance {
UiInstance {
//mouse_position: Vec2::ZERO,
stateful_state: StateRepo::new(),
//event_queue: VecDeque::new(),
// root_elements: Vec::new(),
prev_draw_commands: UiDrawCommandList::default(),
draw_commands: UiDrawCommandList::default(),
draw_call: UiDrawCall::default(),
draw_call_modified: false,
// ftm: FontTextureManager::default(),
text_renderer: TextRenderer::new(),
atlas: {
let mut atlas = TextureAtlasManager::default();
atlas.add_dummy();
atlas
},
painter: PainterInstance::new(),
draw_commands: PaintList::new_empty(),
prev_draw_commands: PaintList::new_empty(),
draw_call_id: 0,
events: EventQueue::new(),
input: UiInputState::new(),
signal: SignalStore::new(),
@ -71,8 +66,9 @@ impl UiInstance {
///
/// ## Panics:
/// If the font data is invalid or corrupt
#[deprecated(since = "0.1.0-alpha.5", note = "Use painter.fonts.add_font() instead")]
pub fn add_font(&mut self, font: &[u8]) -> FontHandle {
self.text_renderer.add_font_from_bytes(font)
self.painter.fonts.add(font)
}
/// Add an image to the texture atlas\
@ -81,14 +77,18 @@ impl UiInstance {
/// Returns an image handle ([`ImageHandle`])\
/// This handle can be used to reference the texture in draw commands\
/// It's a light reference and can be cloned/copied freely, but will not be cleaned up even when dropped
pub fn add_image(&mut self, format: TextureFormat, data: &[u8], width: usize) -> ImageHandle {
self.atlas.add(width, data, format)
#[deprecated(since = "0.1.0-alpha.5", note = "Use painter.atlas.allocate_with_data() instead")]
pub fn add_image(&mut self, format: SourceTextureFormat, data: &[u8], width: usize) -> TextureHandle {
self.painter.atlas.allocate_with_data(format, data, width)
}
//TODO better error handling
/// ## DEPRECATED: This method will be removed in the future
///
/// ---
///
/// Add an image from a file to the texture atlas\
/// (experimental, may be removed in the future)
///
/// Requires the `image` feature
///
@ -96,7 +96,8 @@ impl UiInstance {
/// - If the file exists but contains invalid image data\
/// (this will change to a soft error in the future)
#[cfg(feature = "image")]
pub fn add_image_file_path(&mut self, path: impl AsRef<std::path::Path>) -> Result<ImageHandle, std::io::Error> {
#[deprecated(since = "0.1.0-alpha.5", note = "Will be removed in the future in favor of modular image loading in hui-painter")]
pub fn add_image_file_path(&mut self, path: impl AsRef<std::path::Path>) -> Result<TextureHandle, std::io::Error> {
use std::io::{Read, Seek};
// Open the file (and wrap it in a bufreader)
@ -115,8 +116,9 @@ impl UiInstance {
let image_rgba = image.as_rgba8().unwrap();
//Add the image to the atlas
let handle = self.add_image(
TextureFormat::Rgba,
let handle = self.painter.atlas.allocate_with_data(
SourceTextureFormat::RGBA8,
image_rgba,
image.width() as usize
);
@ -124,28 +126,6 @@ impl UiInstance {
Ok(handle)
}
/// Push a font to the font stack\
/// The font will be used for all text rendering until it is popped
///
/// This function is useful for replacing the default font, use sparingly\
/// (This library attempts to be stateless, however passing the font to every text element is not very practical)
pub fn push_font(&mut self, font: FontHandle) {
self.text_renderer.push_font(font);
}
/// Pop a font from the font stack\
///
/// ## Panics:
/// If the font stack is empty
pub fn pop_font(&mut self) {
self.text_renderer.pop_font();
}
/// Get the current default font
pub fn current_font(&self) -> FontHandle {
self.text_renderer.current_font()
}
/// Add an element or an element tree to the UI
///
/// Use the `rect` parameter to specify the position and size of the element\
@ -165,18 +145,13 @@ impl UiInstance {
let measure = element.measure(MeasureContext {
state: &self.stateful_state,
layout: &layout,
text_measure: self.text_renderer.to_measure(),
current_font: self.text_renderer.current_font(),
images: self.atlas.context(),
painter: &self.painter,
});
element.process(ProcessContext {
measure: &measure,
state: &mut self.stateful_state,
layout: &layout,
draw: &mut self.draw_commands,
text_measure: self.text_renderer.to_measure(),
current_font: self.text_renderer.current_font(),
images: self.atlas.context(),
painter: &mut self.painter,
input: self.input.ctx(),
signal: &mut self.signal,
});
@ -202,10 +177,10 @@ impl UiInstance {
//then, reset the draw commands
std::mem::swap(&mut self.prev_draw_commands, &mut self.draw_commands);
self.draw_commands.commands.clear();
self.draw_call_modified = false;
// self.draw_call_modified = false;
//reset atlas modification flag
self.atlas.reset_modified();
// self.atlas.reset_modified();
}
/// End the frame and prepare the UI for rendering\
@ -225,8 +200,7 @@ impl UiInstance {
}
//if they have, rebuild the draw call and set the modified flag
self.draw_call = UiDrawCall::build(&self.draw_commands, &mut self.atlas, &mut self.text_renderer);
self.draw_call_modified = true;
self.draw_call_id += 1;
}
/// Get the draw call information for the current frame
@ -234,18 +208,19 @@ impl UiInstance {
/// This function should only be used by the 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 buffers have been modified since the last frame
///
/// You should only call this function *after* [`UiInstance::end`]\
/// Calling it in the middle of a frame will result in a warning but will not cause a panic\
/// (please note that doing so is probably a mistake and should be fixed in your code)\
/// Doing so anyway will return draw call data for the previous frame, but the `modified` flag will *always* be incorrect until [`UiInstance::end`] is called
///
pub fn draw_call(&self) -> (bool, &UiDrawCall) {
pub fn draw_call(&self) -> RenderInfo{
if self.state {
log::warn!("UiInstance::draw_call called while in the middle of a frame, this is probably a mistake");
}
(self.draw_call_modified, &self.draw_call)
RenderInfo {
id: self.draw_call_id,
list: &self.draw_commands,
}
}
/// Get the texture atlas size and data for the current frame

View file

@ -11,6 +11,9 @@
#![forbid(unsafe_op_in_unsafe_fn)]
#![allow(unused_parens)]
// Re-export hui-painter
pub use hui_painter as painter;
pub use hui_shared::*;
mod instance;
@ -19,10 +22,8 @@ pub mod layout;
pub mod element;
pub mod event;
pub mod input;
pub mod draw;
pub mod measure;
pub mod state;
pub mod text;
pub mod signal;
pub mod frame;

View file

@ -1,106 +0,0 @@
//! text rendering, styling, measuring
use std::sync::Arc;
use fontdue::{Font, FontSettings};
use crate::draw::atlas::TextureAtlasManager;
mod font;
mod ftm;
mod stack;
/// Built-in font handle
#[cfg(feature="builtin_font")]
pub use font::BUILTIN_FONT;
pub use font::FontHandle;
use font::FontManager;
use ftm::FontTextureManager;
use ftm::GlyphCacheEntry;
use stack::FontStack;
pub(crate) struct TextRenderer {
manager: FontManager,
ftm: FontTextureManager,
stack: FontStack,
}
impl TextRenderer {
pub fn new() -> Self {
Self {
manager: FontManager::new(),
ftm: FontTextureManager::default(),
stack: FontStack::new(),
}
}
pub fn add_font_from_bytes(&mut self, font: &[u8]) -> FontHandle {
self.manager.add_font(Font::from_bytes(font, FontSettings::default()).unwrap())
}
pub fn glyph(&mut self, atlas: &mut TextureAtlasManager, font_handle: FontHandle, character: char, size: u8) -> Arc<GlyphCacheEntry> {
self.ftm.glyph(atlas, &self.manager, font_handle, character, size)
}
pub fn push_font(&mut self, font: FontHandle) {
self.stack.push(font);
}
pub fn pop_font(&mut self) {
self.stack.pop();
}
pub fn current_font(&self) -> FontHandle {
self.stack.current_or_default()
}
pub(crate) fn internal_font(&self, handle: FontHandle) -> &Font {
self.manager.get(handle).unwrap()
}
}
impl Default for TextRenderer {
fn default() -> Self {
Self::new()
}
}
/// Size of measured text
pub struct TextMeasureResponse {
pub max_width: f32,
pub height: f32,
}
/// Context for measuring text
#[derive(Clone, Copy)]
pub struct TextMeasure<'a>(&'a TextRenderer);
impl<'a> TextMeasure<'a> {
/// Measure the given string of text with the given font and size
pub fn measure(&self, font: FontHandle, size: u16, 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(),
}
}
}
impl TextRenderer {
pub fn to_measure(&self) -> TextMeasure {
TextMeasure(self)
}
pub fn measure(&self, font: FontHandle, size: u16, text: &str) -> TextMeasureResponse {
TextMeasure(self).measure(font, size, text)
}
}

View file

@ -1,65 +0,0 @@
use fontdue::Font;
/// Font handle, stores the internal font id and can be cheaply copied.
///
/// Only valid for the `UiInstance` that created it.\
/// Using it with other instances may result in panics or unexpected behavior.
///
/// Handle values are not guaranteed to be valid.\
/// Creating or transmuting an invalid handle is allowed and is *not* UB.
///
/// Internal value is an implementation detail and should not be relied upon.
#[derive(Clone, Copy, PartialEq, Eq, Hash, Debug)]
pub struct FontHandle(pub(crate) usize);
#[cfg(feature = "builtin_font")]
pub const BUILTIN_FONT: FontHandle = FontHandle(0);
impl Default for FontHandle {
/// Default font handle is the builtin font, if the feature is enabled;\
/// Otherwise returns an invalid handle.
fn default() -> Self {
#[cfg(feature = "builtin_font")] { BUILTIN_FONT }
#[cfg(not(feature = "builtin_font"))] { Self(usize::MAX) }
}
}
#[cfg(feature = "builtin_font")]
const BUILTIN_FONT_DATA: &[u8] = include_bytes!("../../assets/font/ProggyTiny.ttf");
pub struct FontManager {
fonts: Vec<Font>,
}
impl FontManager {
pub fn new() -> Self {
let mut this = Self {
fonts: Vec::new(),
};
#[cfg(feature = "builtin_font")]
{
let font = Font::from_bytes(
BUILTIN_FONT_DATA,
fontdue::FontSettings::default()
).unwrap();
this.add_font(font);
};
this
}
/// Add a (fontdue) font to the renderer.
pub fn add_font(&mut self, font: Font) -> FontHandle {
self.fonts.push(font);
FontHandle(self.fonts.len() - 1)
}
pub fn get(&self, handle: FontHandle) -> Option<&Font> {
self.fonts.get(handle.0)
}
}
impl Default for FontManager {
fn default() -> Self {
Self::new()
}
}

View file

@ -1,72 +0,0 @@
use std::sync::Arc;
use fontdue::Metrics;
use hashbrown::HashMap;
use crate::draw::atlas::{TextureAtlasManager, ImageHandle};
use super::font::{FontHandle, FontManager};
#[derive(PartialEq, Eq, Hash)]
struct GlyphCacheKey {
font_index: usize,
character: char,
size: u8,
}
pub struct GlyphCacheEntry {
pub metrics: Metrics,
pub texture: ImageHandle,
}
pub struct FontTextureManager {
glyph_cache: HashMap<GlyphCacheKey, Arc<GlyphCacheEntry>>
}
impl FontTextureManager {
pub fn new() -> Self {
FontTextureManager {
glyph_cache: HashMap::new(),
}
}
/// Either looks up the glyph in the cache or renders it and adds it to the cache.
pub fn glyph(
&mut self,
atlas: &mut TextureAtlasManager,
font_manager: &FontManager,
font_handle: FontHandle,
character: char,
size: u8
) -> Arc<GlyphCacheEntry> {
let key = GlyphCacheKey {
font_index: font_handle.0,
character,
size,
};
if let Some(entry) = self.glyph_cache.get(&key) {
return Arc::clone(entry);
}
let font = font_manager.get(font_handle).unwrap();
let (metrics, bitmap) = font.rasterize(character, size as f32);
log::trace!("rasterized glyph: {}, {:?}, {:?}", character, metrics, bitmap);
let texture = atlas.add_grayscale(metrics.width, &bitmap);
let entry = Arc::new(GlyphCacheEntry {
metrics,
texture
});
self.glyph_cache.insert_unique_unchecked(key, Arc::clone(&entry));
entry
}
// pub fn glyph(&mut self, font_manager: &FontManager, font_handle: FontHandle, character: char, size: u8) -> Arc<GlyphCacheEntry> {
// 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 {
fn default() -> Self { Self::new() }
}

View file

@ -1,32 +0,0 @@
use super::FontHandle;
pub struct FontStack {
fonts: Vec<FontHandle>,
}
impl FontStack {
pub fn new() -> Self {
Self {
#[cfg(not(feature = "builtin_font"))]
fonts: Vec::new(),
#[cfg(feature = "builtin_font")]
fonts: vec![super::BUILTIN_FONT],
}
}
pub fn push(&mut self, font: FontHandle) {
self.fonts.push(font);
}
pub fn pop(&mut self) {
assert!(self.fonts.pop().is_some())
}
pub fn current(&self) -> Option<FontHandle> {
self.fonts.last().copied()
}
pub fn current_or_default(&self) -> FontHandle {
self.current().unwrap_or_default()
}
}