Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
43 changes: 32 additions & 11 deletions src/display/glyph_pos.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,8 +24,11 @@ pub struct Effect {
/// (Note that we use `u32` not `usize` since it can be assumed text length
/// will never exceed `u32::MAX`.)
pub start: u32,
/// User-specified value, e.g. index into a colour palette
pub e: u16,
/// User-specified value
///
/// Usage is not specified by `kas-text`, but typically this field will be
/// used as an index into a colour palette or not used at all.
pub color: u16,
/// Effect flags
pub flags: EffectFlags,
}
Expand Down Expand Up @@ -170,8 +173,8 @@ impl<'a> GlyphRun<'a> {

/// Yield glyphs and effects for this run
///
/// The callback `f` receives `glyph, e` where `e` is the [`Effect::e`]
/// value (defaults to 0).
/// The callback `f` receives `glyph, color` where `color` is the
/// [`Effect::color`] value (defaults to 0).
///
/// The callback `g` receives positioning for each underline/strike-through
/// segment: `x1, x2, y_top, h` where `h` is the thickness (height). Note
Expand Down Expand Up @@ -235,15 +238,15 @@ impl<'a> GlyphRun<'a> {
let y_top = position.1 - metrics.position;
let h = metrics.thickness;
let x1 = position.0;
underline = Some((x1, y_top, h, fmt.e));
underline = Some((x1, y_top, h, fmt.color));
}
if fmt.flags.contains(EffectFlags::STRIKETHROUGH)
&& let Some(metrics) = sf.strikethrough_metrics()
{
let y_top = position.1 - metrics.position;
let h = metrics.thickness;
let x1 = position.0;
strikethrough = Some((x1, y_top, h, fmt.e));
strikethrough = Some((x1, y_top, h, fmt.color));
}
}

Expand Down Expand Up @@ -297,7 +300,7 @@ impl<'a> GlyphRun<'a> {
let y_top = glyph.position.1 - metrics.position;
let h = metrics.thickness;
let x1 = glyph.position.0;
underline = Some((x1, y_top, h, fmt.e));
underline = Some((x1, y_top, h, fmt.color));
}
}
if strikethrough.is_some() != fmt.flags.contains(EffectFlags::STRIKETHROUGH) {
Expand All @@ -309,12 +312,12 @@ impl<'a> GlyphRun<'a> {
let y_top = glyph.position.1 - metrics.position;
let h = metrics.thickness;
let x1 = glyph.position.0;
strikethrough = Some((x1, y_top, h, fmt.e));
strikethrough = Some((x1, y_top, h, fmt.color));
}
}
}

f(glyph, fmt.e);
f(glyph, fmt.color);
}

