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:
co-authored by
Antonio Scandurra
Nathan Sobo
parent
ee9ee294ad
commit
3bb5610ad1
+69
-82
@@ -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
|
||||
),
|
||||
]
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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
@@ -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
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user