mirror of
https://github.com/zed-industries/zed.git
synced 2025-01-12 05:15:00 +00:00
786d95b8c8
We were only using it for debugging purposes and that was causing the `Theme` struct to become too big to hold on the stack. Co-Authored-By: Julia Risley <julia@zed.dev>
515 lines
14 KiB
Rust
515 lines
14 KiB
Rust
use crate::{
|
|
color::Color,
|
|
font_cache::FamilyId,
|
|
json::{json, ToJson},
|
|
text_layout::RunStyle,
|
|
FontCache,
|
|
};
|
|
use anyhow::{anyhow, Result};
|
|
pub use font_kit::{
|
|
metrics::Metrics,
|
|
properties::{Properties, Stretch, Style, Weight},
|
|
};
|
|
use ordered_float::OrderedFloat;
|
|
use schemars::JsonSchema;
|
|
use serde::{de, Deserialize, Serialize};
|
|
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, Default, Eq, PartialEq, Serialize, Deserialize, JsonSchema)]
|
|
pub struct Features {
|
|
pub calt: Option<bool>,
|
|
pub case: Option<bool>,
|
|
pub cpsp: Option<bool>,
|
|
pub frac: Option<bool>,
|
|
pub liga: Option<bool>,
|
|
pub onum: Option<bool>,
|
|
pub ordn: Option<bool>,
|
|
pub pnum: Option<bool>,
|
|
pub ss01: Option<bool>,
|
|
pub ss02: Option<bool>,
|
|
pub ss03: Option<bool>,
|
|
pub ss04: Option<bool>,
|
|
pub ss05: Option<bool>,
|
|
pub ss06: Option<bool>,
|
|
pub ss07: Option<bool>,
|
|
pub ss08: Option<bool>,
|
|
pub ss09: Option<bool>,
|
|
pub ss10: Option<bool>,
|
|
pub ss11: Option<bool>,
|
|
pub ss12: Option<bool>,
|
|
pub ss13: Option<bool>,
|
|
pub ss14: Option<bool>,
|
|
pub ss15: Option<bool>,
|
|
pub ss16: Option<bool>,
|
|
pub ss17: Option<bool>,
|
|
pub ss18: Option<bool>,
|
|
pub ss19: Option<bool>,
|
|
pub ss20: Option<bool>,
|
|
pub subs: Option<bool>,
|
|
pub sups: Option<bool>,
|
|
pub swsh: Option<bool>,
|
|
pub titl: Option<bool>,
|
|
pub tnum: Option<bool>,
|
|
pub zero: Option<bool>,
|
|
}
|
|
|
|
#[derive(Clone, Debug)]
|
|
pub struct TextStyle {
|
|
pub color: Color,
|
|
pub font_family_name: Arc<str>,
|
|
pub font_family_id: FamilyId,
|
|
pub font_id: FontId,
|
|
pub font_size: f32,
|
|
pub font_properties: Properties,
|
|
pub underline: Underline,
|
|
}
|
|
|
|
#[derive(Copy, Clone, Debug, Default, PartialEq)]
|
|
pub struct HighlightStyle {
|
|
pub color: Option<Color>,
|
|
pub weight: Option<Weight>,
|
|
pub italic: Option<bool>,
|
|
pub underline: Option<Underline>,
|
|
pub fade_out: Option<f32>,
|
|
}
|
|
|
|
impl Eq for HighlightStyle {}
|
|
|
|
#[derive(Copy, Clone, Debug, Default, PartialEq, Eq)]
|
|
pub struct Underline {
|
|
pub color: Option<Color>,
|
|
pub thickness: OrderedFloat<f32>,
|
|
pub squiggly: bool,
|
|
}
|
|
|
|
#[allow(non_camel_case_types)]
|
|
#[derive(Deserialize)]
|
|
enum WeightJson {
|
|
thin,
|
|
extra_light,
|
|
light,
|
|
normal,
|
|
medium,
|
|
semibold,
|
|
bold,
|
|
extra_bold,
|
|
black,
|
|
}
|
|
|
|
thread_local! {
|
|
static FONT_CACHE: RefCell<Option<Arc<FontCache>>> = Default::default();
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct TextStyleJson {
|
|
color: Color,
|
|
family: String,
|
|
#[serde(default)]
|
|
features: Features,
|
|
weight: Option<WeightJson>,
|
|
size: f32,
|
|
#[serde(default)]
|
|
italic: bool,
|
|
#[serde(default)]
|
|
underline: UnderlineStyleJson,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
struct HighlightStyleJson {
|
|
color: Option<Color>,
|
|
weight: Option<WeightJson>,
|
|
italic: Option<bool>,
|
|
underline: Option<UnderlineStyleJson>,
|
|
fade_out: Option<f32>,
|
|
}
|
|
|
|
#[derive(Deserialize)]
|
|
#[serde(untagged)]
|
|
enum UnderlineStyleJson {
|
|
Underlined(bool),
|
|
UnderlinedWithProperties {
|
|
#[serde(default)]
|
|
color: Option<Color>,
|
|
#[serde(default)]
|
|
thickness: Option<f32>,
|
|
#[serde(default)]
|
|
squiggly: bool,
|
|
},
|
|
}
|
|
|
|
impl TextStyle {
|
|
pub fn new(
|
|
font_family_name: impl Into<Arc<str>>,
|
|
font_size: f32,
|
|
font_properties: Properties,
|
|
font_features: Features,
|
|
underline: Underline,
|
|
color: Color,
|
|
font_cache: &FontCache,
|
|
) -> Result<Self> {
|
|
let font_family_name = font_family_name.into();
|
|
let font_family_id = font_cache.load_family(&[&font_family_name], &font_features)?;
|
|
let font_id = font_cache.select_font(font_family_id, &font_properties)?;
|
|
Ok(Self {
|
|
color,
|
|
font_family_name,
|
|
font_family_id,
|
|
font_id,
|
|
font_size,
|
|
font_properties,
|
|
underline,
|
|
})
|
|
}
|
|
|
|
pub fn with_font_size(mut self, font_size: f32) -> Self {
|
|
self.font_size = font_size;
|
|
self
|
|
}
|
|
|
|
pub fn highlight(mut self, style: HighlightStyle, font_cache: &FontCache) -> Result<Self> {
|
|
let mut font_properties = self.font_properties;
|
|
if let Some(weight) = style.weight {
|
|
font_properties.weight(weight);
|
|
}
|
|
if let Some(italic) = style.italic {
|
|
if italic {
|
|
font_properties.style(Style::Italic);
|
|
} else {
|
|
font_properties.style(Style::Normal);
|
|
}
|
|
}
|
|
|
|
if self.font_properties != font_properties {
|
|
self.font_id = font_cache.select_font(self.font_family_id, &font_properties)?;
|
|
}
|
|
if let Some(color) = style.color {
|
|
self.color = Color::blend(color, self.color);
|
|
}
|
|
if let Some(factor) = style.fade_out {
|
|
self.color.fade_out(factor);
|
|
}
|
|
if let Some(underline) = style.underline {
|
|
self.underline = underline;
|
|
}
|
|
|
|
Ok(self)
|
|
}
|
|
|
|
pub fn to_run(&self) -> RunStyle {
|
|
RunStyle {
|
|
font_id: self.font_id,
|
|
color: self.color,
|
|
underline: self.underline,
|
|
}
|
|
}
|
|
|
|
fn from_json(json: TextStyleJson) -> 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.features,
|
|
underline_from_json(json.underline),
|
|
json.color,
|
|
font_cache,
|
|
)
|
|
} else {
|
|
Err(anyhow!(
|
|
"TextStyle can only be deserialized within a call to with_font_cache"
|
|
))
|
|
}
|
|
})
|
|
}
|
|
|
|
pub fn line_height(&self, font_cache: &FontCache) -> f32 {
|
|
font_cache.line_height(self.font_size)
|
|
}
|
|
|
|
pub fn cap_height(&self, font_cache: &FontCache) -> f32 {
|
|
font_cache.cap_height(self.font_id, self.font_size)
|
|
}
|
|
|
|
pub fn x_height(&self, font_cache: &FontCache) -> f32 {
|
|
font_cache.x_height(self.font_id, self.font_size)
|
|
}
|
|
|
|
pub fn em_width(&self, font_cache: &FontCache) -> f32 {
|
|
font_cache.em_width(self.font_id, self.font_size)
|
|
}
|
|
|
|
pub fn em_advance(&self, font_cache: &FontCache) -> f32 {
|
|
font_cache.em_advance(self.font_id, self.font_size)
|
|
}
|
|
|
|
pub fn descent(&self, font_cache: &FontCache) -> f32 {
|
|
font_cache.metric(self.font_id, |m| m.descent) * self.em_scale(font_cache)
|
|
}
|
|
|
|
pub fn baseline_offset(&self, font_cache: &FontCache) -> f32 {
|
|
font_cache.baseline_offset(self.font_id, self.font_size)
|
|
}
|
|
|
|
fn em_scale(&self, font_cache: &FontCache) -> f32 {
|
|
font_cache.em_scale(self.font_id, self.font_size)
|
|
}
|
|
}
|
|
|
|
impl From<TextStyle> for HighlightStyle {
|
|
fn from(other: TextStyle) -> Self {
|
|
Self::from(&other)
|
|
}
|
|
}
|
|
|
|
impl From<&TextStyle> for HighlightStyle {
|
|
fn from(other: &TextStyle) -> Self {
|
|
Self {
|
|
color: Some(other.color),
|
|
weight: Some(other.font_properties.weight),
|
|
italic: Some(other.font_properties.style == Style::Italic),
|
|
underline: Some(other.underline),
|
|
fade_out: None,
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for UnderlineStyleJson {
|
|
fn default() -> Self {
|
|
Self::Underlined(false)
|
|
}
|
|
}
|
|
|
|
impl Default for TextStyle {
|
|
fn default() -> Self {
|
|
FONT_CACHE.with(|font_cache| {
|
|
let font_cache = font_cache.borrow();
|
|
let font_cache = font_cache
|
|
.as_ref()
|
|
.expect("TextStyle::default can only be called within a call to with_font_cache");
|
|
|
|
let font_family_name = Arc::from("Courier");
|
|
let font_family_id = font_cache
|
|
.load_family(&[&font_family_name], &Default::default())
|
|
.unwrap();
|
|
let font_id = font_cache
|
|
.select_font(font_family_id, &Default::default())
|
|
.unwrap();
|
|
Self {
|
|
color: Default::default(),
|
|
font_family_name,
|
|
font_family_id,
|
|
font_id,
|
|
font_size: 14.,
|
|
font_properties: Default::default(),
|
|
underline: Default::default(),
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
impl HighlightStyle {
|
|
fn from_json(json: HighlightStyleJson) -> Self {
|
|
Self {
|
|
color: json.color,
|
|
weight: json.weight.map(weight_from_json),
|
|
italic: json.italic,
|
|
underline: json.underline.map(underline_from_json),
|
|
fade_out: json.fade_out,
|
|
}
|
|
}
|
|
|
|
pub fn highlight(&mut self, other: HighlightStyle) {
|
|
match (self.color, other.color) {
|
|
(Some(self_color), Some(other_color)) => {
|
|
self.color = Some(Color::blend(other_color, self_color));
|
|
}
|
|
(None, Some(other_color)) => {
|
|
self.color = Some(other_color);
|
|
}
|
|
_ => {}
|
|
}
|
|
|
|
if other.weight.is_some() {
|
|
self.weight = other.weight;
|
|
}
|
|
|
|
if other.italic.is_some() {
|
|
self.italic = other.italic;
|
|
}
|
|
|
|
if other.underline.is_some() {
|
|
self.underline = other.underline;
|
|
}
|
|
|
|
match (other.fade_out, self.fade_out) {
|
|
(Some(source_fade), None) => self.fade_out = Some(source_fade),
|
|
(Some(source_fade), Some(dest_fade)) => {
|
|
let source_alpha = 1. - source_fade;
|
|
let dest_alpha = 1. - dest_fade;
|
|
let blended_alpha = source_alpha + (dest_alpha * source_fade);
|
|
let blended_fade = 1. - blended_alpha;
|
|
self.fade_out = Some(blended_fade);
|
|
}
|
|
_ => {}
|
|
}
|
|
}
|
|
}
|
|
|
|
impl From<Color> for HighlightStyle {
|
|
fn from(color: Color) -> Self {
|
|
Self {
|
|
color: Some(color),
|
|
..Default::default()
|
|
}
|
|
}
|
|
}
|
|
|
|
impl<'de> Deserialize<'de> for TextStyle {
|
|
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
|
|
where
|
|
D: serde::Deserializer<'de>,
|
|
{
|
|
Self::from_json(TextStyleJson::deserialize(deserializer)?).map_err(de::Error::custom)
|
|
}
|
|
}
|
|
|
|
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() {
|
|
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)?,
|
|
..Default::default()
|
|
})
|
|
}
|
|
}
|
|
}
|
|
|
|
fn underline_from_json(json: UnderlineStyleJson) -> Underline {
|
|
match json {
|
|
UnderlineStyleJson::Underlined(false) => Underline::default(),
|
|
UnderlineStyleJson::Underlined(true) => Underline {
|
|
color: None,
|
|
thickness: 1.0.into(),
|
|
squiggly: false,
|
|
},
|
|
UnderlineStyleJson::UnderlinedWithProperties {
|
|
color,
|
|
thickness,
|
|
squiggly,
|
|
} => Underline {
|
|
color,
|
|
thickness: thickness.unwrap_or(1.).into(),
|
|
squiggly,
|
|
},
|
|
}
|
|
}
|
|
|
|
fn properties_from_json(weight: Option<WeightJson>, italic: bool) -> Properties {
|
|
let weight = weight.map(weight_from_json).unwrap_or_default();
|
|
let style = if italic { Style::Italic } else { Style::Normal };
|
|
*Properties::new().weight(weight).style(style)
|
|
}
|
|
|
|
fn weight_from_json(weight: WeightJson) -> Weight {
|
|
match weight {
|
|
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,
|
|
}
|
|
}
|
|
|
|
impl ToJson for Properties {
|
|
fn to_json(&self) -> crate::json::Value {
|
|
json!({
|
|
"style": self.style.to_json(),
|
|
"weight": self.weight.to_json(),
|
|
"stretch": self.stretch.to_json(),
|
|
})
|
|
}
|
|
}
|
|
|
|
impl ToJson for Style {
|
|
fn to_json(&self) -> crate::json::Value {
|
|
match self {
|
|
Style::Normal => json!("normal"),
|
|
Style::Italic => json!("italic"),
|
|
Style::Oblique => json!("oblique"),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ToJson for Weight {
|
|
fn to_json(&self) -> crate::json::Value {
|
|
if self.0 == Weight::THIN.0 {
|
|
json!("thin")
|
|
} else if self.0 == Weight::EXTRA_LIGHT.0 {
|
|
json!("extra light")
|
|
} else if self.0 == Weight::LIGHT.0 {
|
|
json!("light")
|
|
} else if self.0 == Weight::NORMAL.0 {
|
|
json!("normal")
|
|
} else if self.0 == Weight::MEDIUM.0 {
|
|
json!("medium")
|
|
} else if self.0 == Weight::SEMIBOLD.0 {
|
|
json!("semibold")
|
|
} else if self.0 == Weight::BOLD.0 {
|
|
json!("bold")
|
|
} else if self.0 == Weight::EXTRA_BOLD.0 {
|
|
json!("extra bold")
|
|
} else if self.0 == Weight::BLACK.0 {
|
|
json!("black")
|
|
} else {
|
|
json!(self.0)
|
|
}
|
|
}
|
|
}
|
|
|
|
impl ToJson for Stretch {
|
|
fn to_json(&self) -> serde_json::Value {
|
|
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
|
|
})
|
|
}
|