use super::{DecodedGlyph, Rect, Text, TextStyle};
pub type GlyphStyleMap = Box<dyn Fn(DecodedGlyph, TextStyle) -> TextStyle>;
pub struct StyledText {
pub(crate) buf: cosmic_text::Buffer,
pub(crate) first_style: TextStyle,
pub(crate) other_style: Vec<TextStyle>,
pub region: Rect,
pub scale: f32,
pub custom_padding: Option<(usize, usize)>,
pub gradient_region: Option<Rect>,
}
impl StyledText {
pub fn as_text(&self, id: u64) -> Text<'_> {
Text {
id,
text_buffer: &self.buf,
region: self.region,
scale: self.scale,
custom_padding: self.custom_padding,
}
}
pub fn style_at(&self, metadata: usize) -> &TextStyle {
if metadata == 0 {
&self.first_style
} else {
self.other_style
.get(metadata - 1)
.unwrap_or(&self.first_style)
}
}
pub fn styles(&self) -> impl Iterator<Item = &TextStyle> {
std::iter::once(&self.first_style).chain(self.other_style.iter())
}
pub fn buffer(&self) -> &cosmic_text::Buffer {
&self.buf
}
}
pub struct StyledTextBuilder {
inner: StyledText,
spans: Vec<(String, cosmic_text::AttrsOwned)>,
styles: Vec<TextStyle>,
shaping: cosmic_text::Shaping,
align: Option<cosmic_text::Align>,
}
impl StyledTextBuilder {
pub fn new(
region: Rect,
font_system: &mut cosmic_text::FontSystem,
metrics: cosmic_text::Metrics,
) -> Self {
let mut buf = cosmic_text::Buffer::new(font_system, metrics);
buf.set_size(font_system, Some(region.width()), Some(region.height()));
Self {
inner: StyledText {
buf,
first_style: TextStyle::default(),
other_style: Vec::new(),
region,
scale: 1.0,
custom_padding: None,
gradient_region: None,
},
spans: Vec::new(),
styles: Vec::new(),
shaping: cosmic_text::Shaping::Advanced,
align: None,
}
}
pub fn set_scale(&mut self, scale: f32) {
self.inner.scale = scale;
}
pub fn set_gradient_region(&mut self, region: Option<Rect>) {
self.inner.gradient_region = region;
}
pub fn set_align(&mut self, align: Option<cosmic_text::Align>) {
self.align = align;
}
pub fn set_shaping(&mut self, shaping: cosmic_text::Shaping) {
self.shaping = shaping;
}
pub fn push_text(
&mut self,
content: &str,
attrs: &cosmic_text::Attrs<'_>,
style: TextStyle,
) -> usize {
let metadata = self.styles.len();
self.styles.push(style);
let mut owned = cosmic_text::AttrsOwned::new(attrs);
owned.metadata = metadata;
self.spans.push((content.to_owned(), owned));
metadata
}
pub fn push_custom_glyph(&mut self, id: u64, width: f32, height: f32) {
let attrs = cosmic_text::Attrs::new()
.family(cosmic_text::Family::Name(super::CUSTOM_GLYPH_FAMILY))
.metadata(id as usize)
.metrics(cosmic_text::Metrics::new(width, height));
self.spans.push((
super::CUSTOM_GLYPH_CHAR.to_string(),
cosmic_text::AttrsOwned::new(&attrs),
));
}
pub fn finish(
mut self,
font_system: &mut cosmic_text::FontSystem,
default_attrs: &cosmic_text::Attrs<'_>,
) -> StyledText {
if let Some(first) = self.styles.first().copied() {
self.inner.first_style = first;
self.inner.other_style = self.styles[1..].to_vec();
}
let owned_default = cosmic_text::AttrsOwned::new(default_attrs);
let spans = self.spans.iter().map(|(s, a)| (s.as_str(), a.as_attrs()));
self.inner.buf.set_rich_text(
font_system,
spans,
&owned_default.as_attrs(),
self.shaping,
self.align,
);
self.inner.buf.shape_until_scroll(font_system, false);
let min_font_size = self
.inner
.buf
.layout_runs()
.flat_map(|run| run.glyphs.iter().map(|g| g.font_size))
.reduce(f32::min)
.unwrap_or(crate::atlas::DEFAULT_PPEM);
self.inner.custom_padding = self
.styles
.iter()
.filter_map(|s| style_padding(s, min_font_size))
.max_by_key(|(x, _)| *x);
self.inner
}
}
fn style_padding(style: &TextStyle, min_font_size: f32) -> Option<(usize, usize)> {
let outer_extent_px = f32::max(
style.stroke_out.width + style.glow_out.width,
style.shadow.additional_width,
);
let ppem = crate::atlas::DEFAULT_PPEM;
let needed = (outer_extent_px * ppem / min_font_size).ceil() as usize;
(needed > crate::atlas::DEFAULT_PADDING).then_some((needed, needed))
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Outline, Rect, TextStyle};
use cosmic_text::{Attrs, FontSystem, Metrics};
fn font_system() -> FontSystem {
FontSystem::new()
}
fn region() -> Rect {
Rect::from_xywh(0.0, 0.0, 800.0, 600.0)
}
fn metrics() -> Metrics {
Metrics::new(16.0, 20.0)
}
fn style_with_stroke(width: f32) -> TextStyle {
TextStyle {
stroke_out: Outline {
width,
..Default::default()
},
..Default::default()
}
}
#[test]
fn new_default_val() {
let mut fs = font_system();
let r = region();
let text = StyledTextBuilder::new(r, &mut fs, metrics()).finish(&mut fs, &Attrs::new());
assert_eq!(text.region, r);
assert_eq!(text.scale, 1.0);
assert_eq!(text.gradient_region, None);
}
#[test]
fn set_scale_reflected_on_finish() {
let mut fs = font_system();
let mut builder = StyledTextBuilder::new(region(), &mut fs, metrics());
builder.set_scale(2.5);
let text = builder.finish(&mut fs, &Attrs::new());
assert_eq!(text.scale, 2.5);
}
#[test]
fn set_gradient_region_some() {
let mut fs = font_system();
let grad = Rect::from_xywh(10.0, 10.0, 400.0, 300.0);
let mut builder = StyledTextBuilder::new(region(), &mut fs, metrics());
builder.set_gradient_region(Some(grad));
let text = builder.finish(&mut fs, &Attrs::new());
assert_eq!(text.gradient_region, Some(grad));
}
#[test]
fn set_gradient_region_none_clears() {
let mut fs = font_system();
let mut builder = StyledTextBuilder::new(region(), &mut fs, metrics());
builder.set_gradient_region(Some(region()));
builder.set_gradient_region(None);
let text = builder.finish(&mut fs, &Attrs::new());
assert_eq!(text.gradient_region, None);
}
#[test]
fn push_text_first_index_is_zero() {
let mut fs = font_system();
let mut builder = StyledTextBuilder::new(region(), &mut fs, metrics());
let idx = builder.push_text("hello", &Attrs::new(), TextStyle::default());
assert_eq!(idx, 0);
}
#[test]
fn push_text_indices_increment() {
let mut fs = font_system();
let mut builder = StyledTextBuilder::new(region(), &mut fs, metrics());
let a = builder.push_text("a", &Attrs::new(), TextStyle::default());
let b = builder.push_text("b", &Attrs::new(), TextStyle::default());
let c = builder.push_text("c", &Attrs::new(), TextStyle::default());
assert_eq!((a, b, c), (0, 1, 2));
}
#[test]
fn push_custom_glyph_does_not_consume_style_slot() {
let mut fs = font_system();
let mut builder = StyledTextBuilder::new(region(), &mut fs, metrics());
let before = builder.push_text("a", &Attrs::new(), TextStyle::default());
builder.push_custom_glyph(42, 16.0, 16.0);
let after = builder.push_text("b", &Attrs::new(), TextStyle::default());
assert_eq!((before, after), (0, 1));
}
#[test]
fn finish_no_spans_has_default_first_style() {
let mut fs = font_system();
let text =
StyledTextBuilder::new(region(), &mut fs, metrics()).finish(&mut fs, &Attrs::new());
assert_eq!(*text.style_at(0), TextStyle::default());
}
#[test]
fn finish_no_spans_styles_yields_one_item() {
let mut fs = font_system();
let text =
StyledTextBuilder::new(region(), &mut fs, metrics()).finish(&mut fs, &Attrs::new());
assert_eq!(text.styles().count(), 1);
}
#[test]
fn style_at_single_span() {
let mut fs = font_system();
let mut builder = StyledTextBuilder::new(region(), &mut fs, metrics());
builder.push_text("hello", &Attrs::new(), style_with_stroke(3.0));
let text = builder.finish(&mut fs, &Attrs::new());
assert_eq!(text.style_at(0).stroke_out.width, 3.0);
}
#[test]
fn style_at_multiple_spans() {
let mut fs = font_system();
let mut builder = StyledTextBuilder::new(region(), &mut fs, metrics());
builder.push_text("a", &Attrs::new(), style_with_stroke(1.0));
builder.push_text("b", &Attrs::new(), style_with_stroke(2.0));
builder.push_text("c", &Attrs::new(), style_with_stroke(3.0));
let text = builder.finish(&mut fs, &Attrs::new());
assert_eq!(text.style_at(0).stroke_out.width, 1.0);
assert_eq!(text.style_at(1).stroke_out.width, 2.0);
assert_eq!(text.style_at(2).stroke_out.width, 3.0);
}
#[test]
fn style_at_out_of_range_falls_back_to_first() {
let mut fs = font_system();
let mut builder = StyledTextBuilder::new(region(), &mut fs, metrics());
builder.push_text("a", &Attrs::new(), style_with_stroke(1.0));
builder.push_text("b", &Attrs::new(), style_with_stroke(2.0));
let text = builder.finish(&mut fs, &Attrs::new());
assert_eq!(text.style_at(99).stroke_out.width, 1.0);
}
#[test]
fn styles_yields_all_in_push_order() {
let mut fs = font_system();
let mut builder = StyledTextBuilder::new(region(), &mut fs, metrics());
builder.push_text("a", &Attrs::new(), style_with_stroke(1.0));
builder.push_text("b", &Attrs::new(), style_with_stroke(2.0));
builder.push_text("c", &Attrs::new(), style_with_stroke(3.0));
let text = builder.finish(&mut fs, &Attrs::new());
let widths: Vec<f32> = text.styles().map(|s| s.stroke_out.width).collect();
assert_eq!(widths, [1.0, 2.0, 3.0]);
}
#[test]
fn styles_count_matches_span_count() {
let mut fs = font_system();
let mut builder = StyledTextBuilder::new(region(), &mut fs, metrics());
builder.push_text("a", &Attrs::new(), TextStyle::default());
builder.push_text("b", &Attrs::new(), TextStyle::default());
let text = builder.finish(&mut fs, &Attrs::new());
assert_eq!(text.styles().count(), 2);
}
#[test]
fn buffer_accessible() {
let mut fs = font_system();
let text =
StyledTextBuilder::new(region(), &mut fs, metrics()).finish(&mut fs, &Attrs::new());
let _ = text.buffer();
}
#[test]
fn as_text_id_propagated() {
let mut fs = font_system();
let text =
StyledTextBuilder::new(region(), &mut fs, metrics()).finish(&mut fs, &Attrs::new());
assert_eq!(text.as_text(99).id, 99);
}
#[test]
fn as_text_region_propagated() {
let mut fs = font_system();
let r = region();
let text = StyledTextBuilder::new(r, &mut fs, metrics()).finish(&mut fs, &Attrs::new());
assert_eq!(text.as_text(0).region, r);
}
#[test]
fn as_text_scale_propagated() {
let mut fs = font_system();
let mut builder = StyledTextBuilder::new(region(), &mut fs, metrics());
builder.set_scale(3.0);
let text = builder.finish(&mut fs, &Attrs::new());
assert_eq!(text.as_text(0).scale, 3.0);
}
}