use std::any::{Any, TypeId};
use std::collections::HashMap;
use std::fmt;
use std::hash::{Hash, Hasher};
use std::sync::Arc;
use crate::app::ContrastPolicy;
use super::{Color, HostTerminalColors, Paint};
#[cfg_attr(
feature = "terminal-serde",
derive(serde::Serialize, serde::Deserialize)
)]
#[derive(Clone, Copy, Debug)]
pub enum ColorTransform {
Dim(f32),
Lighten(f32),
Opacity(f32),
OpacityToward {
factor: f32,
target: Color,
},
Tint(Color, f32),
}
impl PartialEq for ColorTransform {
fn eq(&self, other: &Self) -> bool {
match (*self, *other) {
(Self::Dim(a), Self::Dim(b))
| (Self::Lighten(a), Self::Lighten(b))
| (Self::Opacity(a), Self::Opacity(b)) => a.to_bits() == b.to_bits(),
(
Self::OpacityToward {
factor: fa,
target: ta,
},
Self::OpacityToward {
factor: fb,
target: tb,
},
) => fa.to_bits() == fb.to_bits() && ta == tb,
(Self::Tint(color_a, alpha_a), Self::Tint(color_b, alpha_b)) => {
color_a == color_b && alpha_a.to_bits() == alpha_b.to_bits()
}
_ => false,
}
}
}
impl Eq for ColorTransform {}
impl Hash for ColorTransform {
fn hash<H: Hasher>(&self, state: &mut H) {
match *self {
Self::Dim(amount) => {
0u8.hash(state);
amount.to_bits().hash(state);
}
Self::Lighten(amount) => {
1u8.hash(state);
amount.to_bits().hash(state);
}
Self::Opacity(amount) => {
2u8.hash(state);
amount.to_bits().hash(state);
}
Self::OpacityToward { factor, target } => {
4u8.hash(state);
factor.to_bits().hash(state);
target.hash(state);
}
Self::Tint(color, alpha) => {
3u8.hash(state);
color.hash(state);
alpha.to_bits().hash(state);
}
}
}
}
impl ColorTransform {
pub fn apply(self, color: Color) -> Color {
self.apply_with_backdrop(color, None)
}
pub fn apply_with_backdrop(self, color: Color, backdrop: Option<Color>) -> Color {
if matches!(color, Color::Transparent | Color::Backdrop) {
return color;
}
match self {
Self::Dim(amount) => color.dim_by(amount),
Self::Lighten(amount) => color.lighten_by(amount),
Self::Opacity(opacity) => backdrop.map_or(color, |bg| {
color.blend_toward(bg, (1.0 - opacity).clamp(0.0, 1.0))
}),
Self::OpacityToward { factor, target } => {
color.blend_toward(target, (1.0 - factor).clamp(0.0, 1.0))
}
Self::Tint(target, alpha) => color.blend_toward(target, alpha),
}
}
pub fn apply_paint(self, paint: Paint) -> Paint {
self.apply_paint_with_backdrop(paint, None)
}
pub fn apply_paint_with_backdrop(self, paint: Paint, backdrop: Option<Paint>) -> Paint {
if matches!(paint, Paint::Solid(Color::Transparent | Color::Backdrop)) {
return paint;
}
if let Self::Opacity(opacity) = self {
let alpha = (paint.alpha_u8() as f32 * opacity.clamp(0.0, 1.0))
.round()
.clamp(0.0, 255.0) as u8;
return Paint::from_color_alpha_u8(paint.color(), alpha);
}
let backdrop = backdrop.map(Paint::color);
match paint {
Paint::Solid(color) => Paint::Solid(self.apply_with_backdrop(color, backdrop)),
Paint::Alpha { color, alpha } => {
Paint::from_color_alpha_u8(self.apply_with_backdrop(color, backdrop), alpha)
}
}
}
pub(crate) fn needs_backdrop(self) -> bool {
matches!(self, Self::Opacity(_))
}
fn normalized(self) -> Self {
match self {
Self::Dim(amount) => Self::Dim(amount.clamp(0.0, 1.0)),
Self::Lighten(amount) => Self::Lighten(amount.clamp(0.0, 1.0)),
Self::Opacity(opacity) => Self::Opacity(opacity.clamp(0.0, 1.0)),
Self::OpacityToward { factor, target } => Self::OpacityToward {
factor: factor.clamp(0.0, 1.0),
target,
},
Self::Tint(color, alpha) => Self::Tint(color, alpha.clamp(0.0, 1.0)),
}
}
}
pub trait ThemeExtension: Clone + fmt::Debug + PartialEq + 'static {}
impl<T> ThemeExtension for T where T: Clone + fmt::Debug + PartialEq + 'static {}
trait ThemeExtensionValue: Any {
fn as_any(&self) -> &dyn Any;
fn eq_value(&self, other: &dyn ThemeExtensionValue) -> bool;
}
impl<T> ThemeExtensionValue for T
where
T: ThemeExtension,
{
fn as_any(&self) -> &dyn Any {
self
}
fn eq_value(&self, other: &dyn ThemeExtensionValue) -> bool {
other.as_any().downcast_ref::<T>() == Some(self)
}
}
#[derive(Clone, Default)]
#[doc(hidden)]
pub struct ThemeExtensions(HashMap<TypeId, Arc<dyn ThemeExtensionValue>>);
impl ThemeExtensions {
fn insert<T>(&mut self, extension: T)
where
T: ThemeExtension,
{
self.0.insert(TypeId::of::<T>(), Arc::new(extension));
}
fn get<T>(&self) -> Option<&T>
where
T: ThemeExtension,
{
self.0
.get(&TypeId::of::<T>())
.and_then(|value| value.as_any().downcast_ref::<T>())
}
fn remove<T>(&mut self)
where
T: ThemeExtension,
{
self.0.remove(&TypeId::of::<T>());
}
}
impl PartialEq for ThemeExtensions {
fn eq(&self, other: &Self) -> bool {
self.0.len() == other.0.len()
&& self.0.iter().all(|(type_id, value)| {
other
.0
.get(type_id)
.is_some_and(|other_value| value.eq_value(other_value.as_ref()))
})
}
}
impl Eq for ThemeExtensions {}
impl fmt::Debug for ThemeExtensions {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("ThemeExtensions")
.field("count", &self.0.len())
.finish()
}
}
#[cfg_attr(
feature = "terminal-serde",
derive(serde::Serialize, serde::Deserialize)
)]
#[derive(Clone, Copy, Debug, Default)]
pub struct Style {
pub fg: Option<Paint>,
pub bg: Option<Paint>,
pub fg_transform: Option<ColorTransform>,
pub bg_transform: Option<ColorTransform>,
pub contrast_policy: Option<ContrastPolicy>,
pub bold: Option<bool>,
pub dim: Option<bool>,
pub italic: Option<bool>,
pub underline: Option<bool>,
pub reverse: Option<bool>,
pub strikethrough: Option<bool>,
pub underline_color: Option<Paint>,
pub dim_amount: Option<f32>,
pub tint: Option<(Color, f32)>,
}
impl PartialEq for Style {
fn eq(&self, other: &Self) -> bool {
self.fg == other.fg
&& self.bg == other.bg
&& self.fg_transform == other.fg_transform
&& self.bg_transform == other.bg_transform
&& self.contrast_policy == other.contrast_policy
&& self.bold == other.bold
&& self.dim == other.dim
&& self.italic == other.italic
&& self.underline == other.underline
&& self.reverse == other.reverse
&& self.strikethrough == other.strikethrough
&& self.underline_color == other.underline_color
&& self.dim_amount.map(f32::to_bits) == other.dim_amount.map(f32::to_bits)
&& self.tint.map(|(c, a)| (c, a.to_bits())) == other.tint.map(|(c, a)| (c, a.to_bits()))
}
}
impl Eq for Style {}
#[cfg(all(test, feature = "terminal-serde"))]
mod terminal_serde_tests {
use super::*;
#[test]
fn color_transform_round_trips() {
let transform = ColorTransform::OpacityToward {
factor: 0.42,
target: Color::rgb(1, 2, 3),
};
let json = serde_json::to_string(&transform).unwrap();
assert_eq!(
serde_json::from_str::<ColorTransform>(&json).unwrap(),
transform
);
}
#[test]
fn style_round_trips() {
let style = Style::default()
.fg(Paint::rgb(20, 30, 40))
.bg(Paint::rgba(1, 2, 3, 180))
.bold()
.underline()
.contrast_policy(ContrastPolicy::BlackOrWhite)
.tint_by(Color::Cyan, 0.25);
let json = serde_json::to_string(&style).unwrap();
assert_eq!(serde_json::from_str::<Style>(&json).unwrap(), style);
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Default)]
pub enum StyleSlot {
#[default]
Inherit,
Extend(Style),
Replace(Style),
}
impl StyleSlot {
pub fn replace(style: Style) -> Self {
Self::Replace(style)
}
pub fn extend(style: Style) -> Self {
Self::Extend(style)
}
pub fn explicit_style(self) -> Option<Style> {
match self {
Self::Inherit => None,
Self::Extend(style) | Self::Replace(style) => Some(style),
}
}
pub fn has_explicit_style(self) -> bool {
self.explicit_style().is_some_and(|style| !style.is_empty())
}
pub fn is_empty(self) -> bool {
matches!(self, Self::Replace(style) if style.is_empty())
}
}
impl From<Style> for StyleSlot {
fn from(style: Style) -> Self {
Self::Replace(style)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub enum ThemeRole {
Base,
Accent,
Selection,
TextSelection,
UnfocusedSelection,
Hover,
DragSource,
DropTarget,
DropTargetActive,
Focus,
Active,
ItemHover,
Border,
Disabled,
Muted,
Error,
InputFocusContent,
TextAreaFocusContent,
DocumentViewFocusContent,
HexAreaFocusContent,
HexAreaCursor,
TerminalFocusContent,
ScrollbarThumb,
ScrollbarThumbFocus,
ScrollbarTrack,
SplitterHover,
SplitterActive,
}
impl Hash for Style {
fn hash<H: Hasher>(&self, state: &mut H) {
self.fg.hash(state);
self.bg.hash(state);
self.fg_transform.hash(state);
self.bg_transform.hash(state);
self.contrast_policy.hash(state);
self.bold.hash(state);
self.dim.hash(state);
self.italic.hash(state);
self.underline.hash(state);
self.reverse.hash(state);
self.strikethrough.hash(state);
self.underline_color.hash(state);
self.dim_amount.map(f32::to_bits).hash(state);
if let Some((c, a)) = self.tint {
c.hash(state);
a.to_bits().hash(state);
}
}
}
impl Style {
pub fn new() -> Self {
Self::default()
}
pub fn fg(mut self, color: impl Into<Paint>) -> Self {
self.fg = Some(color.into());
self
}
pub fn bg(mut self, color: impl Into<Paint>) -> Self {
self.bg = Some(color.into());
self
}
pub fn fg_alpha(mut self, color: Color, alpha: f32) -> Self {
self.fg = Some(Paint::from_color_alpha(color, alpha));
self
}
pub fn bg_alpha(mut self, color: Color, alpha: f32) -> Self {
self.bg = Some(Paint::from_color_alpha(color, alpha));
self
}
pub fn transform_fg(mut self, transform: ColorTransform) -> Self {
self.fg_transform = Some(transform.normalized());
self
}
pub fn transform_bg(mut self, transform: ColorTransform) -> Self {
self.bg_transform = Some(transform.normalized());
self
}
pub fn contrast_policy(mut self, policy: ContrastPolicy) -> Self {
self.contrast_policy = Some(policy);
self
}
pub fn bold(mut self) -> Self {
self.bold = Some(true);
self
}
pub fn not_bold(mut self) -> Self {
self.bold = Some(false);
self
}
pub fn dim(mut self) -> Self {
self.dim = Some(true);
self
}
pub fn dim_by(mut self, amount: f32) -> Self {
let amount = amount.clamp(0.0, 1.0);
self.fg_transform = Some(ColorTransform::Dim(amount));
self.bg_transform = Some(ColorTransform::Dim(amount));
self.dim_amount = Some(amount);
self
}
pub fn tint_by(mut self, color: Color, alpha: f32) -> Self {
let alpha = alpha.clamp(0.0, 1.0);
self.tint = Some((color, alpha));
self
}
pub fn lighten_by(mut self, amount: f32) -> Self {
let amount = amount.clamp(0.0, 1.0);
self.fg_transform = Some(ColorTransform::Lighten(amount));
self.bg_transform = Some(ColorTransform::Lighten(amount));
self
}
pub fn italic(mut self) -> Self {
self.italic = Some(true);
self
}
pub fn underline(mut self) -> Self {
self.underline = Some(true);
self
}
pub fn reverse(mut self) -> Self {
self.reverse = Some(true);
self
}
pub fn strikethrough(mut self) -> Self {
self.strikethrough = Some(true);
self
}
pub fn underline_color(mut self, color: impl Into<Paint>) -> Self {
self.underline_color = Some(color.into());
self.underline = Some(true);
self
}
pub fn is_empty(&self) -> bool {
self.fg.is_none()
&& self.bg.is_none()
&& self.fg_transform.is_none()
&& self.bg_transform.is_none()
&& self.contrast_policy.is_none()
&& self.bold.is_none()
&& self.dim.is_none()
&& self.italic.is_none()
&& self.underline.is_none()
&& self.reverse.is_none()
&& self.strikethrough.is_none()
&& self.underline_color.is_none()
&& self.dim_amount.is_none()
&& self.tint.is_none()
}
pub fn patch(self, other: Style) -> Self {
let (bg, bg_transform) = merge_channel(
self.bg,
self.bg_transform,
other.bg,
other.bg_transform,
None,
);
let backdrop = bg.or(other.bg).or(self.bg);
let (fg, fg_transform) = merge_channel(
self.fg,
self.fg_transform,
other.fg,
other.fg_transform,
backdrop,
);
Self {
fg,
bg,
fg_transform,
bg_transform,
contrast_policy: other.contrast_policy.or(self.contrast_policy),
bold: other.bold.or(self.bold),
dim: other.dim.or(self.dim),
italic: other.italic.or(self.italic),
underline: other.underline.or(self.underline),
reverse: other.reverse.or(self.reverse),
strikethrough: other.strikethrough.or(self.strikethrough),
underline_color: merge_underline_color(other.underline_color, self.underline_color),
dim_amount: other.dim_amount.or(self.dim_amount),
tint: other.tint.or(self.tint),
}
}
pub(crate) fn resolve_color_transforms(self) -> Self {
let bg = resolve_channel(self.bg, self.bg_transform, None);
let mut fg = resolve_channel(self.fg, self.fg_transform, bg);
let mut fg_transform_remaining = None;
if matches!(fg, Some(Paint::Solid(Color::Transparent))) {
if let Some(c) = bg
&& !matches!(c, Paint::Solid(Color::Transparent | Color::Backdrop))
{
fg = if let Some(t) = self.fg_transform {
Some(t.apply_paint_with_backdrop(c, bg))
} else {
Some(c)
};
} else {
fg_transform_remaining = self.fg_transform;
}
}
Self {
fg,
bg,
fg_transform: fg_transform_remaining,
bg_transform: None,
..self
}
}
}
fn merge_underline_color(overlay: Option<Paint>, base: Option<Paint>) -> Option<Paint> {
match overlay {
None => base,
Some(Paint::Solid(Color::Transparent)) => base,
Some(c) => Some(c),
}
}
pub(crate) fn merge_channel(
base_color: Option<Paint>,
base_transform: Option<ColorTransform>,
overlay_color: Option<Paint>,
overlay_transform: Option<ColorTransform>,
backdrop: Option<Paint>,
) -> (Option<Paint>, Option<ColorTransform>) {
let mut color = resolve_channel(base_color, base_transform, backdrop);
let mut transform = None;
if let Some(overlay_color) = overlay_color
&& !matches!(overlay_color, Paint::Solid(Color::Transparent))
{
color = Some(overlay_color);
}
if let Some(overlay_transform) = overlay_transform {
if let Some(current) = color
&& (!overlay_transform.needs_backdrop() || backdrop.is_some())
{
color = Some(overlay_transform.apply_paint_with_backdrop(current, backdrop));
} else {
transform = Some(overlay_transform.normalized());
}
}
(color, transform)
}
pub(crate) fn resolve_channel(
color: Option<Paint>,
transform: Option<ColorTransform>,
backdrop: Option<Paint>,
) -> Option<Paint> {
match (color, transform) {
(Some(color), Some(transform)) => {
Some(transform.apply_paint_with_backdrop(color, backdrop))
}
(color, None) => color,
(None, Some(_)) => None,
}
}
#[cfg(test)]
mod tests {
use super::{ColorTransform, Style, Theme, ThemePalette, ThemeRole};
use crate::app::ContrastPolicy;
use crate::style::{Color, HostTerminalColors, Paint};
fn p(color: Color) -> Option<Paint> {
Some(Paint::Solid(color))
}
#[derive(Clone, Debug, PartialEq)]
struct BrandTheme {
accent_badge: Color,
}
#[test]
fn drag_drop_roles_initially_resolve_to_hover() {
let theme = Theme::default().hover(Style::new().fg(Color::White).bg(Color::Blue));
assert_eq!(theme.role(ThemeRole::DragSource), theme.hover);
assert_eq!(theme.role(ThemeRole::DropTarget), theme.hover);
assert_eq!(theme.role(ThemeRole::DropTargetActive), theme.hover);
}
#[test]
fn text_selection_role_is_distinct_from_item_selection() {
let theme = Theme::default()
.selection(Style::new().fg(Color::Red))
.text_selection(Style::new().fg(Color::Blue));
assert_eq!(theme.role(ThemeRole::Selection), theme.selection);
assert_eq!(theme.role(ThemeRole::TextSelection), theme.text_selection);
assert_ne!(
theme.role(ThemeRole::Selection),
theme.role(ThemeRole::TextSelection)
);
}
#[test]
fn theme_palette_derives_distinct_selection_colors() {
let theme = ThemePalette::new(Color::White, Color::Black, Color::Blue)
.selection(Color::Green)
.text_selection(Color::Magenta)
.into_theme();
assert_eq!(theme.selection.fg, p(Color::Green));
assert_eq!(theme.text_selection.fg, p(Color::Magenta));
}
#[test]
fn from_host_colors_uses_host_palette() {
let mut ansi = std::array::from_fn(|i| Color::rgb(i as u8, i as u8, i as u8));
ansi[1] = Color::rgb(210, 30, 40);
ansi[2] = Color::rgb(30, 210, 40);
ansi[3] = Color::rgb(210, 180, 40);
ansi[4] = Color::rgb(30, 80, 210);
let colors = HostTerminalColors {
fg: Color::rgb(230, 231, 232),
bg: Color::rgb(10, 11, 12),
ansi,
};
let theme = Theme::from_host_colors(colors);
assert_eq!(theme.primary.fg, p(colors.fg));
assert_eq!(theme.primary.bg, p(colors.bg));
assert_eq!(theme.accent.fg, p(colors.ansi[4]));
assert_eq!(theme.status.success, colors.ansi[2]);
assert_eq!(theme.status.warning, colors.ansi[3]);
assert_eq!(theme.status.error, colors.ansi[1]);
assert_eq!(theme.status.info, colors.ansi[4]);
}
#[test]
fn transform_fg_dims_inherited_color() {
let base = Style::new().fg(Color::rgb(100, 120, 140));
let overlay = Style::new().transform_fg(ColorTransform::Dim(0.5));
assert_eq!(
base.patch(overlay).resolve_color_transforms().fg,
p(Color::rgb(50, 60, 70))
);
}
#[test]
fn lower_fg_transform_does_not_affect_overlay_color() {
let base = Style::new()
.fg(Color::rgb(100, 120, 140))
.transform_fg(ColorTransform::Dim(0.5));
let overlay = Style::new().fg(Color::rgb(10, 20, 30));
assert_eq!(
base.patch(overlay).resolve_color_transforms().fg,
p(Color::rgb(10, 20, 30))
);
}
#[test]
fn patch_transparent_fg_preserves_base() {
let base = Style::new().fg(Color::rgb(10, 20, 30));
let overlay = Style::new().fg(Color::Transparent);
assert_eq!(
base.patch(overlay).resolve_color_transforms().fg,
p(Color::rgb(10, 20, 30))
);
}
#[test]
fn patch_transparent_bg_preserves_base() {
let base = Style::new().bg(Color::rgb(40, 50, 60));
let overlay = Style::new().bg(Color::Transparent);
assert_eq!(
base.patch(overlay).resolve_color_transforms().bg,
p(Color::rgb(40, 50, 60))
);
}
#[test]
fn patch_alpha_zero_bg_is_not_transparent_sentinel() {
let base = Style::new().bg(Color::rgb(40, 50, 60));
let overlay = Style::new().bg_alpha(Color::Red, 0.0);
assert_eq!(
base.patch(overlay).resolve_color_transforms().bg,
Some(Paint::Alpha {
color: Color::Red,
alpha: 0,
})
);
}
#[test]
fn color_transform_apply_paint_preserves_alpha() {
let paint = Paint::Alpha {
color: Color::rgb(100, 120, 140),
alpha: 128,
};
assert_eq!(
ColorTransform::Dim(0.5).apply_paint(paint),
Paint::Alpha {
color: Color::rgb(50, 60, 70),
alpha: 128,
}
);
}
#[test]
fn patch_transparent_underline_color_preserves_base() {
let base = Style::new().underline_color(Color::Red);
let overlay = Style::new().underline_color(Color::Transparent);
let patched = base.patch(overlay);
assert_eq!(patched.underline_color, p(Color::Red));
}
#[test]
fn builder_order_does_not_change_transform_result() {
let a = Style::new()
.transform_fg(ColorTransform::Dim(0.5))
.fg(Color::rgb(100, 120, 140));
let b = Style::new()
.fg(Color::rgb(100, 120, 140))
.transform_fg(ColorTransform::Dim(0.5));
assert_eq!(a.resolve_color_transforms(), b.resolve_color_transforms());
assert_eq!(a.resolve_color_transforms().fg, p(Color::rgb(50, 60, 70)));
}
#[test]
fn transform_chain_applies_in_patch_order() {
let style = Style::new()
.fg(Color::rgb(100, 120, 140))
.patch(Style::new().transform_fg(ColorTransform::Dim(0.5)))
.patch(Style::new().transform_fg(ColorTransform::Lighten(0.5)))
.resolve_color_transforms();
assert_eq!(style.fg, p(Color::rgb(153, 158, 163)));
}
#[test]
fn state_cascade_stacks_bg_transforms_on_resolved_color() {
let style = Style::new()
.bg(Color::rgb(100, 100, 100))
.patch(Style::new().transform_bg(ColorTransform::Dim(0.5)))
.patch(Style::new().transform_bg(ColorTransform::Dim(0.5)))
.resolve_color_transforms();
assert_eq!(style.bg, p(Color::rgb(25, 25, 25)));
}
#[test]
fn opacity_turns_foreground_into_alpha_paint() {
let style = Style::new()
.fg(Color::rgb(245, 167, 66))
.bg(Color::rgb(255, 255, 255))
.transform_fg(ColorTransform::Opacity(0.6))
.resolve_color_transforms();
assert_eq!(
style.fg,
Some(Paint::Alpha {
color: Color::rgb(245, 167, 66),
alpha: 153,
})
);
}
#[test]
fn opacity_builder_order_is_independent_when_background_arrives_later() {
let a = Style::new()
.transform_fg(ColorTransform::Opacity(0.6))
.fg(Color::rgb(245, 167, 66))
.bg(Color::rgb(255, 255, 255));
let b = Style::new()
.fg(Color::rgb(245, 167, 66))
.bg(Color::rgb(255, 255, 255))
.transform_fg(ColorTransform::Opacity(0.6));
assert_eq!(a.resolve_color_transforms(), b.resolve_color_transforms());
}
#[test]
fn opacity_toward_uses_fixed_target_not_backdrop() {
let c = Color::rgb(0, 100, 200);
let target = Color::rgb(200, 10, 30);
assert_eq!(
ColorTransform::OpacityToward {
factor: 1.0,
target,
}
.apply_with_backdrop(c, Some(Color::White)),
c
);
assert_eq!(
ColorTransform::OpacityToward {
factor: 0.0,
target,
}
.apply_with_backdrop(c, Some(Color::White)),
target
);
}
#[test]
fn patch_prefers_overlay_contrast_policy() {
let base = Style::new().contrast_policy(ContrastPolicy::Wcag);
let overlay = Style::new().contrast_policy(ContrastPolicy::Off);
assert_eq!(
base.patch(overlay).contrast_policy,
Some(ContrastPolicy::Off)
);
}
#[test]
fn theme_extensions_roundtrip_and_affect_equality() {
let a = Theme::default().with_extension(BrandTheme {
accent_badge: Color::rgb(1, 2, 3),
});
let b = Theme::default().with_extension(BrandTheme {
accent_badge: Color::rgb(1, 2, 3),
});
let c = Theme::default().with_extension(BrandTheme {
accent_badge: Color::rgb(9, 8, 7),
});
assert_eq!(a.extension::<BrandTheme>(), b.extension::<BrandTheme>());
assert_eq!(a, b);
assert_ne!(a, c);
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum CaretShape {
#[default]
Block,
Bar,
Underline,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct BorderGlyphs {
pub top_left: &'static str,
pub top: &'static str,
pub top_right: &'static str,
pub left: &'static str,
pub right: &'static str,
pub bottom_left: &'static str,
pub bottom: &'static str,
pub bottom_right: &'static str,
}
impl Default for BorderGlyphs {
fn default() -> Self {
Self::PLAIN
}
}
impl BorderGlyphs {
pub const PLAIN: Self = Self {
top_left: "┌",
top: "─",
top_right: "┐",
left: "│",
right: "│",
bottom_left: "└",
bottom: "─",
bottom_right: "┘",
};
pub fn new(parts: BorderGlyphsParts) -> Self {
Self {
top_left: parts.top_left,
top: parts.top,
top_right: parts.top_right,
left: parts.left,
right: parts.right,
bottom_left: parts.bottom_left,
bottom: parts.bottom,
bottom_right: parts.bottom_right,
}
}
}
pub struct BorderGlyphsParts {
pub top_left: &'static str,
pub top: &'static str,
pub top_right: &'static str,
pub left: &'static str,
pub right: &'static str,
pub bottom_left: &'static str,
pub bottom: &'static str,
pub bottom_right: &'static str,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum BorderStyle {
#[default]
Plain,
Rounded,
Double,
Thick,
LightDoubleDashed,
HeavyDoubleDashed,
LightTripleDashed,
HeavyTripleDashed,
LightQuadrupleDashed,
HeavyQuadrupleDashed,
Custom {
glyphs: BorderGlyphs,
},
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub enum ScrollbarVariant {
Integrated,
#[default]
Standalone,
}
#[derive(Clone, Debug, Default, PartialEq)]
pub struct ScrollbarConfig {
pub variant: ScrollbarVariant,
pub gap: u16,
pub thumb: Option<char>,
pub thumb_style: Option<Style>,
pub thumb_focus_style: Option<Style>,
pub track_style: Option<Style>,
}
impl ScrollbarConfig {
pub fn new() -> Self {
Self::default()
}
pub fn variant(mut self, variant: ScrollbarVariant) -> Self {
self.variant = variant;
self
}
pub fn gap(mut self, gap: u16) -> Self {
self.gap = gap;
self
}
pub fn thumb(mut self, ch: char) -> Self {
self.thumb = Some(ch);
self
}
pub fn thumb_style(mut self, style: Style) -> Self {
self.thumb_style = Some(style);
self
}
pub fn thumb_focus_style(mut self, style: Style) -> Self {
self.thumb_focus_style = Some(style);
self
}
pub fn track_style(mut self, style: Style) -> Self {
self.track_style = Some(style);
self
}
}
#[derive(Clone, Debug, PartialEq)]
pub struct FileIconPalette {
pub azure: Color,
pub blue: Color,
pub cyan: Color,
pub green: Color,
pub grey: Color,
pub orange: Color,
pub purple: Color,
pub red: Color,
pub yellow: Color,
}
impl Default for FileIconPalette {
fn default() -> Self {
Self {
azure: Color::hex_u24(0x61AFEF), blue: Color::hex_u24(0x4175E6), cyan: Color::hex_u24(0x56B6C2), green: Color::hex_u24(0x98C379), grey: Color::hex_u24(0xABB2BF), orange: Color::hex_u24(0xD19A66), purple: Color::hex_u24(0xC678DD), red: Color::hex_u24(0xE06C75), yellow: Color::hex_u24(0xE5C07B), }
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct GitStatusPalette {
pub modified: Color,
pub added: Color,
pub deleted: Color,
pub renamed: Color,
pub untracked: Color,
pub conflicted: Color,
}
impl Default for GitStatusPalette {
fn default() -> Self {
Self {
modified: Color::hex_u24(0xE5B767),
added: Color::hex_u24(0x7EC699),
deleted: Color::hex_u24(0xE57E7E),
renamed: Color::hex_u24(0x76C5E5),
untracked: Color::hex_u24(0xC59AE5),
conflicted: Color::hex_u24(0xE57E7E),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct ScrollbarPalette {
pub track: Option<Color>,
pub thumb: Color,
pub thumb_focus: Option<Color>,
}
impl Default for ScrollbarPalette {
fn default() -> Self {
Self {
track: None,
thumb: Color::DarkGray,
thumb_focus: Some(Color::Gray),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq)]
pub struct SplitterPalette {
pub hover: Color,
pub active: Color,
}
impl Default for SplitterPalette {
fn default() -> Self {
Self {
hover: Color::hex_u24(0x2563EB),
active: Color::hex_u24(0x22D3EE),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct SurfacePalette {
pub panel: Color,
pub element: Color,
pub menu: Color,
pub backdrop: Color,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct StatusPalette {
pub success: Color,
pub warning: Color,
pub error: Color,
pub info: Color,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct DiffPalette {
pub context: Style,
pub added: Style,
pub removed: Style,
pub empty: Style,
pub added_word: Style,
pub removed_word: Style,
pub added_marker: Style,
pub removed_marker: Style,
pub context_line_number: Style,
pub added_line_number: Style,
pub removed_line_number: Style,
pub context_separator_style: Style,
pub patch_header: Style,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct DocumentPalette {
pub heading_styles: [Style; 6],
pub code_inline: Style,
pub code_block: Style,
pub emphasis: Style,
pub strong: Style,
pub strikethrough: Style,
pub link: Style,
pub blockquote_bar: Style,
pub table_border: Style,
pub table_header: Style,
pub hr: Style,
pub list_item: Style,
pub list_enumeration: Style,
pub diagram_node_fill_style: Style,
pub diagram_node_border_style: Style,
pub diagram_node_label_style: Style,
pub diagram_edge_style: Style,
pub diagram_muted_style: Style,
}
impl Default for DocumentPalette {
fn default() -> Self {
Self {
heading_styles: [
Style::new().bold().fg(Color::LightBlue),
Style::new().bold().fg(Color::LightBlue),
Style::new().bold().fg(Color::LightBlue),
Style::new().bold(),
Style::new().bold(),
Style::new().bold().dim(),
],
code_inline: Style::new().fg(Color::Green),
code_block: Style::default(),
emphasis: Style::new().italic(),
strong: Style::new().bold(),
strikethrough: Style::new().strikethrough(),
link: Style::new().fg(Color::LightBlue).underline(),
blockquote_bar: Style::new().fg(Color::DarkGray).dim(),
table_border: Style::new().fg(Color::DarkGray).dim(),
table_header: Style::new().bold(),
hr: Style::new().fg(Color::DarkGray).dim(),
list_item: Style::new().fg(Color::LightBlue).bold(),
list_enumeration: Style::new().fg(Color::LightBlue).bold(),
diagram_node_fill_style: Style::default(),
diagram_node_border_style: Style::default(),
diagram_node_label_style: Style::default(),
diagram_edge_style: Style::default(),
diagram_muted_style: Style::default(),
}
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
pub struct SyntaxPalette {
pub comment: Style,
pub keyword: Style,
pub string: Style,
pub number: Style,
pub constant: Style,
pub function: Style,
pub builtin: Style,
pub type_name: Style,
pub variable: Style,
pub parameter: Style,
pub operator: Style,
}
impl Default for SyntaxPalette {
fn default() -> Self {
let number = Style::new().fg(Color::Yellow);
let function = Style::new().fg(Color::Cyan);
let variable = Style::new().fg(Color::White);
Self {
comment: Style::new().fg(Color::DarkGray).italic().dim(),
keyword: Style::new().fg(Color::LightMagenta),
string: Style::new().fg(Color::Green),
number,
constant: number.lighten_by(0.12),
function,
builtin: function.italic(),
type_name: Style::new().fg(Color::LightBlue),
variable,
parameter: variable.italic(),
operator: Style::new().fg(Color::LightRed),
}
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct InputPalette {
pub focus: Style,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct TextAreaPalette {
pub focus: Style,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct DocumentViewPalette {
pub focus: Style,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct HexAreaPalette {
pub focus: Style,
pub cursor: Style,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Hash)]
pub struct TerminalPalette {
pub focus: Style,
}
#[derive(Clone, Debug, PartialEq)]
pub struct Theme {
pub primary: Style,
pub accent: Style,
pub selection: Style,
pub text_selection: Style,
pub focus: Style,
pub hover: Style,
pub border: Style,
pub muted: Style,
pub surface: SurfacePalette,
pub status: StatusPalette,
pub border_active: Color,
pub file_icons: FileIconPalette,
pub git_status: GitStatusPalette,
pub diff: DiffPalette,
pub document: DocumentPalette,
pub syntax: SyntaxPalette,
pub input: InputPalette,
pub text_area: TextAreaPalette,
pub document_view: DocumentViewPalette,
pub hex_area: HexAreaPalette,
pub terminal: TerminalPalette,
pub scrollbar: ScrollbarPalette,
pub splitter: SplitterPalette,
#[doc(hidden)]
pub extensions: ThemeExtensions,
}
impl Theme {
pub fn from_host_colors(colors: HostTerminalColors) -> Self {
ThemePalette::new(colors.fg, colors.bg, colors.ansi[4])
.success(colors.ansi[2])
.warning(colors.ansi[3])
.error(colors.ansi[1])
.info(colors.ansi[4])
.into_theme()
}
pub fn role(&self, role: ThemeRole) -> Style {
match role {
ThemeRole::Base => self.primary,
ThemeRole::Accent => {
let mut style = self.accent;
if style.fg.is_none() {
style.fg = self.primary.fg;
}
if style.fg_transform.is_none() {
style.fg_transform = self.primary.fg_transform;
}
style
}
ThemeRole::Selection | ThemeRole::UnfocusedSelection => self.selection,
ThemeRole::TextSelection => self.text_selection,
ThemeRole::Hover
| ThemeRole::DragSource
| ThemeRole::DropTarget
| ThemeRole::DropTargetActive
| ThemeRole::ItemHover => self.hover,
ThemeRole::Focus => self.focus,
ThemeRole::Active => self.selection,
ThemeRole::Border => self.primary.patch(self.border),
ThemeRole::Disabled | ThemeRole::Muted => self.primary.patch(self.muted),
ThemeRole::Error => Style::new().fg(self.status.error),
ThemeRole::InputFocusContent => self.input.focus,
ThemeRole::TextAreaFocusContent => self.text_area.focus,
ThemeRole::DocumentViewFocusContent => self.document_view.focus,
ThemeRole::HexAreaFocusContent => self.hex_area.focus,
ThemeRole::HexAreaCursor => self.hex_area.cursor,
ThemeRole::TerminalFocusContent => self.terminal.focus,
ThemeRole::ScrollbarThumb => Style::new().bg(self.scrollbar.thumb),
ThemeRole::ScrollbarThumbFocus => self
.scrollbar
.thumb_focus
.map(|color| Style::new().bg(color))
.unwrap_or_default(),
ThemeRole::ScrollbarTrack => self
.scrollbar
.track
.map(|color| Style::new().bg(color))
.unwrap_or_default(),
ThemeRole::SplitterHover => Style::new().fg(self.splitter.hover),
ThemeRole::SplitterActive => Style::new().fg(self.splitter.active),
}
}
pub fn custom(primary_fg: Color, primary_bg: Color, accent: Color) -> Self {
let success = Color::Green;
let warning = Color::Yellow;
let error = Color::Red;
let info = accent;
let muted = primary_fg.blend_toward(primary_bg, 0.42);
let border_active = accent.lighten_by(0.08);
Self {
primary: Style::new().fg(primary_fg).bg(primary_bg),
accent: Style::new().fg(accent),
selection: Style::new()
.fg(accent)
.bg(primary_bg.blend_toward(accent, 0.22)),
text_selection: Style::new()
.fg(accent)
.bg(primary_bg.blend_toward(accent, 0.22)),
focus: Style::new().fg(border_active),
hover: Style::default(),
border: Style::new().fg(primary_fg.blend_toward(primary_bg, 0.40)),
muted: Style::new().fg(muted),
surface: SurfacePalette {
panel: primary_bg.elevate(0.07),
element: primary_bg.elevate(0.04),
menu: primary_bg.elevate(0.12),
backdrop: primary_bg,
},
status: StatusPalette {
success,
warning,
error,
info,
},
border_active,
file_icons: FileIconPalette::default(),
git_status: GitStatusPalette::default(),
diff: DiffPalette {
context: Style::default(),
added: Style::new().bg(primary_bg.blend_toward(success, 0.14)),
removed: Style::new().bg(primary_bg.blend_toward(error, 0.16)),
empty: Style::new().dim(),
added_word: Style::new().bg(primary_bg.blend_toward(success, 0.24)),
removed_word: Style::new().bg(primary_bg.blend_toward(error, 0.28)),
added_marker: Style::new().fg(success),
removed_marker: Style::new().fg(error),
context_line_number: Style::new().fg(primary_fg.blend_toward(primary_bg, 0.50)),
added_line_number: Style::default(),
removed_line_number: Style::default(),
context_separator_style: Style::new()
.fg(primary_fg.blend_toward(primary_bg, 0.40))
.dim(),
patch_header: Style::new()
.fg(accent.blend_toward(primary_fg, 0.35))
.bold(),
},
document: DocumentPalette {
heading_styles: [
Style::new().bold().fg(accent.lighten_by(0.20)),
Style::new().bold().fg(accent.lighten_by(0.12)),
Style::new().bold().fg(accent),
Style::new().bold().fg(primary_fg),
Style::new().bold().fg(primary_fg),
Style::new().bold().fg(primary_fg).dim(),
],
code_inline: Style::new().fg(success),
code_block: Style::default(),
emphasis: Style::new().italic(),
strong: Style::new().bold(),
strikethrough: Style::new().strikethrough(),
link: Style::new().fg(accent).underline(),
blockquote_bar: Style::new().fg(muted).dim(),
table_border: Style::new()
.fg(primary_fg.blend_toward(primary_bg, 0.40))
.dim(),
table_header: Style::new().bold(),
hr: Style::new()
.fg(primary_fg.blend_toward(primary_bg, 0.40))
.dim(),
list_item: Style::new().fg(accent).bold(),
list_enumeration: Style::new().fg(accent).bold(),
diagram_node_fill_style: Style::new().bg(primary_bg.blend_toward(accent, 0.10)),
diagram_node_border_style: Style::new().fg(accent.lighten_by(0.08)),
diagram_node_label_style: Style::new().fg(primary_fg),
diagram_edge_style: Style::new().fg(accent.blend_toward(primary_fg, 0.20)),
diagram_muted_style: Style::new().fg(muted).dim(),
},
syntax: SyntaxPalette {
comment: Style::new().fg(muted).italic().dim(),
keyword: Style::new().fg(accent),
string: Style::new().fg(accent.blend_toward(success, 0.55)),
number: Style::new().fg(accent.blend_toward(Color::Yellow, 0.60)),
constant: Style::new()
.fg(accent.blend_toward(Color::Yellow, 0.52).lighten_by(0.10)),
function: Style::new().fg(info.blend_toward(accent, 0.12)),
builtin: Style::new().fg(info.blend_toward(accent, 0.28)).italic(),
type_name: Style::new().fg(accent.blend_toward(info, 0.32)),
variable: Style::new().fg(primary_fg),
parameter: Style::new()
.fg(primary_fg.blend_toward(accent, 0.12))
.italic(),
operator: Style::new().fg(accent.blend_toward(error, 0.45)),
},
input: InputPalette::default(),
text_area: TextAreaPalette::default(),
document_view: DocumentViewPalette::default(),
hex_area: HexAreaPalette {
focus: Style::default(),
cursor: Style::new().fg(accent),
},
terminal: TerminalPalette::default(),
scrollbar: ScrollbarPalette {
track: Some(primary_bg.elevate(0.05)),
thumb: primary_bg.elevate(0.20),
thumb_focus: Some(accent.lighten_by(0.08)),
},
splitter: SplitterPalette {
hover: accent.lighten_by(0.08),
active: accent.lighten_by(0.18),
},
extensions: ThemeExtensions::default(),
}
}
pub fn primary(mut self, style: Style) -> Self {
self.primary = style;
self
}
pub fn accent(mut self, style: Style) -> Self {
self.accent = style;
self
}
pub fn focus(mut self, style: Style) -> Self {
self.focus = style;
self
}
pub fn with_extension<T>(mut self, extension: T) -> Self
where
T: ThemeExtension,
{
self.extensions.insert(extension);
self
}
pub fn without_extension<T>(mut self) -> Self
where
T: ThemeExtension,
{
self.extensions.remove::<T>();
self
}
pub fn extension<T>(&self) -> Option<&T>
where
T: ThemeExtension,
{
self.extensions.get::<T>()
}
pub fn extension_cloned<T>(&self) -> Option<T>
where
T: ThemeExtension,
{
self.extension::<T>().cloned()
}
pub fn selection(mut self, style: Style) -> Self {
self.selection = style;
self
}
pub fn text_selection(mut self, style: Style) -> Self {
self.text_selection = style;
self
}
pub fn hover(mut self, style: Style) -> Self {
self.hover = style;
self
}
pub fn border(mut self, style: Style) -> Self {
self.border = style;
self
}
pub fn muted(mut self, style: Style) -> Self {
self.muted = style;
self
}
pub fn scrollbar(mut self, palette: ScrollbarPalette) -> Self {
self.scrollbar = palette;
self
}
pub fn splitter(mut self, palette: SplitterPalette) -> Self {
self.splitter = palette;
self
}
pub fn file_icons(mut self, palette: FileIconPalette) -> Self {
self.file_icons = palette;
self
}
pub fn git_status(mut self, palette: GitStatusPalette) -> Self {
self.git_status = palette;
self
}
pub fn diff(mut self, palette: DiffPalette) -> Self {
self.diff = palette;
self
}
pub fn document(mut self, palette: DocumentPalette) -> Self {
self.document = palette;
self
}
pub fn syntax(mut self, palette: SyntaxPalette) -> Self {
self.syntax = palette;
self
}
pub fn input(mut self, palette: InputPalette) -> Self {
self.input = palette;
self
}
pub fn text_area(mut self, palette: TextAreaPalette) -> Self {
self.text_area = palette;
self
}
pub fn document_view(mut self, palette: DocumentViewPalette) -> Self {
self.document_view = palette;
self
}
pub fn hex_area(mut self, palette: HexAreaPalette) -> Self {
self.hex_area = palette;
self
}
pub fn terminal(mut self, palette: TerminalPalette) -> Self {
self.terminal = palette;
self
}
}
#[derive(Clone, Debug)]
pub struct ThemePalette {
pub text: Color,
pub background: Color,
pub accent: Color,
pub selection: Option<Color>,
pub text_selection: Option<Color>,
pub border: Option<Color>,
pub muted: Option<Color>,
pub scrollbar: Option<Color>,
pub success: Option<Color>,
pub warning: Option<Color>,
pub error: Option<Color>,
pub info: Option<Color>,
}
impl ThemePalette {
pub fn new(text: Color, background: Color, accent: Color) -> Self {
Self {
text,
background,
accent,
selection: None,
text_selection: None,
border: None,
muted: None,
scrollbar: None,
success: None,
warning: None,
error: None,
info: None,
}
}
pub fn selection(mut self, color: Color) -> Self {
self.selection = Some(color);
self
}
pub fn text_selection(mut self, color: Color) -> Self {
self.text_selection = Some(color);
self
}
pub fn border(mut self, color: Color) -> Self {
self.border = Some(color);
self
}
pub fn muted(mut self, color: Color) -> Self {
self.muted = Some(color);
self
}
pub fn scrollbar(mut self, color: Color) -> Self {
self.scrollbar = Some(color);
self
}
pub fn success(mut self, color: Color) -> Self {
self.success = Some(color);
self
}
pub fn warning(mut self, color: Color) -> Self {
self.warning = Some(color);
self
}
pub fn error(mut self, color: Color) -> Self {
self.error = Some(color);
self
}
pub fn info(mut self, color: Color) -> Self {
self.info = Some(color);
self
}
pub fn into_theme(self) -> Theme {
Theme::from(self)
}
}
impl From<ThemePalette> for Theme {
fn from(p: ThemePalette) -> Self {
let border_color = p
.border
.unwrap_or_else(|| p.text.blend_toward(p.background, 0.40));
let muted_color = p
.muted
.unwrap_or_else(|| p.text.blend_toward(p.background, 0.42));
let scrollbar_thumb = p.scrollbar.unwrap_or_else(|| p.background.elevate(0.20));
let success = p.success.unwrap_or(Color::hex_u24(0x34D399));
let warning = p.warning.unwrap_or(Color::hex_u24(0xFBBF24));
let error = p.error.unwrap_or(Color::hex_u24(0xF43F5E));
let info = p.info.unwrap_or(p.accent);
let border_active = p.border.unwrap_or(p.accent).lighten_by(0.08);
let selection = p.selection.unwrap_or(p.accent);
let text_selection = p.text_selection.unwrap_or(p.accent);
Theme {
primary: Style::new().fg(p.text).bg(p.background),
accent: Style::new().fg(p.accent),
selection: Style::new()
.fg(selection)
.bg(p.background.blend_toward(selection, 0.22)),
text_selection: Style::new()
.fg(text_selection)
.bg(p.background.blend_toward(text_selection, 0.22)),
focus: Style::new().fg(border_active),
hover: Style::default(),
border: Style::new().fg(border_color),
muted: Style::new().fg(muted_color),
surface: SurfacePalette {
panel: p.background.elevate(0.07),
element: p.background.elevate(0.04),
menu: p.background.elevate(0.12),
backdrop: p.background,
},
status: StatusPalette {
success,
warning,
error,
info,
},
border_active,
file_icons: FileIconPalette {
green: success,
red: error,
yellow: warning,
azure: info,
blue: p.accent,
cyan: info.lighten_by(0.10),
grey: muted_color,
orange: warning.blend_toward(error, 0.40),
purple: p.accent.blend_toward(error, 0.30),
},
git_status: GitStatusPalette {
modified: warning,
added: success,
deleted: error,
renamed: info,
untracked: p.accent.blend_toward(error, 0.30),
conflicted: error,
},
diff: DiffPalette {
context: Style::default(),
added: Style::new().bg(p.background.blend_toward(success, 0.14)),
removed: Style::new().bg(p.background.blend_toward(error, 0.16)),
empty: Style::new().dim(),
added_word: Style::new().bg(p.background.blend_toward(success, 0.24)),
removed_word: Style::new().bg(p.background.blend_toward(error, 0.28)),
added_marker: Style::new().fg(success),
removed_marker: Style::new().fg(error),
context_line_number: Style::new().fg(p.text.blend_toward(p.background, 0.50)),
added_line_number: Style::default(),
removed_line_number: Style::default(),
context_separator_style: Style::new()
.fg(p.text.blend_toward(p.background, 0.40))
.dim(),
patch_header: Style::new().fg(p.accent.blend_toward(p.text, 0.25)).bold(),
},
document: DocumentPalette {
heading_styles: [
Style::new().bold().fg(p.accent.lighten_by(0.20)),
Style::new().bold().fg(p.accent.lighten_by(0.12)),
Style::new().bold().fg(p.accent),
Style::new().bold().fg(p.text),
Style::new().bold().fg(p.text),
Style::new().bold().fg(p.text).dim(),
],
code_inline: Style::new().fg(success),
code_block: Style::default(),
emphasis: Style::new().italic(),
strong: Style::new().bold(),
strikethrough: Style::new().strikethrough(),
link: Style::new().fg(p.accent).underline(),
blockquote_bar: Style::new().fg(muted_color).dim(),
table_border: Style::new().fg(border_color).dim(),
table_header: Style::new().bold(),
hr: Style::new().fg(border_color).dim(),
list_item: Style::new().fg(p.accent).bold(),
list_enumeration: Style::new().fg(p.accent).bold(),
diagram_node_fill_style: Style::new().bg(p.background.blend_toward(p.accent, 0.10)),
diagram_node_border_style: Style::new().fg(p.accent.lighten_by(0.08)),
diagram_node_label_style: Style::new().fg(p.text),
diagram_edge_style: Style::new().fg(p.accent.blend_toward(p.text, 0.20)),
diagram_muted_style: Style::new().fg(muted_color).dim(),
},
syntax: SyntaxPalette {
comment: Style::new().fg(muted_color).italic().dim(),
keyword: Style::new().fg(p.accent),
string: Style::new().fg(success.blend_toward(p.accent, 0.15)),
number: Style::new().fg(warning.blend_toward(p.accent, 0.20)),
constant: Style::new().fg(warning.blend_toward(p.text, 0.18)),
function: Style::new().fg(info.blend_toward(p.accent, 0.10)),
builtin: Style::new()
.fg(info.blend_toward(muted_color, 0.22))
.italic(),
type_name: Style::new().fg(p.accent.blend_toward(info, 0.35)),
variable: Style::new().fg(p.text),
parameter: Style::new().fg(p.text).italic(),
operator: Style::new().fg(error.blend_toward(p.accent, 0.45)),
},
input: InputPalette::default(),
text_area: TextAreaPalette::default(),
document_view: DocumentViewPalette::default(),
hex_area: HexAreaPalette {
focus: Style::default(),
cursor: Style::new().fg(p.accent),
},
terminal: TerminalPalette::default(),
scrollbar: ScrollbarPalette {
track: Some(p.background.elevate(0.05)),
thumb: scrollbar_thumb,
thumb_focus: Some(p.accent.lighten_by(0.08)),
},
splitter: SplitterPalette {
hover: p.accent.lighten_by(0.08),
active: p.accent.lighten_by(0.18),
},
extensions: ThemeExtensions::default(),
}
}
}
impl Default for Theme {
fn default() -> Self {
let mut theme: Self = ThemePalette::new(
Color::hex_u24(0xE2E8F0),
Color::hex_u24(0x0B121F),
Color::hex_u24(0x7DCFFF),
)
.success(Color::hex_u24(0x34D399))
.warning(Color::hex_u24(0xFBBF24))
.error(Color::hex_u24(0xF43F5E))
.info(Color::hex_u24(0x38BDF8))
.into();
theme.file_icons = FileIconPalette {
azure: Color::hex_u24(0x7DCFFF),
blue: Color::hex_u24(0x60A5FA),
cyan: Color::hex_u24(0x2DD4BF),
green: Color::hex_u24(0x4ADE80),
grey: Color::hex_u24(0x94A3B8),
orange: Color::hex_u24(0xFB923C),
purple: Color::hex_u24(0xC4B5FD),
red: Color::hex_u24(0xF87171),
yellow: Color::hex_u24(0xFBBF24),
};
theme.git_status = GitStatusPalette {
modified: Color::hex_u24(0xFBBF24),
added: Color::hex_u24(0x34D399),
deleted: Color::hex_u24(0xFB7171),
renamed: Color::hex_u24(0x38BDF8),
untracked: Color::hex_u24(0xA78BFA),
conflicted: Color::hex_u24(0xF43F5E),
};
theme
}
}