Introducing runs to FormattedText

This commit is contained in:
b-guild
2025-05-31 17:52:16 -07:00
parent 2501bddfd6
commit e21e1be604
4 changed files with 512 additions and 91 deletions
+55 -52
View File
@@ -19,6 +19,7 @@
// SOFTWARE.
use crate::font::FontHeight;
use crate::formatted_text::{DrawValueLayer, GlyphDrawValues};
use crate::style::resource::StyleResource;
use crate::{
brush::Brush,
@@ -949,48 +950,55 @@ impl DrawingContext {
material: &MaterialResource,
formatted_text: &FormattedText,
) {
let font = formatted_text.get_font();
#[inline(always)]
fn draw(
formatted_text: &FormattedText,
layer: DrawValueLayer,
ctx: &mut DrawingContext,
clip_bounds: Rect<f32>,
position: Vector2<f32>,
dilation: f32,
offset: Vector2<f32>,
brush: Brush,
font: &FontResource,
material: &MaterialResource,
) {
let Some(mut current_page_index) = formatted_text
.get_glyphs()
.first()
.map(|g| g.atlas_page_index)
else {
return;
};
let mut current_draw_values = None;
for element in formatted_text.get_glyphs() {
// If we've switched to another atlas page, commit the text and start a new batch.
if current_page_index != element.atlas_page_index {
ctx.commit(
clip_bounds,
brush.clone(),
CommandTexture::Font {
font: font.clone(),
page_index: current_page_index,
// Use font size scaled by super sampling scaling to pick correct atlas
// page.
height: FontHeight::from(formatted_text.super_sampled_font_size()),
},
material,
None,
);
current_page_index = element.atlas_page_index;
if let DrawValueLayer::Shadow = layer {
if !formatted_text.shadow_at(element.source_char_index) {
continue;
}
}
let draw_values = formatted_text.get_glyph_draw_values(layer, element);
if current_draw_values.is_none() {
current_draw_values = Some(draw_values)
} else if current_draw_values.as_ref() != Some(&draw_values) {
// If the drawing values have changed, commit the text and start a new batch.
let GlyphDrawValues {
atlas_page_index: page_index,
font,
brush,
height,
} = current_draw_values.unwrap();
let texture = CommandTexture::Font {
font,
page_index,
height,
};
ctx.commit(clip_bounds, brush, texture, material, None);
current_draw_values = Some(draw_values);
}
let bounds = element.bounds;
let dilation = match layer {
DrawValueLayer::Main => 0.0,
DrawValueLayer::Shadow => {
formatted_text.shadow_dilation_at(element.source_char_index)
}
};
let offset = match layer {
DrawValueLayer::Main => Vector2::default(),
DrawValueLayer::Shadow => {
formatted_text.shadow_offset_at(element.source_char_index)
}
};
let final_bounds = Rect::new(
position.x + bounds.x() + offset.x,
@@ -1003,46 +1011,41 @@ impl DrawingContext {
ctx.push_rect_filled(&final_bounds, Some(&element.tex_coords));
}
// Commit the rest.
ctx.commit(
clip_bounds,
if let Some(GlyphDrawValues {
atlas_page_index: page_index,
font,
brush,
CommandTexture::Font {
font: font.clone(),
page_index: current_page_index,
// Use font size scaled by super sampling scaling to pick correct atlas
// page.
height: FontHeight::from(formatted_text.super_sampled_font_size()),
},
material,
None,
);
height,
}) = current_draw_values
{
let texture = CommandTexture::Font {
font,
page_index,
height,
};
// Commit the rest.
ctx.commit(clip_bounds, brush, texture, material, None);
}
}
// Draw shadow, if any.
if *formatted_text.shadow {
if *formatted_text.shadow || !formatted_text.runs.is_empty() {
draw(
formatted_text,
DrawValueLayer::Shadow,
self,
clip_bounds,
position,
*formatted_text.shadow_dilation,
*formatted_text.shadow_offset,
(*formatted_text.shadow_brush).clone(),
&font,
material,
);
}
draw(
formatted_text,
DrawValueLayer::Main,
self,
clip_bounds,
position,
0.0,
Default::default(),
formatted_text.brush(),
&font,
material,
);
}
+184 -39
View File
@@ -24,16 +24,18 @@ use crate::{
algebra::Vector2, color::Color, math::Rect, reflect::prelude::*, uuid_provider,
variable::InheritableVariable, visitor::prelude::*,
},
font::{Font, FontGlyph, FontResource},
font::{Font, FontGlyph, FontHeight, FontResource},
style::StyledProperty,
HorizontalAlignment, VerticalAlignment,
};
pub use run::*;
use std::ops::Range;
use strum_macros::{AsRefStr, EnumString, VariantNames};
mod textwrapper;
use textwrapper::*;
mod run;
mod textwrapper;
/// Defines a position in the text. It is just a coordinates of a character in text.
#[derive(Copy, Clone, PartialEq, Eq, PartialOrd, Ord, Debug, Default, Visit, Reflect)]
pub struct Position {
@@ -49,6 +51,7 @@ pub struct TextGlyph {
pub bounds: Rect<f32>,
pub tex_coords: [Vector2<f32>; 4],
pub atlas_page_index: usize,
pub source_char_index: usize,
}
#[derive(Copy, Clone, Debug, Default)]
@@ -160,6 +163,7 @@ fn build_glyph(
metrics: &mut GlyphMetrics,
mut x: f32,
mut y: f32,
source_char_index: usize,
character: char,
prev_character: Option<char>,
super_sampling_scale: f32,
@@ -187,6 +191,7 @@ fn build_glyph(
bounds: rect,
tex_coords: glyph.tex_coords,
atlas_page_index: glyph.page_index,
source_char_index,
};
let advance = glyph.advance
+ prev_character
@@ -201,6 +206,7 @@ fn build_glyph(
bounds: rect,
tex_coords: [Vector2::default(); 4],
atlas_page_index: 0,
source_char_index,
};
(text_glyph, rect.w())
}
@@ -257,9 +263,40 @@ pub struct FormattedText {
pub shadow_brush: InheritableVariable<Brush>,
pub shadow_dilation: InheritableVariable<f32>,
pub shadow_offset: InheritableVariable<Vector2<f32>>,
#[visit(optional)]
pub runs: RunSet,
}
impl FormattedText {
pub fn font_at(&self, index: usize) -> FontResource {
self.runs.font_at(index).unwrap_or_else(|| self.get_font())
}
pub fn font_size_at(&self, index: usize) -> f32 {
self.runs
.font_size_at(index)
.unwrap_or_else(|| self.font_size().property)
}
pub fn brush_at(&self, index: usize) -> Brush {
self.runs.brush_at(index).unwrap_or_else(|| self.brush())
}
pub fn shadow_at(&self, index: usize) -> bool {
self.runs.shadow_at(index).unwrap_or(*self.shadow)
}
pub fn shadow_brush_at(&self, index: usize) -> Brush {
self.runs
.shadow_brush_at(index)
.unwrap_or_else(|| self.shadow_brush.clone_inner())
}
pub fn shadow_dilation_at(&self, index: usize) -> f32 {
self.runs
.shadow_dilation_at(index)
.unwrap_or(*self.shadow_dilation)
}
pub fn shadow_offset_at(&self, index: usize) -> Vector2<f32> {
self.runs
.shadow_offset_at(index)
.unwrap_or(*self.shadow_offset)
}
pub fn nearest_valid_position(&self, start: Position) -> Position {
if self.lines.is_empty() {
return Position::default();
@@ -476,6 +513,31 @@ impl FormattedText {
&self.glyphs
}
pub fn get_glyph_draw_values(
&self,
layer: DrawValueLayer,
glyph: &TextGlyph,
) -> GlyphDrawValues {
let atlas_page_index = glyph.atlas_page_index;
let i = glyph.source_char_index;
let font = self.font_at(i);
let height = FontHeight::from(self.font_size_at(i) * self.super_sampling_scale);
match layer {
DrawValueLayer::Main => GlyphDrawValues {
atlas_page_index,
font,
brush: self.brush_at(i),
height,
},
DrawValueLayer::Shadow => GlyphDrawValues {
atlas_page_index,
font,
brush: self.shadow_brush_at(i),
height,
},
}
}
pub fn get_font(&self) -> FontResource {
(*self.font).clone()
}
@@ -637,23 +699,18 @@ impl FormattedText {
}
pub fn build(&mut self) -> Vector2<f32> {
let mut font_state = self.font.state();
let Some(font) = font_state.data() else {
return Default::default();
};
let mut metrics = GlyphMetrics {
font,
size: **self.font_size(),
};
let line_height: f32 = metrics.ascender();
self.lines.clear();
let mut lines = std::mem::take(&mut self.lines);
lines.clear();
let sink = WrapSink {
lines: &mut self.lines,
lines: &mut lines,
max_width: self.constraint.x,
};
if let Some(mask) = *self.mask_char {
let advance = metrics.advance(mask);
let advance = GlyphMetrics {
font: &mut self.font.data_ref(),
size: **self.font_size,
}
.advance(mask);
match *self.wrap {
WrapMode::NoWrap => wrap_mask(NoWrap::new(sink), self.text.len(), mask, advance),
WrapMode::Letter => wrap_mask(
@@ -665,16 +722,24 @@ impl FormattedText {
WrapMode::Word => wrap_mask(WordWrap::new(sink), self.text.len(), mask, advance),
}
} else {
let source = self.text.iter().enumerate().map(|(i, c)| {
let a = GlyphMetrics {
font: &mut self.font_at(i).data_ref(),
size: self.font_size_at(i),
}
.advance(*c);
(*c, a)
});
match *self.wrap {
WrapMode::NoWrap => wrap(NoWrap::new(sink), &mut metrics, self.text.as_slice()),
WrapMode::Letter => wrap(LetterWrap::new(sink), &mut metrics, self.text.as_slice()),
WrapMode::Word => wrap(WordWrap::new(sink), &mut metrics, self.text.as_slice()),
WrapMode::NoWrap => wrap(NoWrap::new(sink), source),
WrapMode::Letter => wrap(LetterWrap::new(sink), source),
WrapMode::Word => wrap(WordWrap::new(sink), source),
}
}
let total_height = line_height * self.lines.len() as f32;
let mut total_height = 0.0;
// Align lines according to desired alignment.
for line in self.lines.iter_mut() {
for line in lines.iter_mut() {
match *self.horizontal_alignment {
HorizontalAlignment::Left => line.x_offset = 0.0,
HorizontalAlignment::Center => {
@@ -694,6 +759,26 @@ impl FormattedText {
HorizontalAlignment::Stretch => line.x_offset = 0.0,
}
}
// Calculate line height
for line in lines.iter_mut() {
if self.mask_char.is_some() || self.runs.is_empty() {
line.height = GlyphMetrics {
font: &mut self.font.data_ref(),
size: **self.font_size,
}
.ascender();
} else {
for i in line.begin..line.end {
let h = GlyphMetrics {
font: &mut self.font_at(i).data_ref(),
size: self.font_size_at(i),
}
.ascender();
line.height = line.height.max(h);
}
}
total_height += line.height;
}
// Generate glyphs for each text line.
self.glyphs.clear();
@@ -716,29 +801,40 @@ impl FormattedText {
}
VerticalAlignment::Stretch => 0.0,
};
let mut y: f32 = cursor_y_start.floor();
for line in self.lines.iter_mut() {
for line in lines.iter_mut() {
let mut x = line.x_offset.floor();
if let Some(mask) = *self.mask_char {
let mut prev = None;
let mut metrics = GlyphMetrics {
font: &mut self.font.data_ref(),
size: **self.font_size,
};
for c in std::iter::repeat_n(mask, line.len()) {
let (glyph, advance) =
build_glyph(&mut metrics, x, y, c, prev, self.super_sampling_scale);
build_glyph(&mut metrics, x, y, 0, c, prev, self.super_sampling_scale);
self.glyphs.push(glyph);
x += advance;
prev = Some(c);
}
} else {
let mut prev = None;
for c in self.text.iter().take(line.end).skip(line.begin).cloned() {
for (i, &c) in self.text.iter().enumerate().take(line.end).skip(line.begin) {
let font = self.font_at(i);
let font = &mut font.data_ref();
let mut metrics = GlyphMetrics {
font,
size: self.font_size_at(i),
};
match c {
'\n' => {
x += metrics.newline_advance();
}
_ => {
let y1 = y + line.height - metrics.ascender();
let scale = self.super_sampling_scale;
let (glyph, advance) =
build_glyph(&mut metrics, x, y, c, prev, self.super_sampling_scale);
build_glyph(&mut metrics, x, y1, i, c, prev, scale);
self.glyphs.push(glyph);
x += advance;
}
@@ -746,26 +842,56 @@ impl FormattedText {
prev = Some(c);
}
}
line.height = line_height;
line.y_offset = y;
y += line_height;
y += line.height;
}
let size_x = self
.lines
.iter()
.map(|line| line.width)
.max_by(f32::total_cmp)
.unwrap_or_default();
// Minus here is because descender has negative value.
let size_y = total_height - metrics.descender();
let size_x = if self.constraint.x.is_finite() {
self.constraint.x
} else {
lines
.iter()
.map(|line| line.width)
.max_by(f32::total_cmp)
.unwrap_or_default()
};
let size_y = if self.constraint.y.is_finite() {
self.constraint.y
} else {
let descender = if self.mask_char.is_some() || self.runs.is_empty() {
GlyphMetrics {
font: &mut self.font.data_ref(),
size: **self.font_size,
}
.descender()
} else if let Some(line) = self.lines.last() {
(line.begin..line.end)
.map(|i| {
GlyphMetrics {
font: &mut self.font_at(i).data_ref(),
size: self.font_size_at(i),
}
.descender()
})
.min_by(f32::total_cmp)
.unwrap_or_default()
} else {
0.0
};
// Minus here is because descender has negative value.
total_height - descender
};
self.lines = lines;
Vector2::new(size_x, size_y)
}
}
fn wrap<W: TextWrapper>(mut wrapper: W, metrics: &mut GlyphMetrics, text: &[char]) {
for &character in text.iter() {
let advance = metrics.advance(character);
fn wrap<W, I>(mut wrapper: W, source: I)
where
W: TextWrapper,
I: Iterator<Item = (char, f32)>,
{
for (character, advance) in source {
wrapper.push(character, advance);
}
wrapper.finish();
@@ -793,6 +919,7 @@ pub struct FormattedTextBuilder {
shadow_offset: Vector2<f32>,
font_size: StyledProperty<f32>,
super_sampling_scaling: f32,
runs: Vec<Run>,
}
impl FormattedTextBuilder {
@@ -813,6 +940,7 @@ impl FormattedTextBuilder {
shadow_offset: Vector2::new(1.0, 1.0),
font_size: 14.0f32.into(),
super_sampling_scaling: 1.0,
runs: Vec::default(),
}
}
@@ -887,6 +1015,22 @@ impl FormattedTextBuilder {
self
}
/// Adds the given run to the text to set the style for a portion of the text.
/// Later runs potentially overriding earlier runs if the ranges of the runs overlap and the later run
/// sets a property that conflicts with an earlier run.
pub fn with_run(mut self, run: Run) -> Self {
self.runs.push(run);
self
}
/// Adds multiple runs to the text to set the style of portions of the text.
/// Later runs potentially overriding earlier runs if the ranges of the runs overlap and the later run
/// sets a property that conflicts with an earlier run.
pub fn with_runs<I: IntoIterator<Item = Run>>(mut self, runs: I) -> Self {
self.runs.extend(runs);
self
}
pub fn build(self) -> FormattedText {
FormattedText {
text: self.text.chars().collect::<Vec<char>>().into(),
@@ -905,6 +1049,7 @@ impl FormattedTextBuilder {
font: self.font.into(),
shadow_dilation: self.shadow_dilation.into(),
shadow_offset: self.shadow_offset.into(),
runs: self.runs.into(),
}
}
}
+253
View File
@@ -0,0 +1,253 @@
// Copyright (c) 2019-present Dmitry Stepanov and Fyrox Engine contributors.
//
// Permission is hereby granted, free of charge, to any person obtaining a copy
// of this software and associated documentation files (the "Software"), to deal
// in the Software without restriction, including without limitation the rights
// to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
// copies of the Software, and to permit persons to whom the Software is
// furnished to do so, subject to the following conditions:
//
// The above copyright notice and this permission notice shall be included in all
// copies or substantial portions of the Software.
//
// THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
// IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
// FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
// AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
// LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
// OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
// SOFTWARE.
use std::ops::{Deref, DerefMut};
use crate::font::FontHeight;
use super::*;
#[derive(Clone, PartialEq, Debug, Default, Reflect)]
pub struct RunSet(Vec<Run>);
impl From<&[Run]> for RunSet {
fn from(value: &[Run]) -> Self {
Self(value.to_vec())
}
}
impl From<Vec<Run>> for RunSet {
fn from(value: Vec<Run>) -> Self {
Self(value)
}
}
impl Visit for RunSet {
fn visit(&mut self, name: &str, visitor: &mut Visitor) -> VisitResult {
self.0.visit(name, visitor)
}
}
impl Deref for RunSet {
type Target = Vec<Run>;
fn deref(&self) -> &Self::Target {
&self.0
}
}
impl DerefMut for RunSet {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.0
}
}
impl RunSet {
pub fn font_at(&self, index: usize) -> Option<FontResource> {
for run in self.0.iter().rev() {
if run.range.contains(&(index as u32)) && run.font().is_some() {
return Some(run.font().unwrap().clone());
}
}
None
}
pub fn font_size_at(&self, index: usize) -> Option<f32> {
for run in self.0.iter().rev() {
if run.range.contains(&(index as u32)) && run.font_size().is_some() {
return Some(run.font_size().unwrap());
}
}
None
}
pub fn brush_at(&self, index: usize) -> Option<Brush> {
for run in self.0.iter().rev() {
if run.range.contains(&(index as u32)) && run.brush().is_some() {
return Some(run.brush().unwrap().clone());
}
}
None
}
pub fn shadow_at(&self, index: usize) -> Option<bool> {
for run in self.0.iter().rev() {
if run.range.contains(&(index as u32)) && run.shadow().is_some() {
return Some(run.shadow().unwrap());
}
}
None
}
pub fn shadow_brush_at(&self, index: usize) -> Option<Brush> {
for run in self.0.iter().rev() {
if run.range.contains(&(index as u32)) && run.shadow_brush().is_some() {
return Some(run.shadow_brush().unwrap().clone());
}
}
None
}
pub fn shadow_dilation_at(&self, index: usize) -> Option<f32> {
for run in self.0.iter().rev() {
if run.range.contains(&(index as u32)) && run.shadow_dilation().is_some() {
return Some(run.shadow_dilation().unwrap());
}
}
None
}
pub fn shadow_offset_at(&self, index: usize) -> Option<Vector2<f32>> {
for run in self.0.iter().rev() {
if run.range.contains(&(index as u32)) && run.shadow_offset().is_some() {
return Some(run.shadow_offset().unwrap());
}
}
None
}
}
/// The style of a partion of text within a range.
#[derive(Clone, PartialEq, Debug, Default, Visit, Reflect)]
pub struct Run {
/// The range of characters that this run applies to within the text.
pub range: Range<u32>,
font: Option<FontResource>,
brush: Option<Brush>,
font_size: Option<f32>,
shadow: Option<bool>,
shadow_brush: Option<Brush>,
shadow_dilation: Option<f32>,
shadow_offset: Option<Vector2<f32>>,
}
impl Run {
/// The font of the characters in this run, or None if the font is unmodified.
pub fn font(&self) -> Option<&FontResource> {
self.font.as_ref()
}
/// The brush of the characters in this run, or None if the brush is unmodified.
pub fn brush(&self) -> Option<&Brush> {
self.brush.as_ref()
}
/// The size of the characters in this run, or None if the size is unmodified.
pub fn font_size(&self) -> Option<f32> {
self.font_size
}
/// True if the characters in this run should have a shadow. None if this run does not change
/// whether the characters are shadowed.
pub fn shadow(&self) -> Option<bool> {
self.shadow
}
/// The brush for the shadow of the characters in this run, or None if the brush is unmodified.
pub fn shadow_brush(&self) -> Option<&Brush> {
self.shadow_brush.as_ref()
}
/// The dilation for the shadow in this run, or None if the dilation is unmodified.
pub fn shadow_dilation(&self) -> Option<f32> {
self.shadow_dilation
}
/// The offset for the shadow in this run, or None if the offset is unmodified.
pub fn shadow_offset(&self) -> Option<Vector2<f32>> {
self.shadow_offset
}
}
#[derive(Clone, Copy, Eq, PartialEq)]
pub enum DrawValueLayer {
Main,
Shadow,
}
#[derive(Clone, PartialEq, Debug)]
pub struct GlyphDrawValues {
pub atlas_page_index: usize,
pub font: FontResource,
pub brush: Brush,
/// Font size scaled by super sampling scaling to pick correct atlas page.
pub height: FontHeight,
}
pub struct RunBuilder {
range: Range<u32>,
font: Option<FontResource>,
brush: Option<Brush>,
font_size: Option<f32>,
shadow: Option<bool>,
shadow_brush: Option<Brush>,
shadow_dilation: Option<f32>,
shadow_offset: Option<Vector2<f32>>,
}
impl RunBuilder {
pub fn new(range: Range<u32>) -> Self {
Self {
range,
font: None,
brush: None,
font_size: None,
shadow: None,
shadow_brush: None,
shadow_dilation: None,
shadow_offset: None,
}
}
pub fn build(self) -> Run {
Run {
range: self.range,
font: self.font,
brush: self.brush,
font_size: self.font_size,
shadow: self.shadow,
shadow_brush: self.shadow_brush,
shadow_dilation: self.shadow_dilation,
shadow_offset: self.shadow_offset,
}
}
/// Set this run to modify the font of the text within the range.
pub fn with_font(mut self, font: FontResource) -> Self {
self.font = Some(font);
self
}
/// Set this run to modify the brush of the text within the range.
pub fn with_brush(mut self, brush: Brush) -> Self {
self.brush = Some(brush);
self
}
/// Set this run to modify the size of the text within the range.
pub fn with_size(mut self, size: f32) -> Self {
self.font_size = Some(size);
self
}
/// Set this run to enable or disable the shadow of the text within the range.
pub fn with_shadow(mut self, shadow: bool) -> Self {
self.shadow = Some(shadow);
self
}
/// Set this run to modify the brush of the shadow within the range.
pub fn with_shadow_brush(mut self, brush: Brush) -> Self {
self.shadow_brush = Some(brush);
self
}
/// Set this run to modify the dilation of the shadow within the range.
pub fn with_shadow_dilation(mut self, size: f32) -> Self {
self.shadow_dilation = Some(size);
self
}
/// Set this run to modify the offset of the shadow within the range.
pub fn with_shadow_offset(mut self, offset: Vector2<f32>) -> Self {
self.shadow_offset = Some(offset);
self
}
}
+20
View File
@@ -23,6 +23,7 @@
#![warn(missing_docs)]
use crate::formatted_text::Run;
use crate::style::StyledProperty;
use crate::{
brush::Brush,
@@ -502,6 +503,7 @@ pub struct TextBuilder {
shadow_dilation: f32,
shadow_offset: Vector2<f32>,
font_size: Option<StyledProperty<f32>>,
runs: Vec<Run>,
}
impl TextBuilder {
@@ -519,6 +521,7 @@ impl TextBuilder {
shadow_dilation: 1.0,
shadow_offset: Vector2::new(1.0, 1.0),
font_size: None,
runs: Vec::default(),
}
}
@@ -589,6 +592,22 @@ impl TextBuilder {
self
}
/// Adds the given run to the text to set the style for a portion of the text.
/// Later runs potentially overriding earlier runs if the ranges of the runs overlap and the later run
/// sets a property that conflicts with an earlier run.
pub fn with_run(mut self, run: Run) -> Self {
self.runs.push(run);
self
}
/// Adds multiple runs to the text to set the style of portions of the text.
/// Later runs potentially overriding earlier runs if the ranges of the runs overlap and the later run
/// sets a property that conflicts with an earlier run.
pub fn with_runs<I: IntoIterator<Item = Run>>(mut self, runs: I) -> Self {
self.runs.extend(runs);
self
}
/// Finishes text widget creation and registers it in the user interface, returning its handle to you.
pub fn build(mut self, ctx: &mut BuildContext) -> Handle<UiNode> {
let font = if let Some(font) = self.font {
@@ -617,6 +636,7 @@ impl TextBuilder {
self.font_size
.unwrap_or_else(|| ctx.style.property(Style::FONT_SIZE)),
)
.with_runs(self.runs)
.build(),
),
};