Skip to content

Commit

Permalink
Implement proper text shaping
Browse files Browse the repository at this point in the history
This replaces rusttypes with cosmic_text, which
implements a layout engine and proper text shaping.

We don't use it to the fullest yet, but that would require a redesign of how we handle text in our interface.

I changed the sampler for text to nearest, which results in much shaper text.
  • Loading branch information
hasenbanck committed Nov 22, 2024
1 parent 0e949d7 commit 3546ff2
Show file tree
Hide file tree
Showing 19 changed files with 1,511 additions and 426 deletions.
941 changes: 817 additions & 124 deletions Cargo.lock

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ bumpalo = "3"
bytemuck = "1.17"
cgmath = "0.18"
chrono = "0.4"
cosmic-text = "0.12"
cpal = "0.15"
derive-new = "0.7"
etherparse = "0.16"
Expand Down Expand Up @@ -37,7 +38,6 @@ random_color = "1.0"
rayon = "1.10"
reqwest = "0.12"
ron = "0.8"
rusttype = "0.9"
serde = "1.0"
serde-xml-rs = "0.6"
spin_sleep = "1.2"
Expand Down
2 changes: 1 addition & 1 deletion korangar/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ bumpalo = { workspace = true, features = ["allocator_api"] }
bytemuck = { workspace = true, features = ["derive", "extern_crate_std", "min_const_generics"] }
cgmath = { workspace = true, features = ["mint", "serde"] }
chrono = { workspace = true }
cosmic-text = { workspace = true }
derive-new = { workspace = true }
hashbrown = { workspace = true }
image = { workspace = true, features = ["bmp", "png", "tga", "rayon"] }
Expand All @@ -29,7 +30,6 @@ rand = { workspace = true }
random_color = { workspace = true, optional = true }
rayon = { workspace = true }
ron = { workspace = true }
rusttype = { workspace = true, features = ["gpu_cache"] }
serde = { workspace = true }
serde-xml-rs = { workspace = true }
spin_sleep = { workspace = true }
Expand Down
12 changes: 12 additions & 0 deletions korangar/src/graphics/color.rs
Original file line number Diff line number Diff line change
Expand Up @@ -132,6 +132,18 @@ impl Color {
}
}

impl From<Color> for cosmic_text::Color {
fn from(value: Color) -> Self {
Self::rgba(value.red_as_u8(), value.green_as_u8(), value.blue_as_u8(), value.alpha_as_u8())
}
}

impl From<cosmic_text::Color> for Color {
fn from(value: cosmic_text::Color) -> Self {
Self::rgba_u8(value.r(), value.g(), value.b(), value.a())
}
}

