Initial public release of Warp.

Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
David Stern
2026-04-28 08:43:33 -05:00
commit 0dbd3d567a
4982 changed files with 1431549 additions and 0 deletions
+338
View File
@@ -0,0 +1,338 @@
use warpui::{
fonts::{FamilyId, Weight},
Entity, ModelContext, SingletonEntity,
};
use super::{builder::UiBuilder, theme::WarpTheme};
/// The standard font size to use for headers (e.g.: in dialogs).
const HEADER_FONT_SIZE: f32 = 18.;
const OVERLINE_FONT_SIZE: f32 = 10.;
pub const DEFAULT_UI_FONT_SIZE: f32 = 12.0;
pub const DEFAULT_COMMAND_PALETTE_FONT_SIZE: f32 = 14.0;
/// Holds visual settings that are so widely used that it's best
/// to invalidate all views when they change rather than forcing views
/// to individually listen for changes. The most prominent examples are
/// settings related to themes and fonts.
pub struct Appearance {
theme: WarpTheme,
monospace_font_family: FamilyId,
monospace_font_size: f32,
monospace_font_weight: Weight,
line_height_ratio: f32,
ui_builder: UiBuilder,
// We cache the family id for the ui font - note that this
// isn't actually a changeable setting right now.
ui_font_family: FamilyId,
ai_font_family: FamilyId,
/// A font that is used for password fields.
password_font_family: FamilyId,
}
/// Defines appearance change events.
///
/// For any properties that are read from appearance (e.g.: theme, font, etc.),
/// users should listen for these events rather than directly listenting to
/// settings change events for the underlying properties.
///
/// NOTE: You do NOT need to set up subscriptions for these events and use them
/// to invalidate views! All views are automatically invalidated on changes to
/// fields in [`Appearance`]. If you appear to need to subscribe to one of
/// these events and call `ctx.notify()` for proper behavior, there is probably
/// a bug in [`Appearance`].
#[derive(Debug)]
pub enum AppearanceEvent {
ThemeChanged,
UiFontFamilyChanged {
previous_family_id: FamilyId,
current_family_id: FamilyId,
},
MonospaceFontSizeChanged {
previous_font_size: f32,
current_font_size: f32,
},
MonospaceFontFamilyChanged {
previous_family_id: FamilyId,
current_family_id: FamilyId,
},
MonospaceFontWeightChanged {
previous_font_weight: Weight,
current_font_weight: Weight,
},
LineHeightRatioChanged {
previous_line_height_ratio: f32,
current_line_height_ratio: f32,
},
}
impl Appearance {
#[allow(clippy::too_many_arguments)]
pub fn new(
theme: WarpTheme,
monospace_font_family: FamilyId,
monospace_font_size: f32,
monospace_font_weight: Weight,
ui_font_family: FamilyId,
line_height_ratio: f32,
ai_font_family: FamilyId,
password_font_family: FamilyId,
) -> Self {
Self {
theme: theme.clone(),
monospace_font_family,
monospace_font_size,
monospace_font_weight,
ui_font_family,
line_height_ratio,
ui_builder: UiBuilder::new(
theme,
ui_font_family,
DEFAULT_UI_FONT_SIZE,
DEFAULT_COMMAND_PALETTE_FONT_SIZE,
line_height_ratio,
),
ai_font_family,
password_font_family,
}
}
#[cfg(feature = "test-util")]
pub fn mock() -> Self {
use warpui::color::ColorU;
use crate::ui::theme::{mock_terminal_colors, Details, Fill};
let mock_theme = WarpTheme::new(
Fill::Solid(ColorU::from_u32(0x000000ff)),
ColorU::from_u32(0xffffffff),
Fill::Solid(ColorU::new(18, 123, 156, 255)),
None,
Some(Details::Darker),
mock_terminal_colors(),
None,
Some("Dark".to_string()),
);
let line_height_ratio = 1.4;
let ui_font_family = FamilyId(1);
Self {
theme: mock_theme.clone(),
monospace_font_family: FamilyId(0),
monospace_font_size: 13.,
monospace_font_weight: Weight::Normal,
line_height_ratio,
ui_builder: UiBuilder::new(
mock_theme,
ui_font_family,
DEFAULT_UI_FONT_SIZE,
DEFAULT_COMMAND_PALETTE_FONT_SIZE,
line_height_ratio,
),
ui_font_family,
ai_font_family: FamilyId(0),
password_font_family: FamilyId(0),
}
}
pub fn set_theme(&mut self, new_theme: WarpTheme, ctx: &mut ModelContext<Self>) {
self.theme = new_theme;
self.ui_builder = UiBuilder::new(
self.theme.clone(),
self.ui_font_family,
self.ui_font_size(),
DEFAULT_COMMAND_PALETTE_FONT_SIZE,
self.line_height_ratio,
);
// Request a redraw of all windows.
ctx.invalidate_all_views();
// Allow listeners who specifically care about theme changes to know the theme has changed.
ctx.emit(AppearanceEvent::ThemeChanged);
// Notify listeners that appearance-related configuration
// has changed.
ctx.notify();
}
pub fn set_monospace_font_family(
&mut self,
new_family: FamilyId,
ctx: &mut ModelContext<Self>,
) {
let previous_family_id = self.monospace_font_family;
self.monospace_font_family = new_family;
// Request a redraw of all windows.
ctx.invalidate_all_views();
ctx.emit(AppearanceEvent::MonospaceFontFamilyChanged {
previous_family_id,
current_family_id: new_family,
});
}
pub fn set_ui_font_family(&mut self, new_family: FamilyId, ctx: &mut ModelContext<Self>) {
let previous_family_id = self.ui_font_family;
self.ui_font_family = new_family;
self.ui_builder = UiBuilder::new(
self.theme.clone(),
self.ui_font_family,
self.ui_font_size(),
DEFAULT_COMMAND_PALETTE_FONT_SIZE,
self.line_height_ratio,
);
// Request a redraw of all windows.
ctx.invalidate_all_views();
// We fire the same event as monospace font family change - performance is likely not going to be an issue.
ctx.emit(AppearanceEvent::UiFontFamilyChanged {
previous_family_id,
current_family_id: new_family,
});
}
pub fn set_ai_font_family(&mut self, new_family: FamilyId, ctx: &mut ModelContext<Self>) {
let previous_family_id = self.ai_font_family;
self.ai_font_family = new_family;
// Request a redraw of all windows.
ctx.invalidate_all_views();
// We fire the same event as monospace font family change - performance is likely not going to be an issue.
ctx.emit(AppearanceEvent::MonospaceFontFamilyChanged {
previous_family_id,
current_family_id: new_family,
});
}
pub fn set_monospace_font_size(&mut self, new_font_size: f32, ctx: &mut ModelContext<Self>) {
let previous_font_size = self.monospace_font_size;
self.monospace_font_size = new_font_size;
// Request a redraw of all windows.
ctx.invalidate_all_views();
ctx.emit(AppearanceEvent::MonospaceFontSizeChanged {
current_font_size: self.monospace_font_size,
previous_font_size,
});
}
pub fn set_monospace_font_weight(
&mut self,
new_font_weight: Weight,
ctx: &mut ModelContext<Self>,
) {
let previous_font_weight = self.monospace_font_weight;
self.monospace_font_weight = new_font_weight;
// Request a redraw of all windows.
ctx.invalidate_all_views();
ctx.emit(AppearanceEvent::MonospaceFontWeightChanged {
current_font_weight: self.monospace_font_weight,
previous_font_weight,
});
}
#[cfg(feature = "test-util")]
pub fn set_monospace_font_size_test(&mut self, new_font_size: f32) {
self.monospace_font_size = new_font_size;
}
pub fn set_line_height_ratio(
&mut self,
new_line_height_ratio: f32,
ctx: &mut ModelContext<Self>,
) {
let previous_line_height_ratio = self.line_height_ratio;
self.line_height_ratio = new_line_height_ratio;
self.ui_builder = UiBuilder::new(
self.theme.clone(),
self.ui_font_family,
DEFAULT_UI_FONT_SIZE,
DEFAULT_COMMAND_PALETTE_FONT_SIZE,
self.line_height_ratio,
);
// Request a redraw of all windows.
ctx.invalidate_all_views();
ctx.emit(AppearanceEvent::LineHeightRatioChanged {
current_line_height_ratio: self.line_height_ratio,
previous_line_height_ratio,
});
}
pub fn ui_builder(&self) -> &UiBuilder {
&self.ui_builder
}
pub fn theme(&self) -> &WarpTheme {
&self.theme
}
pub fn monospace_font_family(&self) -> FamilyId {
self.monospace_font_family
}
pub fn ai_font_family(&self) -> FamilyId {
self.ai_font_family
}
pub fn monospace_font_size(&self) -> f32 {
self.monospace_font_size
}
pub fn monospace_ui_scalar(&self) -> f32 {
self.monospace_font_size / DEFAULT_UI_FONT_SIZE
}
pub fn monospace_font_weight(&self) -> Weight {
self.monospace_font_weight
}
pub fn ui_font_family(&self) -> FamilyId {
self.ui_font_family
}
pub fn ui_font_size(&self) -> f32 {
DEFAULT_UI_FONT_SIZE
}
pub fn header_font_family(&self) -> FamilyId {
self.ui_font_family
}
pub fn header_font_size(&self) -> f32 {
HEADER_FONT_SIZE
}
pub fn overline_font_family(&self) -> FamilyId {
self.ui_font_family
}
pub fn overline_font_size(&self) -> f32 {
OVERLINE_FONT_SIZE
}
pub fn line_height_ratio(&self) -> f32 {
self.line_height_ratio
}
pub fn password_font_family(&self) -> FamilyId {
self.password_font_family
}
}
impl Entity for Appearance {
type Event = AppearanceEvent;
}
impl SingletonEntity for Appearance {}
File diff suppressed because it is too large Load Diff
+48
View File
@@ -0,0 +1,48 @@
use warpui::color::ColorU;
pub trait Blend<Rhs = Self> {
type Output;
fn blend(&self, rhs: &Rhs) -> Self::Output;
}
impl Blend for ColorU {
type Output = ColorU;
/// Color blending computation.
/// This function calculates the color, assuming that "self" is a background, and "other" is
/// a new color on top.
/// It simply calculates a weighted sum of each channel, and averages out the opacity.
/// Note that due to rounding errors, the result of computation maybe slightly different than
/// what comes out of figma (ie. 181818 instead of 191918) - differences shouldn't be
/// noticeable though, due to nature of the rounding error.
fn blend(&self, other: &ColorU) -> ColorU {
// Helper function that computes a weighted sum using the overlay color's opacity as weight.
fn add_channels(c1: u8, c2: u8, ratio: f32) -> u8 {
((c1 as f32 * (1. - ratio)) + (c2 as f32 * ratio)) as u8
}
// background not visible, lets return other
if self.is_fully_transparent() || other.a == super::OPAQUE {
return *other;
}
// other not visible, self it is.
if other.is_fully_transparent() {
return *self;
}
// alpha value for new color, opaque if background is opaque already, otherwise simple avg
let alpha = if self.is_opaque() {
super::OPAQUE
} else {
// doing type conversion, since adding two arbitrary alphas may result in u8 overflow
((self.a as f32 + other.a as f32) / 2.) as u8
};
// basically overlay color's opacity expressed as %, rounded to 2 digits after decimal
let ratio = ((other.a as f32 / 255.) * 100.).ceil() / 100.;
ColorU::new(
add_channels(self.r, other.r, ratio),
add_channels(self.g, other.g, ratio),
add_channels(self.b, other.b, ratio),
alpha,
)
}
}
@@ -0,0 +1,38 @@
use super::*;
#[test]
fn coloru_with_opacity_test() {
assert_eq!(
coloru_with_opacity(ColorU::from_u32(0x000000ff), 10),
ColorU::new(0, 0, 0, 25)
);
assert_eq!(
coloru_with_opacity(ColorU::from_u32(0x000000ff), 0),
ColorU::new(0, 0, 0, 0)
);
assert_eq!(
coloru_with_opacity(ColorU::from_u32(0x000000ff), 100),
ColorU::new(0, 0, 0, OPAQUE)
);
}
#[test]
fn darker_lighter_test() {
assert_eq!(
darken(ColorU::new(255, 128, 0, OPAQUE)),
ColorU::new(123, 62, 0, OPAQUE)
);
assert_eq!(
lighten(ColorU::new(255, 128, 0, OPAQUE)),
ColorU::new(255, 192, 128, OPAQUE)
);
}
#[test]
fn pick_foreground_test() {
assert_eq!(ColorU::white(), pick_foreground_color(ColorU::black()));
assert_eq!(ColorU::black(), pick_foreground_color(ColorU::white()));
assert_eq!(
ColorU::white(),
pick_foreground_color(ColorU::new(100, 100, 100, OPAQUE))
);
}
+183
View File
@@ -0,0 +1,183 @@
use warpui::color::ColorU;
use super::{blend::Blend, coloru_with_opacity, Rgb};
/// Offset to the relative luminance when computing the contrast ratio per the formula defined in
/// the [W3C Spec](https://www.w3.org/TR/WCAG20-TECHS/G17.html). This offset is included to
/// compensate for contrast ratios that occur when a value is at or near zero, and for ambient light
/// effects. See <https://juicystudio.com/article/luminositycontrastratioalgorithm.php> for more
/// details.
const LUMINANCE_OFFSET_FOR_CONTRAST_RATIO: f32 = 0.05;
/// Returns a new foreground color that when rendered against `background_color` would have a
/// contrast of at least `minimum_allowed_contrast`. NOTE the `background_color` must be fully
/// opaque in in order to perform proper contrast checking.
///
/// If `foreground_color` already meets the minimum contrast, it is returned unchanged.
///
/// Color shifting is performed by computing the color that would produce the max contrast against
/// the `background_color` and then binary searching across all opacities to find an opacity that
/// would produce a color with at least the `minimum_allowed_contrast` when blended with the
/// `foreground_color`.
///
/// This is _heavily_ inspired by Chromium's approach to color shifting. See
/// <https://source.chromium.org/chromium/chromium/src/+/main:ui/gfx/color_utils.cc;l=634;drc=9f7b5c10efd74425f135fd5aad2076a7cc78607a>.
pub fn foreground_color_with_minimum_contrast(
foreground_color: ColorU,
background_color: Rgb,
minimum_allowed_contrast: MinimumAllowedContrast,
) -> ColorU {
// Convert the `RGB` into a fully opaque `ColorU` so that we can use existing blending functions
// that rely on `ColorU`s.
let background_color = ColorU::from(background_color);
let foreground_color = background_color.blend(&foreground_color);
if high_enough_contrast(foreground_color, background_color, minimum_allowed_contrast) {
return foreground_color;
}
// Determine the color that would have the maximum contrast against the background. Contrast
// is determined by the formula C = (L1 + 0.05) /(L2 + 0.05) where L1 is the relative luminance
// of the lighter color and L2 is the relative luminance of the darker color. Since black has a
// luminance of 0, and white has a luminance of 1, we know that white or black must produce the
// color with most contrast against the background. In other words, if the background is
// "light", then a luminance of 1 (black) in the denominator would produce the maximum possible
// contrast. Alternately, if the background is a "dark" color, then a value of 0 (white) in the
// numerator would produce the maximum possible contrast.
let color_with_max_contrast =
pick_constrasting_color(background_color, ColorU::white(), ColorU::black());
// Perform binary search across all possible opacities (0,100) to find the best color that meets
// the minimum allowed contrast. The returned color is computed by blending the current alpha
// with the target foreground color and foreground color.
let mut low_opacity = 0;
let mut high_opacity = 101;
let mut best_color = foreground_color;
while low_opacity < high_opacity {
let opacity = (low_opacity + high_opacity) / 2;
let color = foreground_color.blend(&coloru_with_opacity(color_with_max_contrast, opacity));
let contrast = contrast_ratio(color, background_color);
if contrast >= minimum_allowed_contrast.get() {
best_color = color;
high_opacity = opacity;
} else {
low_opacity = opacity + 1;
}
}
best_color
}
fn relative_luminance_for_channel(channel: u8) -> f32 {
let srgb_channel = channel as f32 / 255.;
if srgb_channel <= 0.03928 {
srgb_channel / 12.92
} else {
((srgb_channel + 0.055) / 1.055).powf(2.4)
}
}
/// Computed based on the WCAG recommendations:
/// https://www.w3.org/TR/WCAG20/#relativeluminancedef
pub fn relative_luminance(color: ColorU) -> f32 {
let r = relative_luminance_for_channel(color.r);
let g = relative_luminance_for_channel(color.g);
let b = relative_luminance_for_channel(color.b);
0.2126 * r + 0.7152 * g + 0.0722 * b
}
/// More on calculating contrast ration here:
/// https://medium.muz.li/the-science-of-color-contrast-an-expert-designers-guide-33e84c41d156
fn contrast_ratio(color1: ColorU, color2: ColorU) -> f32 {
let luminance1 = relative_luminance(color1) + LUMINANCE_OFFSET_FOR_CONTRAST_RATIO;
let luminance2 = relative_luminance(color2) + LUMINANCE_OFFSET_FOR_CONTRAST_RATIO;
// dividend here is supposed to be a lighter color than the divisor
if luminance1 > luminance2 {
return luminance1 / luminance2;
}
luminance2 / luminance1
}
/// This method picks the color option (option1 or option2) that has the highest contrast relative
/// to background color.
pub(super) fn pick_constrasting_color(
background: ColorU,
option1: ColorU,
option2: ColorU,
) -> ColorU {
let contrast_option1 = contrast_ratio(background, option1);
let contrast_option2 = contrast_ratio(background, option2);
if contrast_option1 > contrast_option2 {
return option1;
}
option2
}
/// Enum that species the desired contrast ratio based on the type of content in the foreground.
#[derive(Copy, Clone, Debug)]
pub enum MinimumAllowedContrast {
/// Text is on the foreground.
Text,
/// A non-text element (such as an icon or a UI component) is on the foreground.
NonText,
}
impl MinimumAllowedContrast {
/// Returns the minimum acceptable contrast ratio per the [WCAG (Web Content Accessibility
/// Guidelines)](https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html) of a
/// foreground color against a background color.
fn get(&self) -> f32 {
match self {
MinimumAllowedContrast::Text => {
// Normal sized text should have a contrast of at least 4.5:1. Source:
// https://www.w3.org/WAI/WCAG21/Understanding/contrast-minimum.html
4.5
}
MinimumAllowedContrast::NonText => {
// Graphical elements should have a contrast of at least 3:1. Source:
// https://www.w3.org/WAI/WCAG21/Techniques/general/G207
3.0
}
}
}
}
/// This method determines what font color should be used based on the background color it's
/// written on.
/// Most of the time, we juggle between background and foreground colors, assuming one of them
/// is dark, and the other is bright. If that's not the case and the contrast between both
/// background and foreground against provided color is not high enough, we simply fallback to white and
/// black for base font colors.
pub fn pick_best_foreground_color(
bg: ColorU,
option1: ColorU,
option2: ColorU,
minimum_allowed_contrast: MinimumAllowedContrast,
) -> ColorU {
let contrasting_color = pick_constrasting_color(bg, option1, option2);
if high_enough_contrast(bg, contrasting_color, minimum_allowed_contrast) {
return contrasting_color;
}
// if the above didn't have enough contrast, we fallback to using black or white.
// we assume that since luminance for black is 0 and 1 for white, we will always pick a
// color that has high enough contrast.
pick_constrasting_color(bg, ColorU::black(), ColorU::white())
}
/// Returns whether `color1` has a contrast of at least `minimum_allowed_contrast` when rendered
/// against `color2`.
pub fn high_enough_contrast(
color1: ColorU,
color2: ColorU,
minimum_allowed_contrast: MinimumAllowedContrast,
) -> bool {
contrast_ratio(color1, color2) > minimum_allowed_contrast.get()
}
#[cfg(test)]
#[path = "contrast_tests.rs"]
mod tests;
@@ -0,0 +1,138 @@
use super::*;
use rand::prelude::StdRng;
use rand::{Rng, SeedableRng};
#[test]
fn foreground_color_with_minimum_contrast_foreground_already_meets_minimum() {
assert_eq!(
ColorU::black(),
foreground_color_with_minimum_contrast(
ColorU::black(),
ColorU::white().into(),
MinimumAllowedContrast::Text
)
)
}
#[test]
fn foreground_color_with_minimum_contrast_foreground_blend_darker() {
let light_grey = ColorU::from_u32(0xAAAAAAFF);
let white = ColorU::white();
// Grey on white should not meet the contrast requirements.
assert!(!high_enough_contrast(
light_grey,
white,
MinimumAllowedContrast::NonText
));
let result = foreground_color_with_minimum_contrast(
light_grey,
white.into(),
MinimumAllowedContrast::NonText,
);
assert_ne!(light_grey, result);
// The suggested color should meet the contrast requirements.
assert!(contrast_ratio(result, white) > MinimumAllowedContrast::NonText.get());
}
#[test]
fn foreground_color_with_minimum_contrast_blend_lighter() {
let minimum_allowed_contrast = MinimumAllowedContrast::NonText;
let grey = ColorU::from_u32(0x333333FF);
let black = ColorU::black();
// Grey on black should not meet the contrast requirements.
assert!(!high_enough_contrast(
grey,
black,
MinimumAllowedContrast::NonText
));
let suggested_color =
foreground_color_with_minimum_contrast(grey, black.into(), minimum_allowed_contrast);
assert_ne!(grey, suggested_color);
// The suggested color should meet the contrast requirements.
assert!(contrast_ratio(suggested_color, black) > minimum_allowed_contrast.get());
}
#[test]
fn compute_foreground_color_with_minimum_contrast_already_meets_contrast() {
let white = ColorU::white();
let black = ColorU::black();
// White on black should meet the contrast requirements.
assert!(high_enough_contrast(
white,
black,
MinimumAllowedContrast::NonText
));
// Since white on black has enough contrast, we shouldn't need to change the color.
assert_eq!(
foreground_color_with_minimum_contrast(
white,
black.into(),
MinimumAllowedContrast::NonText,
),
white
);
}
#[test]
fn compute_foreground_color_with_minimum_contrast_same_color() {
let black = ColorU::black();
// black on black should _not_ meet the contrast requirements.
assert!(!high_enough_contrast(
black,
black,
MinimumAllowedContrast::NonText
));
// Since white on black has enough contrast, we shouldn't need to change the color.
let suggested_color = foreground_color_with_minimum_contrast(
black,
black.into(),
MinimumAllowedContrast::NonText,
);
assert!(high_enough_contrast(
suggested_color,
black,
MinimumAllowedContrast::NonText
));
}
/// Test that ensures that a random foreground color against a background color produces
/// a new foreground color that has a minimum contrast after calling
/// `foreground_color_with_minimum_contrast`.
#[test]
fn compute_foreground_color_with_minimum_contrast_random() {
let minimum_allowed_contrast = MinimumAllowedContrast::NonText;
for seed in 0..1000 {
let mut rng = StdRng::seed_from_u64(seed);
let foreground_color = ColorU::from_u32(rng.gen());
let background_color = ColorU::from_u32(rng.gen());
let suggested_color = foreground_color_with_minimum_contrast(
foreground_color,
background_color.into(),
minimum_allowed_contrast,
);
let actual_contrast_ratio = contrast_ratio(suggested_color, background_color);
let desired_contrast_ratio = minimum_allowed_contrast.get();
assert!(
high_enough_contrast(suggested_color, background_color, minimum_allowed_contrast),
"{foreground_color:?} on {background_color:?} does not have contrast. Expected contrast = {desired_contrast_ratio:?}, actual contrast {actual_contrast_ratio:?}"
);
}
}
@@ -0,0 +1,90 @@
use serde::{de, Deserialize, Deserializer, Serialize, Serializer};
use std::{borrow::Cow, fmt};
use warpui::color::ColorU;
use super::OPAQUE;
const SHORT_COLOR_LEN: usize = 3;
const FULL_COLOR_LEN: usize = 6;
#[derive(PartialEq, Eq, Debug, Clone)]
pub enum HexColorError {
HashPrefix,
InvalidLength,
InvalidValue,
}
impl fmt::Display for HexColorError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
HexColorError::HashPrefix => {
write!(f, "Expected hex color string starting with #.")
}
HexColorError::InvalidLength => write!(
f,
"Expected hex color string starting with # followed by 3 or 6 characters."
),
HexColorError::InvalidValue => write!(f, "Invalid hex color string"),
}
}
}
pub fn coloru_from_hex_string(s: &str) -> Result<ColorU, HexColorError> {
if !s.starts_with('#') {
return Err(HexColorError::HashPrefix);
}
let mut s: Cow<str> = s[1..].into();
if s.len() != SHORT_COLOR_LEN && s.len() != FULL_COLOR_LEN {
return Err(HexColorError::InvalidLength);
}
// for a shorter color representation we want to "normalize" it to the standard 6-character
// one, so #123 becomes #112233.
if s.len() == SHORT_COLOR_LEN {
s = s
.chars()
.flat_map(|c| std::iter::repeat_n(c, 2))
.collect::<String>()
.into();
}
let parsed = (0..s.len())
.step_by(2)
.map(|i| u8::from_str_radix(&s[i..i + 2], 16))
.collect::<Result<Vec<_>, _>>();
match parsed {
Ok(bytes) if bytes.len() == 3 => Ok(ColorU {
r: bytes[0],
g: bytes[1],
b: bytes[2],
a: OPAQUE,
}),
_ => Err(HexColorError::InvalidValue),
}
}
pub fn coloru_to_hex_string(coloru: &ColorU) -> String {
format!("#{:02x}{:02x}{:02x}", coloru.r, coloru.g, coloru.b)
}
pub fn deserialize<'de, D, C>(deserializer: D) -> Result<C, D::Error>
where
C: From<ColorU>,
D: Deserializer<'de>,
{
let s: String = Deserialize::deserialize(deserializer)?;
coloru_from_hex_string(&s)
.map(Into::into)
.map_err(de::Error::custom)
}
pub fn serialize<S, C>(color: &C, serializer: S) -> Result<S::Ok, S::Error>
where
C: Into<ColorU> + Clone,
S: Serializer,
{
let coloru: ColorU = color.to_owned().into();
coloru_to_hex_string(&coloru).serialize(serializer)
}
+125
View File
@@ -0,0 +1,125 @@
use warpui::color::ColorU;
use self::contrast::{high_enough_contrast, pick_constrasting_color, MinimumAllowedContrast};
pub mod blend;
pub mod contrast;
pub mod hex_color;
/// Opacity of the given color expressed as %. Allowing for range 0..100 inclusive.
/// TODO: use a bounded type instead
pub type Opacity = u8;
pub const OPAQUE: u8 = 255;
/// Claude brand orange color (#E8704E)
pub const CLAUDE_ORANGE: ColorU = ColorU {
r: 232,
g: 112,
b: 78,
a: OPAQUE,
};
/// Simple type representing a color _without_ an alpha channel.
pub struct Rgb {
r: u8,
g: u8,
b: u8,
}
impl From<Rgb> for ColorU {
fn from(rgb: Rgb) -> Self {
Self {
r: rgb.r,
g: rgb.g,
b: rgb.b,
a: OPAQUE,
}
}
}
impl From<ColorU> for Rgb {
fn from(color: ColorU) -> Self {
Self {
r: color.r,
g: color.g,
b: color.b,
}
}
}
pub fn coloru_with_opacity(color: ColorU, opacity: Opacity) -> ColorU {
let new_alpha: u8 = (color.a as f32 * (opacity as f32 / 100.)) as u8;
ColorU::new(color.r, color.g, color.b, new_alpha)
}
/// mid_coloru determines a color 'in-between' the 2 colors (or simply, an average of 2 colors).
/// Currently used to figure the midpoint color for gradients (which is then needed for the font
/// color computation etc.).
pub fn mid_coloru(c1: ColorU, c2: ColorU) -> ColorU {
let r = (c1.r as f32 + c2.r as f32) / 2.;
let g = (c1.g as f32 + c2.g as f32) / 2.;
let b = (c1.b as f32 + c2.b as f32) / 2.;
ColorU::new(r as u8, g as u8, b as u8, OPAQUE)
}
/// "those are kinda arbitrary" -- Agata. We could tweak these factors.
const DARKEN_COLORU_SHADE_FACTOR: f32 = 0.52;
const LIGHTEN_COLORU_SHADE_FACTOR: f32 = 0.5;
/// Finds a darker version of the given color using DARKEN_COLORU_SHADE_FACTOR form factor.
pub fn darken(c: ColorU) -> ColorU {
let shade_factor = 1. - DARKEN_COLORU_SHADE_FACTOR;
let r = ((c.r as f32) * shade_factor).ceil() as u8;
let g = ((c.g as f32) * shade_factor).ceil() as u8;
let b = ((c.b as f32) * shade_factor).ceil() as u8;
ColorU::new(r, g, b, c.a)
}
/// Finds a ligher version of the given color using LIGHTEN_COLORU_SHADE_FACTOR form factor.
pub fn lighten(c: ColorU) -> ColorU {
// aplying the shade factor only to the difference between 255 and channel
// (doing so to the actual channel value could produce incorrect results
// since channels are capped at 255 value).
let r = ((OPAQUE - c.r) as f32 * LIGHTEN_COLORU_SHADE_FACTOR).ceil() as u8;
let g = ((OPAQUE - c.g) as f32 * LIGHTEN_COLORU_SHADE_FACTOR).ceil() as u8;
let b = ((OPAQUE - c.b) as f32 * LIGHTEN_COLORU_SHADE_FACTOR).ceil() as u8;
// in the result, we add the computed value to the current channel value
// to get the actual lighter color.
ColorU::new(r + c.r, g + c.g, b + c.b, c.a)
}
pub fn pick_foreground_color(background: ColorU) -> ColorU {
pick_constrasting_color(background, ColorU::black(), ColorU::white())
}
pub trait ContrastingColor<Rhs = Self> {
type Output;
fn on_background(
self,
background: Rhs,
minimum_allowed_contrast: MinimumAllowedContrast,
) -> Self::Output;
}
impl ContrastingColor for ColorU {
type Output = ColorU;
fn on_background(
self,
background: ColorU,
minimum_allowed_contrast: MinimumAllowedContrast,
) -> ColorU {
if !high_enough_contrast(background, self, minimum_allowed_contrast) {
return contrast::foreground_color_with_minimum_contrast(
self,
background.into(),
minimum_allowed_contrast,
);
}
self
}
}
#[cfg(test)]
#[path = "color_tests.rs"]
mod tests;
@@ -0,0 +1,42 @@
use crate::ui::theme::Fill;
use warpui::elements::Icon as WarpUiIcon;
pub enum ExternalProductIcon {
Heroku,
Notion,
Linear,
Figma,
Github,
Slack,
}
impl ExternalProductIcon {
pub fn from_string(s: &str) -> Option<ExternalProductIcon> {
let s_lower = s.to_ascii_lowercase();
match s_lower.as_str() {
"heroku" => Some(ExternalProductIcon::Heroku),
"notion" => Some(ExternalProductIcon::Notion),
"linear" => Some(ExternalProductIcon::Linear),
"figma" => Some(ExternalProductIcon::Figma),
"github" => Some(ExternalProductIcon::Github),
"slack" => Some(ExternalProductIcon::Slack),
_other => None,
}
}
pub fn get_path(&self) -> &'static str {
match self {
ExternalProductIcon::Heroku => "bundled/svg/heroku.svg",
ExternalProductIcon::Notion => "bundled/svg/notion.svg",
ExternalProductIcon::Linear => "bundled/svg/linear.svg",
ExternalProductIcon::Figma => "bundled/svg/figma.svg",
ExternalProductIcon::Github => "bundled/svg/github.svg",
ExternalProductIcon::Slack => "bundled/svg/slack-logo.svg",
}
}
pub fn to_warpui_icon(&self, color: Fill) -> WarpUiIcon {
let path = self.get_path();
WarpUiIcon::new(path, color.into_solid())
}
}
+633
View File
@@ -0,0 +1,633 @@
use crate::ui::theme::Fill;
use warpui::elements::Icon as WarpUiIcon;
/// Default icon dimensions that apply to all icons used within the ui system.
pub const ICON_DIMENSIONS: f32 = 24.;
/// Icon enum to be used within the app in place of the warpui::elements::Icon directly. It
/// abstracts things like svg paths out and provides a utility method to convert into the actual Icon.
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
pub enum Icon {
File,
NodeJS,
AtSign,
Menu,
Plus,
Copy,
Circle,
CircleFilled,
Queued,
Triangle,
CopyMenuItem,
Duplicate,
Notebook,
Workflow,
X,
DotsVertical,
DotsHorizontal,
Trash,
Terminal,
TerminalInput,
TextInput,
ListCollapsed,
ListOpen,
AddTeammates,
Folder,
Find,
FindAll,
Globe,
Globe4,
Search,
Lightbulb,
LightbulbFilled,
Gear,
Settings,
Keyboard,
AiAssistant,
Rename,
Share,
Share3,
LogOut,
Move,
TextBlock,
RunnableCommandBlock,
BulletedListBlock,
OrderedListBlock,
HorizontalRuleBlock,
EmbedBlock,
TaskListBlock,
HeaderBlock,
Cloud,
CloudFilled,
CloudOffline,
Compass,
CreateTeam,
WarpDrive,
Warp,
WarpLogoLight,
ArrowLeft,
ArrowBlockLeft,
ArrowBlockUp,
ArrowRight,
ArrowUp,
ArrowDown,
ArrowSplit,
LinkExternal,
CheckCircleBroken,
Link,
Refresh,
RefreshCcw,
RefreshCw04,
AlertTriangle,
Laptop,
Tool,
Tool2,
CalendarDate,
Gift,
CalendarCheck,
AlphaDescending,
AlphaAscending,
ReverseLeft,
Pencil,
Sort,
Check,
FilterFunnel,
FilterFunnelFilled,
FilterOff,
Bug,
Code1,
Code2,
Explain,
Rocket,
Rocket1,
Regex,
CaseSensitivity,
PreserveCase,
Import,
Download,
XCircle,
SearchSmall,
VimNormalMode,
VimInsertMode,
VimVisualMode,
VimReplaceMode,
Info,
LinkHorizontal,
Clock,
Maximize,
Minimize,
Sharing,
DistributeSpacingVertical,
ChevronLeft,
ChevronDown,
ChevronRight,
ChevronUp,
ChevronRightDouble,
ChevronLeftDouble,
ArrowDropDown,
AlertCircle,
Bold,
Italic,
Underline,
InlineCode,
Strikethrough,
Stars,
AgentMode,
AmbientAgentMode,
Github,
Docker,
Linear,
Slack,
Loading,
Warning,
HelpCircle,
ThumbsUp,
ThumbsDown,
Repeat,
Edit,
EditLine,
Eye,
Slash,
ClockRefresh,
ClockRewind,
ClockLoader,
Paperclip,
EnvVarCollection,
CornerRight,
DashedRectangle,
CornersOfBox,
Microphone,
Stop,
StopFilled,
MinusCircle,
Minus,
Key,
OnePassword,
LastPass,
SlashCircle,
User,
Users,
CoinsStacked,
Phone,
Navigation,
AutoUpdate,
Bell,
GitBranch,
CheckSkinny,
Lock,
Save,
CornerDownLeft,
Powershell,
GitBash,
Ubuntu,
Debian,
Kali,
Arch,
Linux,
Cancelled,
BookOpen,
LoadingDots0,
LoadingDots1,
LoadingDots2,
LoadingDots3,
LoadingBlinker0,
LoadingBlinker1,
LoadingBlinker2,
PackageCheck,
Liftoff0,
Liftoff1,
Liftoff2,
Liftoff3,
Liftoff4,
LoadingAgents0,
LoadingAgents1,
LoadingAgents2,
LoadingAgents3,
LoadingAgents4,
LoadingAgents5,
LoadingAgents6,
LoadingAgents7,
Warping0,
Warping1,
Warping2,
Warping3,
Warping4,
Warping5,
Warping6,
Warping7,
Warping8,
Warping9,
Warping10,
Warping11,
FlipForward,
PaintBrush,
GamingPad,
Neurology,
Lightning,
Orbit,
PlusCircle,
Dataflow,
Play,
MessageText,
NewConversation,
Image,
FastForward,
FastForwardFilled,
ContextWindowTwentyPct,
ContextWindowFourtyPct,
ContextWindowSixtyPct,
ContextWindowWarning,
ContextWindowSummarized,
ConversationContext0,
ConversationContext10,
ConversationContext20,
ConversationContext30,
ConversationContext40,
ConversationContext50,
ConversationContext60,
ConversationContext70,
ConversationContext80,
ConversationContext90,
ConversationContext100,
LeftSidebarOpen,
LeftSidebarClose,
Diff,
ExpandUp,
ExpandDown,
ExpandUpAndDown,
Psychology,
History,
SlashCommands,
MessagePlusSquare,
Hash,
FolderClosed,
FileCopy,
Credits,
AddressedComment,
ClockSnooze,
Hand,
ArrowCircleBrokenUp,
FilterLines,
ChatDashed,
ClaudeLogo,
GeminiLogo,
OpenAILogo,
AmpLogo,
DroidLogo,
OpenCodeLogo,
CopilotLogo,
PiLogo,
AuggieLogo,
CursorLogo,
NLD,
Oz,
OzCloud,
Conversation,
Prompt,
Grid,
Figma,
FigmaColored,
StripeLogo,
CalloutTriangleBorderDown,
CalloutTriangleFillDown,
CalloutTriangleBorderUp,
CalloutTriangleFillUp,
CalloutTriangleBorderLeft,
CalloutTriangleFillLeft,
DragIndicator,
Ellipse,
Inbox,
Menu01,
LayoutAlt01,
Dataflow02,
Sliders,
MessageCheckSquare,
Phone01,
GitCommit,
UploadCloud,
ClockPlus,
SwitchHorizontal01,
HeartHand,
MessageChatSquare,
}
impl From<Icon> for &'static str {
fn from(icon: Icon) -> &'static str {
match icon {
Icon::Menu => "bundled/svg/layout-left.svg",
Icon::AtSign => "bundled/svg/at-sign.svg",
Icon::Plus => "bundled/svg/plus.svg",
Icon::Copy => "bundled/svg/copy.svg",
Icon::Circle => "bundled/svg/circle.svg",
Icon::CircleFilled => "bundled/svg/circle-filled.svg",
Icon::Triangle => "bundled/svg/triangle.svg",
Icon::Queued => "bundled/svg/queued.svg",
Icon::CopyMenuItem => "bundled/svg/copy-05.svg",
Icon::Duplicate => "bundled/svg/copy-07.svg",
Icon::Notebook => "bundled/svg/notebook.svg",
Icon::Workflow => "bundled/svg/workflow.svg",
Icon::X => "bundled/svg/x-close.svg",
Icon::ChevronLeft => "bundled/svg/chevron-left.svg",
Icon::DotsVertical => "bundled/svg/dots-vertical.svg",
Icon::DotsHorizontal => "bundled/svg/dots-horizontal.svg",
Icon::Globe => "bundled/svg/globe-01.svg",
Icon::Globe4 => "bundled/svg/globe-04.svg",
Icon::Trash => "bundled/svg/trash-02.svg",
Icon::Terminal => "bundled/svg/terminal.svg",
Icon::TerminalInput => "bundled/svg/terminal-input.svg",
Icon::TextInput => "bundled/svg/text-input.svg",
Icon::ListCollapsed => "bundled/svg/chevron-right-2.svg",
Icon::ListOpen => "bundled/svg/chevron-down-2.svg",
Icon::Microphone => "bundled/svg/microphone.svg",
Icon::Stop => "bundled/svg/stop.svg",
Icon::StopFilled => "bundled/svg/stop-filled.svg",
Icon::AddTeammates => "bundled/svg/user-plus-01.svg",
Icon::Folder => "bundled/svg/folder.svg",
Icon::Find => "bundled/svg/find.svg",
Icon::FindAll => "bundled/svg/find-all.svg",
Icon::Search => "bundled/svg/search.svg",
Icon::Lightbulb => "bundled/svg/lightbulb.svg",
Icon::LightbulbFilled => "bundled/svg/lightbulb-filled.svg",
Icon::Gear => "bundled/svg/gear.svg",
Icon::Settings => "bundled/svg/settings.svg",
Icon::Keyboard => "bundled/svg/keyboard.svg",
Icon::AiAssistant => "bundled/svg/ai-assistant.svg",
Icon::Rename => "bundled/svg/pencil-line.svg",
Icon::Share => "bundled/svg/share-01.svg",
Icon::Share3 => "bundled/svg/share-03.svg",
Icon::LogOut => "bundled/svg/log-out-01.svg",
Icon::Move => "bundled/svg/move.svg",
Icon::TextBlock => "bundled/svg/block-text.svg",
Icon::RunnableCommandBlock => "bundled/svg/block-command.svg",
Icon::HeaderBlock => "bundled/svg/block-header.svg",
Icon::HorizontalRuleBlock => "bundled/svg/block-horizontal-rule.svg",
Icon::EmbedBlock => "bundled/svg/block-embed.svg",
Icon::BulletedListBlock => "bundled/svg/block-bulletedlist.svg",
Icon::OrderedListBlock => "bundled/svg/block-ordered-list.svg",
Icon::TaskListBlock => "bundled/svg/block-tasklist.svg",
Icon::Cloud => "bundled/svg/cloud-01.svg",
Icon::CloudFilled => "bundled/svg/cloud-filled.svg",
Icon::CloudOffline => "bundled/svg/cloud-offline.svg",
Icon::Compass => "bundled/svg/compass-3.svg",
Icon::CreateTeam => "bundled/svg/create-team.svg",
Icon::WarpDrive => "bundled/svg/warp.svg",
Icon::Warp => "bundled/svg/warp-drive.svg",
Icon::WarpLogoLight => "bundled/svg/warp-logo-light.svg",
Icon::ArrowLeft => "bundled/svg/arrow-left.svg",
Icon::ArrowBlockLeft => "bundled/svg/arrow-block-left.svg",
Icon::ArrowBlockUp => "bundled/svg/arrow-block-up.svg",
Icon::ArrowRight => "bundled/svg/arrow-right.svg",
Icon::ArrowUp => "bundled/svg/arrow-narrow-up.svg",
Icon::ArrowDown => "bundled/svg/arrow-narrow-down.svg",
Icon::ArrowSplit => "bundled/svg/arrow-split.svg",
Icon::SwitchHorizontal01 => "bundled/svg/switch-horizontal-01.svg",
Icon::ArrowDropDown => "bundled/svg/arrow-drop-down.svg",
Icon::CheckCircleBroken => "bundled/svg/check-circle-broken.svg",
Icon::LinkExternal => "bundled/svg/link-external-02.svg",
Icon::Link => "bundled/svg/link-03.svg",
Icon::Refresh => "bundled/svg/refresh.svg",
Icon::RefreshCcw => "bundled/svg/refresh-ccw-01.svg",
Icon::RefreshCw04 => "bundled/svg/refresh-cw-04.svg",
Icon::AlertTriangle => "bundled/svg/alert-triangle.svg",
Icon::Laptop => "bundled/svg/laptop.svg",
Icon::Tool => "bundled/svg/tool-01.svg",
Icon::Tool2 => "bundled/svg/tool-02.svg",
Icon::CalendarDate => "bundled/svg/calendar-date.svg",
Icon::Gift => "bundled/svg/gift-01.svg",
Icon::CalendarCheck => "bundled/svg/calendar-check-01.svg",
Icon::AlphaDescending => "bundled/svg/Alpha descending.svg",
Icon::AlphaAscending => "bundled/svg/Alpha ascending.svg",
Icon::ReverseLeft => "bundled/svg/reverse-left.svg",
Icon::Pencil => "bundled/svg/pencil-02.svg",
Icon::Sort => "bundled/svg/sort.svg",
Icon::Check => "bundled/svg/check.svg",
Icon::FilterFunnel => "bundled/svg/filter-funnel.svg",
Icon::FilterFunnelFilled => "bundled/svg/filter-funnel-filled.svg",
Icon::FilterOff => "bundled/svg/filter-list-off.svg",
Icon::Bug => "bundled/svg/bug.svg",
Icon::Code1 => "bundled/svg/code-01.svg",
Icon::Code2 => "bundled/svg/code-02.svg",
Icon::Explain => "bundled/svg/explain.svg",
Icon::Rocket => "bundled/svg/rocket.svg",
Icon::Rocket1 => "bundled/svg/rocket-01.svg",
Icon::Regex => "bundled/svg/regex.svg",
Icon::CaseSensitivity => "bundled/svg/case-sensitive.svg",
Icon::PreserveCase => "bundled/svg/preserve-case.svg",
Icon::Import => "bundled/svg/import.svg",
Icon::Download => "bundled/svg/download-02.svg",
Icon::XCircle => "bundled/svg/x-circle.svg",
Icon::SearchSmall => "bundled/svg/search-small.svg",
Icon::VimNormalMode => "bundled/svg/vim-normal-mode.svg",
Icon::VimInsertMode => "bundled/svg/vim-insert-mode.svg",
Icon::VimVisualMode => "bundled/svg/vim-visual-mode.svg",
Icon::VimReplaceMode => "bundled/svg/vim-replace-mode.svg",
Icon::Info => "bundled/svg/info.svg",
Icon::LinkHorizontal => "bundled/svg/link-horizontal.svg",
Icon::Clock => "bundled/svg/clock.svg",
Icon::Maximize => "bundled/svg/maximize-01.svg",
Icon::Minimize => "bundled/svg/minimize-01.svg",
Icon::Sharing => "bundled/svg/sharing.svg",
Icon::DistributeSpacingVertical => "bundled/svg/distribute-spacing-vertical.svg",
Icon::ChevronDown => "bundled/svg/chevron-down.svg",
Icon::ChevronUp => "bundled/svg/chevron-up.svg",
Icon::ChevronRightDouble => "bundled/svg/chevron-right-double.svg",
Icon::ChevronLeftDouble => "bundled/svg/chevron-left-double.svg",
Icon::AlertCircle => "bundled/svg/alert-circle.svg",
Icon::Bold => "bundled/svg/edit-bold.svg",
Icon::Italic => "bundled/svg/edit-italic.svg",
Icon::Underline => "bundled/svg/edit-underline.svg",
Icon::InlineCode => "bundled/svg/edit-code.svg",
Icon::Strikethrough => "bundled/svg/edit-strikethrough.svg",
Icon::Stars => "bundled/svg/stars-01.svg",
Icon::AgentMode => "bundled/svg/agentmode.svg",
Icon::AmbientAgentMode => "bundled/svg/ambient-agent-mode.svg",
Icon::Github => "bundled/svg/github.svg",
Icon::Docker => "bundled/svg/docker.svg",
Icon::Linear => "bundled/svg/linear.svg",
Icon::Slack => "bundled/svg/slack-logo.svg",
Icon::ChevronRight => "bundled/svg/chevron-right.svg",
Icon::Loading => "bundled/svg/loading-02.svg",
Icon::Warning => "bundled/svg/warning.svg",
Icon::HelpCircle => "bundled/svg/help-circle.svg",
Icon::ThumbsUp => "bundled/svg/thumbs-up.svg",
Icon::ThumbsDown => "bundled/svg/thumbs-down.svg",
Icon::Repeat => "bundled/svg/repeat-01.svg",
Icon::Edit => "bundled/svg/edit-01.svg",
Icon::EditLine => "bundled/svg/edit-03.svg",
Icon::Eye => "bundled/svg/eye.svg",
Icon::Slash => "bundled/svg/slash.svg",
Icon::ClockRefresh => "bundled/svg/clock-refresh.svg",
Icon::ClockRewind => "bundled/svg/clock-rewind.svg",
Icon::ClockLoader => "bundled/svg/clock-loader.svg",
Icon::Paperclip => "bundled/svg/paperclip.svg",
Icon::EnvVarCollection => "bundled/svg/env-var-collection.svg",
Icon::CornerRight => "bundled/svg/corner-right.svg",
Icon::DashedRectangle => "bundled/svg/dashed-rectangle.svg",
Icon::CornersOfBox => "bundled/svg/corners-of-box.svg",
Icon::MinusCircle => "bundled/svg/minus-circle.svg",
Icon::Minus => "bundled/svg/minus.svg",
Icon::Key => "bundled/svg/key.svg",
Icon::OnePassword => "bundled/svg/onepassword.svg",
Icon::LastPass => "bundled/svg/lastpass.svg",
Icon::SlashCircle => "bundled/svg/slash-circle-01.svg",
Icon::User => "bundled/svg/user-02.svg",
Icon::Users => "bundled/svg/users-02.svg",
Icon::CoinsStacked => "bundled/svg/coins-stacked-02.svg",
Icon::Phone => "bundled/svg/phone.svg",
Icon::Navigation => "bundled/svg/navigation.svg",
Icon::AutoUpdate => "bundled/svg/autoupdate.svg",
Icon::Bell => "bundled/svg/bell.svg",
Icon::GitBranch => "bundled/svg/git-branch-02.svg",
Icon::CheckSkinny => "bundled/svg/check-skinny.svg",
Icon::Lock => "bundled/svg/lock-unlocked-01.svg",
Icon::Save => "bundled/svg/download-01.svg",
Icon::CornerDownLeft => "bundled/svg/corner-down-left.svg",
Icon::Powershell => "bundled/svg/powershell.svg",
Icon::GitBash => "bundled/svg/git-bash.svg",
Icon::Ubuntu => "bundled/svg/ubuntu.svg",
Icon::Debian => "bundled/svg/debian.svg",
Icon::Kali => "bundled/svg/kali.svg",
Icon::Arch => "bundled/svg/arch.svg",
Icon::Linux => "bundled/svg/linux.svg",
Icon::Cancelled => "bundled/svg/cancelled.svg",
Icon::BookOpen => "bundled/svg/book-open.svg",
Icon::LoadingDots0 => "bundled/svg/dots-0.svg",
Icon::LoadingDots1 => "bundled/svg/dots-1.svg",
Icon::LoadingDots2 => "bundled/svg/dots-2.svg",
Icon::LoadingDots3 => "bundled/svg/dots-3.svg",
Icon::LoadingBlinker0 => "bundled/svg/blinker-0.svg",
Icon::LoadingBlinker1 => "bundled/svg/blinker-1.svg",
Icon::LoadingBlinker2 => "bundled/svg/blinker-2.svg",
Icon::PackageCheck => "bundled/svg/package-check.svg",
Icon::Liftoff0 => "bundled/svg/liftoff-0.svg",
Icon::Liftoff1 => "bundled/svg/liftoff-1.svg",
Icon::Liftoff2 => "bundled/svg/liftoff-2.svg",
Icon::Liftoff3 => "bundled/svg/liftoff-3.svg",
Icon::Liftoff4 => "bundled/svg/liftoff-4.svg",
Icon::LoadingAgents0 => "bundled/svg/loading-agents-01.svg",
Icon::LoadingAgents1 => "bundled/svg/loading-agents-02.svg",
Icon::LoadingAgents2 => "bundled/svg/loading-agents-03.svg",
Icon::LoadingAgents3 => "bundled/svg/loading-agents-04.svg",
Icon::LoadingAgents4 => "bundled/svg/loading-agents-05.svg",
Icon::LoadingAgents5 => "bundled/svg/loading-agents-06.svg",
Icon::LoadingAgents6 => "bundled/svg/loading-agents-07.svg",
Icon::LoadingAgents7 => "bundled/svg/loading-agents-08.svg",
Icon::Warping0 => "bundled/svg/warp-loading-0.svg",
Icon::Warping1 => "bundled/svg/warp-loading-1.svg",
Icon::Warping2 => "bundled/svg/warp-loading-2.svg",
Icon::Warping3 => "bundled/svg/warp-loading-3.svg",
Icon::Warping4 => "bundled/svg/warp-loading-4.svg",
Icon::Warping5 => "bundled/svg/warp-loading-5.svg",
Icon::Warping6 => "bundled/svg/warp-loading-6.svg",
Icon::Warping7 => "bundled/svg/warp-loading-7.svg",
Icon::Warping8 => "bundled/svg/warp-loading-8.svg",
Icon::Warping9 => "bundled/svg/warp-loading-9.svg",
Icon::Warping10 => "bundled/svg/warp-loading-10.svg",
Icon::Warping11 => "bundled/svg/warp-loading-11.svg",
Icon::FlipForward => "bundled/svg/flip-forward.svg",
Icon::PaintBrush => "bundled/svg/brush-01.svg",
Icon::GamingPad => "bundled/svg/gaming-pad-01.svg",
Icon::Neurology => "bundled/svg/neurology.svg",
Icon::Lightning => "bundled/svg/lightning-02.svg",
Icon::Orbit => "bundled/svg/orbit.svg",
Icon::PlusCircle => "bundled/svg/plus-circle.svg",
Icon::Dataflow => "bundled/svg/dataflow.svg",
Icon::Play => "bundled/svg/play-white.svg",
Icon::MessageText => "bundled/svg/message-text-square-02.svg",
Icon::NewConversation => "bundled/svg/new-conversation.svg",
Icon::Image => "bundled/svg/image-01.svg",
Icon::File => "bundled/svg/file.svg",
Icon::NodeJS => "bundled/svg/nodejs-logo.svg",
Icon::FastForward => "bundled/svg/fast-forward.svg",
Icon::FastForwardFilled => "bundled/svg/fast-forward-filled.svg",
Icon::ContextWindowTwentyPct => "bundled/svg/context-window-20-pct.svg",
Icon::ContextWindowFourtyPct => "bundled/svg/context-window-40-pct.svg",
Icon::ContextWindowSixtyPct => "bundled/svg/context-window-60-pct.svg",
Icon::ContextWindowWarning => "bundled/svg/context-window-warning.svg",
Icon::ContextWindowSummarized => "bundled/svg/context-window-summarized.svg",
Icon::ConversationContext0 => "bundled/svg/conversation-context-0.svg",
Icon::ConversationContext10 => "bundled/svg/conversation-context-10.svg",
Icon::ConversationContext20 => "bundled/svg/conversation-context-20.svg",
Icon::ConversationContext30 => "bundled/svg/conversation-context-30.svg",
Icon::ConversationContext40 => "bundled/svg/conversation-context-40.svg",
Icon::ConversationContext50 => "bundled/svg/conversation-context-50.svg",
Icon::ConversationContext60 => "bundled/svg/conversation-context-60.svg",
Icon::ConversationContext70 => "bundled/svg/conversation-context-70.svg",
Icon::ConversationContext80 => "bundled/svg/conversation-context-80.svg",
Icon::ConversationContext90 => "bundled/svg/conversation-context-90.svg",
Icon::ConversationContext100 => "bundled/svg/conversation-context-100.svg",
Icon::LeftSidebarOpen => "bundled/svg/left-panel-open.svg",
Icon::LeftSidebarClose => "bundled/svg/left-panel-close.svg",
Icon::Diff => "bundled/svg/diff.svg",
Icon::ExpandUp => "bundled/svg/expand-up.svg",
Icon::ExpandDown => "bundled/svg/expand-down.svg",
Icon::ExpandUpAndDown => "bundled/svg/expand-up-and-down.svg",
Icon::Psychology => "bundled/svg/psychology.svg",
Icon::History => "bundled/svg/history.svg",
Icon::SlashCommands => "bundled/svg/slash-square.svg",
Icon::MessagePlusSquare => "bundled/svg/message-plus-square.svg",
Icon::Hash => "bundled/svg/hash-02.svg",
Icon::FolderClosed => "bundled/svg/folder-closed.svg",
Icon::FileCopy => "bundled/svg/file_copy.svg",
Icon::Credits => "bundled/svg/credits.svg",
Icon::AddressedComment => "bundled/svg/addressed-comment.svg",
Icon::ClockSnooze => "bundled/svg/clock-snooze.svg",
Icon::Hand => "bundled/svg/hand.svg",
Icon::ArrowCircleBrokenUp => "bundled/svg/arrow-circle-broken-up.svg",
Icon::FilterLines => "bundled/svg/filter-lines.svg",
Icon::ChatDashed => "bundled/svg/chat-dashed.svg",
Icon::ClaudeLogo => "bundled/svg/claude.svg",
Icon::GeminiLogo => "bundled/svg/gemini_cli.svg",
Icon::OpenAILogo => "bundled/svg/openai.svg",
Icon::AmpLogo => "bundled/svg/amp.svg",
Icon::DroidLogo => "bundled/svg/droid.svg",
Icon::OpenCodeLogo => "bundled/svg/opencode.svg",
Icon::CopilotLogo => "bundled/svg/copilot.svg",
Icon::PiLogo => "bundled/svg/pi.svg",
Icon::AuggieLogo => "bundled/svg/auggie.svg",
Icon::CursorLogo => "bundled/svg/cursor.svg",
Icon::NLD => "bundled/svg/nld.svg",
Icon::Oz => "bundled/svg/oz.svg",
Icon::OzCloud => "bundled/svg/oz-cloud.svg",
Icon::Conversation => "bundled/svg/conversation.svg",
Icon::Prompt => "bundled/svg/prompt.svg",
Icon::Grid => "bundled/svg/grid.svg",
Icon::Figma => "bundled/svg/figma.svg",
Icon::FigmaColored => "bundled/svg/figma-colored.svg",
Icon::StripeLogo => "bundled/svg/stripe.svg",
Icon::CalloutTriangleBorderDown => "bundled/svg/callout-triangle-border-down.svg",
Icon::CalloutTriangleFillDown => "bundled/svg/callout-triangle-fill-down.svg",
Icon::CalloutTriangleBorderUp => "bundled/svg/callout-triangle-border-up.svg",
Icon::CalloutTriangleFillUp => "bundled/svg/callout-triangle-fill-up.svg",
Icon::CalloutTriangleBorderLeft => "bundled/svg/callout-triangle-border-left.svg",
Icon::CalloutTriangleFillLeft => "bundled/svg/callout-triangle-fill-left.svg",
Icon::DragIndicator => "bundled/svg/drag_indicator.svg",
Icon::Ellipse => "bundled/svg/ellipse.svg",
Icon::Inbox => "bundled/svg/inbox-01.svg",
Icon::Menu01 => "bundled/svg/menu-01.svg",
Icon::LayoutAlt01 => "bundled/svg/layout-alt-01.svg",
Icon::Dataflow02 => "bundled/svg/dataflow-02.svg",
Icon::Sliders => "bundled/svg/sliders-04.svg",
Icon::MessageCheckSquare => "bundled/svg/message-check-square.svg",
Icon::Phone01 => "bundled/svg/phone-01.svg",
Icon::GitCommit => "bundled/svg/git-commit.svg",
Icon::UploadCloud => "bundled/svg/upload-cloud-01.svg",
Icon::ClockPlus => "bundled/svg/clock-plus.svg",
Icon::HeartHand => "bundled/svg/heart-hand.svg",
Icon::MessageChatSquare => "bundled/svg/message-chat-square.svg",
}
}
}
impl Icon {
pub fn to_warpui_icon(self, color: Fill) -> WarpUiIcon {
WarpUiIcon::new(self.into(), color.into_solid())
}
pub fn icon_for_key(key: &str) -> Option<WarpUiIcon> {
match key {
"" => Some(Self::CornerDownLeft.to_warpui_icon(Fill::black())),
_ => None,
}
}
}
+8
View File
@@ -0,0 +1,8 @@
pub mod appearance;
pub mod builder;
pub mod color;
pub mod external_product_icon;
pub mod icons;
pub mod theme;
pub use icons::Icon;
+599
View File
@@ -0,0 +1,599 @@
//! Module providing utility functions to retrieve the colors used within our ui system and
//! designs.
//! These colors can be further understood here:
//! https://docs.google.com/document/d/1YMovEoXsPRziPk99a4i9LZNEKGm_rjEyzhcHsFkT3ac/edit.
use self::internal_colors::{
accent_overlay_2, fg_overlay_1, fg_overlay_2, fg_overlay_3, neutral_1, neutral_2, neutral_3,
neutral_4,
};
use super::{AnsiColor, AnsiColorIdentifier, Fill, TerminalColors, WarpTheme};
use crate::ui::color::{
blend::Blend,
contrast::{pick_best_foreground_color, MinimumAllowedContrast},
Opacity,
};
use getset::Getters;
use serde::{Deserialize, Serialize};
use warpui::color::ColorU;
const BLOCK_SELECTION_OPACITY: Opacity = 10;
#[derive(Serialize, Copy, Clone, Debug, Deserialize, Getters, PartialEq, Eq)]
#[get = "pub"]
// TODO handle optional fields (so users can specify some and not all)
pub struct CustomDetails {
pub main_text_opacity: Opacity,
pub sub_text_opacity: Opacity,
pub hint_text_opacity: Opacity,
pub disabled_text_opacity: Opacity,
pub foreground_button_opacity: Opacity, // foreground color overlay on the std button
pub accent_button_opacity: Opacity, // foreground color overlay on the active button
pub button_hover_opacity: Opacity, // foreground color overlay on the button
pub button_click_opacity: Opacity, // bg color overlay on the button
pub keybinding_row_overlay_opacity: Opacity,
pub welcome_tips_completion_overlay_opacity: Opacity,
}
const DARKER_DETAILS: CustomDetails = CustomDetails {
main_text_opacity: 90,
sub_text_opacity: 60,
hint_text_opacity: 40,
disabled_text_opacity: 20,
foreground_button_opacity: 30,
accent_button_opacity: 0,
button_hover_opacity: 10,
button_click_opacity: 20,
keybinding_row_overlay_opacity: 40,
welcome_tips_completion_overlay_opacity: 90,
};
const LIGHTER_DETAILS: CustomDetails = CustomDetails {
main_text_opacity: 90,
sub_text_opacity: 60,
hint_text_opacity: 40,
disabled_text_opacity: 20,
foreground_button_opacity: 30,
accent_button_opacity: 0,
button_hover_opacity: 10,
button_click_opacity: 20,
keybinding_row_overlay_opacity: 40,
welcome_tips_completion_overlay_opacity: 90,
};
impl CustomDetails {
pub fn darker_details() -> Self {
DARKER_DETAILS
}
pub fn lighter_details() -> Self {
LIGHTER_DETAILS
}
}
impl Default for CustomDetails {
fn default() -> Self {
DARKER_DETAILS
}
}
// Core colors
impl WarpTheme {
pub fn accent(&self) -> Fill {
self.accent
}
pub fn foreground(&self) -> Fill {
Fill::Solid(self.foreground)
}
/// Background color for large backgrounds like that of the terminal view.
/// Allows gradients because these are meant to be very large surfaces.
pub fn background(&self) -> Fill {
self.background
}
pub fn terminal_colors(&self) -> &TerminalColors {
&self.terminal_colors
}
/// Background color for UI elements that need to stand out from the main
/// `background()` color and the `surface_1()`` and `surface_2()`` backgrounds.
/// Doesn't allow gradients because these surfaces will often be too small
/// for the gradients to look appealing.
pub fn surface_3(&self) -> Fill {
Fill::Solid(neutral_3(self))
}
/// Background color for UI elements that need to stand out from the main
/// `background()` color and the `surface_1()` color.
/// Doesn't allow gradients because these surfaces will often be too small
/// for the gradients to look appealing.
pub fn surface_2(&self) -> Fill {
Fill::Solid(neutral_2(self))
}
/// Background color for UI elements that need to stand out from the main
/// `background()` color.
/// Doesn't allow gradients because these surfaces will often be too small
/// for the gradients to look appealing.
pub fn surface_1(&self) -> Fill {
Fill::Solid(neutral_1(self))
}
pub fn cursor(&self) -> Fill {
self.cursor.unwrap_or(self.accent())
}
pub fn ui_warning_color(&self) -> ColorU {
ColorU::new(194, 128, 0, 255)
}
pub fn ui_error_color(&self) -> ColorU {
ColorU::new(188, 54, 42, 255)
}
pub fn ui_yellow_color(&self) -> ColorU {
ColorU::new(229, 160, 26, 255)
}
pub fn ui_green_color(&self) -> ColorU {
ColorU::new(28, 160, 90, 255)
}
pub fn outline(&self) -> Fill {
fg_overlay_2(self)
}
// text colors
pub fn font_color(&self, background: impl Into<ColorU>) -> Fill {
Fill::Solid(pick_best_foreground_color(
background.into(),
self.background().into(),
self.foreground().into(),
MinimumAllowedContrast::Text,
))
}
pub fn main_text_color(&self, background: Fill) -> Fill {
internal_colors::text_main(self, background).into()
}
pub fn sub_text_color(&self, background: Fill) -> Fill {
internal_colors::text_sub(self, background).into()
}
pub fn hint_text_color(&self, background: Fill) -> Fill {
let details = self.details();
self.font_color(background)
.with_opacity(details.hint_text_opacity)
}
pub fn disabled_text_color(&self, background: Fill) -> Fill {
internal_colors::text_disabled(self, background).into()
}
pub fn active_ui_text_color(&self) -> Fill {
self.main_text_color(self.surface_2())
}
pub fn nonactive_ui_text_color(&self) -> Fill {
self.sub_text_color(self.surface_2())
}
pub fn disabled_ui_text_color(&self) -> Fill {
self.disabled_text_color(self.surface_2())
}
pub fn active_highlighted_text_color(&self) -> Fill {
self.main_text_color(self.accent())
}
pub fn settings_import_config_hover_opacity(&self) -> Opacity {
10
}
pub fn dark_overlay(&self) -> Fill {
let details = self.details();
Fill::black().with_opacity(details.button_click_opacity)
}
pub fn keybinding_row_overlay(&self) -> Fill {
let details = self.details();
Fill::black().with_opacity(details.keybinding_row_overlay_opacity)
}
pub fn welcome_tips_completion_overlay(&self) -> Fill {
let details = self.details();
self.surface_2()
.with_opacity(details.welcome_tips_completion_overlay_opacity)
}
pub fn blurred_background_overlay(&self) -> Fill {
Fill::black().with_opacity(70)
}
}
// Feature-specific theme colors
impl WarpTheme {
pub fn foreground_button_color(&self) -> Fill {
let details = self.details();
self.background.blend(
&self
.foreground()
.with_opacity(details.foreground_button_opacity),
)
}
pub fn accent_button_color(&self) -> Fill {
let details = self.details();
self.accent.blend(
&self
.foreground()
.with_opacity(details.accent_button_opacity),
)
}
pub fn button_hover_opacity(&self, button: Fill) -> Fill {
let details = self.details();
button.blend(&self.foreground().with_opacity(details.button_hover_opacity))
}
pub fn split_pane_border_color(&self) -> Fill {
fg_overlay_3(self)
}
pub fn accent_overlay(&self) -> Fill {
accent_overlay_2(self)
}
pub fn surface_overlay_3(&self) -> Fill {
fg_overlay_3(self)
}
pub fn surface_overlay_2(&self) -> Fill {
fg_overlay_2(self)
}
pub fn surface_overlay_1(&self) -> Fill {
fg_overlay_1(self)
}
pub fn yellow_overlay_1(&self) -> Fill {
let yellow: Fill = self.ui_yellow_color().into();
yellow.with_opacity(10)
}
pub fn green_overlay_1(&self) -> Fill {
let green: Fill = self.ansi_fg_green().into();
green.with_opacity(10)
}
pub fn green_overlay_2(&self) -> Fill {
let green: Fill = self.ui_green_color().into();
green.with_opacity(50)
}
pub fn block_selection_color(&self) -> Fill {
accent_overlay_2(self)
}
pub fn block_selection_as_context_background_color(&self) -> Fill {
let color_fill: Fill = self.terminal_colors.normal.yellow.into();
color_fill.with_opacity(BLOCK_SELECTION_OPACITY)
}
pub fn block_selection_as_context_border_color(&self) -> Fill {
let color_fill: Fill = self.terminal_colors.normal.yellow.into();
color_fill
}
// Although text selection colors aren't yet themed, declaring them in this file
// will make it easier to theme text selection colors in the future!
pub fn text_selection_color(&self) -> Fill {
Fill::Solid(ColorU::new(118, 167, 250, (0.4 * 255.) as u8))
}
pub fn text_selection_as_context_color(&self) -> Fill {
self.ansi_overlay_2(self.terminal_colors.normal.yellow)
.into()
}
pub fn find_bar_button_selection_color(&self) -> Fill {
accent_overlay_2(self)
}
pub fn failed_block_color(&self) -> Fill {
Fill::Solid(self.terminal_colors().normal.red.into())
}
pub fn active_ui_detail(&self) -> Fill {
self.main_text_color(self.surface_2())
}
pub fn nonactive_ui_detail(&self) -> Fill {
self.disabled_text_color(self.surface_2())
}
/// We apply an overlay over the terminal view background. The default overlay opacity is low
/// so it doesn't conflict with window opacity adjustments.
pub fn ai_blocks_overlay(&self) -> Fill {
fg_overlay_1(self)
}
/// We apply an overlay over the terminal view background. The default overlay opacity is low
/// so it doesn't conflict with window opacity adjustments.
pub fn restored_blocks_overlay(&self) -> Fill {
fg_overlay_2(self)
}
/// We apply an overlay over the terminal view background. The default overlay opacity is low
/// so it doesn't conflict with window opacity adjustments.
pub fn restored_ai_blocks_overlay(&self) -> Fill {
fg_overlay_3(self)
}
pub fn inactive_pane_overlay(&self) -> Fill {
fg_overlay_2(self)
}
pub fn subshell_background(&self) -> Fill {
Fill::Solid(neutral_4(self))
}
pub fn block_banner_background(&self) -> Fill {
Fill::Solid(neutral_3(self))
}
/// Background color for tooltips.
/// Uses neutral_6 for better contrast with text.
pub fn tooltip_background(&self) -> ColorU {
internal_colors::neutral_6(self)
}
}
// ANSI color blends
impl WarpTheme {
pub fn ansi_bg(&self, ansi_color: AnsiColor) -> ColorU {
let ansi_fill = Fill::from(ansi_color);
self.background()
.blend(&ansi_fill.with_opacity(50))
.into_solid()
}
pub fn ansi_fg(&self, ansi_color: AnsiColor) -> ColorU {
let ansi_fill = Fill::from(ansi_color);
self.foreground()
.blend(&ansi_fill.with_opacity(50))
.into_solid()
}
pub fn ansi_overlay_1(&self, ansi_color: AnsiColor) -> ColorU {
Fill::from(ansi_color).with_opacity(10).into_solid()
}
pub fn ansi_overlay_2(&self, ansi_color: AnsiColor) -> ColorU {
Fill::from(ansi_color).with_opacity(50).into_solid()
}
pub fn ansi_fg_red(&self) -> ColorU {
self.ansi_fg(AnsiColorIdentifier::Red.to_ansi_color(&self.terminal_colors().normal))
}
pub fn ansi_bg_red(&self) -> ColorU {
self.ansi_bg(AnsiColorIdentifier::Red.to_ansi_color(&self.terminal_colors().normal))
}
pub fn ansi_fg_blue(&self) -> ColorU {
self.ansi_fg(AnsiColorIdentifier::Blue.to_ansi_color(&self.terminal_colors().normal))
}
pub fn ansi_fg_green(&self) -> ColorU {
self.ansi_fg(AnsiColorIdentifier::Green.to_ansi_color(&self.terminal_colors().normal))
}
pub fn ansi_bg_green(&self) -> ColorU {
self.ansi_bg(AnsiColorIdentifier::Green.to_ansi_color(&self.terminal_colors().normal))
}
pub fn ansi_fg_yellow(&self) -> ColorU {
self.ansi_fg(AnsiColorIdentifier::Yellow.to_ansi_color(&self.terminal_colors().normal))
}
pub fn ansi_fg_magenta(&self) -> ColorU {
self.ansi_fg(AnsiColorIdentifier::Magenta.to_ansi_color(&self.terminal_colors().normal))
}
pub fn ansi_fg_cyan(&self) -> ColorU {
self.ansi_fg(AnsiColorIdentifier::Cyan.to_ansi_color(&self.terminal_colors().normal))
}
}
/// Internal color system tokens, defined in "Colors" [Figma project](https://www.figma.com/design/dnvTdLbfFaosFSP00F30S0/Colors).
/// Should not be used directly outside of reusable components. Use color methods on `WarpTheme` instead.
pub mod internal_colors {
use warpui::color::ColorU;
use super::{Fill, WarpTheme};
use crate::ui::color::blend::Blend;
use crate::ui::color::coloru_with_opacity;
/// Calculates the font color based on contrast needs for text legibility.
/// The font color is a mixture of the `warp_theme`'s background and foreground
/// colors, and the supplied `background` color.
fn font_color(warp_theme: &WarpTheme, background: impl Into<ColorU>) -> ColorU {
warp_theme.font_color(background).into_solid()
}
/// Used for UI elements like buttons to which we want to call attention.
/// Allows gradients so shouldn't be used for small elements.
pub fn accent(warp_theme: &WarpTheme) -> Fill {
warp_theme.accent()
}
/// Hover state for UI elements like buttons to which we want to call attention.
/// Allows gradients so shouldn't be used for small elements.
pub fn accent_hover(warp_theme: &WarpTheme) -> Fill {
warp_theme
.accent()
.blend(&warp_theme.foreground().with_opacity(40))
}
/// Pressed state for UI elements like buttons
/// to which we want to call attention.
/// Allows gradients so shouldn't be used for small elements.
#[allow(dead_code)]
pub fn accent_pressed(warp_theme: &WarpTheme) -> Fill {
warp_theme
.accent()
.blend(&warp_theme.background().with_opacity(30))
}
/// The color of most text throughout the UI.
pub fn text_main(warp_theme: &WarpTheme, background: impl Into<ColorU>) -> ColorU {
coloru_with_opacity(font_color(warp_theme, background), 90)
}
/// The color of subheaders and similar lower priority text.
pub fn text_sub(warp_theme: &WarpTheme, background: impl Into<ColorU>) -> ColorU {
coloru_with_opacity(font_color(warp_theme, background), 60)
}
/// The color of text elements that are disabled or the lowest priority.
pub fn text_disabled(warp_theme: &WarpTheme, background: impl Into<ColorU>) -> ColorU {
coloru_with_opacity(font_color(warp_theme, background), 40)
}
// TODO (roland): evaluate whether text_disabled above is intentionally different or if it should be consolidated with this
// which matches figma mocks.
pub fn semantic_text_disabled(warp_theme: &WarpTheme) -> ColorU {
warp_theme
.background()
.blend(&fg_overlay_5(warp_theme))
.into()
}
pub fn neutral_1(warp_theme: &WarpTheme) -> ColorU {
warp_theme
.background()
.blend(&warp_theme.foreground().with_opacity(5))
.into_solid()
}
pub fn neutral_2(warp_theme: &WarpTheme) -> ColorU {
warp_theme
.background()
.blend(&warp_theme.foreground().with_opacity(10))
.into_solid()
}
pub fn neutral_3(warp_theme: &WarpTheme) -> ColorU {
warp_theme
.background()
.blend(&warp_theme.foreground().with_opacity(15))
.into_solid()
}
pub fn neutral_4(warp_theme: &WarpTheme) -> ColorU {
warp_theme
.background()
.blend(&warp_theme.foreground().with_opacity(20))
.into_solid()
}
pub fn neutral_5(warp_theme: &WarpTheme) -> ColorU {
warp_theme
.background()
.blend(&warp_theme.foreground().with_opacity(40))
.into_solid()
}
pub fn neutral_6(warp_theme: &WarpTheme) -> ColorU {
warp_theme
.background()
.blend(&warp_theme.foreground().with_opacity(60))
.into_solid()
}
pub fn neutral_7(warp_theme: &WarpTheme) -> ColorU {
warp_theme
.background()
.blend(&warp_theme.foreground().with_opacity(90))
.into_solid()
}
pub fn fg_overlay_1(warp_theme: &WarpTheme) -> Fill {
warp_theme.foreground().with_opacity(5)
}
pub fn fg_overlay_2(warp_theme: &WarpTheme) -> Fill {
warp_theme.foreground().with_opacity(10)
}
pub fn fg_overlay_3(warp_theme: &WarpTheme) -> Fill {
warp_theme.foreground().with_opacity(15)
}
pub fn fg_overlay_4(warp_theme: &WarpTheme) -> Fill {
warp_theme.foreground().with_opacity(20)
}
pub fn fg_overlay_5(warp_theme: &WarpTheme) -> Fill {
warp_theme.foreground().with_opacity(40)
}
pub fn fg_overlay_6(warp_theme: &WarpTheme) -> Fill {
warp_theme.foreground().with_opacity(60)
}
pub fn fg_overlay_7(warp_theme: &WarpTheme) -> Fill {
warp_theme.foreground().with_opacity(90)
}
pub fn accent_bg_strong(warp_theme: &WarpTheme) -> Fill {
Fill::Solid(warp_theme.background().into_solid())
.blend(&warp_theme.accent().with_opacity(60))
}
pub fn accent_bg(warp_theme: &WarpTheme) -> Fill {
Fill::Solid(warp_theme.background().into_solid())
.blend(&warp_theme.accent().with_opacity(40))
}
pub fn accent_fg_strong(warp_theme: &WarpTheme) -> Fill {
warp_theme
.foreground()
.blend(&warp_theme.accent().with_opacity(60))
}
pub fn accent_fg(warp_theme: &WarpTheme) -> Fill {
warp_theme
.foreground()
.blend(&warp_theme.accent().with_opacity(40))
}
pub fn accent_overlay_1(warp_theme: &WarpTheme) -> Fill {
warp_theme.accent().with_opacity(10)
}
pub fn accent_overlay_2(warp_theme: &WarpTheme) -> Fill {
warp_theme.accent().with_opacity(25)
}
pub fn accent_overlay_3(warp_theme: &WarpTheme) -> Fill {
warp_theme.accent().with_opacity(40)
}
pub fn accent_overlay_4(warp_theme: &WarpTheme) -> Fill {
warp_theme.accent().with_opacity(60)
}
}
+697
View File
@@ -0,0 +1,697 @@
pub mod color;
pub mod phenomenon;
use std::path::PathBuf;
use crate::paths::themes_dir;
use super::color::{
blend::Blend,
coloru_with_opacity,
contrast::{pick_best_foreground_color, MinimumAllowedContrast},
hex_color, mid_coloru, ContrastingColor, Opacity, OPAQUE,
};
// Import relative_luminance from contrast module for brightness calculation
use crate::ui::color::contrast::relative_luminance;
use self::color::CustomDetails;
use dirs::home_dir;
use serde::{Deserialize, Serialize};
use warpui::{assets::asset_cache::AssetSource, color::ColorU, geometry::vector::vec2f};
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct Image {
pub source: AssetSource,
pub opacity: Opacity,
}
/// This is a helper struct used for deserialization.
#[derive(Clone, Debug, PartialEq, Eq, Deserialize, Serialize)]
struct SerializedBackgroundThemeImage {
path: String,
#[serde(default = "default_image_opacity")]
pub opacity: Opacity,
}
impl Serialize for Image {
// We only serialize Images that are sourced from local files. Currently,
// there is no need in our app to serialize a theme that contains a bundled image.
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let AssetSource::LocalFile { path } = self.source.clone() else {
return Err(serde::ser::Error::custom(
"image path was serialized but it's not a local file",
));
};
let serialized = SerializedBackgroundThemeImage {
path,
opacity: self.opacity,
};
serialized.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for Image {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
let value: SerializedBackgroundThemeImage =
SerializedBackgroundThemeImage::deserialize(deserializer)?;
// The user is allowed to specify a relative path. It's our responsibility to
// deserialize this into an absolute path.
let path = {
let expanded_path = expand_tilde(value.path.into());
if expanded_path.is_absolute() {
expanded_path
} else {
themes_dir().join(expanded_path)
}
};
Ok(Image {
source: AssetSource::LocalFile {
path: path.to_str().unwrap_or_default().to_owned(),
},
opacity: value.opacity,
})
}
}
/// Returns the default opacity for serde to use for an [`Image`] if one is not
/// specified.
fn default_image_opacity() -> Opacity {
100
}
/// Performs tilde expansion to expand a _leading_ tilde to the user's home dir. Any intermediate
/// tildes are not expanded. If the path does not begin with a tilde, then the existing path is
/// returned unchanged.
fn expand_tilde(path: PathBuf) -> PathBuf {
let home_dir = match home_dir() {
Some(home_dir) => home_dir,
None => return path,
};
match path.strip_prefix("~") {
Ok(stripped) => home_dir.join(stripped),
Err(_) => path,
}
}
impl Image {
pub fn source(&self) -> AssetSource {
self.source.clone()
}
}
#[derive(Copy, Clone, Debug, PartialEq, Eq)]
pub struct AnsiColor {
pub r: u8,
pub g: u8,
pub b: u8,
}
impl From<AnsiColor> for ColorU {
fn from(color: AnsiColor) -> Self {
ColorU {
r: color.r,
g: color.g,
b: color.b,
a: OPAQUE, // ansi colors are at full opacity
}
}
}
impl From<ColorU> for AnsiColor {
fn from(color: ColorU) -> Self {
AnsiColor {
r: color.r,
g: color.g,
b: color.b,
}
}
}
impl From<AnsiColor> for Fill {
fn from(color: AnsiColor) -> Fill {
Fill::Solid(color.into())
}
}
impl AnsiColor {
pub const fn from_u32(color: u32) -> Self {
AnsiColor {
r: (color >> 24) as u8,
g: ((color >> 16) & 0xff) as u8,
b: ((color >> 8) & 0xff) as u8,
}
}
}
#[derive(Serialize, Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
pub struct VerticalGradient {
#[serde(with = "hex_color")]
top: ColorU,
#[serde(with = "hex_color")]
bottom: ColorU,
}
impl VerticalGradient {
pub fn new(top: ColorU, bottom: ColorU) -> Self {
VerticalGradient { top, bottom }
}
fn midcolor(&self) -> ColorU {
mid_coloru(self.top, self.bottom)
}
pub fn get_most_opaque(&self) -> ColorU {
if self.top.a > self.bottom.a {
self.top
} else {
self.bottom
}
}
}
impl Blend for VerticalGradient {
type Output = VerticalGradient;
fn blend(&self, other: &VerticalGradient) -> VerticalGradient {
VerticalGradient::new(self.top.blend(&other.top), self.bottom.blend(&other.bottom))
}
}
impl ContrastingColor<ColorU> for VerticalGradient {
type Output = VerticalGradient;
fn on_background(
self,
background: ColorU,
minimum_allowed_contrast: MinimumAllowedContrast,
) -> VerticalGradient {
VerticalGradient::new(
self.top.on_background(background, minimum_allowed_contrast),
self.bottom
.on_background(background, minimum_allowed_contrast),
)
}
}
#[derive(Serialize, Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
pub struct HorizontalGradient {
#[serde(with = "hex_color")]
left: ColorU,
#[serde(with = "hex_color")]
right: ColorU,
}
impl HorizontalGradient {
pub fn new(left: ColorU, right: ColorU) -> Self {
HorizontalGradient { left, right }
}
fn midcolor(&self) -> ColorU {
mid_coloru(self.left, self.right)
}
pub fn get_most_opaque(&self) -> ColorU {
if self.left.a > self.right.a {
self.left
} else {
self.right
}
}
}
impl Blend for HorizontalGradient {
type Output = HorizontalGradient;
fn blend(&self, other: &HorizontalGradient) -> HorizontalGradient {
HorizontalGradient::new(self.left.blend(&other.left), self.right.blend(&other.right))
}
}
impl ContrastingColor<ColorU> for HorizontalGradient {
type Output = HorizontalGradient;
fn on_background(
self,
background: ColorU,
minimum_allowed_contrast: MinimumAllowedContrast,
) -> HorizontalGradient {
HorizontalGradient::new(
self.left
.on_background(background, minimum_allowed_contrast),
self.right
.on_background(background, minimum_allowed_contrast),
)
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ColorScheme {
/// Light foreground colors on a dark background (light mode).
LightOnDark,
/// Dark foreground colors on a light background (dark mode).
DarkOnLight,
}
impl ColorScheme {
fn infer_from_foreground_color(foreground_color: ColorU) -> Self {
// We actually are picking whether the foreground color is most visible
// on a light or dark _background_, despite the helper function name.
if pick_best_foreground_color(
foreground_color,
ColorU::white(),
ColorU::black(),
MinimumAllowedContrast::Text,
) == ColorU::white()
{
ColorScheme::DarkOnLight
} else {
ColorScheme::LightOnDark
}
}
}
#[derive(Serialize, Clone, Copy, Debug, Deserialize, PartialEq, Eq)]
#[serde(untagged, rename_all = "lowercase")]
pub enum Fill {
#[serde(with = "hex_color")]
Solid(ColorU),
VerticalGradient(VerticalGradient),
HorizontalGradient(HorizontalGradient),
}
impl Fill {
pub fn black() -> Fill {
Fill::Solid(ColorU::from_u32(0x000000ff))
}
pub fn white() -> Fill {
Fill::Solid(ColorU::from_u32(0xffffffff))
}
pub fn warn() -> Fill {
Fill::Solid(ColorU::from_u32(0xC28000FF))
}
pub fn error() -> Fill {
Fill::Solid(ColorU::new(188, 54, 42, 255))
}
// Translucent black used for blur backdrop
pub fn blur() -> Fill {
Fill::Solid(ColorU::new(0, 0, 0, 179))
}
/// Green color used for elements that show success status.
pub fn success() -> Fill {
Fill::Solid(ColorU::new(0, 142, 65, 255))
}
pub fn with_opacity(&self, opacity: Opacity) -> Self {
match self {
Fill::Solid(c) => Fill::Solid(coloru_with_opacity(*c, opacity)),
Fill::VerticalGradient(g) => Fill::VerticalGradient(VerticalGradient::new(
coloru_with_opacity(g.top, opacity),
coloru_with_opacity(g.bottom, opacity),
)),
Fill::HorizontalGradient(g) => Fill::HorizontalGradient(HorizontalGradient::new(
coloru_with_opacity(g.left, opacity),
coloru_with_opacity(g.right, opacity),
)),
}
}
/// Convert this fill into a solid color, taking the midpoint color for gradients
pub fn into_solid(self) -> ColorU {
match self {
Fill::Solid(c) => c,
Fill::VerticalGradient(g) => g.midcolor(),
Fill::HorizontalGradient(g) => g.midcolor(),
}
}
/// Convert this Fill into a solid color, taking the top color for vertical gradients and the
/// midpoint color for horizontal gradients.
pub fn into_solid_bias_top_color(self) -> ColorU {
match self {
Fill::Solid(c) => c,
Fill::VerticalGradient(g) => g.top,
Fill::HorizontalGradient(g) => g.midcolor(),
}
}
/// Convert this Fill into a solid color, taking the right color for horizontal gradients and
/// the midpoint color for vertical gradients.
pub fn into_solid_bias_right_color(self) -> ColorU {
match self {
Self::Solid(c) => c,
Self::HorizontalGradient(g) => g.right,
Self::VerticalGradient(g) => g.midcolor(),
}
}
/// Convert this Fill into a version of itself whose color is adaptively faded based on the
/// background brightness. Uses less aggressive fading on light backgrounds and more aggressive
/// fading on dark backgrounds to maintain optimal contrast.
pub fn fade_into_background(self, background_color: &Self) -> Self {
let background_luminance = relative_luminance(background_color.into_solid());
// Threshold for determining if background is "light" vs "dark"
// 0.5 is approximately middle gray in terms of perceived brightness
let is_light_background = background_luminance > 0.2;
// Use different opacity levels based on background brightness:
// - Light backgrounds: Use higher opacity (85%) to maintain contrast with focused diffs
// - Dark backgrounds: Use lower opacity (65%) since the contrast is naturally better
let fade_opacity = if is_light_background {
85 // More aggressive fading on light backgrounds
} else {
65 // Less aggressive fading on dark backgrounds
};
self.blend(&background_color.with_opacity(fade_opacity))
}
}
impl Blend for Fill {
type Output = Fill;
fn blend(&self, other: &Fill) -> Fill {
match (self, other) {
(Fill::Solid(c1), Fill::Solid(c2)) => Fill::Solid(c1.blend(c2)),
(Fill::VerticalGradient(g), Fill::Solid(c)) => {
Fill::VerticalGradient(VerticalGradient::new(g.top.blend(c), g.bottom.blend(c)))
}
(Fill::Solid(c), Fill::VerticalGradient(g)) => {
Fill::VerticalGradient(VerticalGradient::new(c.blend(&g.top), c.blend(&g.bottom)))
}
(Fill::HorizontalGradient(g), Fill::Solid(c)) => {
Fill::HorizontalGradient(HorizontalGradient::new(g.left.blend(c), g.right.blend(c)))
}
(Fill::Solid(c), Fill::HorizontalGradient(g)) => Fill::HorizontalGradient(
HorizontalGradient::new(c.blend(&g.left), c.blend(&g.right)),
),
(Fill::VerticalGradient(g1), Fill::VerticalGradient(g2)) => {
Fill::VerticalGradient(g1.blend(g2))
}
(Fill::HorizontalGradient(g1), Fill::HorizontalGradient(g2)) => {
Fill::HorizontalGradient(g1.blend(g2))
}
(Fill::HorizontalGradient(g1), Fill::VerticalGradient(g2)) => {
Fill::VerticalGradient(VerticalGradient::new(
g1.midcolor().blend(&g2.top),
g1.midcolor().blend(&g2.bottom),
))
}
(Fill::VerticalGradient(g1), Fill::HorizontalGradient(g2)) => {
Fill::HorizontalGradient(HorizontalGradient::new(
g1.midcolor().blend(&g2.left),
g1.midcolor().blend(&g2.right),
))
}
}
}
}
impl ContrastingColor for Fill {
type Output = Fill;
fn on_background(
self,
background: Fill,
minimum_allowed_contrast: MinimumAllowedContrast,
) -> Fill {
match self {
Fill::Solid(c) => {
Fill::Solid(c.on_background(background.into(), minimum_allowed_contrast))
}
Fill::HorizontalGradient(g) => Fill::HorizontalGradient(
g.on_background(background.into(), minimum_allowed_contrast),
),
Fill::VerticalGradient(g) => {
Fill::VerticalGradient(g.on_background(background.into(), minimum_allowed_contrast))
}
}
}
}
impl From<Fill> for warpui::elements::Fill {
fn from(theme: Fill) -> Self {
match theme {
Fill::Solid(c) => warpui::elements::Fill::Solid(c),
Fill::HorizontalGradient(g) => warpui::elements::Fill::Gradient {
start: vec2f(0.0, 0.0),
end: vec2f(1.0, 0.0),
start_color: g.left,
end_color: g.right,
},
Fill::VerticalGradient(g) => warpui::elements::Fill::Gradient {
start: vec2f(0.0, 0.0),
end: vec2f(0.0, 1.0),
start_color: g.top,
end_color: g.bottom,
},
}
}
}
impl From<Fill> for ColorU {
fn from(color: Fill) -> Self {
match color {
Fill::Solid(c) => c,
Fill::VerticalGradient(g) => g.midcolor(),
Fill::HorizontalGradient(g) => g.midcolor(),
}
}
}
impl From<ColorU> for Fill {
fn from(color: ColorU) -> Fill {
Fill::Solid(color)
}
}
#[derive(Serialize, Copy, Clone, Debug, Deserialize, PartialEq, Eq)]
pub struct AnsiColors {
#[serde(with = "hex_color")]
pub black: AnsiColor,
#[serde(with = "hex_color")]
pub red: AnsiColor,
#[serde(with = "hex_color")]
pub green: AnsiColor,
#[serde(with = "hex_color")]
pub yellow: AnsiColor,
#[serde(with = "hex_color")]
pub blue: AnsiColor,
#[serde(with = "hex_color")]
pub magenta: AnsiColor,
#[serde(with = "hex_color")]
pub cyan: AnsiColor,
#[serde(with = "hex_color")]
pub white: AnsiColor,
}
impl AnsiColors {
#[allow(clippy::too_many_arguments)]
pub const fn new(
black: AnsiColor,
red: AnsiColor,
green: AnsiColor,
yellow: AnsiColor,
blue: AnsiColor,
magenta: AnsiColor,
cyan: AnsiColor,
white: AnsiColor,
) -> Self {
AnsiColors {
black,
red,
green,
yellow,
blue,
magenta,
cyan,
white,
}
}
}
#[derive(
Serialize,
Copy,
Clone,
Debug,
Deserialize,
PartialEq,
Eq,
schemars::JsonSchema,
settings_value::SettingsValue,
)]
#[schemars(
description = "One of the eight standard ANSI terminal colors.",
rename_all = "snake_case"
)]
#[serde(rename_all = "lowercase")]
pub enum AnsiColorIdentifier {
Black,
Red,
Green,
Yellow,
Blue,
Magenta,
Cyan,
White,
}
impl std::fmt::Display for AnsiColorIdentifier {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
let color_name = match self {
Self::Black => "Black",
Self::Red => "Red",
Self::Green => "Green",
Self::Yellow => "Yellow",
Self::Blue => "Blue",
Self::Magenta => "Magenta",
Self::Cyan => "Cyan",
Self::White => "White",
};
write!(f, "{color_name}")
}
}
impl AnsiColorIdentifier {
pub fn to_ansi_color(self, colors: &AnsiColors) -> AnsiColor {
match self {
Self::Black => colors.black,
Self::Red => colors.red,
Self::Green => colors.green,
Self::Yellow => colors.yellow,
Self::Blue => colors.blue,
Self::Magenta => colors.magenta,
Self::Cyan => colors.cyan,
Self::White => colors.white,
}
}
}
#[derive(Serialize, Clone, Debug, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Details {
Darker,
Lighter,
Custom(CustomDetails),
}
#[derive(Serialize, Clone, Debug, Deserialize, PartialEq, Eq)]
pub struct TerminalColors {
pub normal: AnsiColors,
pub bright: AnsiColors,
}
impl TerminalColors {
pub fn new(normal: AnsiColors, bright: AnsiColors) -> Self {
TerminalColors { normal, bright }
}
}
#[derive(Serialize, Clone, Debug, Deserialize, PartialEq, Eq)]
pub struct WarpTheme {
background: Fill,
accent: Fill,
#[serde(with = "hex_color")]
foreground: ColorU,
#[serde(default, skip_serializing_if = "Option::is_none")]
cursor: Option<Fill>,
#[serde(skip_serializing_if = "Option::is_none")]
background_image: Option<Image>,
details: Details,
terminal_colors: TerminalColors,
// If name is None, we construct the name by processing the theme .yaml file name
name: Option<String>,
}
impl WarpTheme {
#[allow(clippy::too_many_arguments)]
pub fn new(
bg: Fill,
foreground: ColorU,
accent: Fill,
cursor: Option<Fill>,
details: Option<Details>,
terminal_colors: TerminalColors,
background_image: Option<Image>,
name: Option<String>,
) -> Self {
WarpTheme {
background: bg,
foreground,
accent,
cursor,
details: details.unwrap_or_else(|| Details::Custom(CustomDetails::default())),
terminal_colors,
background_image,
name,
}
}
pub fn name(&self) -> Option<String> {
self.name.clone()
}
pub fn set_name(&mut self, name: String) {
self.name = Some(name);
}
pub fn details(&self) -> CustomDetails {
match self.details {
Details::Darker => CustomDetails::darker_details(),
Details::Lighter => CustomDetails::lighter_details(),
Details::Custom(details) => details,
}
}
pub fn inferred_color_scheme(&self) -> ColorScheme {
ColorScheme::infer_from_foreground_color(self.foreground)
}
pub fn background_image(&self) -> Option<Image> {
self.background_image.clone()
}
}
#[cfg(any(test, feature = "test-util"))]
pub fn mock_terminal_colors() -> TerminalColors {
TerminalColors::new(
AnsiColors::new(
AnsiColor::from_u32(0x616161FF),
AnsiColor::from_u32(0xFF8272FF),
AnsiColor::from_u32(0xB4FA72FF),
AnsiColor::from_u32(0xFEFDC2FF),
AnsiColor::from_u32(0xA5D5FEFF),
AnsiColor::from_u32(0xFF8FFDFF),
AnsiColor::from_u32(0xD0D1FEFF),
AnsiColor::from_u32(0xF1F1F1FF),
),
AnsiColors::new(
AnsiColor::from_u32(0x8E8E8EFF),
AnsiColor::from_u32(0xFFC4BDFF),
AnsiColor::from_u32(0xD6FCB9FF),
AnsiColor::from_u32(0xFEFDD5FF),
AnsiColor::from_u32(0xC1E3FEFF),
AnsiColor::from_u32(0xFFB1FEFF),
AnsiColor::from_u32(0xE5E6FEFF),
AnsiColor::from_u32(0xFEFFFFFF),
),
)
}
#[cfg(test)]
#[path = "theme_tests.rs"]
mod tests;
+158
View File
@@ -0,0 +1,158 @@
use warpui::color::ColorU;
use crate::ui::color::blend::Blend;
use super::Fill;
const PHENOMENON_BACKGROUND: u32 = 0x121212FF;
const PHENOMENON_FOREGROUND: u32 = 0xFAF9F6FF;
const PHENOMENON_ACCENT: u32 = 0x2E5D9EFF;
const PHENOMENON_BLUE: u32 = 0x3780E9FF;
const PHENOMENON_BODY_TEXT: u32 = 0xFAF9F6E5;
const PHENOMENON_LABEL_TEXT: u32 = 0xFAF9F699;
const PHENOMENON_DISABLED_LABEL_TEXT: u32 = 0xFAF9F680;
const PHENOMENON_SUBTLE_BORDER: u32 = 0xFAF9F633;
const PHENOMENON_MODAL_BACKGROUND: u32 = 0x2A2A2AFF;
const PHENOMENON_MODAL_BADGE_BACKGROUND: u32 = 0xFF8FFD1A;
const PHENOMENON_MODAL_BADGE_TEXT: u32 = 0xFF8FFDFF;
const PHENOMENON_MODAL_TITLE_TEXT: u32 = 0xFFFFFFFF;
const PHENOMENON_MODAL_FEATURE_TITLE_TEXT: u32 = 0xE6E6E6FF;
const PHENOMENON_MODAL_FEATURE_DESCRIPTION_TEXT: u32 = 0x9B9B9BFF;
const PHENOMENON_MODAL_BUTTON_BACKGROUND: u32 = 0xFFFFFFFF;
const PHENOMENON_MODAL_BUTTON_TEXT: u32 = 0x050505FF;
const PHENOMENON_MODAL_BUTTON_HOVER_OVERLAY: u32 = 0x0505051F;
const PHENOMENON_MODAL_CLOSE_BUTTON_TEXT: u32 = 0xFFFFFFFF;
const PHENOMENON_MODAL_CLOSE_BUTTON_HOVER: u32 = 0x050505BF;
pub struct PhenomenonStyle;
impl PhenomenonStyle {
pub fn background() -> ColorU {
ColorU::from_u32(PHENOMENON_BACKGROUND)
}
pub fn foreground() -> ColorU {
ColorU::from_u32(PHENOMENON_FOREGROUND)
}
pub fn accent() -> ColorU {
ColorU::from_u32(PHENOMENON_ACCENT)
}
pub fn blue() -> ColorU {
ColorU::from_u32(PHENOMENON_BLUE)
}
pub fn body_text() -> ColorU {
ColorU::from_u32(PHENOMENON_BODY_TEXT)
}
pub fn label_text() -> ColorU {
ColorU::from_u32(PHENOMENON_LABEL_TEXT)
}
pub fn disabled_label_text() -> ColorU {
ColorU::from_u32(PHENOMENON_DISABLED_LABEL_TEXT)
}
pub fn subtle_border() -> ColorU {
ColorU::from_u32(PHENOMENON_SUBTLE_BORDER)
}
pub fn modal_background() -> ColorU {
ColorU::from_u32(PHENOMENON_MODAL_BACKGROUND)
}
pub fn modal_badge_background() -> ColorU {
ColorU::from_u32(PHENOMENON_MODAL_BADGE_BACKGROUND)
}
pub fn modal_badge_text() -> ColorU {
ColorU::from_u32(PHENOMENON_MODAL_BADGE_TEXT)
}
pub fn modal_title_text() -> ColorU {
ColorU::from_u32(PHENOMENON_MODAL_TITLE_TEXT)
}
pub fn modal_feature_title_text() -> ColorU {
ColorU::from_u32(PHENOMENON_MODAL_FEATURE_TITLE_TEXT)
}
pub fn modal_feature_description_text() -> ColorU {
ColorU::from_u32(PHENOMENON_MODAL_FEATURE_DESCRIPTION_TEXT)
}
pub fn modal_button_background() -> ColorU {
ColorU::from_u32(PHENOMENON_MODAL_BUTTON_BACKGROUND)
}
pub fn modal_button_text() -> ColorU {
ColorU::from_u32(PHENOMENON_MODAL_BUTTON_TEXT)
}
pub fn modal_button_hover_overlay() -> ColorU {
ColorU::from_u32(PHENOMENON_MODAL_BUTTON_HOVER_OVERLAY)
}
pub fn modal_close_button_text() -> ColorU {
ColorU::from_u32(PHENOMENON_MODAL_CLOSE_BUTTON_TEXT)
}
pub fn modal_close_button_hover() -> ColorU {
ColorU::from_u32(PHENOMENON_MODAL_CLOSE_BUTTON_HOVER)
}
pub fn tinted_surface() -> Fill {
Fill::Solid(Self::background()).blend(&Fill::Solid(Self::blue()).with_opacity(50))
}
pub fn surface_border() -> ColorU {
Self::blue()
}
pub fn primary_button_background(hovered: bool) -> Fill {
Fill::Solid(if hovered {
Self::blue()
} else {
Self::accent()
})
}
pub fn primary_button_text() -> ColorU {
Self::foreground()
}
pub fn modal_button_background_fill(hovered: bool) -> Fill {
if hovered {
Fill::Solid(Self::modal_button_background())
.blend(&Fill::Solid(Self::modal_button_hover_overlay()))
} else {
Fill::Solid(Self::modal_button_background())
}
}
pub fn segmented_control_background() -> Fill {
Fill::Solid(Self::foreground()).with_opacity(8)
}
pub fn selected_chip_background() -> Fill {
Fill::Solid(Self::foreground())
}
pub fn selected_chip_text() -> ColorU {
Self::background()
}
pub fn selected_chip_border() -> Fill {
Fill::Solid(Self::accent())
}
pub fn unselected_chip_background() -> Fill {
Fill::Solid(Self::foreground()).with_opacity(8)
}
pub fn unselected_chip_text() -> ColorU {
Self::body_text()
}
}
@@ -0,0 +1,341 @@
use super::*;
#[test]
fn serialize_test() {
let theme = WarpTheme::new(
Fill::Solid(ColorU::from_u32(0x20A5BAFF)),
ColorU::from_u32(0x20A5BAFF),
Fill::Solid(ColorU::from_u32(0x20A5BAFF)),
None,
Some(Details::Darker),
mock_terminal_colors(),
None,
Some("test_theme".to_string()),
);
assert_eq!(
r##"---
background: "#20a5ba"
accent: "#20a5ba"
foreground: "#20a5ba"
details: darker
terminal_colors:
normal:
black: "#616161"
red: "#ff8272"
green: "#b4fa72"
yellow: "#fefdc2"
blue: "#a5d5fe"
magenta: "#ff8ffd"
cyan: "#d0d1fe"
white: "#f1f1f1"
bright:
black: "#8e8e8e"
red: "#ffc4bd"
green: "#d6fcb9"
yellow: "#fefdd5"
blue: "#c1e3fe"
magenta: "#ffb1fe"
cyan: "#e5e6fe"
white: "#feffff"
name: test_theme
"##,
serde_yaml::to_string(&theme).expect("Couldn't serialize")
);
}
#[test]
fn deserialize_with_name_test() {
let theme = serde_yaml::from_str::<WarpTheme>(
r##"---
background: "#20a5ba"
accent: "#20a5ba"
foreground: "#20a5ba"
details: darker
terminal_colors:
normal:
black: "#616161"
red: "#ff8272"
green: "#b4fa72"
yellow: "#fefdc2"
blue: "#a5d5fe"
magenta: "#ff8ffd"
cyan: "#d0d1fe"
white: "#f1f1f1"
bright:
black: "#8e8e8e"
red: "#ffc4bd"
green: "#d6fcb9"
yellow: "#fefdd5"
blue: "#c1e3fe"
magenta: "#ffb1fe"
cyan: "#e5e6fe"
white: "#feffff"
name: test_theme
"##,
)
.expect("Couldn't deserialize");
let expected_theme = WarpTheme::new(
Fill::Solid(ColorU::from_u32(0x20A5BAFF)),
ColorU::from_u32(0x20A5BAFF),
Fill::Solid(ColorU::from_u32(0x20A5BAFF)),
None,
Some(Details::Darker),
mock_terminal_colors(),
None,
Some("test_theme".to_string()),
);
assert_eq!(expected_theme, theme);
}
#[test]
fn deserialize_without_name_test() {
let theme = serde_yaml::from_str::<WarpTheme>(
r##"---
background: "#20a5ba"
accent: "#20a5ba"
foreground: "#20a5ba"
details: darker
terminal_colors:
normal:
black: "#616161"
red: "#ff8272"
green: "#b4fa72"
yellow: "#fefdc2"
blue: "#a5d5fe"
magenta: "#ff8ffd"
cyan: "#d0d1fe"
white: "#f1f1f1"
bright:
black: "#8e8e8e"
red: "#ffc4bd"
green: "#d6fcb9"
yellow: "#fefdd5"
blue: "#c1e3fe"
magenta: "#ffb1fe"
cyan: "#e5e6fe"
white: "#feffff"
"##,
)
.expect("Couldn't deserialize");
let expected_theme = WarpTheme::new(
Fill::Solid(ColorU::from_u32(0x20A5BAFF)),
ColorU::from_u32(0x20A5BAFF),
Fill::Solid(ColorU::from_u32(0x20A5BAFF)),
None,
Some(Details::Darker),
mock_terminal_colors(),
None,
None,
);
assert_eq!(expected_theme, theme);
}
#[test]
fn blend_gradient_test() {
let (c1, c2, c3, c4) = (
ColorU::from_u32(0x002b36ff),
ColorU::from_u32(0xcb4b16ff),
ColorU::from_u32(0xffffff19),
ColorU::from_u32(0xffffff19),
);
let g1 = VerticalGradient::new(c1, c2);
let g2 = VerticalGradient::new(c3, c4);
assert_eq!(
g1.blend(&g2),
VerticalGradient::new(c1.blend(&c3), c2.blend(&c4))
);
}
#[test]
fn blend_coloru_test() {
let c1 = ColorU::from_u32(0x002b36ff);
let c2 = ColorU::from_u32(0xF8F8F2FF);
assert_eq!(
c1.blend(&coloru_with_opacity(c2, 10)),
ColorU::from_u32(0x183f48ff)
);
assert_eq!(
ColorU::from_u32(0x000000ff).blend(&coloru_with_opacity(c2, 10)),
ColorU::from_u32(0x181818ff)
);
}
/// TODO(CORE-3626): write an equivalent test with Windows paths.
#[cfg(not(windows))]
#[test]
fn test_deserialize_image() {
// Paths that start with `~` should expand to include the home dir.
let a = "
path: ~/warp.jpg
opacity: 60
";
let image: Image = serde_yaml::from_str(a).unwrap();
assert_eq!(image.opacity, 60);
assert_eq!(
image.source,
AssetSource::LocalFile {
path: home_dir()
.unwrap()
.join("warp.jpg")
.to_str()
.unwrap_or_default()
.to_owned()
}
);
// Absolute paths should be unchanged.
let b = "
path: /warp.jpg
opacity: 60
";
let image: Image = serde_yaml::from_str(b).unwrap();
assert_eq!(image.opacity, 60);
assert_eq!(
image.source,
AssetSource::LocalFile {
path: "/warp.jpg".to_owned()
}
);
// Relative paths should expand to include the theme dir.
let c = "
path: warp.jpg
opacity: 60
";
let image: Image = serde_yaml::from_str(c).unwrap();
assert_eq!(image.opacity, 60);
assert_eq!(
image.source,
AssetSource::LocalFile {
path: themes_dir()
.join("warp.jpg")
.to_str()
.unwrap_or_default()
.to_owned()
}
);
// No opacity should become the default
let d = "
path: warp.jpg
";
let image: Image = serde_yaml::from_str(d).unwrap();
assert_eq!(image.opacity, default_image_opacity());
}
#[test]
fn ansi_color_deserializing_test() {
let raw = r##"
black: "#000000"
red: "#ff0000"
green: "#00ff00"
yellow: "#00ffff"
blue: "#0000ff"
magenta: "#ff0000"
cyan: "#0000ff"
white: "#ffffff"
"##;
let ansi_colors: AnsiColors = serde_yaml::from_str(raw).expect("Couldn't deserialize");
assert_eq!(ansi_colors.black, AnsiColor::from_u32(0x000000ff));
assert_eq!(ansi_colors.red, AnsiColor::from_u32(0xff0000ff));
assert_eq!(ansi_colors.green, AnsiColor::from_u32(0x00ff00ff));
assert_eq!(ansi_colors.yellow, AnsiColor::from_u32(0x00ffffff));
assert_eq!(ansi_colors.blue, AnsiColor::from_u32(0x0000ffff));
assert_eq!(ansi_colors.magenta, AnsiColor::from_u32(0xff0000ff));
assert_eq!(ansi_colors.cyan, AnsiColor::from_u32(0x0000ffff));
assert_eq!(ansi_colors.white, AnsiColor::from_u32(0xffffffff));
}
#[test]
fn ansi_color_serializing_test() {
let ansi_colors = AnsiColors::new(
AnsiColor::from_u32(0x000000ff),
AnsiColor::from_u32(0xff0000ff),
AnsiColor::from_u32(0x00ff00ff),
AnsiColor::from_u32(0x00ffffff),
AnsiColor::from_u32(0x0000ffff),
AnsiColor::from_u32(0xff0000ff),
AnsiColor::from_u32(0x0000ffff),
AnsiColor::from_u32(0xffffffff),
);
let serialized = serde_yaml::to_string(&ansi_colors).expect("Couldn't serialize");
let raw = r##"---
black: "#000000"
red: "#ff0000"
green: "#00ff00"
yellow: "#00ffff"
blue: "#0000ff"
magenta: "#ff0000"
cyan: "#0000ff"
white: "#ffffff"
"##;
assert_eq!(serialized, raw);
let ansi_colors2: AnsiColors = serde_yaml::from_str(&serialized).expect("Couldn't deserialize");
assert_eq!(ansi_colors2, ansi_colors);
}
#[test]
fn from_hex_negative_test() {
assert_eq!(
hex_color::coloru_from_hex_string("#0").unwrap_err(),
hex_color::HexColorError::InvalidLength
);
assert_eq!(
hex_color::coloru_from_hex_string("#00").unwrap_err(),
hex_color::HexColorError::InvalidLength
);
assert_eq!(
hex_color::coloru_from_hex_string("#00000").unwrap_err(),
hex_color::HexColorError::InvalidLength
);
assert_eq!(
hex_color::coloru_from_hex_string("#0000000").unwrap_err(),
hex_color::HexColorError::InvalidLength
);
assert_eq!(
hex_color::coloru_from_hex_string("0000").unwrap_err(),
hex_color::HexColorError::HashPrefix
);
assert_eq!(
hex_color::coloru_from_hex_string("#ZXD").unwrap_err(),
hex_color::HexColorError::InvalidValue
);
}
#[test]
fn from_hex_positive_test() {
assert_eq!(
hex_color::coloru_from_hex_string("#000").unwrap(),
ColorU::from_u32(0x000000ff)
);
assert_eq!(
hex_color::coloru_from_hex_string("#000000").unwrap(),
ColorU::from_u32(0x000000ff)
);
assert_eq!(
hex_color::coloru_from_hex_string("#123").unwrap(),
ColorU::from_u32(0x112233ff)
);
assert_eq!(
hex_color::coloru_from_hex_string("#112233").unwrap(),
ColorU::from_u32(0x112233ff)
);
}
#[test]
fn infer_from_foreground_color_test() {
assert_eq!(
ColorScheme::infer_from_foreground_color(ColorU::white()),
ColorScheme::LightOnDark
);
assert_eq!(
ColorScheme::infer_from_foreground_color(ColorU::black()),
ColorScheme::DarkOnLight
);
}