hui 0.1.0-alpha.6

Simple UI library for games and other interactive applications
Documentation
//! 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(f32::INFINITY);
          let mut max = Vec2::splat(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
  }
}