Overhaul handling of font families

* Specify font families in the theme.
* Load fonts eagerly when loading themes, instead of loading
  them lazily when rendering.

Co-Authored-By: Antonio Scandurra <me@as-cii.com>
Co-Authored-By: Nathan Sobo <nathan@zed.dev>
This commit is contained in:
Max Brunsfeld
2021-08-26 15:06:00 -07:00
co-authored by Antonio Scandurra Nathan Sobo
parent ee9ee294ad
commit 3bb5610ad1
23 changed files with 438 additions and 397 deletions
+69 -82
View File
@@ -1,6 +1,5 @@
use crate::{
color::Color,
font_cache::FamilyId,
fonts::{FontId, TextStyle},
geometry::{
rect::RectF,
@@ -8,8 +7,7 @@ use crate::{
},
json::{ToJson, Value},
text_layout::Line,
DebugContext, Element, Event, EventContext, FontCache, LayoutContext, PaintContext,
SizeConstraint,
DebugContext, Element, Event, EventContext, LayoutContext, PaintContext, SizeConstraint,
};
use serde::Deserialize;
use serde_json::json;
@@ -17,37 +15,32 @@ use smallvec::{smallvec, SmallVec};
pub struct Label {
text: String,
family_id: FamilyId,
font_size: f32,
style: LabelStyle,
highlight_indices: Vec<usize>,
}
#[derive(Clone, Debug, Default, Deserialize)]
#[derive(Clone, Debug, Deserialize)]
pub struct LabelStyle {
pub text: TextStyle,
pub highlight_text: Option<TextStyle>,
}
impl Label {
pub fn new(text: String, family_id: FamilyId, font_size: f32) -> Self {
Self {
impl From<TextStyle> for LabelStyle {
fn from(text: TextStyle) -> Self {
LabelStyle {
text,
family_id,
font_size,
highlight_indices: Default::default(),
style: Default::default(),
highlight_text: None,
}
}
}
pub fn with_style(mut self, style: &LabelStyle) -> Self {
self.style = style.clone();
self
}
pub fn with_default_color(mut self, color: Color) -> Self {
self.style.text.color = color;
self
impl Label {
pub fn new(text: String, style: impl Into<LabelStyle>) -> Self {
Self {
text,
highlight_indices: Default::default(),
style: style.into(),
}
}
pub fn with_highlights(mut self, indices: Vec<usize>) -> Self {
@@ -55,11 +48,8 @@ impl Label {
self
}
fn compute_runs(
&self,
font_cache: &FontCache,
font_id: FontId,
) -> SmallVec<[(usize, FontId, Color); 8]> {
fn compute_runs(&self) -> SmallVec<[(usize, FontId, Color); 8]> {
let font_id = self.style.text.font_id;
if self.highlight_indices.is_empty() {
return smallvec![(self.text.len(), font_id, self.style.text.color)];
}
@@ -68,12 +58,7 @@ impl Label {
.style
.highlight_text
.as_ref()
.and_then(|style| {
font_cache
.select_font(self.family_id, &style.font_properties)
.ok()
})
.unwrap_or(font_id);
.map_or(font_id, |style| style.font_id);
let mut highlight_indices = self.highlight_indices.iter().copied().peekable();
let mut runs = SmallVec::new();
@@ -123,18 +108,18 @@ impl Element for Label {
constraint: SizeConstraint,
cx: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) {
let font_id = cx
.font_cache
.select_font(self.family_id, &self.style.text.font_properties)
.unwrap();
let runs = self.compute_runs(&cx.font_cache, font_id);
let line =
cx.text_layout_cache
.layout_str(self.text.as_str(), self.font_size, runs.as_slice());
let runs = self.compute_runs();
let line = cx.text_layout_cache.layout_str(
self.text.as_str(),
self.style.text.font_size,
runs.as_slice(),
);
let size = vec2f(
line.width().max(constraint.min.x()).min(constraint.max.x()),
cx.font_cache.line_height(font_id, self.font_size).ceil(),
cx.font_cache
.line_height(self.style.text.font_id, self.style.text.font_size)
.ceil(),
);
(size, line)
@@ -169,15 +154,13 @@ impl Element for Label {
bounds: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
cx: &DebugContext,
_: &DebugContext,
) -> Value {
json!({
"type": "Label",
"bounds": bounds.to_json(),
"text": &self.text,
"highlight_indices": self.highlight_indices,
"font_family": cx.font_cache.family_name(self.family_id).unwrap(),
"font_size": self.font_size,
"style": self.style.to_json(),
})
}
@@ -201,48 +184,52 @@ mod tests {
#[crate::test(self)]
fn test_layout_label_with_highlights(cx: &mut crate::MutableAppContext) {
let menlo = cx.font_cache().load_family(&["Menlo"]).unwrap();
let menlo_regular = cx
.font_cache()
.select_font(menlo, &FontProperties::new())
.unwrap();
let menlo_bold = cx
.font_cache()
.select_font(menlo, FontProperties::new().weight(Weight::BOLD))
.unwrap();
let black = Color::black();
let red = Color::new(255, 0, 0, 255);
let default_style = TextStyle::new(
"Menlo",
12.,
Default::default(),
Color::black(),
cx.font_cache(),
)
.unwrap();
let highlight_style = TextStyle::new(
"Menlo",
12.,
*FontProperties::new().weight(Weight::BOLD),
Color::new(255, 0, 0, 255),
cx.font_cache(),
)
.unwrap();
let label = Label::new(
".αβγδε.ⓐⓑⓒⓓⓔ.abcde.".to_string(),
LabelStyle {
text: default_style.clone(),
highlight_text: Some(highlight_style.clone()),
},
)
.with_highlights(vec![
".α".len(),
".αβ".len(),
".αβγδ".len(),
".αβγδε.ⓐ".len(),
".αβγδε.ⓐⓑ".len(),
]);
let label = Label::new(".αβγδε.ⓐⓑⓒⓓⓔ.abcde.".to_string(), menlo, 12.0)
.with_style(&LabelStyle {
text: TextStyle {
color: black,
font_properties: Default::default(),
},
highlight_text: Some(TextStyle {
color: red,
font_properties: *FontProperties::new().weight(Weight::BOLD),
}),
})
.with_highlights(vec![
".α".len(),
".αβ".len(),
".αβγδ".len(),
".αβγδε.ⓐ".len(),
".αβγδε.ⓐⓑ".len(),
]);
let runs = label.compute_runs(cx.font_cache().as_ref(), menlo_regular);
let runs = label.compute_runs();
assert_eq!(
runs.as_slice(),
&[
(".α".len(), menlo_regular, black),
("βγ".len(), menlo_bold, red),
("δ".len(), menlo_regular, black),
("ε".len(), menlo_bold, red),
(".ⓐ".len(), menlo_regular, black),
("ⓑⓒ".len(), menlo_bold, red),
("ⓓⓔ.abcde.".len(), menlo_regular, black),
(".α".len(), default_style.font_id, default_style.color),
("βγ".len(), highlight_style.font_id, highlight_style.color),
("δ".len(), default_style.font_id, default_style.color),
("ε".len(), highlight_style.font_id, highlight_style.color),
(".ⓐ".len(), default_style.font_id, default_style.color),
("ⓑⓒ".len(), highlight_style.font_id, highlight_style.color),
(
"ⓓⓔ.abcde.".len(),
default_style.font_id,
default_style.color
),
]
);
}
+8 -26
View File
@@ -1,6 +1,5 @@
use crate::{
color::Color,
font_cache::FamilyId,
fonts::TextStyle,
geometry::{
rect::RectF,
@@ -14,8 +13,6 @@ use serde_json::json;
pub struct Text {
text: String,
family_id: FamilyId,
font_size: f32,
style: TextStyle,
}
@@ -25,18 +22,8 @@ pub struct LayoutState {
}
impl Text {
pub fn new(text: String, family_id: FamilyId, font_size: f32) -> Self {
Self {
text,
family_id,
font_size,
style: Default::default(),
}
}
pub fn with_style(mut self, style: &TextStyle) -> Self {
self.style = style.clone();
self
pub fn new(text: String, style: TextStyle) -> Self {
Self { text, style }
}
pub fn with_default_color(mut self, color: Color) -> Self {
@@ -54,20 +41,17 @@ impl Element for Text {
constraint: SizeConstraint,
cx: &mut LayoutContext,
) -> (Vector2F, Self::LayoutState) {
let font_id = cx
.font_cache
.select_font(self.family_id, &self.style.font_properties)
.unwrap();
let line_height = cx.font_cache.line_height(font_id, self.font_size);
let font_id = self.style.font_id;
let line_height = cx.font_cache.line_height(font_id, self.style.font_size);
let mut wrapper = cx.font_cache.line_wrapper(font_id, self.font_size);
let mut wrapper = cx.font_cache.line_wrapper(font_id, self.style.font_size);
let mut lines = Vec::new();
let mut line_count = 0;
let mut max_line_width = 0_f32;
for line in self.text.lines() {
let shaped_line = cx.text_layout_cache.layout_str(
line,
self.font_size,
self.style.font_size,
&[(line.len(), font_id, self.style.color)],
);
let wrap_boundaries = wrapper
@@ -123,14 +107,12 @@ impl Element for Text {
bounds: RectF,
_: &Self::LayoutState,
_: &Self::PaintState,
cx: &DebugContext,
_: &DebugContext,
) -> Value {
json!({
"type": "Label",
"type": "Text",
"bounds": bounds.to_json(),
"text": &self.text,
"font_family": cx.font_cache.family_name(self.family_id).unwrap(),
"font_size": self.font_size,
"style": self.style.to_json(),
})
}
+134 -53
View File
@@ -1,32 +1,35 @@
use crate::{
color::Color,
json::{json, ToJson},
FontCache,
};
use anyhow::anyhow;
pub use font_kit::{
metrics::Metrics,
properties::{Properties, Stretch, Style, Weight},
};
use serde::{de, Deserialize};
use serde_json::Value;
use std::{cell::RefCell, sync::Arc};
#[derive(Copy, Clone, Debug, Eq, PartialEq, Hash)]
pub struct FontId(pub usize);
pub type GlyphId = u32;
#[derive(Clone, Debug, PartialEq, Eq)]
#[derive(Clone, Debug)]
pub struct TextStyle {
pub color: Color,
pub font_family_name: Arc<str>,
pub font_id: FontId,
pub font_size: f32,
pub font_properties: Properties,
}
impl Default for TextStyle {
fn default() -> Self {
Self {
color: Color::from_u32(0xff0000ff),
font_properties: Default::default(),
}
}
#[derive(Clone, Debug, Default)]
pub struct HighlightStyle {
pub color: Color,
pub font_properties: Properties,
}
#[allow(non_camel_case_types)]
@@ -43,24 +46,117 @@ enum WeightJson {
black,
}
thread_local! {
static FONT_CACHE: RefCell<Option<Arc<FontCache>>> = Default::default();
}
#[derive(Deserialize)]
struct TextStyleJson {
color: Color,
family: String,
weight: Option<WeightJson>,
#[serde(default)]
italic: bool,
size: f32,
}
#[derive(Deserialize)]
struct HighlightStyleJson {
color: Color,
weight: Option<WeightJson>,
#[serde(default)]
italic: bool,
}
impl TextStyle {
pub fn new(
font_family_name: impl Into<Arc<str>>,
font_size: f32,
font_properties: Properties,
color: Color,
font_cache: &FontCache,
) -> anyhow::Result<Self> {
let font_family_name = font_family_name.into();
let family_id = font_cache.load_family(&[&font_family_name])?;
let font_id = font_cache.select_font(family_id, &font_properties)?;
Ok(Self {
color,
font_family_name,
font_id,
font_size,
font_properties,
})
}
fn from_json(json: TextStyleJson) -> anyhow::Result<Self> {
FONT_CACHE.with(|font_cache| {
if let Some(font_cache) = font_cache.borrow().as_ref() {
let font_properties = properties_from_json(json.weight, json.italic);
Self::new(
json.family,
json.size,
font_properties,
json.color,
font_cache,
)
} else {
Err(anyhow!(
"TextStyle can only be deserialized within a call to with_font_cache"
))
}
})
}
}
impl HighlightStyle {
fn from_json(json: HighlightStyleJson) -> Self {
let font_properties = properties_from_json(json.weight, json.italic);
Self {
color: json.color,
font_properties,
}
}
}
impl From<Color> for HighlightStyle {
fn from(color: Color) -> Self {
Self {
color,
font_properties: Default::default(),
}
}
}
impl<'de> Deserialize<'de> for TextStyle {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let json = Value::deserialize(deserializer)?;
Ok(Self::from_json(TextStyleJson::deserialize(deserializer)?)
.map_err(|e| de::Error::custom(e))?)
}
}
impl ToJson for TextStyle {
fn to_json(&self) -> Value {
json!({
"color": self.color.to_json(),
"font_family": self.font_family_name.as_ref(),
"font_properties": self.font_properties.to_json(),
})
}
}
impl<'de> Deserialize<'de> for HighlightStyle {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let json = serde_json::Value::deserialize(deserializer)?;
if json.is_object() {
let style_json: TextStyleJson =
serde_json::from_value(json).map_err(de::Error::custom)?;
Ok(style_json.into())
Ok(Self::from_json(
serde_json::from_value(json).map_err(de::Error::custom)?,
))
} else {
Ok(Self {
color: serde_json::from_value(json).map_err(de::Error::custom)?,
@@ -70,47 +166,20 @@ impl<'de> Deserialize<'de> for TextStyle {
}
}
impl From<Color> for TextStyle {
fn from(color: Color) -> Self {
Self {
color,
font_properties: Default::default(),
}
}
}
impl ToJson for TextStyle {
fn to_json(&self) -> Value {
json!({
"color": self.color.to_json(),
"font_properties": self.font_properties.to_json(),
})
}
}
impl Into<TextStyle> for TextStyleJson {
fn into(self) -> TextStyle {
let weight = match self.weight.unwrap_or(WeightJson::normal) {
WeightJson::thin => Weight::THIN,
WeightJson::extra_light => Weight::EXTRA_LIGHT,
WeightJson::light => Weight::LIGHT,
WeightJson::normal => Weight::NORMAL,
WeightJson::medium => Weight::MEDIUM,
WeightJson::semibold => Weight::SEMIBOLD,
WeightJson::bold => Weight::BOLD,
WeightJson::extra_bold => Weight::EXTRA_BOLD,
WeightJson::black => Weight::BLACK,
};
let style = if self.italic {
Style::Italic
} else {
Style::Normal
};
TextStyle {
color: self.color,
font_properties: *Properties::new().weight(weight).style(style),
}
}
fn properties_from_json(weight: Option<WeightJson>, italic: bool) -> Properties {
let weight = match weight.unwrap_or(WeightJson::normal) {
WeightJson::thin => Weight::THIN,
WeightJson::extra_light => Weight::EXTRA_LIGHT,
WeightJson::light => Weight::LIGHT,
WeightJson::normal => Weight::NORMAL,
WeightJson::medium => Weight::MEDIUM,
WeightJson::semibold => Weight::SEMIBOLD,
WeightJson::bold => Weight::BOLD,
WeightJson::extra_bold => Weight::EXTRA_BOLD,
WeightJson::black => Weight::BLACK,
};
let style = if italic { Style::Italic } else { Style::Normal };
*Properties::new().weight(weight).style(style)
}
impl ToJson for Properties {
@@ -164,3 +233,15 @@ impl ToJson for Stretch {
json!(self.0)
}
}
pub fn with_font_cache<F, T>(font_cache: Arc<FontCache>, callback: F) -> T
where
F: FnOnce() -> T,
{
FONT_CACHE.with(|cache| {
*cache.borrow_mut() = Some(font_cache);
let result = callback();
cache.borrow_mut().take();
result
})
}