Initial public release of Warp.
Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
@@ -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
@@ -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))
|
||||
);
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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())
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
@@ -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
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user