diff --git a/hui-examples/examples/ui_test3.rs b/hui-examples/examples/ui_test3.rs new file mode 100644 index 0000000..1692a34 --- /dev/null +++ b/hui-examples/examples/ui_test3.rs @@ -0,0 +1,63 @@ +use std::f32::consts::PI; + +use glam::{vec4, Vec2}; +use hui::{ + element::{ + container::Container, + text::Text, + transformer::ElementTransformExt, + UiElementExt + }, + layout::Alignment, + rectangle::Corners, + text::FontHandle, + size, +}; + +#[path = "../boilerplate.rs"] +#[macro_use] +mod boilerplate; + +ui_main!( + init: |ui| { + let font = ui.add_font(include_bytes!("../assets/blink/Blink-ynYZ.otf")); + ui.push_font(font); + (std::time::Instant::now(),) + }, + run: |ui, size, (instant,)| { + let elapsed_sec = instant.elapsed().as_secs_f32(); + Container::default() + .with_background(Corners { + top_left: vec4(0.2, 0.2, 0.3, 1.), + top_right: vec4(0.3, 0.3, 0.4, 1.), + bottom_left: vec4(0.2, 0.3, 0.2, 1.), + bottom_right: vec4(0.5, 0.4, 0.4, 1.), + }) + .with_size(size!(100%)) + .with_align(Alignment::Center) + .with_children(|ui| { + Container::default() + .with_align((Alignment::Center, Alignment::Begin)) + .with_padding(15.) + .with_gap(10.) + .with_corner_radius(8.) + .with_background((0., 0., 0., 0.5)) + .with_children(|ui| { + Text::default() + .with_text("Did you know?") + .with_text_size(18) + .add_child(ui); + Text::default() + .with_text("You can die by jumping into the spike pit! :D\nCheck out the tutorial section for more tips.") + .with_text_size(24) + .with_font(FontHandle::default()) + .add_child(ui); + }) + .add_child(ui); + }) + .transform() + .scale(Vec2::splat(elapsed_sec.sin() * 0.1 + 1.)) + .rotate(elapsed_sec * PI / 4.) + .add_root(ui, size); + } +); diff --git a/hui/src/draw.rs b/hui/src/draw.rs index 93f5950..1e240e9 100644 --- a/hui/src/draw.rs +++ b/hui/src/draw.rs @@ -16,7 +16,7 @@ pub use corner_radius::RoundedCorners; use std::borrow::Cow; use fontdue::layout::{Layout, CoordinateSystem, TextStyle}; -use glam::{Vec2, Vec4, vec2}; +use glam::{vec2, Vec2, Affine2, Vec4}; //TODO: circle draw command @@ -51,6 +51,10 @@ pub enum UiDrawCommand { ///Font handle to use font: FontHandle, }, + /// Push a transformation matrix to the stack + PushTransform(Affine2), + /// Pop a transformation matrix from the stack + PopTransform, } /// List of draw commands @@ -91,9 +95,45 @@ pub struct UiDrawCall { 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(); 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, rounded_corners } => { let uvs = texture .map(|x| atlas.get_uv(x)) @@ -257,8 +297,11 @@ impl UiDrawCall { feature = "pixel_perfect_text", not(feature = "pixel_perfect") ))] { - for vtx in &mut draw_call.vertices[(vidx as usize)..] { - vtx.position = vtx.position.round() + //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() + } } } } diff --git a/hui/src/element/builtin.rs b/hui/src/element/builtin.rs index e3edfe8..613129d 100644 --- a/hui/src/element/builtin.rs +++ b/hui/src/element/builtin.rs @@ -12,3 +12,8 @@ pub mod progress_bar; #[cfg(feature = "builtin_elements")] pub mod text; + +#[cfg(feature = "builtin_elements")] +pub mod transformer; + +//TODO add: Button, Checkbox, Dropdown, Input, Radio, Slider, Textarea, Toggle, etc. diff --git a/hui/src/element/builtin/button.rs b/hui/src/element/builtin/button.rs deleted file mode 100644 index e69de29..0000000 diff --git a/hui/src/element/builtin/interactable.rs b/hui/src/element/builtin/interactable.rs index 059f299..f9ef1ec 100644 --- a/hui/src/element/builtin/interactable.rs +++ b/hui/src/element/builtin/interactable.rs @@ -1,3 +1,7 @@ +//TODO this thing? +//not sure if this is a good idea... +//but having the ability to add a click event to any element would be nice, and this is a naive way to do it + // use crate::element::{UiElement, MeasureContext, ProcessContext}; // pub struct Interactable { diff --git a/hui/src/element/builtin/transformer.rs b/hui/src/element/builtin/transformer.rs new file mode 100644 index 0000000..bff9321 --- /dev/null +++ b/hui/src/element/builtin/transformer.rs @@ -0,0 +1,68 @@ +use glam::{Affine2, Vec2}; +use crate::{ + draw::UiDrawCommand, element::{MeasureContext, ProcessContext, UiElement}, measure::Response +}; + +pub struct Transformer { + pub transform: Affine2, + pub element: Box, +} + +impl Transformer { + pub fn new(element: Box) -> Self { + Self { + transform: Affine2::IDENTITY, + element, + } + } + + pub fn translate(mut self, v: impl Into) -> Self { + self.transform *= Affine2::from_translation(v.into()); + self + } + + pub fn scale(mut self, v: impl Into) -> Self { + self.transform *= Affine2::from_scale(v.into()); + self + } + + pub fn rotate(mut self, radians: f32) -> Self { + self.transform *= Affine2::from_angle(radians); + self + } +} + +impl UiElement for Transformer { + fn measure(&self, ctx: MeasureContext) -> Response { + self.element.measure(ctx) + } + + fn process(&self, ctx: ProcessContext) { + ctx.draw.add(UiDrawCommand::PushTransform(self.transform)); + //This is stupid: + self.element.process(ProcessContext { + measure: ctx.measure, + state: ctx.state, + layout: ctx.layout, + draw: ctx.draw, + text_measure: ctx.text_measure, + current_font: ctx.current_font, + }); + ctx.draw.add(UiDrawCommand::PopTransform); + } +} + +pub trait ElementTransformExt { + fn transform(self) -> Transformer; +} + +impl ElementTransformExt for T { + /// Wrap the element in a [`Transformer`] + /// + /// This allows you to apply various transformations to the element, such as translation, rotation, or scaling\ + /// Use sparingly, as this is an experimental feature and may not work as expected\ + /// Transform is applied around the center of the element's bounding box. + fn transform(self) -> Transformer { + Transformer::new(Box::new(self)) + } +}