use core::fmt;
use std::f32::consts::{PI, TAU};
use std::hash::{DefaultHasher, Hasher};
use glam::{Mat2, Vec2, Vec4, u8vec2, vec2};
use parley::{Alignment, AlignmentOptions, Glyph, GlyphRun, Layout, PositionedLayoutItem};
use swash::scale::Scaler;
use swash::scale::image::Content;
use swash::zeno::Vector;
use crate::Color;
use crate::TextureHandle;
use crate::batch::HyperQuad;
use crate::common::upload_subtexture;
use crate::quadlas::{Land, Quadlas};
use crate::{DrawCommand, RawImage};
use super::Renderer;
use super::quadlas::{GlyphKey, InsertGlyphError};
#[derive(Clone)]
pub struct TextLayout {
layout: Layout<()>,
text: String,
alignment: Alignment,
color: Color,
real_size: Vec2,
}
impl fmt::Debug for TextLayout {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "TextLayout ({}x{})", self.real_size.x, self.real_size.y)
}
}
impl TextLayout {
pub fn new(layout: Layout<()>, text: String, alignment: Alignment, color: Color, scale_factor: f32) -> Self {
let mut layout = Self {
layout,
text,
alignment,
color,
real_size: Vec2::ZERO,
};
layout.set_max_advance(None, scale_factor);
layout
}
pub fn set_max_advance(&mut self, max_advance: Option<f32>, scale_factor: f32) {
let max_advance = max_advance.map(|x| x * scale_factor);
self.layout.break_all_lines(max_advance);
self.layout.align(
max_advance,
self.alignment,
AlignmentOptions {
align_when_overflowing: true,
},
);
self.update_size(scale_factor);
}
pub fn realign(&mut self, alignment: Alignment, scale_factor: f32) {
if self.alignment != alignment {
self.alignment = alignment;
self.layout.align(
None,
self.alignment,
AlignmentOptions {
align_when_overflowing: true,
},
);
self.update_size(scale_factor);
}
}
fn update_size(&mut self, scale_factor: f32) {
let width = self.layout.width();
let height = self.layout.height();
self.real_size = vec2(width, height) / scale_factor;
}
pub fn color(&self) -> Color {
self.color
}
pub fn set_color(&mut self, color: Color) {
self.color = color;
}
pub fn size(&self) -> Vec2 {
self.real_size
}
pub fn width(&self) -> f32 {
self.real_size.x
}
pub fn height(&self) -> f32 {
self.real_size.y
}
pub fn layout(&self) -> &Layout<()> {
&self.layout
}
pub fn text(&self) -> &str {
&self.text
}
}
impl Renderer {
pub fn render_text(&mut self, text: &TextLayout, pos: Vec2, scale_factor: f32) {
self.render_text_raw(text.layout(), text.real_size, text.color, pos, scale_factor)
}
pub fn render_text_rotated(
&mut self,
text: &TextLayout,
pos: Vec2,
origin: Vec2,
rotation: f32,
scale_factor: f32,
) {
self.render_text_raw_rotated(
text.layout(),
text.real_size,
text.color,
pos,
origin,
rotation,
scale_factor,
)
}
pub fn render_text_raw(
&mut self,
layout: &Layout<()>,
real_size: Vec2,
color: Color,
pos: Vec2,
scale_factor: f32,
) {
self.render_text_raw_rotated(layout, real_size, color, pos, Vec2::ZERO, 0.0, scale_factor)
}
pub fn render_text_raw_rotated(
&mut self,
layout: &Layout<()>,
mut real_size: Vec2,
color: Color,
mut pos: Vec2,
origin: Vec2,
rotation: f32,
scale_factor: f32,
) {
let is_not_rotated = rotation.abs() % (PI / 2.0) <= f32::EPSILON;
if is_not_rotated {
pos = pos.round();
real_size = real_size.ceil();
}
let px_advance = Mat2::from_angle(rotation);
let mut tl_pos = pos - px_advance * (origin * real_size);
let mut br_pos = pos + px_advance * ((1.0 - origin) * real_size);
if is_not_rotated {
tl_pos = tl_pos.floor();
br_pos = br_pos.ceil();
}
let min_boundary = tl_pos.min(br_pos);
let max_boundary = tl_pos.max(br_pos);
if (min_boundary.x >= self.viewport.x || max_boundary.x <= 0.0)
|| (min_boundary.y >= self.viewport.y || max_boundary.y <= 0.0)
{
return;
}
for line in layout.lines() {
for item in line.items() {
match item {
PositionedLayoutItem::GlyphRun(glyph_run) => {
self.render_glyph_run(&glyph_run, color, tl_pos, px_advance, rotation, scale_factor);
}
PositionedLayoutItem::InlineBox(inline_box) => {
self.push_draw_command(DrawCommand::Quad(HyperQuad {
pos: tl_pos + px_advance * vec2(inline_box.x, inline_box.y),
size: vec2(inline_box.width, inline_box.height),
uv_pos: Vec2::ZERO,
uv_size: Vec2::ONE,
color: 0x000000ff,
texture: None,
has_borders: false,
box_blur: 0.0,
border: Vec4::ZERO,
radius: Vec4::ZERO,
rotation,
origin: Vec2::ZERO,
}));
}
};
}
}
}
fn render_glyph_run(
&mut self,
glyph_run: &GlyphRun<'_, ()>,
color: Color,
offset_pos: Vec2,
px_advance: Mat2,
rotation: f32,
scale_factor: f32,
) {
let run_pos = vec2(glyph_run.offset(), glyph_run.baseline());
let run = glyph_run.run();
let run_metrics = run.metrics();
let font = run.font();
let font_size = run.font_size();
let normalized_coords = run.normalized_coords();
{
let pos = offset_pos + px_advance * (run_pos - vec2(0.0, run_metrics.ascent)) / scale_factor;
let size = vec2(glyph_run.advance(), font_size) / scale_factor;
let pos_end = pos + px_advance * size;
let min_boundary = pos.min(pos_end);
let max_boundary = pos.max(pos_end);
if (min_boundary.x >= self.viewport.x || max_boundary.x <= 0.0)
|| (min_boundary.y >= self.viewport.y || max_boundary.y <= 0.0)
{
return;
}
}
let font_ref = swash::FontRef::from_index(font.data.as_ref(), font.index as usize).unwrap();
let style_hash = {
let attrs = run.font_attrs();
let mut hasher = DefaultHasher::new();
hasher.write_u64(font.data.id());
match attrs.style {
parley::FontStyle::Normal => hasher.write_u8(0),
parley::FontStyle::Italic => hasher.write_u8(1),
parley::FontStyle::Oblique(None) => hasher.write_u8(2),
parley::FontStyle::Oblique(Some(v)) => hasher.write_u32(v.to_bits()),
};
hasher.write_u16(attrs.weight.value() as u16);
hasher.write_u32(attrs.width.ratio().to_bits());
hasher.write_u32(u32::from_ne_bytes(font_size.to_ne_bytes()));
(hasher.finish() % (u32::MAX as u64 + 1)) as u32
};
for attempt in 0..=10 {
if attempt == 10 {
panic!("There have been 10 failed attempts at drawing a single glyph run. What the fuck.");
}
let mut no_space_left = false;
let prev_command_count = self.command_buffer.len();
let mut scaler = (self.text.scale_cx)
.builder(font_ref)
.size(font_size)
.hint(false)
.normalized_coords(normalized_coords)
.build();
for glyph in glyph_run.positioned_glyphs() {
let glyph_pos = vec2(glyph.x, glyph.y);
let render_result = render_glyph(
&self.gl,
&mut self.text.quadlas,
self.text.quadlas_texture,
&mut self.text.scale_render,
&mut self.command_buffer,
&mut scaler,
self.current_frame,
color,
style_hash,
glyph,
glyph_pos,
offset_pos,
px_advance,
rotation,
scale_factor,
);
match render_result {
Ok(()) => {}
Err(InsertGlyphError::NoSpaceLeft) => {
no_space_left = true;
break;
}
Err(InsertGlyphError::EmptyImage) => {}
Err(InsertGlyphError::TooBig) => {}
}
}
if no_space_left {
self.command_buffer.truncate(prev_command_count);
self.draw();
self.text.quadlas.grow();
self.reupload_glyph_atlas_texture();
} else {
break;
}
}
let style = glyph_run.style();
if let Some(decoration) = &style.underline {
let offset = (decoration.offset).unwrap_or(run_metrics.underline_offset);
let size = decoration.size.unwrap_or(run_metrics.underline_size);
render_decoration(self, glyph_run, color, offset, size, offset_pos, scale_factor);
}
if let Some(decoration) = &style.strikethrough {
let offset = (decoration.offset).unwrap_or(run_metrics.strikethrough_offset);
let size = decoration.size.unwrap_or(run_metrics.strikethrough_size);
render_decoration(self, glyph_run, color, offset, size, offset_pos, scale_factor);
}
}
}
#[allow(clippy::too_many_arguments)]
fn render_glyph(
gl: &glow::Context,
quadlas: &mut Quadlas,
quadlas_texture: glow::Texture,
scale_render: &mut swash::scale::Render<'_>,
draw_commands: &mut Vec<DrawCommand>,
scaler: &mut Scaler<'_>,
current_frame: u64,
color: Color,
style_hash: u32,
glyph: Glyph,
glyph_pos: Vec2,
offset_pos: Vec2,
px_advance: Mat2,
rotation: f32,
scale: f32,
) -> Result<(), InsertGlyphError> {
let fquantize = u8vec2(4, 2).as_vec2();
let fract_offset = glyph_pos.fract();
let fract_offset = (fract_offset.fract() * fquantize).floor() / fquantize;
let glyph_pos = glyph_pos.floor();
let glyph_key = GlyphKey {
id: (glyph.id % (u16::MAX as u32)) as u16, quantization: (fract_offset * fquantize).floor().as_u8vec2(),
style_hash,
};
let placement;
let final_color;
let land_uv;
let is_colored;
let land_padding = 1;
if let Some((land, gce)) = quadlas.get_cached_glyph(glyph_key, land_padding, current_frame) {
placement = gce.placement;
land_uv = quadlas.to_land_uv(Land {
pos: land.pos,
size: land.size,
});
is_colored = gce.is_colored;
final_color = match gce.is_colored {
true => 0xffffffff,
false => color,
};
} else {
let fract_offset = Vector::new(fract_offset.x, fract_offset.y);
let Some(rendered_glyph) = scale_render.offset(fract_offset).render(scaler, glyph_key.id) else {
return Ok(());
};
placement = rendered_glyph.placement;
let (land, color) = match rendered_glyph.content {
Content::Color => {
is_colored = true;
let glyph_image = RawImage {
width: placement.width,
height: placement.height,
pixels: &rendered_glyph.data,
};
let (land, _) = quadlas.insert_glyph_unless_cached(
glyph_key,
rendered_glyph.placement,
is_colored,
glyph_image,
land_padding,
current_frame,
)?;
unsafe {
upload_subtexture(
gl,
quadlas_texture,
land.pos.x - land_padding,
land.pos.y - land_padding,
land.size.x + 2 * land_padding,
land.size.y + 2 * land_padding,
None,
);
upload_subtexture(
gl,
quadlas_texture,
land.pos.x,
land.pos.y,
land.size.x,
land.size.y,
Some(glyph_image.pixels),
);
}
(land, 0xffffffff)
}
Content::Mask => {
is_colored = false;
let rgba_data = (rendered_glyph.data.iter())
.flat_map(|&a| [0xff, 0xff, 0xff, a])
.collect::<Vec<_>>();
let glyph_image = RawImage {
width: placement.width,
height: placement.height,
pixels: &rgba_data,
};
let (land, _) = quadlas.insert_glyph_unless_cached(
glyph_key,
rendered_glyph.placement,
is_colored,
glyph_image,
land_padding,
current_frame,
)?;
unsafe {
upload_subtexture(
gl,
quadlas_texture,
land.pos.x - land_padding,
land.pos.y - land_padding,
land.size.x + 2 * land_padding,
land.size.y + 2 * land_padding,
None,
);
upload_subtexture(
gl,
quadlas_texture,
land.pos.x,
land.pos.y,
land.size.x,
land.size.y,
Some(glyph_image.pixels),
);
}
(land, color)
}
Content::SubpixelMask => unimplemented!(),
};
land_uv = quadlas.to_land_uv(land);
final_color = color;
}
let final_glyph_size = vec2(placement.width as f32, placement.height as f32) / scale;
let mut final_glyph_pos = (glyph_pos + vec2(placement.left as f32, -placement.top as f32)) / scale;
if !is_colored && rotation.abs() % TAU > f32::EPSILON {
final_glyph_pos -= vec2(fract_offset.x, 1.0 - fract_offset.y);
}
draw_commands.push(DrawCommand::Quad(HyperQuad {
pos: offset_pos + px_advance * final_glyph_pos,
size: final_glyph_size,
uv_pos: land_uv.pos,
uv_size: land_uv.size,
color: final_color,
texture: Some(TextureHandle(quadlas_texture)),
has_borders: false,
box_blur: 0.0,
border: Vec4::ZERO,
radius: Vec4::ZERO,
rotation,
origin: Vec2::ZERO,
}));
Ok(())
}
fn render_decoration(
renderer: &mut Renderer,
glyph_run: &GlyphRun<'_, ()>,
color: Color,
offset: f32,
thickness: f32,
offset_pos: Vec2,
scale_factor: f32,
) {
let pos = offset_pos + vec2(glyph_run.offset(), glyph_run.baseline() - offset) / scale_factor;
let size = vec2(glyph_run.advance(), thickness * 2.0) / scale_factor;
if pos.x >= renderer.viewport.x || pos.x + size.x <= 0.0 || pos.y >= renderer.viewport.y || pos.y + size.y <= 0.0 {
return;
}
renderer.push_draw_command(DrawCommand::Quad(HyperQuad {
pos,
size,
uv_pos: Vec2::ZERO,
uv_size: Vec2::ONE,
color,
texture: None,
has_borders: false,
box_blur: 0.0,
border: Vec4::ZERO,
radius: Vec4::splat(thickness) / 1.5,
rotation: 0.0,
origin: Vec2::ZERO,
}));
}