use gpui_kit::{
Font, FontFallbacks, FontFeatures, FontStyle, FontWeight, Hsla, Pixels, SharedString, Size,
TextRun, Window, px, size,
};
use crate::terminal::frame::CursorShape;
use crate::theme::ThemeTokens;
#[derive(Clone, Debug, PartialEq)]
#[non_exhaustive]
pub struct TerminalAppearance {
pub(crate) font_family: Option<SharedString>,
pub(crate) font_fallbacks: Vec<String>,
pub(crate) font_size: Option<Pixels>,
pub(crate) font_weight: FontWeight,
pub(crate) cell_height_adjust: f32,
pub(crate) padding: Option<Pixels>,
pub(crate) padding_balance: bool,
pub(crate) cursor_blink: Option<bool>,
pub(crate) cursor_shape: Option<CursorShape>,
pub(crate) copy_on_select: bool,
pub(crate) trim_trailing_spaces: bool,
}
impl Default for TerminalAppearance {
fn default() -> Self {
Self {
font_family: None,
font_fallbacks: Vec::new(),
font_size: None,
font_weight: FontWeight::NORMAL,
cell_height_adjust: 0.0,
padding: None,
padding_balance: false,
cursor_blink: None,
cursor_shape: None,
copy_on_select: false,
trim_trailing_spaces: true,
}
}
}
impl TerminalAppearance {
#[must_use]
pub fn with_font_family(mut self, family: impl Into<SharedString>) -> Self {
self.font_family = Some(family.into());
self
}
#[must_use]
pub fn with_font_fallbacks(
mut self,
families: impl IntoIterator<Item = impl Into<String>>,
) -> Self {
self.font_fallbacks = families.into_iter().map(Into::into).collect();
self
}
#[must_use]
pub fn with_font_size(mut self, size: impl Into<Pixels>) -> Self {
self.font_size = Some(size.into());
self
}
#[must_use]
pub fn with_font_weight(mut self, weight: impl Into<FontWeight>) -> Self {
self.font_weight = weight.into();
self
}
#[must_use]
pub fn with_cell_height_adjust(mut self, fraction: f32) -> Self {
self.cell_height_adjust = fraction;
self
}
#[must_use]
pub fn with_padding(mut self, padding: impl Into<Pixels>) -> Self {
self.padding = Some(padding.into());
self
}
#[must_use]
pub fn with_padding_balance(mut self, balance: bool) -> Self {
self.padding_balance = balance;
self
}
#[must_use]
pub fn with_cursor_blink(mut self, blink: Option<bool>) -> Self {
self.cursor_blink = blink;
self
}
#[must_use]
pub fn with_cursor_shape(mut self, shape: Option<CursorShape>) -> Self {
self.cursor_shape = shape;
self
}
#[must_use]
pub fn with_copy_on_select(mut self, copy: bool) -> Self {
self.copy_on_select = copy;
self
}
#[must_use]
pub fn with_trim_trailing_spaces(mut self, trim: bool) -> Self {
self.trim_trailing_spaces = trim;
self
}
pub fn font_family(&self) -> Option<&SharedString> {
self.font_family.as_ref()
}
pub fn font_fallbacks(&self) -> &[String] {
&self.font_fallbacks
}
pub fn font_size(&self) -> Option<Pixels> {
self.font_size
}
pub fn font_weight(&self) -> FontWeight {
self.font_weight
}
pub fn cell_height_adjust(&self) -> f32 {
self.cell_height_adjust
}
pub fn padding(&self) -> Option<Pixels> {
self.padding
}
pub fn padding_balance(&self) -> bool {
self.padding_balance
}
pub fn cursor_blink(&self) -> Option<bool> {
self.cursor_blink
}
pub fn cursor_shape(&self) -> Option<CursorShape> {
self.cursor_shape
}
pub fn copy_on_select(&self) -> bool {
self.copy_on_select
}
pub fn trim_trailing_spaces(&self) -> bool {
self.trim_trailing_spaces
}
pub(crate) fn resolved(&self, theme: &ThemeTokens) -> Self {
let mut resolved = self.clone();
resolved
.font_family
.get_or_insert_with(|| theme.mono_font_family().clone());
resolved
.font_size
.get_or_insert(theme.base.typography.mono_md.size);
resolved
.padding
.get_or_insert(theme.metrics.terminal_padding);
resolved
}
pub(crate) fn size(&self) -> Pixels {
self.font_size.unwrap_or_default()
}
pub(crate) fn inset(&self) -> Pixels {
self.padding.unwrap_or_default()
}
pub(crate) fn font(&self) -> Font {
let mut font = gpui_kit::font(self.font_family.clone().unwrap_or_default());
if !self.font_fallbacks.is_empty() {
font.fallbacks = Some(FontFallbacks::from_fonts(self.font_fallbacks.clone()));
}
font.features = FontFeatures::disable_ligatures();
font.weight = self.font_weight;
font.style = FontStyle::Normal;
font
}
pub(crate) fn cell_height(&self, natural: Pixels, scale: f32) -> Pixels {
let adjusted = f32::from(natural) * (1.0 + self.cell_height_adjust);
let scale = if scale > 0.0 { scale } else { 1.0 };
px(((adjusted * scale).round() / scale).max(1.0))
}
pub(crate) fn cell_size(&self, window: &mut Window) -> Size<Pixels> {
let font = self.font();
let sample = window.text_system().shape_line(
"M".into(),
self.size(),
&[TextRun {
len: 1,
font,
color: Hsla::default(),
background_color: None,
underline: None,
strikethrough: None,
}],
None,
);
size(
sample.width.ceil().max(px(1.0)),
self.cell_height(sample.ascent + sample.descent, window.scale_factor()),
)
}
pub(crate) fn cursor_blinks(&self, frame_requests_blink: bool) -> bool {
self.cursor_blink.unwrap_or(frame_requests_blink)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn cell_height_snaps_to_physical_pixels_and_never_collapses() {
let appearance = TerminalAppearance::default();
assert_eq!(appearance.cell_height(px(17.0), 1.0), px(17.0));
let taller = appearance.clone().with_cell_height_adjust(0.2);
assert_eq!(taller.cell_height(px(10.0), 1.0), px(12.0));
assert_eq!(taller.cell_height(px(17.3), 2.0), px(21.0));
let crushed = appearance.with_cell_height_adjust(-2.0);
assert_eq!(crushed.cell_height(px(17.0), 1.0), px(1.0));
}
#[test]
fn an_explicit_cursor_blink_setting_overrides_the_program_request() {
let appearance = TerminalAppearance::default();
assert!(appearance.cursor_blinks(true));
assert!(!appearance.cursor_blinks(false));
assert!(
!appearance
.clone()
.with_cursor_blink(Some(false))
.cursor_blinks(true)
);
assert!(
appearance
.with_cursor_blink(Some(true))
.cursor_blinks(false)
);
}
}