impl Add for Color {
type Output = Color;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ fn draw_text(
instance: InstanceData,
texture_coordinates: vec2<f32>,
) -> vec4<f32> {
let coverage = textureSample(font_atlas, linear_sampler, texture_coordinates).r;
let coverage = textureSample(font_atlas, nearest_sampler, texture_coordinates).r;
return vec4<f32>(instance.color.rgb, coverage * instance.color.a);
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -145,7 +145,7 @@ fn draw_text(
instance: InstanceData,
texture_coordinates: vec2<f32>,
) -> vec4<f32> {
let coverage = textureSample(font_atlas, linear_sampler, texture_coordinates).r;
let coverage = textureSample(font_atlas, nearest_sampler, texture_coordinates).r;
return vec4<f32>(instance.color.rgb, coverage * instance.color.a);
}

Expand Down
4 changes: 2 additions & 2 deletions korangar/src/graphics/vertices/native.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
use cgmath::{InnerSpace, Point3, Vector2, Vector3};
use cgmath::{EuclideanSpace, InnerSpace, Point2, Point3, Vector2, Vector3};
use derive_new::new;
use korangar_util::texture_atlas::AtlasAllocation;

Expand All @@ -21,7 +21,7 @@ impl NativeModelVertex {
ModelVertex::new(
self.position,
self.normal,
allocation.map_to_atlas(self.texture_coordinates),
allocation.map_to_atlas(Point2::from_vec(self.texture_coordinates)).to_vec(),
self.color,
self.wind_affinity,
)
Expand Down
17 changes: 3 additions & 14 deletions korangar/src/interface/elements/miscellanious/chat/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -55,10 +55,11 @@ impl Element<InterfaceSettings> for Chat {
for message in self.messages.get().iter() {
height += self
.font_loader
.borrow()
.borrow_mut()
.get_text_dimensions(
&message.text,
theme.chat.font_size.get().scaled(application.get_scaling()),
1.0,
placement_resolver.get_available().width,
)
.height
Expand Down Expand Up @@ -90,18 +91,6 @@ impl Element<InterfaceSettings> for Chat {
let mut offset = 0.0;

for message in self.messages.get().iter() {
let text = &message.text;

renderer.render_text(
text,
ScreenPosition {
left: 0.2,
top: offset + 0.2,
},
Color::BLACK,
theme.chat.font_size.get(),
);

let message_color = match message.color {
korangar_networking::MessageColor::Rgb { red, green, blue } => Color::rgb_u8(red, green, blue),
korangar_networking::MessageColor::Broadcast => theme.chat.broadcast_color.get(),
Expand All @@ -113,7 +102,7 @@ impl Element<InterfaceSettings> for Chat {
// Dividing by the scaling is done to counteract the scaling being applied
// twice per message. It's not the cleanest solution but it works.
offset += renderer.render_text(
text,
&message.text,
ScreenPosition::only_top(offset),
message_color,
theme.chat.font_size.get(),
Expand Down
164 changes: 164 additions & 0 deletions korangar/src/loaders/font/color_span_iterator.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,164 @@
use cosmic_text::Attrs;

use crate::graphics::Color;

pub(crate) struct ColorSpanIterator<'r, 's> {
text: &'s str,
default_color: cosmic_text::Color,
attributes: Attrs<'r>,
position: usize,
}

impl<'r, 's> ColorSpanIterator<'r, 's> {
pub(crate) fn new(text: &'s str, default_color: Color, attributes: Attrs<'r>) -> Self {
Self {
text,
default_color: default_color.into(),
attributes,
position: 0,
}
}
}

impl<'r, 's> Iterator for ColorSpanIterator<'r, 's> {
type Item = (&'s str, Attrs<'r>);

fn next(&mut self) -> Option<Self::Item> {
if self.position >= self.text.len() {
return None;
}

let start_position = self.position;
let mut current_position = self.position;
let text = &self.text[current_position..];

while let Some(color_position) = text[current_position - self.position..].find('^') {
let absolute_color_position = current_position + color_position;

// Change the font color if the color value is valid.
if absolute_color_position + 7 <= self.text.len() {
let potential_color = &self.text[absolute_color_position + 1..absolute_color_position + 7];
if potential_color.chars().all(|c| c.is_ascii_hexdigit()) {
if absolute_color_position > start_position {
let span_text = &self.text[start_position..absolute_color_position];
self.position = absolute_color_position;
return Some((span_text, self.attributes));
}

self.position = absolute_color_position + 7;
self.attributes = match potential_color {
"000000" => self.attributes.color(self.default_color),
code => self.attributes.color(Color::rgb_hex(code).into()),
};

return self.next();
}
}

// Invalid color code - continue searching.
current_position = absolute_color_position + 1;
if current_position >= self.text.len() {
break;
}
}

// Return remaining text.
if self.position < self.text.len() {
let span_text = &self.text[self.position..];
self.position = self.text.len();
Some((span_text, self.attributes))
} else {
None
}
}
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn test_basic_color_change() {
let attributes = Attrs::new();
let text = "Hello ^FF0000Red ^00FF00Green";
let spans: Vec<_> = ColorSpanIterator::new(text, Color::BLACK, attributes).collect();

assert_eq!(spans.len(), 3);
assert_eq!(spans[0].0, "Hello ");
assert_eq!(spans[1].0, "Red ");
assert_eq!(spans[2].0, "Green");
}

#[test]
fn test_reset_to_default_color() {
let attributes = Attrs::new();
let text = "^FF0000Red ^000000Default";
let spans: Vec<_> = ColorSpanIterator::new(text, Color::BLACK, attributes).collect();

assert_eq!(spans.len(), 2);
assert_eq!(spans[0].0, "Red ");
assert_eq!(spans[1].0, "Default");
}

#[test]
fn test_invalid_color_codes() {
let attributes = Attrs::new();
let text = "^FFInvalid ^FF00 ^FFFF00Valid";
let spans: Vec<_> = ColorSpanIterator::new(text, Color::BLACK, attributes).collect();

assert_eq!(spans.len(), 2);
assert_eq!(spans[0].0, "^FFInvalid ^FF00 ");
assert_eq!(spans[1].0, "Valid");
}

#[test]
fn test_empty_text_between_colors() {
let attributes = Attrs::new();
let text = "^FF0000^00FF00^0000FF";
let spans: Vec<_> = ColorSpanIterator::new(text, Color::BLACK, attributes).collect();

assert_eq!(spans.len(), 0);
}

#[test]
fn test_color_code_at_end() {
let attributes = Attrs::new();
let text = "Text^FF0000";
let spans: Vec<_> = ColorSpanIterator::new(text, Color::BLACK, attributes).collect();

assert_eq!(spans.len(), 1);
assert_eq!(spans[0].0, "Text");
}

#[test]
fn test_caret_at_end() {
let attributes = Attrs::new();
let text = "Normal text ^FFFF00Colored text^";
let spans: Vec<_> = ColorSpanIterator::new(text, Color::BLACK, attributes).collect();

assert_eq!(spans.len(), 2);
assert_eq!(spans[0].0, "Normal text ");
assert_eq!(spans[1].0, "Colored text^");
}

#[test]
fn test_consecutive_color_changes() {
let attributes = Attrs::new();
let text = "^AAAAAA^BBBBBBtext^CCCCCCmore^DDDDDDlast";
let spans: Vec<_> = ColorSpanIterator::new(text, Color::BLACK, attributes).collect();

assert_eq!(spans.len(), 3);
assert_eq!(spans[0].0, "text");
assert_eq!(spans[1].0, "more");
assert_eq!(spans[2].0, "last");
}

#[test]
fn test_empty_input() {
let attributes = Attrs::new();
let text = "";
let spans: Vec<_> = ColorSpanIterator::new(text, Color::BLACK, attributes).collect();

assert_eq!(spans.len(), 0);
}
}
Loading

0 comments on commit 3546ff2

Please sign in to comment.