Text paragraphs, styling, fontconfig
This commit is contained in:
@@ -45,10 +45,10 @@ fn layout_element(
|
||||
if let Some(text) = element.text_node() {
|
||||
let content = inset_rect(rect, element.style.padding);
|
||||
if content.size.width > 0.0 && content.size.height > 0.0 {
|
||||
scene.push_text(text_system.prepare(
|
||||
text.text.clone(),
|
||||
scene.push_text(text_system.prepare_spans(
|
||||
text.spans.clone(),
|
||||
content.origin,
|
||||
text.style.with_bounds(content.size),
|
||||
text.style.clone().with_bounds(content.size),
|
||||
));
|
||||
}
|
||||
return;
|
||||
@@ -148,7 +148,12 @@ fn intrinsic_main_size(
|
||||
FlexDirection::Row => (Some(available_main.max(0.0)), Some(cross_size.max(0.0))),
|
||||
FlexDirection::Column => (Some(cross_size.max(0.0)), None),
|
||||
};
|
||||
let content = text_system.measure(&text.text, text.style, constraints.0, constraints.1);
|
||||
let content = text_system.measure_spans(
|
||||
text.spans.clone(),
|
||||
text.style.clone(),
|
||||
constraints.0,
|
||||
constraints.1,
|
||||
);
|
||||
let padding = main_axis_padding(child.style.padding, direction);
|
||||
return main_axis_size(content, direction) + padding;
|
||||
}
|
||||
@@ -169,9 +174,9 @@ fn intrinsic_size(
|
||||
text_system: &mut TextSystem,
|
||||
) -> UiSize {
|
||||
if let Some(text) = element.text_node() {
|
||||
let measured = text_system.measure(
|
||||
&text.text,
|
||||
text.style,
|
||||
let measured = text_system.measure_spans(
|
||||
text.spans.clone(),
|
||||
text.style.clone(),
|
||||
Some(available_size.width.max(0.0)),
|
||||
Some(available_size.height.max(0.0)),
|
||||
);
|
||||
|
||||
@@ -25,7 +25,10 @@ pub use scene::{
|
||||
Color, DisplayItem, GlyphInstance, Point, PreparedText, Quad, Rect, SceneSnapshot, Translation,
|
||||
UiSize,
|
||||
};
|
||||
pub use text::{TextAlign, TextStyle, TextSystem, TextWrap};
|
||||
pub use text::{
|
||||
TextAlign, TextFontFamily, TextSpan, TextSpanSlant, TextSpanWeight, TextStyle, TextSystem,
|
||||
TextWrap,
|
||||
};
|
||||
pub use tree::{Edges, Element, FlexDirection, Style};
|
||||
pub use window::{
|
||||
DecorationMode, WindowConfigured, WindowId, WindowLifecycle, WindowSpec, WindowUpdate,
|
||||
|
||||
@@ -93,6 +93,7 @@ pub struct GlyphInstance {
|
||||
pub glyph: String,
|
||||
pub position: Point,
|
||||
pub advance: f32,
|
||||
pub color: Color,
|
||||
pub cache_key: Option<CacheKey>,
|
||||
}
|
||||
|
||||
@@ -123,6 +124,7 @@ impl PreparedText {
|
||||
glyph: ch.to_string(),
|
||||
position: Point::new(x, origin.y),
|
||||
advance,
|
||||
color,
|
||||
cache_key: None,
|
||||
});
|
||||
x += advance;
|
||||
|
||||
@@ -1,4 +1,10 @@
|
||||
use cosmic_text::{Attrs, Buffer, FontSystem, Metrics, Shaping, Wrap};
|
||||
use std::collections::{BTreeSet, HashMap};
|
||||
|
||||
use cosmic_text::{
|
||||
Attrs, Buffer, Color as CosmicColor, FontSystem, Metrics, Shaping, Style as CosmicStyle,
|
||||
Weight as CosmicWeight, Wrap, fontdb,
|
||||
};
|
||||
use fontconfig::Fontconfig;
|
||||
|
||||
use crate::{Color, GlyphInstance, Point, PreparedText, UiSize};
|
||||
|
||||
@@ -9,6 +15,78 @@ pub enum TextAlign {
|
||||
End,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum TextSpanWeight {
|
||||
Normal,
|
||||
Medium,
|
||||
Semibold,
|
||||
Bold,
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum TextSpanSlant {
|
||||
Normal,
|
||||
Italic,
|
||||
Oblique,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Eq, Hash, PartialEq)]
|
||||
pub enum TextFontFamily {
|
||||
SansSerif,
|
||||
Serif,
|
||||
Monospace,
|
||||
Cursive,
|
||||
Fantasy,
|
||||
Named(String),
|
||||
}
|
||||
|
||||
impl TextFontFamily {
|
||||
pub fn named(name: impl Into<String>) -> Self {
|
||||
Self::Named(name.into())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct TextSpan {
|
||||
pub text: String,
|
||||
pub color: Option<Color>,
|
||||
pub weight: TextSpanWeight,
|
||||
pub slant: TextSpanSlant,
|
||||
pub font_family: Option<TextFontFamily>,
|
||||
}
|
||||
|
||||
impl TextSpan {
|
||||
pub fn new(text: impl Into<String>) -> Self {
|
||||
Self {
|
||||
text: text.into(),
|
||||
color: None,
|
||||
weight: TextSpanWeight::Normal,
|
||||
slant: TextSpanSlant::Normal,
|
||||
font_family: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn color(mut self, color: Color) -> Self {
|
||||
self.color = Some(color);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn weight(mut self, weight: TextSpanWeight) -> Self {
|
||||
self.weight = weight;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn slant(mut self, slant: TextSpanSlant) -> Self {
|
||||
self.slant = slant;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn family(mut self, family: TextFontFamily) -> Self {
|
||||
self.font_family = Some(family);
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum TextWrap {
|
||||
None,
|
||||
@@ -24,11 +102,12 @@ impl TextWrap {
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Copy, Debug, PartialEq)]
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub struct TextStyle {
|
||||
pub font_size: f32,
|
||||
pub line_height: f32,
|
||||
pub color: Color,
|
||||
pub font_family: TextFontFamily,
|
||||
pub bounds: Option<UiSize>,
|
||||
pub wrap: TextWrap,
|
||||
pub align: TextAlign,
|
||||
@@ -41,6 +120,7 @@ impl TextStyle {
|
||||
font_size,
|
||||
line_height: font_size * 1.2,
|
||||
color,
|
||||
font_family: TextFontFamily::SansSerif,
|
||||
bounds: None,
|
||||
wrap: TextWrap::None,
|
||||
align: TextAlign::Start,
|
||||
@@ -53,6 +133,11 @@ impl TextStyle {
|
||||
self
|
||||
}
|
||||
|
||||
pub fn with_font_family(mut self, font_family: TextFontFamily) -> Self {
|
||||
self.font_family = font_family;
|
||||
self
|
||||
}
|
||||
|
||||
pub const fn with_bounds(mut self, bounds: UiSize) -> Self {
|
||||
self.bounds = Some(bounds);
|
||||
self
|
||||
@@ -76,6 +161,7 @@ impl TextStyle {
|
||||
|
||||
pub struct TextSystem {
|
||||
font_system: FontSystem,
|
||||
family_resolver: FontFamilyResolver,
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
@@ -84,6 +170,11 @@ struct TextLayout {
|
||||
size: UiSize,
|
||||
}
|
||||
|
||||
struct FontFamilyResolver {
|
||||
fontconfig: Option<Fontconfig>,
|
||||
cache: HashMap<(TextFontFamily, TextSpanSlant), Option<String>>,
|
||||
}
|
||||
|
||||
impl Default for TextSystem {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
@@ -92,8 +183,12 @@ impl Default for TextSystem {
|
||||
|
||||
impl TextSystem {
|
||||
pub fn new() -> Self {
|
||||
let mut font_system = FontSystem::new();
|
||||
let mut family_resolver = FontFamilyResolver::new();
|
||||
configure_default_font_families(&mut font_system, &mut family_resolver);
|
||||
Self {
|
||||
font_system: FontSystem::new(),
|
||||
font_system,
|
||||
family_resolver,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -103,10 +198,20 @@ impl TextSystem {
|
||||
origin: Point,
|
||||
style: TextStyle,
|
||||
) -> PreparedText {
|
||||
let text = text.into();
|
||||
self.prepare_spans([TextSpan::new(text)], origin, style)
|
||||
}
|
||||
|
||||
pub fn prepare_spans(
|
||||
&mut self,
|
||||
spans: impl IntoIterator<Item = TextSpan>,
|
||||
origin: Point,
|
||||
style: TextStyle,
|
||||
) -> PreparedText {
|
||||
let spans: Vec<TextSpan> = spans.into_iter().collect();
|
||||
let text = combined_text(&spans);
|
||||
let layout = self.layout(
|
||||
&text,
|
||||
style,
|
||||
&spans,
|
||||
style.clone(),
|
||||
style.bounds.map(|bounds| bounds.width),
|
||||
style.bounds.map(|bounds| bounds.height),
|
||||
);
|
||||
@@ -117,6 +222,7 @@ impl TextSystem {
|
||||
glyph: glyph.glyph,
|
||||
position: Point::new(origin.x + glyph.position.x, origin.y + glyph.position.y),
|
||||
advance: glyph.advance,
|
||||
color: glyph.color,
|
||||
cache_key: glyph.cache_key,
|
||||
})
|
||||
.collect();
|
||||
@@ -139,22 +245,57 @@ impl TextSystem {
|
||||
width: Option<f32>,
|
||||
height: Option<f32>,
|
||||
) -> UiSize {
|
||||
self.layout(text, style, width, height).size
|
||||
self.measure_spans([TextSpan::new(text)], style, width, height)
|
||||
}
|
||||
|
||||
pub fn measure_spans(
|
||||
&mut self,
|
||||
spans: impl IntoIterator<Item = TextSpan>,
|
||||
style: TextStyle,
|
||||
width: Option<f32>,
|
||||
height: Option<f32>,
|
||||
) -> UiSize {
|
||||
let spans: Vec<TextSpan> = spans.into_iter().collect();
|
||||
self.layout(&spans, style, width, height).size
|
||||
}
|
||||
|
||||
fn layout(
|
||||
&mut self,
|
||||
text: &str,
|
||||
spans: &[TextSpan],
|
||||
style: TextStyle,
|
||||
width: Option<f32>,
|
||||
height: Option<f32>,
|
||||
) -> TextLayout {
|
||||
let default_family = self.resolve_font_family(&style.font_family, TextSpanSlant::Normal);
|
||||
let resolved_span_families: Vec<_> = spans
|
||||
.iter()
|
||||
.map(|span| {
|
||||
self.resolve_font_family(
|
||||
span.font_family.as_ref().unwrap_or(&style.font_family),
|
||||
span.slant,
|
||||
)
|
||||
})
|
||||
.collect();
|
||||
let mut buffer = Buffer::new_empty(Metrics::new(style.font_size, style.line_height));
|
||||
{
|
||||
let mut borrowed = buffer.borrow_with(&mut self.font_system);
|
||||
borrowed.set_wrap(style.wrap.to_cosmic());
|
||||
borrowed.set_size(width, height);
|
||||
borrowed.set_text(text, &Attrs::new(), Shaping::Advanced, None);
|
||||
let default_attrs = default_attrs_for_style(&style, default_family.as_deref());
|
||||
borrowed.set_rich_text(
|
||||
spans
|
||||
.iter()
|
||||
.zip(resolved_span_families.iter())
|
||||
.map(|(span, resolved_family)| {
|
||||
(
|
||||
span.text.as_str(),
|
||||
attrs_for_span(span, &style, resolved_family.as_deref()),
|
||||
)
|
||||
}),
|
||||
&default_attrs,
|
||||
Shaping::Advanced,
|
||||
None,
|
||||
);
|
||||
}
|
||||
|
||||
let mut measured_width: f32 = 0.0;
|
||||
@@ -173,6 +314,7 @@ impl TextSystem {
|
||||
glyph: run.text[glyph.start..glyph.end].to_string(),
|
||||
position: Point::new(physical.x as f32, physical.y as f32),
|
||||
advance: glyph.w,
|
||||
color: glyph.color_opt.map_or(style.color, color_from_cosmic),
|
||||
cache_key: Some(physical.cache_key),
|
||||
}
|
||||
}));
|
||||
@@ -186,6 +328,15 @@ impl TextSystem {
|
||||
size: UiSize::new(measured_width.max(0.0), measured_height.max(0.0)),
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve_font_family(
|
||||
&mut self,
|
||||
family: &TextFontFamily,
|
||||
slant: TextSpanSlant,
|
||||
) -> Option<String> {
|
||||
self.family_resolver
|
||||
.resolve(self.font_system.db(), family, slant)
|
||||
}
|
||||
}
|
||||
|
||||
fn aligned_line_offset(align: TextAlign, width: Option<f32>, line_width: f32) -> f32 {
|
||||
@@ -200,10 +351,263 @@ fn aligned_line_offset(align: TextAlign, width: Option<f32>, line_width: f32) ->
|
||||
}
|
||||
}
|
||||
|
||||
fn combined_text(spans: &[TextSpan]) -> String {
|
||||
let total_len = spans.iter().map(|span| span.text.len()).sum();
|
||||
let mut text = String::with_capacity(total_len);
|
||||
for span in spans {
|
||||
text.push_str(&span.text);
|
||||
}
|
||||
text
|
||||
}
|
||||
|
||||
fn default_attrs_for_style<'a>(
|
||||
style: &'a TextStyle,
|
||||
resolved_family: Option<&'a str>,
|
||||
) -> Attrs<'a> {
|
||||
Attrs::new()
|
||||
.family(font_family_to_cosmic(&style.font_family, resolved_family))
|
||||
.color(to_cosmic_color(style.color))
|
||||
.metrics(Metrics::new(style.font_size, style.line_height))
|
||||
}
|
||||
|
||||
fn attrs_for_span<'a>(
|
||||
span: &'a TextSpan,
|
||||
style: &'a TextStyle,
|
||||
resolved_family: Option<&'a str>,
|
||||
) -> Attrs<'a> {
|
||||
let font_family = span.font_family.as_ref().unwrap_or(&style.font_family);
|
||||
default_attrs_for_style(style, resolved_family)
|
||||
.family(font_family_to_cosmic(font_family, resolved_family))
|
||||
.color(to_cosmic_color(span.color.unwrap_or(style.color)))
|
||||
.weight(match span.weight {
|
||||
TextSpanWeight::Normal => CosmicWeight::NORMAL,
|
||||
TextSpanWeight::Medium => CosmicWeight::MEDIUM,
|
||||
TextSpanWeight::Semibold => CosmicWeight::SEMIBOLD,
|
||||
TextSpanWeight::Bold => CosmicWeight::BOLD,
|
||||
})
|
||||
.style(match span.slant {
|
||||
TextSpanSlant::Normal => CosmicStyle::Normal,
|
||||
TextSpanSlant::Italic => CosmicStyle::Italic,
|
||||
TextSpanSlant::Oblique => CosmicStyle::Oblique,
|
||||
})
|
||||
}
|
||||
|
||||
fn to_cosmic_color(color: Color) -> CosmicColor {
|
||||
CosmicColor::rgba(color.r, color.g, color.b, color.a)
|
||||
}
|
||||
|
||||
fn color_from_cosmic(color: CosmicColor) -> Color {
|
||||
Color::rgba(color.r(), color.g(), color.b(), color.a())
|
||||
}
|
||||
|
||||
fn font_family_to_cosmic<'a>(
|
||||
family: &'a TextFontFamily,
|
||||
resolved_family: Option<&'a str>,
|
||||
) -> fontdb::Family<'a> {
|
||||
if let Some(resolved_family) = resolved_family {
|
||||
return fontdb::Family::Name(resolved_family);
|
||||
}
|
||||
|
||||
match family {
|
||||
TextFontFamily::SansSerif => fontdb::Family::SansSerif,
|
||||
TextFontFamily::Serif => fontdb::Family::Serif,
|
||||
TextFontFamily::Monospace => fontdb::Family::Monospace,
|
||||
TextFontFamily::Cursive => fontdb::Family::Cursive,
|
||||
TextFontFamily::Fantasy => fontdb::Family::Fantasy,
|
||||
TextFontFamily::Named(name) => fontdb::Family::Name(name.as_str()),
|
||||
}
|
||||
}
|
||||
|
||||
const SANS_SERIF_FAMILY_CANDIDATES: &[&str] = &[
|
||||
"Noto Sans",
|
||||
"DejaVu Sans",
|
||||
"Liberation Sans",
|
||||
"Ubuntu",
|
||||
"Cantarell",
|
||||
"Arial",
|
||||
"Open Sans",
|
||||
];
|
||||
const SERIF_FAMILY_CANDIDATES: &[&str] = &[
|
||||
"Noto Serif",
|
||||
"DejaVu Serif",
|
||||
"Liberation Serif",
|
||||
"Times New Roman",
|
||||
"Open Serif",
|
||||
];
|
||||
const MONOSPACE_FAMILY_CANDIDATES: &[&str] = &[
|
||||
"Noto Sans Mono",
|
||||
"DejaVu Sans Mono",
|
||||
"Liberation Mono",
|
||||
"Ubuntu Mono",
|
||||
"Courier New",
|
||||
];
|
||||
|
||||
impl FontFamilyResolver {
|
||||
fn new() -> Self {
|
||||
Self {
|
||||
fontconfig: Fontconfig::new(),
|
||||
cache: HashMap::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn resolve(
|
||||
&mut self,
|
||||
db: &fontdb::Database,
|
||||
family: &TextFontFamily,
|
||||
slant: TextSpanSlant,
|
||||
) -> Option<String> {
|
||||
let key = (family.clone(), slant);
|
||||
if let Some(cached) = self.cache.get(&key) {
|
||||
return cached.clone();
|
||||
}
|
||||
|
||||
let resolved = self.resolve_uncached(db, family, slant);
|
||||
self.cache.insert(key, resolved.clone());
|
||||
resolved
|
||||
}
|
||||
|
||||
fn resolve_uncached(
|
||||
&self,
|
||||
db: &fontdb::Database,
|
||||
family: &TextFontFamily,
|
||||
slant: TextSpanSlant,
|
||||
) -> Option<String> {
|
||||
if let Some(fontconfig) = self.fontconfig.as_ref()
|
||||
&& let Some(font) =
|
||||
fontconfig.find(family.fontconfig_query_name(), fontconfig_style_name(slant))
|
||||
{
|
||||
if let Some(family_name) = family_name_for_font_match(db, &font) {
|
||||
return Some(family_name);
|
||||
}
|
||||
return Some(font.name);
|
||||
}
|
||||
|
||||
fallback_family_name(db, family)
|
||||
}
|
||||
}
|
||||
|
||||
fn configure_default_font_families(
|
||||
font_system: &mut FontSystem,
|
||||
family_resolver: &mut FontFamilyResolver,
|
||||
) {
|
||||
if let Some(sans_family) = family_resolver.resolve(
|
||||
font_system.db(),
|
||||
&TextFontFamily::SansSerif,
|
||||
TextSpanSlant::Normal,
|
||||
) {
|
||||
font_system.db_mut().set_sans_serif_family(&sans_family);
|
||||
}
|
||||
if let Some(serif_family) = family_resolver.resolve(
|
||||
font_system.db(),
|
||||
&TextFontFamily::Serif,
|
||||
TextSpanSlant::Normal,
|
||||
) {
|
||||
font_system.db_mut().set_serif_family(&serif_family);
|
||||
}
|
||||
if let Some(monospace_family) = family_resolver.resolve(
|
||||
font_system.db(),
|
||||
&TextFontFamily::Monospace,
|
||||
TextSpanSlant::Normal,
|
||||
) {
|
||||
font_system.db_mut().set_monospace_family(&monospace_family);
|
||||
}
|
||||
}
|
||||
|
||||
fn fallback_family_name(db: &fontdb::Database, family: &TextFontFamily) -> Option<String> {
|
||||
match family {
|
||||
TextFontFamily::Named(name) => Some(name.clone()),
|
||||
TextFontFamily::SansSerif => {
|
||||
configured_generic_family_name(db, SANS_SERIF_FAMILY_CANDIDATES)
|
||||
}
|
||||
TextFontFamily::Serif => configured_generic_family_name(db, SERIF_FAMILY_CANDIDATES),
|
||||
TextFontFamily::Monospace => {
|
||||
configured_generic_family_name(db, MONOSPACE_FAMILY_CANDIDATES)
|
||||
}
|
||||
TextFontFamily::Cursive | TextFontFamily::Fantasy => db
|
||||
.faces()
|
||||
.find(|face| !face.monospaced && !face.post_script_name.contains("Emoji"))
|
||||
.and_then(|face| face.families.first().map(|(name, _)| name.clone())),
|
||||
}
|
||||
}
|
||||
|
||||
fn configured_generic_family_name(
|
||||
db: &fontdb::Database,
|
||||
candidates: &[&'static str],
|
||||
) -> Option<String> {
|
||||
if let Some(preferred) = preferred_family_name(
|
||||
db.faces()
|
||||
.flat_map(|face| face.families.iter().map(|(name, _)| name.as_str())),
|
||||
candidates,
|
||||
) {
|
||||
return Some(preferred.to_owned());
|
||||
}
|
||||
|
||||
db.faces()
|
||||
.find(|face| {
|
||||
!face.monospaced
|
||||
&& !face.post_script_name.contains("Emoji")
|
||||
&& face.style == fontdb::Style::Normal
|
||||
&& face.weight == fontdb::Weight::NORMAL
|
||||
})
|
||||
.and_then(|face| face.families.first().map(|(name, _)| name.clone()))
|
||||
}
|
||||
|
||||
fn family_name_for_font_match(db: &fontdb::Database, font: &fontconfig::Font) -> Option<String> {
|
||||
db.faces()
|
||||
.find(|face| match &face.source {
|
||||
fontdb::Source::File(path) | fontdb::Source::SharedFile(path, _) => {
|
||||
path == &font.path
|
||||
&& font
|
||||
.index
|
||||
.is_none_or(|index| face.index == index.max(0) as u32)
|
||||
}
|
||||
fontdb::Source::Binary(_) => false,
|
||||
})
|
||||
.and_then(|face| face.families.first().map(|(name, _)| name.clone()))
|
||||
}
|
||||
|
||||
fn fontconfig_style_name(slant: TextSpanSlant) -> Option<&'static str> {
|
||||
match slant {
|
||||
TextSpanSlant::Normal => None,
|
||||
TextSpanSlant::Italic => Some("Italic"),
|
||||
TextSpanSlant::Oblique => Some("Oblique"),
|
||||
}
|
||||
}
|
||||
|
||||
fn preferred_family_name<'a>(
|
||||
available_families: impl IntoIterator<Item = &'a str>,
|
||||
candidates: &[&'static str],
|
||||
) -> Option<&'static str> {
|
||||
let available: BTreeSet<&str> = available_families.into_iter().collect();
|
||||
candidates
|
||||
.iter()
|
||||
.copied()
|
||||
.find(|candidate| available.contains(candidate))
|
||||
}
|
||||
|
||||
impl TextFontFamily {
|
||||
fn fontconfig_query_name(&self) -> &str {
|
||||
match self {
|
||||
TextFontFamily::SansSerif => "sans-serif",
|
||||
TextFontFamily::Serif => "serif",
|
||||
TextFontFamily::Monospace => "monospace",
|
||||
TextFontFamily::Cursive => "cursive",
|
||||
TextFontFamily::Fantasy => "fantasy",
|
||||
TextFontFamily::Named(name) => name.as_str(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::{TextAlign, TextStyle, TextSystem, TextWrap};
|
||||
use super::{
|
||||
MONOSPACE_FAMILY_CANDIDATES, TextAlign, TextFontFamily, TextSpan, TextSpanSlant,
|
||||
TextSpanWeight, TextStyle, TextSystem, TextWrap, configured_generic_family_name,
|
||||
family_name_for_font_match, font_family_to_cosmic, fontdb, preferred_family_name,
|
||||
};
|
||||
use crate::{Color, Point, UiSize};
|
||||
use fontconfig::Font;
|
||||
use std::path::PathBuf;
|
||||
|
||||
#[test]
|
||||
fn max_lines_limits_measured_height() {
|
||||
@@ -213,7 +617,7 @@ mod tests {
|
||||
.with_wrap(TextWrap::Word);
|
||||
let unclamped = text_system.measure(
|
||||
"alpha beta gamma delta epsilon zeta eta theta iota kappa",
|
||||
style,
|
||||
style.clone(),
|
||||
Some(120.0),
|
||||
None,
|
||||
);
|
||||
@@ -248,4 +652,106 @@ mod tests {
|
||||
assert!(!centered.glyphs.is_empty());
|
||||
assert!(centered.glyphs[0].position.x > start.glyphs[0].position.x);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn rich_spans_preserve_per_glyph_colors() {
|
||||
let mut text_system = TextSystem::new();
|
||||
let prepared = text_system.prepare_spans(
|
||||
[
|
||||
TextSpan::new("Red ").color(Color::rgb(0xFF, 0x33, 0x33)),
|
||||
TextSpan::new("Blue")
|
||||
.color(Color::rgb(0x33, 0x66, 0xFF))
|
||||
.weight(TextSpanWeight::Bold)
|
||||
.slant(TextSpanSlant::Italic),
|
||||
],
|
||||
Point::new(0.0, 0.0),
|
||||
TextStyle::new(18.0, Color::rgb(0xEE, 0xEE, 0xEE))
|
||||
.with_wrap(TextWrap::Word)
|
||||
.with_bounds(UiSize::new(240.0, 80.0)),
|
||||
);
|
||||
|
||||
assert!(
|
||||
prepared
|
||||
.glyphs
|
||||
.iter()
|
||||
.any(|glyph| glyph.color == Color::rgb(0xFF, 0x33, 0x33))
|
||||
);
|
||||
assert!(
|
||||
prepared
|
||||
.glyphs
|
||||
.iter()
|
||||
.any(|glyph| glyph.color == Color::rgb(0x33, 0x66, 0xFF))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn preferred_family_name_uses_first_available_candidate() {
|
||||
let selected = preferred_family_name(
|
||||
["DejaVu Serif", "Liberation Sans", "Noto Sans Mono"],
|
||||
&["Noto Sans", "Liberation Sans", "Arial"],
|
||||
);
|
||||
assert_eq!(selected, Some("Liberation Sans"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn configured_generic_family_name_falls_back_to_first_available_normal_face() {
|
||||
let mut db = fontdb::Database::new();
|
||||
db.push_face_info(fontdb::FaceInfo {
|
||||
id: fontdb::ID::dummy(),
|
||||
source: fontdb::Source::Binary(std::sync::Arc::new(Vec::new())),
|
||||
index: 0,
|
||||
families: vec![(
|
||||
String::from("Fallback Sans"),
|
||||
fontdb::Language::English_UnitedStates,
|
||||
)],
|
||||
post_script_name: String::from("FallbackSans-Regular"),
|
||||
style: fontdb::Style::Normal,
|
||||
weight: fontdb::Weight::NORMAL,
|
||||
stretch: fontdb::Stretch::Normal,
|
||||
monospaced: false,
|
||||
});
|
||||
|
||||
assert_eq!(
|
||||
configured_generic_family_name(&db, MONOSPACE_FAMILY_CANDIDATES),
|
||||
Some(String::from("Fallback Sans"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn font_family_to_cosmic_prefers_resolved_name() {
|
||||
match font_family_to_cosmic(&TextFontFamily::SansSerif, Some("Resolved Sans")) {
|
||||
fontdb::Family::Name(name) => assert_eq!(name, "Resolved Sans"),
|
||||
other => panic!("expected resolved named family, got {other:?}"),
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn family_name_for_font_match_uses_path_and_index() {
|
||||
let mut db = fontdb::Database::new();
|
||||
db.push_face_info(fontdb::FaceInfo {
|
||||
id: fontdb::ID::dummy(),
|
||||
source: fontdb::Source::File(PathBuf::from("/tmp/example-font.ttf")),
|
||||
index: 2,
|
||||
families: vec![(
|
||||
String::from("Example Sans"),
|
||||
fontdb::Language::English_UnitedStates,
|
||||
)],
|
||||
post_script_name: String::from("ExampleSans-Italic"),
|
||||
style: fontdb::Style::Italic,
|
||||
weight: fontdb::Weight::NORMAL,
|
||||
stretch: fontdb::Stretch::Normal,
|
||||
monospaced: false,
|
||||
});
|
||||
|
||||
let font = Font {
|
||||
name: String::from("Example Sans"),
|
||||
path: PathBuf::from("/tmp/example-font.ttf"),
|
||||
index: Some(2),
|
||||
};
|
||||
|
||||
assert_eq!(
|
||||
family_name_for_font_match(&db, &font),
|
||||
Some(String::from("Example Sans"))
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use crate::scene::Color;
|
||||
use crate::text::{TextStyle, TextWrap};
|
||||
use crate::text::{TextSpan, TextStyle, TextWrap};
|
||||
|
||||
#[derive(Clone, Copy, Debug, Eq, PartialEq)]
|
||||
pub enum FlexDirection {
|
||||
@@ -75,7 +75,7 @@ enum ElementContent {
|
||||
|
||||
#[derive(Clone, Debug, PartialEq)]
|
||||
pub(crate) struct TextNode {
|
||||
pub text: String,
|
||||
pub spans: Vec<TextSpan>,
|
||||
pub style: TextStyle,
|
||||
}
|
||||
|
||||
@@ -96,11 +96,15 @@ impl Element {
|
||||
}
|
||||
|
||||
pub fn text(text: impl Into<String>, style: TextStyle) -> Self {
|
||||
Self::spans([TextSpan::new(text)], style)
|
||||
}
|
||||
|
||||
pub fn spans(spans: impl IntoIterator<Item = TextSpan>, style: TextStyle) -> Self {
|
||||
Self {
|
||||
style: Style::default(),
|
||||
children: Vec::new(),
|
||||
content: ElementContent::Text(TextNode {
|
||||
text: text.into(),
|
||||
spans: spans.into_iter().collect(),
|
||||
style,
|
||||
}),
|
||||
}
|
||||
@@ -110,6 +114,10 @@ impl Element {
|
||||
Self::text(text, style.with_wrap(TextWrap::Word))
|
||||
}
|
||||
|
||||
pub fn rich_paragraph(spans: impl IntoIterator<Item = TextSpan>, style: TextStyle) -> Self {
|
||||
Self::spans(spans, style.with_wrap(TextWrap::Word))
|
||||
}
|
||||
|
||||
pub fn row() -> Self {
|
||||
Self::new().direction(FlexDirection::Row)
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user