use crate::attributes::Attributes;
use crate::metadata::Metadata;
use crate::text::{FontSystemAndDefaults, Text};
use crate::text_layout::TextLayout;
use crate::{cvt_color, cvt_family, cvt_style, cvt_weight, FontError, POINTS_PER_INCH};
use cosmic_text as ct;
use ct::{Attrs, Buffer, BufferLine, Metrics};
use piet::{util, Error, TextAlignment, TextAttribute, TextStorage};
use std::cmp;
use std::fmt;
use std::mem;
use std::ops::{Range, RangeBounds};
use tinyvec::TinyVec;
pub struct TextLayoutBuilder {
handle: Text,
string: Box<dyn TextStorage>,
defaults: util::LayoutDefaults,
max_width: f64,
alignment: Option<TextAlignment>,
range_attributes: Attributes,
last_range_start_pos: usize,
error: Option<Error>,
}
impl fmt::Debug for TextLayoutBuilder {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TextLayoutBuilder")
.field("string", &self.string.as_str())
.field("max_width", &self.max_width)
.field("range_attributes", &self.range_attributes)
.finish_non_exhaustive()
}
}
impl TextLayoutBuilder {
pub(crate) fn new(text: Text, string: impl TextStorage) -> Self {
Self {
handle: text,
string: Box::new(string),
defaults: util::LayoutDefaults::default(),
max_width: f64::INFINITY,
alignment: None,
last_range_start_pos: 0,
range_attributes: Attributes::default(),
error: None,
}
}
fn shaping(&self) -> ct::Shaping {
ct::Shaping::Advanced
}
}
impl piet::TextLayoutBuilder for TextLayoutBuilder {
type Out = TextLayout;
fn alignment(mut self, alignment: TextAlignment) -> Self {
self.alignment = Some(alignment);
self
}
fn max_width(mut self, width: f64) -> Self {
self.max_width = width;
self
}
fn default_attribute(mut self, attribute: impl Into<TextAttribute>) -> Self {
self.defaults.set(attribute);
self
}
fn range_attribute(
mut self,
range: impl RangeBounds<usize>,
attribute: impl Into<TextAttribute>,
) -> Self {
let range = util::resolve_range(range, self.string.len());
let attribute = attribute.into();
debug_assert!(
range.start >= self.last_range_start_pos,
"attributes must be added in non-decreasing start order"
);
self.last_range_start_pos = range.start;
self.range_attributes.push(range, attribute);
self
}
fn build(self) -> Result<Self::Out, Error> {
let shaping = self.shaping();
let Self {
handle,
string,
defaults,
max_width,
mut range_attributes,
error,
..
} = self;
if let Some(error) = error {
return Err(error);
}
let mut font_system_guard = handle
.borrow_font_system()
.ok_or(Error::BackendError(FontError::AlreadyBorrowed.into()))?;
let font_system = match font_system_guard.get() {
Some(font_system) => font_system,
None => {
warn!("Still waiting for font system to be loaded, returning error");
return Err(Error::BackendError(FontError::NotLoaded.into()));
}
};
let font_size = defaults.font_size * handle.dpi() / POINTS_PER_INCH;
let metrics = Metrics::new(font_size as _, font_size as _);
let default_attrs = {
let mut metadata = Metadata::new();
metadata.set_underline(defaults.underline);
metadata.set_strikethrough(defaults.strikethrough);
metadata.set_boldness(defaults.weight);
let mut attrs = Attrs::new()
.family(cvt_family(&defaults.font))
.weight(cvt_weight(defaults.weight))
.style(cvt_style(defaults.style))
.metadata(metadata.into_raw());
if defaults.fg_color != util::DEFAULT_TEXT_COLOR {
attrs = attrs.color(cvt_color(defaults.fg_color));
}
font_system.fix_attrs(attrs)
};
let mut buffer_lines = handle.take_buffer();
let mut offset = 0;
for line in ct::BidiParagraphs::new(&string) {
let start = offset;
let end = start + line.len() + 1;
let attrs_list = range_attributes.text_attributes(
font_system,
start..end,
default_attrs.as_attrs(),
)?;
let mut line = BufferLine::new(line, attrs_list, shaping);
line.set_align(self.alignment.map(|a| match a {
TextAlignment::Start => ct::Align::Left,
TextAlignment::Center => ct::Align::Center,
TextAlignment::End => ct::Align::Right,
TextAlignment::Justified => ct::Align::Justified,
}));
buffer_lines.push(line);
offset = end;
}
let mut buffer = {
let FontSystemAndDefaults { system, .. } = font_system;
let mut buffer = Buffer::new(system, metrics);
buffer.lines = buffer_lines;
buffer.set_size(system, max_width as f32, f32::INFINITY);
buffer.set_wrap(system, ct::Wrap::Word);
buffer.shape_until_scroll(system);
buffer
};
fix_shaping_holes(
&mut buffer,
&mut range_attributes,
default_attrs.as_attrs(),
font_system,
)?;
Ok(TextLayout::new(
handle.clone(),
buffer,
string,
font_size as i32,
&mut font_system.system,
))
}
}
fn fix_shaping_holes(
buffer: &mut Buffer,
attributes: &mut Attributes,
attrs: Attrs<'_>,
system: &mut FontSystemAndDefaults,
) -> Result<(), Error> {
if fill_holes(buffer, system, attrs, attributes, FillType::ClearFont)? {
buffer.shape_until_scroll(&mut system.system);
} else {
return Ok(());
}
if fill_holes(buffer, system, attrs, attributes, FillType::ClearStyle)? {
buffer.shape_until_scroll(&mut system.system);
} else {
return Ok(());
}
#[cfg(feature = "tracing")]
{
if !find_holes(&buffer.lines[0]).is_empty() {
trace!("Failed to fill holes in text");
}
}
Ok(())
}
#[derive(Clone, Copy)]
enum FillType {
ClearStyle,
ClearFont,
}
fn fill_holes(
buffer: &mut Buffer,
system: &mut FontSystemAndDefaults,
defaults: Attrs<'_>,
attributes: &mut Attributes,
ty: FillType,
) -> Result<bool, Error> {
let mut found_holes = false;
let mut offset = 0;
for line in &mut buffer.lines {
let holes = find_holes(line);
if holes.is_empty() {
continue;
}
found_holes = true;
let original = line.attrs_list();
for range in holes {
match ty {
FillType::ClearFont => {
let family = match original.get_span(range.start).family {
ct::Family::Cursive => piet::FontFamily::SERIF,
ct::Family::Monospace => piet::FontFamily::MONOSPACE,
ct::Family::SansSerif => piet::FontFamily::SANS_SERIF,
ct::Family::Serif => piet::FontFamily::SERIF,
ct::Family::Fantasy => piet::FontFamily::SANS_SERIF,
ct::Family::Name(name) => {
let mut family = piet::FontFamily::SANS_SERIF;
let name = name.to_ascii_lowercase();
if name.contains("serif") {
family = piet::FontFamily::SERIF;
} else if name.contains("mono") {
family = piet::FontFamily::MONOSPACE;
}
family
}
};
attributes.push(range, TextAttribute::FontFamily(family));
}
FillType::ClearStyle => {
attributes.push(
range.clone(),
TextAttribute::Style(piet::FontStyle::Regular),
);
attributes.push(range, TextAttribute::Weight(piet::FontWeight::NORMAL));
}
};
}
let end = offset + line.text().len() + 1;
let attrs_list = attributes.text_attributes(system, offset..end, defaults)?;
line.set_attrs_list(attrs_list);
offset = end;
}
Ok(found_holes)
}
fn find_holes(line: &BufferLine) -> TinyVec<[Range<usize>; 1]> {
let mut holes = TinyVec::new();
let shape = match line.shape_opt().as_ref() {
Some(shape) => shape,
None => return holes,
};
let mut current_range = 0..0;
let mut in_hole = false;
for word in shape.spans.iter().flat_map(|span| &span.words) {
if word.blank {
if in_hole {
let end = word
.glyphs
.iter()
.map(|glyph| glyph.end)
.chain(Some(current_range.end))
.max()
.unwrap();
let start = word
.glyphs
.iter()
.map(|glyph| glyph.start)
.chain(Some(current_range.start))
.min()
.unwrap();
current_range = start..end;
}
continue;
}
for glyph in &word.glyphs {
if glyph.glyph_id == 0 {
if !in_hole {
in_hole = true;
current_range = glyph.start..glyph.end;
} else {
current_range.start = cmp::min(current_range.start, glyph.start);
current_range.end = cmp::max(current_range.end, glyph.end);
}
} else if mem::replace(&mut in_hole, false) {
holes.push(current_range);
current_range = 0..0;
}
}
}
if in_hole && !current_range.is_empty() {
holes.push(current_range);
}
holes
}