// Effects end at the following glyph's start (or end of this run part)
Expand Down Expand Up @@ -446,8 +449,11 @@ impl TextDisplay {
/// free).
///
/// An [`Effect`] sequence supports underline, strikethrough and custom
/// indexing (e.g. for a color palette). Pass `&[]` if effects are not
/// required. (The default effect is always [`Effect::default()`].)
/// indexing (e.g. for a color palette). This sequence may be the result of
/// [`FormattableText::effect_tokens`], `&[]`, or any other sequence such
/// that [`Effect::start`] values are strictly increasing and compatible
/// with text `char` indices (see also [`FormattableText::effect_tokens`]).
/// (It is not required to re-prepare text when changing the sequence.)
///
/// Runs are yielded in undefined order. The total number of
/// glyphs yielded will equal [`TextDisplay::num_glyphs`].
Expand All @@ -459,6 +465,21 @@ impl TextDisplay {
offset: Vec2,
effects: &'a [Effect],
) -> impl Iterator<Item = GlyphRun<'a>> + 'a {
#[cfg(debug_assertions)]
{
let mut start = None;
for effect in effects {
if let Some(i) = start
&& effect.start <= i
{
panic!(
"TextDisplay::runs: Effect::start indices are not strictly increasing in {effects:?}"
);
}
start = Some(effect.start);
}
}

self.wrapped_runs
.iter()
.filter(|part| !part.glyph_range.is_empty())
Expand Down
17 changes: 14 additions & 3 deletions src/display/text_runs.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,13 @@ impl TextDisplay {
///
/// This updates the result of [`TextDisplay::prepare_runs`] due to change
/// in font size.
pub fn resize_runs<F: FormattableText + ?Sized>(&mut self, text: &F, mut dpem: f32) {
let mut font_tokens = text.font_tokens(dpem);
pub fn resize_runs<F: FormattableText + ?Sized>(
&mut self,
text: &F,
font: FontSelector,
mut dpem: f32,
) {
let mut font_tokens = text.font_tokens(dpem, font);
let mut next_fmt = font_tokens.next();

let text = text.as_str();
Expand Down Expand Up @@ -171,7 +176,7 @@ impl TextDisplay {

self.runs.clear();

let mut font_tokens = text.font_tokens(dpem);
let mut font_tokens = text.font_tokens(dpem, font);
let mut next_fmt = font_tokens.next();
if let Some(fmt) = next_fmt.as_ref()
&& fmt.start == 0
Expand Down Expand Up @@ -292,6 +297,12 @@ impl TextDisplay {
font = fmt.font;
dpem = fmt.dpem;
next_fmt = font_tokens.next();
debug_assert!(
next_fmt
.as_ref()
.map(|fmt| to_usize(fmt.start) > index)
.unwrap_or(true)
);
}

let mut new_script = None;
Expand Down
160 changes: 29 additions & 131 deletions src/format.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,8 @@

//! Parsers for formatted text

use crate::Effect;
use crate::fonts::FontSelector;
use crate::{Effect, OwningVecIter};
#[allow(unused)]
use crate::{Text, TextDisplay}; // for doc-links

Expand All @@ -18,14 +18,7 @@ mod markdown;
pub use markdown::{Error as MarkdownError, Markdown};

/// Text, optionally with formatting data
///
/// Any `F: FormattableText` automatically support [`FormattableTextDyn`].
/// Implement either this or [`FormattableTextDyn`], not both.
pub trait FormattableText: std::cmp::PartialEq + std::fmt::Debug {
type FontTokenIter<'a>: Iterator<Item = FontToken>
where
Self: 'a;

pub trait FormattableText: std::cmp::PartialEq {
/// Length of text
///
/// Default implementation uses [`FormattableText::as_str`].
Expand All @@ -37,148 +30,53 @@ pub trait FormattableText: std::cmp::PartialEq + std::fmt::Debug {
/// Access whole text as contiguous `str`
fn as_str(&self) -> &str;

/// Construct an iterator over formatting items
/// Return an iterator of font tokens
///
/// It is expected that [`FontToken::start`] of yielded items is strictly
/// increasing; if not, formatting may not be applied correctly.
/// These tokens are used to select the font and font size.
/// Each text object has a configured
/// [font size][crate::Text::set_font_size] and [`FontSelector`]; these
/// values are passed as a reference (`dpem` and `font`).
///
/// The default [font size][crate::Text::set_font_size] (`dpem`) is passed
/// as a reference.
/// The iterator is expected to yield a stream of tokens such that
/// [`FontToken::start`] values are strictly increasing, less than
/// [`Self::str_len`] and at `char` boundaries (i.e. an index value returned
/// by [`str::char_indices`]. In case the returned iterator is empty or the
/// first [`FontToken::start`] value is greater than zero the reference
/// `dpem` and `font` values are used.
///
/// For plain text this iterator will be empty.
fn font_tokens<'a>(&'a self, dpem: f32) -> Self::FontTokenIter<'a>;
/// Any changes to the result of this method require full re-preparation of
/// text since this affects run breaking and font resolution.
fn font_tokens(&self, dpem: f32, font: FontSelector) -> impl Iterator<Item = FontToken>;

/// Get the sequence of effect tokens
/// Return the sequence of effect tokens
///
/// These tokens are used to select the font color and
/// [effects](crate::EffectFlags).
///
/// This method has some limitations: (1) it may only return a reference to
/// an existing sequence, (2) effect tokens cannot be generated dependent
/// on input state, and (3) it does not incorporate color information. For
/// most uses it should still be sufficient, but for other cases it may be
/// preferable not to use this method (use a dummy implementation returning
/// `&[]` and use inherent methods on the text object via [`Text::text`]).
/// The values of [`Effect::start`] are expected to be strictly increasing
/// in order, less than [`Self::str_len`]. In case the slice is empty or the
/// first [`Effect::start`] value is greater than zero, values from
/// [`Effect::default()`] are used.
///
/// Changes to the result of this method do not require any re-preparation
/// of text.
fn effect_tokens(&self) -> &[Effect];
}

impl<F: FormattableText + ?Sized> FormattableText for &F {
type FontTokenIter<'a>
= F::FontTokenIter<'a>
where
Self: 'a;

fn as_str(&self) -> &str {
F::as_str(self)
}

fn font_tokens<'a>(&'a self, dpem: f32) -> Self::FontTokenIter<'a> {
F::font_tokens(self, dpem)
fn font_tokens(&self, dpem: f32, font: FontSelector) -> impl Iterator<Item = FontToken> {
F::font_tokens(self, dpem, font)
}

fn effect_tokens(&self) -> &[Effect] {
F::effect_tokens(self)
}
}

/// Text, optionally with formatting data
///
/// This is an object-safe version of the [`FormattableText`] trait (i.e.
/// `dyn FormattableTextDyn` is a valid type).
///
/// This trait is auto-implemented for every implementation of [`FormattableText`].
/// The type `&dyn FormattableTextDyn` implements [`FormattableText`].
/// Implement either this or (preferably) [`FormattableText`], not both.
pub trait FormattableTextDyn: std::fmt::Debug {
/// Produce a boxed clone of self
fn clone_boxed(&self) -> Box<dyn FormattableTextDyn>;

/// Length of text
fn str_len(&self) -> usize;

/// Access whole text as contiguous `str`
fn as_str(&self) -> &str;

/// Construct an iterator over formatting items
///
/// It is expected that [`FontToken::start`] of yielded items is strictly
/// increasing; if not, formatting may not be applied correctly.
///
/// The default [font size][crate::Text::set_font_size] (`dpem`) is passed
/// as a reference.
///
/// For plain text this iterator will be empty.
fn font_tokens(&self, dpem: f32) -> OwningVecIter<FontToken>;

/// Get the sequence of effect tokens
///
/// This method has some limitations: (1) it may only return a reference to
/// an existing sequence, (2) effect tokens cannot be generated dependent
/// on input state, and (3) it does not incorporate color information. For
/// most uses it should still be sufficient, but for other cases it may be
/// preferable not to use this method (use a dummy implementation returning
/// `&[]` and use inherent methods on the text object via [`Text::text`]).
fn effect_tokens(&self) -> &[Effect];
}

impl<F: FormattableText + Clone + 'static> FormattableTextDyn for F {
fn clone_boxed(&self) -> Box<dyn FormattableTextDyn> {
Box::new(self.clone())
}

fn str_len(&self) -> usize {
FormattableText::str_len(self)
}
fn as_str(&self) -> &str {
FormattableText::as_str(self)
}

fn font_tokens(&self, dpem: f32) -> OwningVecIter<FontToken> {
let iter = FormattableText::font_tokens(self, dpem);
OwningVecIter::new(iter.collect())
}

fn effect_tokens(&self) -> &[Effect] {
FormattableText::effect_tokens(self)
}
}

/// References to [`FormattableTextDyn`] always compare unequal
impl<'t> PartialEq for &'t dyn FormattableTextDyn {
fn eq(&self, _: &Self) -> bool {
false
}
}

impl<'t> FormattableText for &'t dyn FormattableTextDyn {
type FontTokenIter<'a>
= OwningVecIter<FontToken>
where
Self: 'a;

#[inline]
fn str_len(&self) -> usize {
FormattableTextDyn::str_len(*self)
}

#[inline]
fn as_str(&self) -> &str {
FormattableTextDyn::as_str(*self)
}

#[inline]
fn font_tokens(&self, dpem: f32) -> OwningVecIter<FontToken> {
FormattableTextDyn::font_tokens(*self, dpem)
}

fn effect_tokens(&self) -> &[Effect] {
FormattableTextDyn::effect_tokens(*self)
}
}

impl Clone for Box<dyn FormattableTextDyn> {
fn clone(&self) -> Self {
(**self).clone_boxed()
}
}

/// Font formatting token
#[derive(Clone, Debug, PartialEq)]
pub struct FontToken {
Expand Down
Loading
Loading