Initial public release of Warp.
Repo-Sync-Origin: warpdotdev/warp-internal@12af1d983b
This commit is contained in:
@@ -0,0 +1,631 @@
|
||||
use asset_macro::bundled_or_fetched_asset;
|
||||
use pathfinder_color::ColorU;
|
||||
use warp_core::ui::{
|
||||
color::{blend::Blend, coloru_with_opacity, OPAQUE},
|
||||
theme::{
|
||||
color::CustomDetails, AnsiColor, AnsiColors, Details, Fill, HorizontalGradient, Image,
|
||||
TerminalColors, VerticalGradient, WarpTheme,
|
||||
},
|
||||
};
|
||||
|
||||
const DARK_MODE_NORMAL_COLORS: AnsiColors = 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),
|
||||
);
|
||||
const DARK_MODE_BRIGHT_COLORS: AnsiColors = 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),
|
||||
);
|
||||
|
||||
const LIGHT_MODE_NORMAL_COLORS: AnsiColors = AnsiColors::new(
|
||||
AnsiColor::from_u32(0x212121FF),
|
||||
AnsiColor::from_u32(0xC30771FF),
|
||||
AnsiColor::from_u32(0x10A778FF),
|
||||
AnsiColor::from_u32(0xA89C14FF),
|
||||
AnsiColor::from_u32(0x008EC4FF),
|
||||
AnsiColor::from_u32(0x523C79FF),
|
||||
AnsiColor::from_u32(0x20A5BAFF),
|
||||
AnsiColor::from_u32(0xE0E0E0FF),
|
||||
);
|
||||
const LIGHT_MODE_BRIGHT_COLORS: AnsiColors = AnsiColors::new(
|
||||
AnsiColor::from_u32(0x212121FF),
|
||||
AnsiColor::from_u32(0xFB007AFF),
|
||||
AnsiColor::from_u32(0x5FD7AFFF),
|
||||
AnsiColor::from_u32(0xF3E430FF),
|
||||
AnsiColor::from_u32(0x20BBFCFF),
|
||||
AnsiColor::from_u32(0x6855DEFF),
|
||||
AnsiColor::from_u32(0x4FB8CCFF),
|
||||
AnsiColor::from_u32(0xF1F1F1FF),
|
||||
);
|
||||
|
||||
const SOLARIZED_DARK_NORMAL_COLORS: AnsiColors = AnsiColors::new(
|
||||
AnsiColor::from_u32(0x073642FF),
|
||||
AnsiColor::from_u32(0xDC322FFF),
|
||||
AnsiColor::from_u32(0x859900FF),
|
||||
AnsiColor::from_u32(0xB58900FF),
|
||||
AnsiColor::from_u32(0x268BD2FF),
|
||||
AnsiColor::from_u32(0xD33682FF),
|
||||
AnsiColor::from_u32(0x2AA198FF),
|
||||
AnsiColor::from_u32(0xEEE8D5FF),
|
||||
);
|
||||
const SOLARIZED_DARK_BRIGHT_COLORS: AnsiColors = AnsiColors::new(
|
||||
AnsiColor::from_u32(0x002B36FF),
|
||||
AnsiColor::from_u32(0xCB4B16FF),
|
||||
AnsiColor::from_u32(0x586E75FF),
|
||||
AnsiColor::from_u32(0x657B83FF),
|
||||
AnsiColor::from_u32(0x839496FF),
|
||||
AnsiColor::from_u32(0x6C71C4FF),
|
||||
AnsiColor::from_u32(0x93A1A1FF),
|
||||
AnsiColor::from_u32(0xFDF6E3FF),
|
||||
);
|
||||
|
||||
const SOLARIZED_LIGHT_NORMAL_COLORS: AnsiColors = AnsiColors::new(
|
||||
AnsiColor::from_u32(0x073642FF),
|
||||
AnsiColor::from_u32(0xDC322FFF),
|
||||
AnsiColor::from_u32(0x859900FF),
|
||||
AnsiColor::from_u32(0xB58900FF),
|
||||
AnsiColor::from_u32(0x268BD2FF),
|
||||
AnsiColor::from_u32(0xD33682FF),
|
||||
AnsiColor::from_u32(0x2AA198FF),
|
||||
AnsiColor::from_u32(0xEEE8D5FF),
|
||||
);
|
||||
const SOLARIZED_LIGHT_BRIGHT_COLORS: AnsiColors = AnsiColors::new(
|
||||
AnsiColor::from_u32(0x002B36FF),
|
||||
AnsiColor::from_u32(0xCB4B16FF),
|
||||
AnsiColor::from_u32(0x586E75FF),
|
||||
AnsiColor::from_u32(0x657B83FF),
|
||||
AnsiColor::from_u32(0x839496FF),
|
||||
AnsiColor::from_u32(0x6C71C4FF),
|
||||
AnsiColor::from_u32(0x93A1A1FF),
|
||||
AnsiColor::from_u32(0xFDF6E3FF),
|
||||
);
|
||||
|
||||
const DRACULA_NORMAL_COLORS: AnsiColors = AnsiColors::new(
|
||||
AnsiColor::from_u32(0x000000FF),
|
||||
AnsiColor::from_u32(0xFF5555FF),
|
||||
AnsiColor::from_u32(0x50FA7BFF),
|
||||
AnsiColor::from_u32(0xF1FA8CFF),
|
||||
AnsiColor::from_u32(0xBD93F9FF),
|
||||
AnsiColor::from_u32(0xFF79C6FF),
|
||||
AnsiColor::from_u32(0x8BE9FDFF),
|
||||
AnsiColor::from_u32(0xBBBBBBFF),
|
||||
);
|
||||
const DRACULA_BRIGHT_COLORS: AnsiColors = AnsiColors::new(
|
||||
AnsiColor::from_u32(0x555555FF),
|
||||
AnsiColor::from_u32(0xFF5555FF),
|
||||
AnsiColor::from_u32(0x50FA7BFF),
|
||||
AnsiColor::from_u32(0xF1FA8CFF),
|
||||
AnsiColor::from_u32(0xCAA9FAFF),
|
||||
AnsiColor::from_u32(0xFF79C6FF),
|
||||
AnsiColor::from_u32(0x8BE9FDFF),
|
||||
AnsiColor::from_u32(0xFFFFFFFF),
|
||||
);
|
||||
|
||||
const PHENOMENON_NORMAL_COLORS: AnsiColors = AnsiColors::new(
|
||||
AnsiColor::from_u32(0x121212FF),
|
||||
AnsiColor::from_u32(0xD22D1EFF),
|
||||
AnsiColor::from_u32(0x1CA05AFF),
|
||||
AnsiColor::from_u32(0xE5A01AFF),
|
||||
AnsiColor::from_u32(0x3780E9FF),
|
||||
AnsiColor::from_u32(0xBF409DFF),
|
||||
AnsiColor::from_u32(0x799C92FF),
|
||||
AnsiColor::from_u32(0xFAF9F6FF),
|
||||
);
|
||||
const PHENOMENON_BRIGHT_COLORS: AnsiColors = AnsiColors::new(
|
||||
AnsiColor::from_u32(0x292929FF),
|
||||
AnsiColor::from_u32(0xAE756FFF),
|
||||
AnsiColor::from_u32(0x789B88FF),
|
||||
AnsiColor::from_u32(0xBD9F65FF),
|
||||
AnsiColor::from_u32(0x6F839FFF),
|
||||
AnsiColor::from_u32(0xA57899FF),
|
||||
AnsiColor::from_u32(0xBFC5C3FF),
|
||||
AnsiColor::from_u32(0xFFFFFFFF),
|
||||
);
|
||||
|
||||
const GRUVBOX_DARK_NORMAL_COLORS: AnsiColors = AnsiColors::new(
|
||||
AnsiColor::from_u32(0x282828FF),
|
||||
AnsiColor::from_u32(0xCC241DFF),
|
||||
AnsiColor::from_u32(0x98971AFF),
|
||||
AnsiColor::from_u32(0xD79921FF),
|
||||
AnsiColor::from_u32(0x458588FF),
|
||||
AnsiColor::from_u32(0xB16286FF),
|
||||
AnsiColor::from_u32(0x689D6AFF),
|
||||
AnsiColor::from_u32(0xA89984FF),
|
||||
);
|
||||
const GRUVBOX_DARK_BRIGHT_COLORS: AnsiColors = AnsiColors::new(
|
||||
AnsiColor::from_u32(0x928374FF),
|
||||
AnsiColor::from_u32(0xFB4934FF),
|
||||
AnsiColor::from_u32(0xB8BB26FF),
|
||||
AnsiColor::from_u32(0xFABD2FFF),
|
||||
AnsiColor::from_u32(0x83A598FF),
|
||||
AnsiColor::from_u32(0xD3869BFF),
|
||||
AnsiColor::from_u32(0x8EC07CFF),
|
||||
AnsiColor::from_u32(0xEBDBB2FF),
|
||||
);
|
||||
|
||||
const GRUVBOX_LIGHT_NORMAL_COLORS: AnsiColors = AnsiColors::new(
|
||||
AnsiColor::from_u32(0xFBF1C7FF),
|
||||
AnsiColor::from_u32(0xCC241DFF),
|
||||
AnsiColor::from_u32(0x98971AFF),
|
||||
AnsiColor::from_u32(0xD79921FF),
|
||||
AnsiColor::from_u32(0x458588FF),
|
||||
AnsiColor::from_u32(0xB16286FF),
|
||||
AnsiColor::from_u32(0x689D6AFF),
|
||||
AnsiColor::from_u32(0x7C6F64FF),
|
||||
);
|
||||
const GRUVBOX_LIGHT_BRIGHT_COLORS: AnsiColors = AnsiColors::new(
|
||||
AnsiColor::from_u32(0x928374FF),
|
||||
AnsiColor::from_u32(0x9D0006FF),
|
||||
AnsiColor::from_u32(0x79740EFF),
|
||||
AnsiColor::from_u32(0xB57614FF),
|
||||
AnsiColor::from_u32(0x076678FF),
|
||||
AnsiColor::from_u32(0x8F3F71FF),
|
||||
AnsiColor::from_u32(0x427B58FF),
|
||||
AnsiColor::from_u32(0x3C3836FF),
|
||||
);
|
||||
|
||||
const SOLARFLARE_NORMAL_COLORS: AnsiColors = AnsiColors::new(
|
||||
AnsiColor::from_u32(0x2E333DFF),
|
||||
AnsiColor::from_u32(0xD66060FF),
|
||||
AnsiColor::from_u32(0x64AF86FF),
|
||||
AnsiColor::from_u32(0xCAA358FF),
|
||||
AnsiColor::from_u32(0x5C80B2FF),
|
||||
AnsiColor::from_u32(0xB766A1FF),
|
||||
AnsiColor::from_u32(0x8069A1FF),
|
||||
AnsiColor::from_u32(0xF0F4F7FF),
|
||||
);
|
||||
const SOLARFLARE_BRIGHT_COLORS: AnsiColors = AnsiColors::new(
|
||||
AnsiColor::from_u32(0x37404AFF),
|
||||
AnsiColor::from_u32(0xEB8282FF),
|
||||
AnsiColor::from_u32(0x64AF86FF),
|
||||
AnsiColor::from_u32(0xCAA358FF),
|
||||
AnsiColor::from_u32(0x5C80B2FF),
|
||||
AnsiColor::from_u32(0xB766A1FF),
|
||||
AnsiColor::from_u32(0x8069A1FF),
|
||||
AnsiColor::from_u32(0xFFFFFFFF),
|
||||
);
|
||||
|
||||
const ADEBERRY_NORMAL_COLORS: AnsiColors = AnsiColors::new(
|
||||
AnsiColor::from_u32(0x121212FF),
|
||||
AnsiColor::from_u32(0xC76156FF),
|
||||
AnsiColor::from_u32(0x57C78AFF),
|
||||
AnsiColor::from_u32(0xC8A35AFF),
|
||||
AnsiColor::from_u32(0x5785C7FF),
|
||||
AnsiColor::from_u32(0xC756A9FF),
|
||||
AnsiColor::from_u32(0x57C7C3FF),
|
||||
AnsiColor::from_u32(0xEEEDEBFF),
|
||||
);
|
||||
const ADEBERRY_BRIGHT_COLORS: AnsiColors = AnsiColors::new(
|
||||
AnsiColor::from_u32(0x292929FF),
|
||||
AnsiColor::from_u32(0xD22D1EFF),
|
||||
AnsiColor::from_u32(0x1CA05AFF),
|
||||
AnsiColor::from_u32(0xE5A01AFF),
|
||||
AnsiColor::from_u32(0x1458B8FF),
|
||||
AnsiColor::from_u32(0xA43787FF),
|
||||
AnsiColor::from_u32(0x4D9989FF),
|
||||
AnsiColor::from_u32(0xFFFFFFFF),
|
||||
);
|
||||
|
||||
pub(super) fn light_mode_colors() -> TerminalColors {
|
||||
TerminalColors::new(LIGHT_MODE_NORMAL_COLORS, LIGHT_MODE_BRIGHT_COLORS)
|
||||
}
|
||||
|
||||
pub(super) fn dark_mode_colors() -> TerminalColors {
|
||||
TerminalColors::new(DARK_MODE_NORMAL_COLORS, DARK_MODE_BRIGHT_COLORS)
|
||||
}
|
||||
|
||||
pub(super) fn solarized_light_colors() -> TerminalColors {
|
||||
TerminalColors::new(SOLARIZED_LIGHT_NORMAL_COLORS, SOLARIZED_LIGHT_BRIGHT_COLORS)
|
||||
}
|
||||
|
||||
pub(super) fn solarized_dark_colors() -> TerminalColors {
|
||||
TerminalColors::new(SOLARIZED_DARK_NORMAL_COLORS, SOLARIZED_DARK_BRIGHT_COLORS)
|
||||
}
|
||||
|
||||
pub(super) fn dracula_colors() -> TerminalColors {
|
||||
TerminalColors::new(DRACULA_NORMAL_COLORS, DRACULA_BRIGHT_COLORS)
|
||||
}
|
||||
|
||||
pub(super) fn phenomenon_colors() -> TerminalColors {
|
||||
TerminalColors::new(PHENOMENON_NORMAL_COLORS, PHENOMENON_BRIGHT_COLORS)
|
||||
}
|
||||
|
||||
pub(super) fn gruvbox_dark_colors() -> TerminalColors {
|
||||
TerminalColors::new(GRUVBOX_DARK_NORMAL_COLORS, GRUVBOX_DARK_BRIGHT_COLORS)
|
||||
}
|
||||
|
||||
pub(super) fn gruvbox_light_colors() -> TerminalColors {
|
||||
TerminalColors::new(GRUVBOX_LIGHT_NORMAL_COLORS, GRUVBOX_LIGHT_BRIGHT_COLORS)
|
||||
}
|
||||
|
||||
pub(super) fn solarflare_colors() -> TerminalColors {
|
||||
TerminalColors::new(SOLARFLARE_NORMAL_COLORS, SOLARFLARE_BRIGHT_COLORS)
|
||||
}
|
||||
|
||||
pub(super) fn adeberry_colors() -> TerminalColors {
|
||||
TerminalColors::new(ADEBERRY_NORMAL_COLORS, ADEBERRY_BRIGHT_COLORS)
|
||||
}
|
||||
|
||||
/// Default bundled themes
|
||||
pub fn dark_theme() -> WarpTheme {
|
||||
WarpTheme::new(
|
||||
Fill::Solid(ColorU::from_u32(0x000000FF)),
|
||||
ColorU::from_u32(0xffffffff),
|
||||
Fill::Solid(ColorU::from_u32(0x19AAD8FF)),
|
||||
None,
|
||||
Some(Details::Darker),
|
||||
dark_mode_colors(),
|
||||
None,
|
||||
Some("Dark".to_string()),
|
||||
)
|
||||
}
|
||||
|
||||
pub fn light_theme() -> WarpTheme {
|
||||
WarpTheme::new(
|
||||
Fill::Solid(ColorU::white()),
|
||||
ColorU::new(17, 17, 17, OPAQUE),
|
||||
Fill::Solid(ColorU::from_u32(0x00c2ffff)),
|
||||
None,
|
||||
Some(Details::Lighter),
|
||||
light_mode_colors(),
|
||||
None,
|
||||
Some("Light".to_string()),
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn dracula() -> WarpTheme {
|
||||
WarpTheme::new(
|
||||
Fill::Solid(ColorU::from_u32(0x282A36FF)),
|
||||
ColorU::from_u32(0xF8F8F2FF),
|
||||
Fill::Solid(ColorU::from_u32(0xFF79C6FF)),
|
||||
None,
|
||||
Some(Details::Darker),
|
||||
dracula_colors(),
|
||||
None,
|
||||
Some("Dracula".to_string()),
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn solarized_light() -> WarpTheme {
|
||||
WarpTheme::new(
|
||||
Fill::Solid(ColorU::from_u32(0xFDF6E3FF)),
|
||||
ColorU::from_u32(0x586E75FF),
|
||||
Fill::Solid(ColorU::from_u32(0x66B5A9FF)),
|
||||
None,
|
||||
Some(Details::Lighter),
|
||||
solarized_light_colors(),
|
||||
None,
|
||||
Some("Solarized Light".to_string()),
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn solarized_dark() -> WarpTheme {
|
||||
WarpTheme::new(
|
||||
Fill::Solid(ColorU::from_u32(0x002B36FF)),
|
||||
ColorU::from_u32(0xF8F8F2FF),
|
||||
Fill::Solid(ColorU::from_u32(0xCB4B16FF)),
|
||||
None,
|
||||
Some(Details::Darker),
|
||||
solarized_dark_colors(),
|
||||
None,
|
||||
Some("Solarized Dark".to_string()),
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn gruvbox_dark() -> WarpTheme {
|
||||
WarpTheme::new(
|
||||
Fill::Solid(ColorU::from_u32(0x282828FF)),
|
||||
ColorU::from_u32(0xEBDBB2FF),
|
||||
Fill::Solid(ColorU::from_u32(0xFC802DFF)),
|
||||
None,
|
||||
Some(Details::Darker),
|
||||
gruvbox_dark_colors(),
|
||||
None,
|
||||
Some("Gruvbox Dark".to_string()),
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn gruvbox_light() -> WarpTheme {
|
||||
WarpTheme::new(
|
||||
Fill::Solid(ColorU::from_u32(0xFBF1C7FF)),
|
||||
ColorU::from_u32(0x3C3836FF),
|
||||
Fill::Solid(ColorU::from_u32(0xAD3B14FF)),
|
||||
None,
|
||||
Some(Details::Lighter),
|
||||
gruvbox_light_colors(),
|
||||
None,
|
||||
Some("Gruvbox Light".to_string()),
|
||||
)
|
||||
}
|
||||
|
||||
/// Bundled gradient themes
|
||||
pub(super) fn cyber_wave() -> WarpTheme {
|
||||
WarpTheme::new(
|
||||
Fill::VerticalGradient(VerticalGradient::new(
|
||||
ColorU::black().blend(&coloru_with_opacity(ColorU::from_u32(0x00C2FFFF), 20)),
|
||||
ColorU::black(),
|
||||
)),
|
||||
ColorU::white(),
|
||||
Fill::HorizontalGradient(HorizontalGradient::new(
|
||||
ColorU::from_u32(0x007972FF),
|
||||
ColorU::from_u32(0x7B008FFF),
|
||||
)),
|
||||
None,
|
||||
Some(Details::Darker),
|
||||
dark_mode_colors(),
|
||||
None,
|
||||
Some("Cyber Wave".to_string()),
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn willow_dream() -> WarpTheme {
|
||||
WarpTheme::new(
|
||||
Fill::VerticalGradient(VerticalGradient::new(
|
||||
ColorU::from_u32(0x206169FF),
|
||||
ColorU::from_u32(0x022F27FF),
|
||||
)),
|
||||
ColorU::white(),
|
||||
Fill::HorizontalGradient(HorizontalGradient::new(
|
||||
ColorU::from_u32(0xF9AEA8FF),
|
||||
ColorU::from_u32(0xDD6258FF),
|
||||
)),
|
||||
None,
|
||||
Some(Details::Darker),
|
||||
dark_mode_colors(),
|
||||
None,
|
||||
Some("Willow Dream".to_string()),
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn fancy_dracula() -> WarpTheme {
|
||||
WarpTheme::new(
|
||||
Fill::VerticalGradient(VerticalGradient::new(
|
||||
ColorU::from_u32(0x252630FF),
|
||||
ColorU::from_u32(0x3D3F4FFF),
|
||||
)),
|
||||
ColorU::white(),
|
||||
Fill::HorizontalGradient(HorizontalGradient::new(
|
||||
ColorU::from_u32(0xBCA1F6FF),
|
||||
ColorU::from_u32(0xA3E7FCFF),
|
||||
)),
|
||||
None,
|
||||
Some(Details::Darker),
|
||||
dracula_colors(),
|
||||
None,
|
||||
Some("Fancy Dracula".to_string()),
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn phenomenon() -> WarpTheme {
|
||||
WarpTheme::new(
|
||||
Fill::Solid(ColorU::from_u32(0x121212FF)),
|
||||
ColorU::from_u32(0xFAF9F6FF),
|
||||
Fill::Solid(ColorU::from_u32(0x2E5D9EFF)),
|
||||
None,
|
||||
Some(Details::Darker),
|
||||
phenomenon_colors(),
|
||||
Some(Image {
|
||||
source: bundled_or_fetched_asset!("jpg/phenomenon_bg.jpg"),
|
||||
opacity: 100,
|
||||
}),
|
||||
Some("Phenomenon".to_string()),
|
||||
)
|
||||
}
|
||||
|
||||
/// Bundled themes with background images
|
||||
pub(super) fn jellyfish() -> WarpTheme {
|
||||
WarpTheme::new(
|
||||
Fill::Solid(ColorU::from_u32(0x1B1718FF)),
|
||||
ColorU::white(),
|
||||
Fill::Solid(ColorU::from_u32(0x538682FF)),
|
||||
None,
|
||||
Some(Details::Darker),
|
||||
dark_mode_colors(),
|
||||
Some(Image {
|
||||
source: bundled_or_fetched_asset!("jpg/jellyfish_bg.jpg"),
|
||||
opacity: 30,
|
||||
}),
|
||||
Some("Jellyfish".to_string()),
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn koi() -> WarpTheme {
|
||||
WarpTheme::new(
|
||||
Fill::Solid(ColorU::from_u32(0x211719FF)),
|
||||
ColorU::white(),
|
||||
Fill::Solid(ColorU::from_u32(0xFF3131FF)),
|
||||
None,
|
||||
Some(Details::Darker),
|
||||
dark_mode_colors(),
|
||||
Some(Image {
|
||||
source: bundled_or_fetched_asset!("jpg/koi_bg.jpg"),
|
||||
opacity: 30,
|
||||
}),
|
||||
Some("Koi".to_string()),
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn leafy() -> WarpTheme {
|
||||
WarpTheme::new(
|
||||
Fill::Solid(ColorU::black()),
|
||||
ColorU::white(),
|
||||
Fill::Solid(ColorU::from_u32(0x55972DFF)),
|
||||
None,
|
||||
Some(Details::Darker),
|
||||
dark_mode_colors(),
|
||||
Some(Image {
|
||||
source: bundled_or_fetched_asset!("jpg/leafy_bg.jpg"),
|
||||
opacity: 30,
|
||||
}),
|
||||
Some("Leafy".to_string()),
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn marble() -> WarpTheme {
|
||||
WarpTheme::new(
|
||||
Fill::Solid(ColorU::from_u32(0xE3E3E3FF)),
|
||||
ColorU::black(),
|
||||
Fill::Solid(ColorU::from_u32(0x585858FF)),
|
||||
None,
|
||||
Some(Details::Lighter),
|
||||
light_mode_colors(),
|
||||
Some(Image {
|
||||
source: bundled_or_fetched_asset!("jpg/marble_bg.jpg"),
|
||||
opacity: 50,
|
||||
}),
|
||||
Some("Marble".to_string()),
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn pink_city() -> WarpTheme {
|
||||
let details = CustomDetails {
|
||||
..CustomDetails::lighter_details()
|
||||
};
|
||||
WarpTheme::new(
|
||||
Fill::Solid(ColorU::from_u32(0xFBEFF6FF)),
|
||||
ColorU::black(),
|
||||
Fill::Solid(ColorU::from_u32(0xE10087FF)),
|
||||
None,
|
||||
Some(Details::Custom(details)),
|
||||
light_mode_colors(),
|
||||
Some(Image {
|
||||
source: bundled_or_fetched_asset!("jpg/pink_city_bg.jpg"),
|
||||
opacity: 40,
|
||||
}),
|
||||
Some("Pink City".to_string()),
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn snowy() -> WarpTheme {
|
||||
WarpTheme::new(
|
||||
Fill::VerticalGradient(VerticalGradient::new(
|
||||
ColorU::from_u32(0xFFFFFFFF),
|
||||
ColorU::from_u32(0xDEE6EBFF),
|
||||
)),
|
||||
ColorU::black(),
|
||||
Fill::Solid(ColorU::from_u32(0x647E90FF)),
|
||||
None,
|
||||
Some(Details::Lighter),
|
||||
light_mode_colors(),
|
||||
Some(Image {
|
||||
source: bundled_or_fetched_asset!("jpg/snowy_bg.jpg"),
|
||||
opacity: 20,
|
||||
}),
|
||||
Some("Snowy".to_string()),
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn red_rock() -> WarpTheme {
|
||||
WarpTheme::new(
|
||||
Fill::VerticalGradient(VerticalGradient::new(
|
||||
ColorU::from_u32(0x211719FF)
|
||||
.blend(&coloru_with_opacity(ColorU::from_u32(0x4C3435FF), 45)),
|
||||
ColorU::from_u32(0x211719FF)
|
||||
.blend(&coloru_with_opacity(ColorU::from_u32(0xD3032FF), 45)),
|
||||
)),
|
||||
ColorU::white(),
|
||||
Fill::Solid(ColorU::from_u32(0x9F4147FF)),
|
||||
None,
|
||||
Some(Details::Darker),
|
||||
dark_mode_colors(),
|
||||
Some(Image {
|
||||
source: bundled_or_fetched_asset!("jpg/red_rock_bg.jpg"),
|
||||
opacity: 30,
|
||||
}),
|
||||
Some("Red Rock".to_string()),
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn dark_city() -> WarpTheme {
|
||||
WarpTheme::new(
|
||||
Fill::VerticalGradient(VerticalGradient::new(
|
||||
ColorU::from_u32(0x01181FFF)
|
||||
.blend(&coloru_with_opacity(ColorU::from_u32(0x1A363FFF), 45)),
|
||||
ColorU::from_u32(0x01181FFF)
|
||||
.blend(&coloru_with_opacity(ColorU::from_u32(0x1A4551FF), 45)),
|
||||
)),
|
||||
ColorU::white(),
|
||||
Fill::Solid(ColorU::from_u32(0xE9072DFF)),
|
||||
None,
|
||||
Some(Details::Darker),
|
||||
dark_mode_colors(),
|
||||
Some(Image {
|
||||
source: bundled_or_fetched_asset!("jpg/dark_city_bg.jpg"),
|
||||
opacity: 20,
|
||||
}),
|
||||
Some("Dark City".to_string()),
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn sent_referral_reward() -> WarpTheme {
|
||||
WarpTheme::new(
|
||||
Fill::Solid(ColorU::from_u32(0x334567FF)),
|
||||
ColorU::white(),
|
||||
Fill::Solid(ColorU::from_u32(0xCD51FFFF)),
|
||||
None,
|
||||
Some(Details::Darker),
|
||||
dark_mode_colors(),
|
||||
Some(Image {
|
||||
source: bundled_or_fetched_asset!("jpg/sent_referral_reward_bg.jpg"),
|
||||
opacity: 100,
|
||||
}),
|
||||
Some("Warp Referral".to_string()),
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn solar_flare() -> WarpTheme {
|
||||
WarpTheme::new(
|
||||
Fill::Solid(ColorU::from_u32(0x1B1C18FF)),
|
||||
ColorU::from_u32(0xDDE6EEFF),
|
||||
Fill::Solid(ColorU::from_u32(0x34895CFF)),
|
||||
None,
|
||||
Some(Details::Darker),
|
||||
solarflare_colors(),
|
||||
Some(Image {
|
||||
source: bundled_or_fetched_asset!("jpg/solarflare_bg.jpg"),
|
||||
opacity: 20,
|
||||
}),
|
||||
Some("Solar Flare".to_string()),
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn adeberry() -> WarpTheme {
|
||||
WarpTheme::new(
|
||||
Fill::Solid(ColorU::from_u32(0x1D2022FF)),
|
||||
ColorU::from_u32(0xE4EEF5FF),
|
||||
Fill::Solid(ColorU::from_u32(0x6C96B4FF)),
|
||||
None,
|
||||
Some(Details::Darker),
|
||||
adeberry_colors(),
|
||||
None,
|
||||
Some("Adeberry".to_string()),
|
||||
)
|
||||
}
|
||||
|
||||
pub(super) fn received_referral_reward() -> WarpTheme {
|
||||
WarpTheme::new(
|
||||
Fill::Solid(ColorU::from_u32(0xFFFFFFFF)),
|
||||
ColorU::black(),
|
||||
Fill::Solid(ColorU::from_u32(0xCD51FFFF)),
|
||||
None,
|
||||
Some(Details::Lighter),
|
||||
light_mode_colors(),
|
||||
Some(Image {
|
||||
source: bundled_or_fetched_asset!("jpg/received_referral_reward_bg.jpg"),
|
||||
opacity: 100,
|
||||
}),
|
||||
Some("Received Referral Reward".to_string()),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
pub mod default_themes;
|
||||
pub mod theme;
|
||||
pub mod theme_chooser;
|
||||
pub mod theme_creator;
|
||||
pub mod theme_creator_body;
|
||||
pub mod theme_creator_modal;
|
||||
pub mod theme_deletion_body;
|
||||
pub mod theme_deletion_modal;
|
||||
|
||||
use warp_core::ui::theme::WarpTheme;
|
||||
|
||||
pub fn onboarding_theme_picker_themes() -> [WarpTheme; 4] {
|
||||
[
|
||||
default_themes::phenomenon(),
|
||||
default_themes::dark_theme(),
|
||||
default_themes::light_theme(),
|
||||
default_themes::adeberry(),
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,567 @@
|
||||
use super::default_themes::*;
|
||||
use anyhow::Result;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::collections::HashMap;
|
||||
use std::iter::FromIterator;
|
||||
use std::path::PathBuf;
|
||||
use warp_core::ui::color::pick_foreground_color;
|
||||
use warpui::assets::asset_cache::AssetSource;
|
||||
use warpui::{
|
||||
color::ColorU,
|
||||
elements::{
|
||||
Align, Border, ConstrainedBox, Container, Element, Empty, Flex, ParentElement, Rect,
|
||||
Shrinkable, Stack, Text,
|
||||
},
|
||||
fonts::FamilyId,
|
||||
};
|
||||
|
||||
use super::theme_creator::{pick_accent_color_from_options, top_colors_for_image};
|
||||
|
||||
pub use warp_core::ui::color::blend::Blend;
|
||||
pub use warp_core::ui::theme::*;
|
||||
|
||||
const THUMBNAIL_MARGIN: f32 = 10.;
|
||||
|
||||
// We use the discriminant of enum variants to determine the order of theme types in the
|
||||
// theme chooser view.
|
||||
#[derive(
|
||||
Clone,
|
||||
Debug,
|
||||
Default,
|
||||
PartialEq,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
Hash,
|
||||
Eq,
|
||||
Ord,
|
||||
PartialOrd,
|
||||
schemars::JsonSchema,
|
||||
settings_value::SettingsValue,
|
||||
)]
|
||||
#[schemars(description = "The color theme.", rename_all = "snake_case")]
|
||||
pub enum ThemeKind {
|
||||
// Need an alias for backwards-compatibility: Originally we only had a single reward theme
|
||||
// so it was named `ReferralReward`.
|
||||
#[serde(alias = "ReferralReward")]
|
||||
#[schemars(skip)]
|
||||
SentReferralReward,
|
||||
#[schemars(skip)]
|
||||
ReceivedReferralReward,
|
||||
#[schemars(description = "Adeberry")]
|
||||
Adeberry,
|
||||
#[schemars(description = "Phenomenon")]
|
||||
Phenomenon,
|
||||
#[default]
|
||||
#[schemars(description = "Dark")]
|
||||
Dark,
|
||||
#[schemars(description = "Dracula")]
|
||||
Dracula,
|
||||
#[schemars(description = "Fancy Dracula")]
|
||||
FancyDracula,
|
||||
#[schemars(description = "Cyber Wave")]
|
||||
CyberWave,
|
||||
#[schemars(description = "Solar Flare")]
|
||||
SolarFlare,
|
||||
#[schemars(description = "Solarized Dark")]
|
||||
SolarizedDark,
|
||||
#[schemars(description = "Willow Dream")]
|
||||
WillowDream,
|
||||
#[schemars(description = "Light")]
|
||||
Light,
|
||||
#[schemars(description = "Dark City")]
|
||||
DarkCity,
|
||||
#[schemars(description = "Gruvbox Dark")]
|
||||
GruvboxDark,
|
||||
#[schemars(description = "Red Rock")]
|
||||
RedRock,
|
||||
#[schemars(description = "Jellyfish")]
|
||||
JellyFish,
|
||||
#[schemars(description = "Leafy")]
|
||||
Leafy,
|
||||
#[schemars(description = "Koi")]
|
||||
Koi,
|
||||
#[schemars(description = "Solarized Light")]
|
||||
SolarizedLight,
|
||||
#[schemars(description = "Snowy")]
|
||||
Snowy,
|
||||
#[schemars(description = "Gruvbox Light")]
|
||||
GruvboxLight,
|
||||
#[schemars(description = "Pink City")]
|
||||
PinkCity,
|
||||
#[schemars(description = "Marble")]
|
||||
Marble,
|
||||
#[schemars(description = "A user-provided custom theme loaded from a file.")]
|
||||
Custom(CustomTheme),
|
||||
/// Base16 themes are a special case of custom themes with their own semantics for ANSI colors that override "bright" color variants.
|
||||
#[schemars(description = "A custom theme using the Base16 color scheme format.")]
|
||||
CustomBase16(CustomTheme),
|
||||
#[schemars(skip)]
|
||||
InMemory(InMemoryThemeOptions),
|
||||
}
|
||||
|
||||
impl From<CustomTheme> for ThemeKind {
|
||||
fn from(custom_theme: CustomTheme) -> ThemeKind {
|
||||
if custom_theme.name.as_str().starts_with("Base16") {
|
||||
ThemeKind::CustomBase16(custom_theme)
|
||||
} else {
|
||||
ThemeKind::Custom(custom_theme)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ThemeKind {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
let value = match &self {
|
||||
ThemeKind::Light => "Light",
|
||||
ThemeKind::Dark => "Dark",
|
||||
ThemeKind::Dracula => "Dracula",
|
||||
ThemeKind::SolarizedDark => "Solarized Dark",
|
||||
ThemeKind::SolarizedLight => "Solarized Light",
|
||||
ThemeKind::GruvboxDark => "Gruvbox Dark",
|
||||
ThemeKind::GruvboxLight => "Gruvbox Light",
|
||||
ThemeKind::JellyFish => "Jellyfish",
|
||||
ThemeKind::Koi => "Koi",
|
||||
ThemeKind::Leafy => "Leafy",
|
||||
ThemeKind::Marble => "Marble",
|
||||
ThemeKind::PinkCity => "Pink City",
|
||||
ThemeKind::Snowy => "Snowy",
|
||||
ThemeKind::DarkCity => "Dark City",
|
||||
ThemeKind::RedRock => "Red Rock",
|
||||
ThemeKind::CyberWave => "Cyber Wave",
|
||||
ThemeKind::WillowDream => "Willow Dream",
|
||||
ThemeKind::FancyDracula => "Fancy Dracula",
|
||||
ThemeKind::Phenomenon => "Phenomenon",
|
||||
ThemeKind::SolarFlare => "Solar Flare",
|
||||
ThemeKind::Adeberry => "Adeberry",
|
||||
ThemeKind::SentReferralReward => "Warp Referral",
|
||||
ThemeKind::ReceivedReferralReward => "Referred to Warp",
|
||||
ThemeKind::Custom(custom_theme) => custom_theme.name.as_str(),
|
||||
ThemeKind::CustomBase16(custom_theme) => custom_theme.name.as_str(),
|
||||
ThemeKind::InMemory(in_memory_theme) => in_memory_theme.name.as_str(),
|
||||
};
|
||||
write!(f, "{value}")
|
||||
}
|
||||
}
|
||||
|
||||
impl ThemeKind {
|
||||
pub fn matches(&self, query: &str) -> bool {
|
||||
let theme_name = format!("{self}").to_lowercase();
|
||||
theme_name.contains(&query.to_lowercase())
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Clone,
|
||||
Hash,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
schemars::JsonSchema,
|
||||
settings_value::SettingsValue,
|
||||
)]
|
||||
#[schemars(description = "A user-provided custom theme.")]
|
||||
pub struct CustomTheme {
|
||||
#[schemars(description = "The display name of the custom theme.")]
|
||||
name: String,
|
||||
#[schemars(description = "The file path to the custom theme definition.")]
|
||||
path: PathBuf,
|
||||
}
|
||||
|
||||
impl CustomTheme {
|
||||
pub fn new(s: String, p: PathBuf) -> Self {
|
||||
CustomTheme { name: s, path: p }
|
||||
}
|
||||
|
||||
pub fn name(&self) -> String {
|
||||
self.name.clone()
|
||||
}
|
||||
|
||||
pub fn path(&self) -> PathBuf {
|
||||
self.path.clone()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Debug,
|
||||
Clone,
|
||||
Hash,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
PartialOrd,
|
||||
Ord,
|
||||
settings_value::SettingsValue,
|
||||
)]
|
||||
pub struct InMemoryThemeOptions {
|
||||
name: String,
|
||||
path: PathBuf,
|
||||
#[serde(skip)]
|
||||
possible_bg_colors: Vec<ColorU>,
|
||||
#[serde(skip)]
|
||||
chosen_bg_color_index: usize,
|
||||
}
|
||||
|
||||
impl InMemoryThemeOptions {
|
||||
pub async fn new(name: String, path: PathBuf) -> Result<Self> {
|
||||
top_colors_for_image(path.clone()).map(|top_colors| InMemoryThemeOptions {
|
||||
name,
|
||||
path,
|
||||
possible_bg_colors: top_colors,
|
||||
chosen_bg_color_index: 0,
|
||||
})
|
||||
}
|
||||
|
||||
pub fn chosen_bg_color(&self) -> ColorU {
|
||||
self.possible_bg_colors[self.chosen_bg_color_index]
|
||||
}
|
||||
|
||||
pub fn possible_bg_colors(&self) -> Vec<ColorU> {
|
||||
self.possible_bg_colors.clone()
|
||||
}
|
||||
|
||||
pub fn chosen_bg_color_index(&self) -> usize {
|
||||
self.chosen_bg_color_index
|
||||
}
|
||||
|
||||
pub fn set_chosen_bg_color_index(&mut self, index: usize) {
|
||||
self.chosen_bg_color_index = index;
|
||||
}
|
||||
|
||||
pub fn name(&self) -> String {
|
||||
self.name.clone()
|
||||
}
|
||||
|
||||
pub fn set_name(&mut self, name: String) {
|
||||
self.name = name;
|
||||
}
|
||||
|
||||
pub fn path(&self) -> PathBuf {
|
||||
self.path.clone()
|
||||
}
|
||||
|
||||
pub fn set_path(&mut self, path: PathBuf) {
|
||||
self.path = path;
|
||||
}
|
||||
|
||||
pub fn theme(&self) -> WarpTheme {
|
||||
let bg_color = self.chosen_bg_color();
|
||||
let fg_color = pick_foreground_color(bg_color);
|
||||
let possible_accent_colors: Vec<ColorU> = self
|
||||
.possible_bg_colors
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|(index, _)| *index != self.chosen_bg_color_index)
|
||||
.map(|(_, color)| *color)
|
||||
.collect();
|
||||
let accent_color =
|
||||
pick_accent_color_from_options(&[bg_color, fg_color], &possible_accent_colors[..]);
|
||||
|
||||
let (details, terminal_colors) = if fg_color.eq(&ColorU::white()) {
|
||||
(Details::Darker, dark_mode_colors())
|
||||
} else {
|
||||
(Details::Lighter, light_mode_colors())
|
||||
};
|
||||
|
||||
WarpTheme::new(
|
||||
bg_color.into(),
|
||||
fg_color,
|
||||
accent_color.into(),
|
||||
None,
|
||||
Some(details),
|
||||
terminal_colors,
|
||||
Some(Image {
|
||||
// Note that, as an invariant, in-memory themes come from local files.
|
||||
source: AssetSource::LocalFile {
|
||||
path: self.path().to_str().unwrap_or_default().to_owned(),
|
||||
},
|
||||
opacity: 30,
|
||||
}),
|
||||
Some(self.name()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct WarpThemeConfig {
|
||||
theme_map: HashMap<ThemeKind, WarpTheme>,
|
||||
}
|
||||
|
||||
impl WarpThemeConfig {
|
||||
pub fn new() -> Self {
|
||||
// preload with built-in themes
|
||||
let theme_map: HashMap<ThemeKind, WarpTheme> = HashMap::from_iter([
|
||||
(ThemeKind::SentReferralReward, sent_referral_reward()),
|
||||
(
|
||||
ThemeKind::ReceivedReferralReward,
|
||||
received_referral_reward(),
|
||||
),
|
||||
(ThemeKind::Dark, dark_theme()),
|
||||
(ThemeKind::Light, light_theme()),
|
||||
(ThemeKind::SolarizedDark, solarized_dark()),
|
||||
(ThemeKind::SolarizedLight, solarized_light()),
|
||||
(ThemeKind::Dracula, dracula()),
|
||||
(ThemeKind::GruvboxDark, gruvbox_dark()),
|
||||
(ThemeKind::GruvboxLight, gruvbox_light()),
|
||||
(ThemeKind::JellyFish, jellyfish()),
|
||||
(ThemeKind::Koi, koi()),
|
||||
(ThemeKind::Leafy, leafy()),
|
||||
(ThemeKind::Marble, marble()),
|
||||
(ThemeKind::PinkCity, pink_city()),
|
||||
(ThemeKind::Snowy, snowy()),
|
||||
(ThemeKind::DarkCity, dark_city()),
|
||||
(ThemeKind::RedRock, red_rock()),
|
||||
(ThemeKind::CyberWave, cyber_wave()),
|
||||
(ThemeKind::WillowDream, willow_dream()),
|
||||
(ThemeKind::FancyDracula, fancy_dracula()),
|
||||
(ThemeKind::Phenomenon, phenomenon()),
|
||||
(ThemeKind::SolarFlare, solar_flare()),
|
||||
(ThemeKind::Adeberry, adeberry()),
|
||||
]);
|
||||
WarpThemeConfig { theme_map }
|
||||
}
|
||||
|
||||
pub fn add_new_theme(&mut self, theme_name: ThemeKind, theme: WarpTheme) {
|
||||
self.theme_map.insert(theme_name, theme);
|
||||
}
|
||||
|
||||
pub fn file_to_theme(name: String, path: PathBuf) -> ThemeKind {
|
||||
CustomTheme::new(name, path).into()
|
||||
}
|
||||
|
||||
pub fn theme_items(&self) -> impl Iterator<Item = (&ThemeKind, &WarpTheme)> {
|
||||
self.theme_map.iter()
|
||||
}
|
||||
|
||||
pub fn theme(&self, name: &ThemeKind) -> WarpTheme {
|
||||
self.theme_map.get(name).cloned().unwrap_or_else(dark_theme)
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for WarpThemeConfig {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone, Debug, Default, PartialEq, Eq)]
|
||||
pub enum RespectSystemTheme {
|
||||
#[default]
|
||||
Off,
|
||||
On(SelectedSystemThemes),
|
||||
}
|
||||
|
||||
#[derive(
|
||||
Clone,
|
||||
Debug,
|
||||
PartialEq,
|
||||
Eq,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
schemars::JsonSchema,
|
||||
settings_value::SettingsValue,
|
||||
)]
|
||||
#[schemars(description = "Themes to use when following the system light/dark mode.")]
|
||||
pub struct SelectedSystemThemes {
|
||||
#[schemars(description = "The theme to use in light mode.")]
|
||||
pub light: ThemeKind,
|
||||
#[schemars(description = "The theme to use in dark mode.")]
|
||||
pub dark: ThemeKind,
|
||||
}
|
||||
|
||||
impl RespectSystemTheme {
|
||||
pub fn selected_system_themes(&self) -> Option<&SelectedSystemThemes> {
|
||||
match self {
|
||||
RespectSystemTheme::Off => None,
|
||||
RespectSystemTheme::On(selected) => Some(selected),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for SelectedSystemThemes {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
light: ThemeKind::Light,
|
||||
dark: ThemeKind::Dark,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub struct PromptColors {
|
||||
pub input_prompt_conversation_management: ColorU,
|
||||
pub input_prompt_pwd: ColorU,
|
||||
pub input_prompt_git: ColorU,
|
||||
pub input_prompt_branch: ColorU,
|
||||
pub input_prompt_agent_mode_hint: ColorU,
|
||||
pub input_prompt_agent_mode_tasks: ColorU,
|
||||
pub input_prompt_dirty_color: ColorU,
|
||||
pub input_prompt_virtual_env: ColorU,
|
||||
pub input_prompt_user_and_host: ColorU,
|
||||
pub input_prompt_date: ColorU,
|
||||
pub input_prompt_time: ColorU,
|
||||
pub input_prompt_kubernetes: ColorU,
|
||||
pub input_prompt_svn: ColorU,
|
||||
pub input_prompt_separator: ColorU,
|
||||
pub input_prompt_subshell: ColorU,
|
||||
pub input_prompt_ssh: ColorU,
|
||||
}
|
||||
|
||||
impl From<WarpTheme> for PromptColors {
|
||||
fn from(theme: WarpTheme) -> Self {
|
||||
PromptColors {
|
||||
input_prompt_conversation_management: theme.terminal_colors().normal.white.into(),
|
||||
input_prompt_pwd: theme.terminal_colors().normal.magenta.into(),
|
||||
input_prompt_git: theme.terminal_colors().normal.green.into(),
|
||||
input_prompt_agent_mode_hint: theme.terminal_colors().normal.yellow.into(),
|
||||
input_prompt_agent_mode_tasks: theme.terminal_colors().normal.yellow.into(),
|
||||
input_prompt_branch: theme.terminal_colors().normal.yellow.into(),
|
||||
input_prompt_dirty_color: theme.terminal_colors().normal.green.into(),
|
||||
input_prompt_virtual_env: theme.terminal_colors().normal.yellow.into(),
|
||||
input_prompt_user_and_host: theme.terminal_colors().normal.green.into(),
|
||||
input_prompt_date: theme.terminal_colors().normal.cyan.into(),
|
||||
input_prompt_time: theme.terminal_colors().normal.red.into(),
|
||||
input_prompt_kubernetes: theme.terminal_colors().normal.cyan.into(),
|
||||
input_prompt_ssh: theme.terminal_colors().normal.blue.into(),
|
||||
input_prompt_subshell: theme.terminal_colors().normal.blue.into(),
|
||||
input_prompt_svn: theme.terminal_colors().normal.blue.into(),
|
||||
input_prompt_separator: theme.terminal_colors().normal.magenta.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render_preview(
|
||||
theme: &WarpTheme,
|
||||
font_family: FamilyId,
|
||||
form_factor: Option<f32>,
|
||||
) -> Box<dyn Element> {
|
||||
let text_size = 8. * form_factor.unwrap_or(1.);
|
||||
let margin = THUMBNAIL_MARGIN * form_factor.unwrap_or(1.);
|
||||
let padding = 5. * form_factor.unwrap_or(1.);
|
||||
let text_line_1 = Container::new(
|
||||
Text::new_inline("ls", font_family, text_size)
|
||||
.with_color(theme.foreground().into_solid())
|
||||
.finish(),
|
||||
)
|
||||
.with_margin_left(margin)
|
||||
.with_margin_right(margin)
|
||||
.finish();
|
||||
|
||||
let text_line_2 = Container::new(
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Text::new_inline("dir ", font_family, text_size)
|
||||
.with_color(theme.terminal_colors().normal.blue.into())
|
||||
.finish(),
|
||||
)
|
||||
.with_child(
|
||||
Text::new_inline("executable ", font_family, text_size)
|
||||
.with_color(theme.terminal_colors().normal.red.into())
|
||||
.finish(),
|
||||
)
|
||||
.with_child(
|
||||
Text::new_inline("file", font_family, text_size)
|
||||
.with_color(theme.foreground().into_solid())
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_margin_left(margin)
|
||||
.with_margin_right(margin)
|
||||
.finish();
|
||||
|
||||
let input_box = Shrinkable::new(
|
||||
1.,
|
||||
Align::new(
|
||||
Flex::column()
|
||||
// The border above the input box.
|
||||
.with_child(
|
||||
Container::new(Empty::new().finish())
|
||||
.with_padding_bottom(padding)
|
||||
.with_border(
|
||||
Border::top(1.2 * form_factor.unwrap_or(1.))
|
||||
.with_border_color(theme.outline().into_solid()),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
// The fake cursor within the input box.
|
||||
.with_child(
|
||||
Container::new(
|
||||
ConstrainedBox::new(
|
||||
Rect::new()
|
||||
.with_background_color(theme.accent().into_solid())
|
||||
.finish(),
|
||||
)
|
||||
.with_height(12. * form_factor.unwrap_or(1.))
|
||||
.with_width(2. * form_factor.unwrap_or(1.))
|
||||
.finish(),
|
||||
)
|
||||
.with_margin_left(margin)
|
||||
.with_margin_right(margin)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.bottom_left()
|
||||
.finish(),
|
||||
)
|
||||
.finish();
|
||||
|
||||
let mut thumbnail = Stack::new();
|
||||
let mut background_opacity = 100;
|
||||
if let Some(background_image) = theme.background_image() {
|
||||
thumbnail.add_child(
|
||||
Shrinkable::new(
|
||||
1.,
|
||||
warpui::elements::Image::new(
|
||||
background_image.source(),
|
||||
warpui::elements::CacheOption::BySize,
|
||||
)
|
||||
.cover()
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
);
|
||||
background_opacity -= background_image.opacity;
|
||||
}
|
||||
|
||||
thumbnail.add_child(
|
||||
Container::new(
|
||||
Container::new(
|
||||
Flex::column()
|
||||
.with_child(text_line_1)
|
||||
.with_child(
|
||||
Container::new(text_line_2)
|
||||
.with_padding_top(padding)
|
||||
.finish(),
|
||||
)
|
||||
.with_child(input_box)
|
||||
.finish(),
|
||||
)
|
||||
.with_margin_top(margin)
|
||||
.with_margin_bottom(margin)
|
||||
.finish(),
|
||||
)
|
||||
.with_background(theme.background().with_opacity(background_opacity))
|
||||
.finish(),
|
||||
);
|
||||
|
||||
Align::new(
|
||||
Container::new(
|
||||
ConstrainedBox::new(thumbnail.finish())
|
||||
.with_height(100. * form_factor.unwrap_or(1.))
|
||||
.with_width(190. * form_factor.unwrap_or(1.))
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "theme_test.rs"]
|
||||
mod tests;
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,89 @@
|
||||
use anyhow::{anyhow, Result};
|
||||
use std::path::PathBuf;
|
||||
|
||||
use deltae::*;
|
||||
use kmeans_colors::{get_kmeans_hamerly, Calculate, CentroidData, Sort};
|
||||
use palette::{FromColor, IntoColor, Lab, Pixel, Srgb, Srgba};
|
||||
use pathfinder_color::ColorU;
|
||||
|
||||
use crate::util::color::hex_color::coloru_from_hex_string;
|
||||
|
||||
/// Uses a k-means algorithm to identify the 5 most average colors in an image.
|
||||
pub fn top_colors_for_image(image_path: PathBuf) -> Result<Vec<ColorU>> {
|
||||
image::open(image_path)
|
||||
.map(|dynamic_image| {
|
||||
let image = dynamic_image.into_rgba8();
|
||||
let raw_image = image.as_raw();
|
||||
let lab: Vec<Lab> = Srgba::from_raw_slice(raw_image)
|
||||
.iter()
|
||||
.map(|x| x.into_format::<_, f32>().into_color())
|
||||
.collect();
|
||||
|
||||
let result = get_kmeans_hamerly(5, 20, 5.0, false, &lab, 0_u64);
|
||||
let colors = Lab::sort_indexed_colors(&result.centroids, &result.indices);
|
||||
|
||||
convert_centroids_to_hex_colors(&colors)
|
||||
.iter()
|
||||
.map(|color| match coloru_from_hex_string(color) {
|
||||
Ok(color_u) => color_u,
|
||||
Err(e) => {
|
||||
log::error!("kmeans algorithm did not produce valid hex strings: {e}");
|
||||
ColorU::black()
|
||||
}
|
||||
})
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.map_err(|e| anyhow!(e))
|
||||
}
|
||||
|
||||
fn convert_centroids_to_hex_colors<C: Calculate + Copy + IntoColor<Srgb>>(
|
||||
colors: &[CentroidData<C>],
|
||||
) -> Vec<String> {
|
||||
colors
|
||||
.iter()
|
||||
.map(|color| format!("#{:x}", color.centroid.into_color().into_format::<u8>()))
|
||||
.collect::<Vec<_>>()
|
||||
}
|
||||
|
||||
pub fn pick_accent_color_from_options(
|
||||
known_colors: &[ColorU],
|
||||
accent_options: &[ColorU],
|
||||
) -> ColorU {
|
||||
if let Some(max_accent) = accent_options.iter().max_by_key(|color| {
|
||||
let accent_lab = lab_from_coloru(**color);
|
||||
known_colors
|
||||
.iter()
|
||||
.map(|known_color| {
|
||||
let known_color_lab = lab_from_coloru(*known_color);
|
||||
// Convert DeltaE to i64 to avoid floating point comparison issues. 4 decimal places is enough,
|
||||
// per DeltaE lib code.
|
||||
DeltaE::new(accent_lab, known_color_lab, DE2000)
|
||||
.round_to(4)
|
||||
.value()
|
||||
* 10000.0
|
||||
})
|
||||
.sum::<f32>() as i64
|
||||
}) {
|
||||
*max_accent
|
||||
} else {
|
||||
log::warn!("At least one accent color should be provided.");
|
||||
ColorU::white()
|
||||
}
|
||||
}
|
||||
|
||||
fn lab_from_coloru(color: ColorU) -> LabValue {
|
||||
let palette = Lab::from_color(Srgb::from_components((
|
||||
color.r as f32 / 255.0,
|
||||
color.g as f32 / 255.0,
|
||||
color.b as f32 / 255.0,
|
||||
)));
|
||||
LabValue {
|
||||
l: palette.l,
|
||||
a: palette.a,
|
||||
b: palette.b,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
#[path = "theme_creator_tests.rs"]
|
||||
mod tests;
|
||||
@@ -0,0 +1,658 @@
|
||||
use crate::appearance::{Appearance, AppearanceManager};
|
||||
use crate::editor::{EditorView, Event as EditorEvent};
|
||||
use crate::themes::theme::{InMemoryThemeOptions, ThemeKind};
|
||||
use crate::user_config;
|
||||
#[cfg(feature = "local_fs")]
|
||||
use crate::{
|
||||
send_telemetry_from_ctx, server::telemetry::TelemetryEvent, themes::theme::CustomTheme,
|
||||
};
|
||||
use pathfinder_color::ColorU;
|
||||
use pathfinder_geometry::vector::vec2f;
|
||||
use std::default::Default;
|
||||
use std::fmt;
|
||||
use std::path::PathBuf;
|
||||
#[cfg(feature = "local_fs")]
|
||||
use std::{fs::copy, io::Write};
|
||||
#[cfg(feature = "local_fs")]
|
||||
use warp_core::ui::theme::WarpTheme;
|
||||
use warpui::elements::{
|
||||
Border, ConstrainedBox, Container, CornerRadius, CrossAxisAlignment, DispatchEventResult,
|
||||
EventHandler, Fill, Flex, Icon, MainAxisAlignment, MainAxisSize, MouseStateHandle,
|
||||
ParentElement, Radius, Rect, SavePosition, Shrinkable, Text,
|
||||
};
|
||||
use warpui::fonts::Weight;
|
||||
use warpui::ui_components::button::{ButtonVariant, TextAndIcon, TextAndIconAlignment};
|
||||
use warpui::ui_components::components::{Coords, UiComponent, UiComponentStyles};
|
||||
use warpui::ui_components::text_input::TextInput;
|
||||
use warpui::ViewHandle;
|
||||
use warpui::{
|
||||
platform::Cursor, AppContext, Element, Entity, SingletonEntity, TypedActionView, View,
|
||||
ViewContext,
|
||||
};
|
||||
|
||||
const BUTTON_PADDING: f32 = 12.;
|
||||
const BUTTON_FONT_SIZE: f32 = 14.;
|
||||
const BUTTON_BORDER_RADIUS: f32 = 4.;
|
||||
const BORDER_WIDTH: f32 = 1.;
|
||||
|
||||
const MODAL_SUBHEADER: &str =
|
||||
"Automatically generate a theme based on extracted colors from an image (.png, .jpg).";
|
||||
const IMAGE_PICKER_BUTTON_PRE_SELECT_TEXT: &str = "Select an image";
|
||||
const IMAGE_PICKER_BUTTON_SELECTING_TEXT: &str = "Selecting image...";
|
||||
const IMAGE_PICKER_BUTTON_POST_SELECT_TEXT: &str = "Select a new image";
|
||||
const CANCEL_BUTTON_TEXT: &str = "Cancel";
|
||||
const CREATE_BUTTON_TEXT: &str = "Create theme";
|
||||
|
||||
#[derive(Default)]
|
||||
struct MouseStateHandles {
|
||||
image_picker_mouse_state: MouseStateHandle,
|
||||
cancel_mouse_state: MouseStateHandle,
|
||||
create_mouse_state: MouseStateHandle,
|
||||
}
|
||||
|
||||
pub struct ThemeCreatorBody {
|
||||
button_mouse_states: MouseStateHandles,
|
||||
editor: ViewHandle<EditorView>,
|
||||
theme_options: Option<InMemoryThemeOptions>,
|
||||
image_state: ThemeCreatorImageState,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ThemeCreatorBodyAction {
|
||||
Create,
|
||||
OpenFilePicker,
|
||||
HandleImageSelected(PathBuf),
|
||||
SetBackgroundColor(usize),
|
||||
Cancel,
|
||||
FilePickerCancelled,
|
||||
}
|
||||
|
||||
pub enum ThemeCreatorBodyEvent {
|
||||
Close,
|
||||
OpenFilePicker,
|
||||
SetCustomTheme { theme: ThemeKind },
|
||||
ShowErrorToast { message: String },
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ThemeCreatorImageState {
|
||||
Empty,
|
||||
Uploading,
|
||||
Uploaded,
|
||||
}
|
||||
|
||||
impl fmt::Display for ThemeCreatorImageState {
|
||||
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
|
||||
match self {
|
||||
ThemeCreatorImageState::Empty => write!(f, "{IMAGE_PICKER_BUTTON_PRE_SELECT_TEXT}"),
|
||||
ThemeCreatorImageState::Uploading => {
|
||||
write!(f, "{IMAGE_PICKER_BUTTON_SELECTING_TEXT}")
|
||||
}
|
||||
ThemeCreatorImageState::Uploaded => {
|
||||
write!(f, "{IMAGE_PICKER_BUTTON_POST_SELECT_TEXT}")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl ThemeCreatorBody {
|
||||
pub fn new(ctx: &mut ViewContext<Self>) -> Self {
|
||||
let editor = Self::editor(ctx);
|
||||
|
||||
Self {
|
||||
button_mouse_states: Default::default(),
|
||||
editor,
|
||||
theme_options: None,
|
||||
image_state: ThemeCreatorImageState::Empty,
|
||||
}
|
||||
}
|
||||
|
||||
fn editor(ctx: &mut ViewContext<Self>) -> ViewHandle<EditorView> {
|
||||
let editor = { ctx.add_typed_action_view(|ctx| EditorView::new(Default::default(), ctx)) };
|
||||
ctx.subscribe_to_view(&editor, move |me, _, event, ctx| {
|
||||
me.handle_editor_event(event, ctx);
|
||||
});
|
||||
|
||||
editor
|
||||
}
|
||||
|
||||
pub fn handle_editor_event(&mut self, event: &EditorEvent, ctx: &mut ViewContext<Self>) {
|
||||
if let EditorEvent::Edited(_) = event {
|
||||
if let Some(theme_options) = &mut self.theme_options {
|
||||
self.editor.update(ctx, |editor, ctx| {
|
||||
theme_options.set_name(editor.buffer_text(ctx));
|
||||
|
||||
let theme_kind = ThemeKind::InMemory(theme_options.clone());
|
||||
AppearanceManager::handle(ctx).update(ctx, |appearance_manager, ctx| {
|
||||
appearance_manager.set_transient_theme(theme_kind, ctx);
|
||||
});
|
||||
});
|
||||
}
|
||||
}
|
||||
ctx.notify();
|
||||
}
|
||||
|
||||
pub fn close(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
self.theme_options = None;
|
||||
self.image_state = ThemeCreatorImageState::Empty;
|
||||
|
||||
ctx.emit(ThemeCreatorBodyEvent::Close);
|
||||
}
|
||||
|
||||
pub fn cancel(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
AppearanceManager::handle(ctx).update(ctx, |appearance_manager, ctx| {
|
||||
appearance_manager.clear_transient_theme(ctx);
|
||||
});
|
||||
self.close(ctx);
|
||||
}
|
||||
|
||||
pub fn open_file_picker(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
self.image_state = ThemeCreatorImageState::Uploading;
|
||||
ctx.notify();
|
||||
ctx.emit(ThemeCreatorBodyEvent::OpenFilePicker);
|
||||
}
|
||||
|
||||
pub fn handle_file_picker_cancelled(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
self.image_state = if self.theme_options.is_some() {
|
||||
ThemeCreatorImageState::Uploaded
|
||||
} else {
|
||||
ThemeCreatorImageState::Empty
|
||||
};
|
||||
ctx.notify();
|
||||
}
|
||||
|
||||
#[cfg_attr(not(feature = "local_fs"), allow(unused))]
|
||||
pub fn create_theme(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
if let Some(theme_options) = self.theme_options.as_mut() {
|
||||
let theme_name = theme_options.name();
|
||||
let theme_yaml_file_name = format!("{theme_name}.yaml");
|
||||
let original_theme_image_path = theme_options.path();
|
||||
let original_theme_image_path_clone = original_theme_image_path.clone();
|
||||
|
||||
let image_extension = original_theme_image_path
|
||||
.extension()
|
||||
.and_then(|extension| extension.to_str());
|
||||
|
||||
let Some(image_extension) = image_extension else {
|
||||
self.send_error_toast(
|
||||
"Failed to process selected image. Please try again with a different image."
|
||||
.to_string(),
|
||||
ctx,
|
||||
);
|
||||
return;
|
||||
};
|
||||
|
||||
let dir = user_config::themes_dir();
|
||||
|
||||
theme_options.set_path(dir.join(format!("{theme_name}.{image_extension}")));
|
||||
let mut errored = true;
|
||||
#[cfg(feature = "local_fs")]
|
||||
{
|
||||
ThemeCreatorBody::write_theme(
|
||||
&theme_options.theme(),
|
||||
dir,
|
||||
theme_yaml_file_name,
|
||||
Some((
|
||||
original_theme_image_path_clone,
|
||||
theme_name.clone(),
|
||||
image_extension,
|
||||
)),
|
||||
|path| {
|
||||
send_telemetry_from_ctx!(TelemetryEvent::CreateCustomTheme, ctx);
|
||||
ctx.emit(ThemeCreatorBodyEvent::SetCustomTheme {
|
||||
theme: ThemeKind::Custom(CustomTheme::new(theme_name, path)),
|
||||
});
|
||||
errored = false;
|
||||
self.close(ctx);
|
||||
ctx.notify();
|
||||
},
|
||||
);
|
||||
}
|
||||
#[cfg(not(feature = "local_fs"))]
|
||||
log::warn!("Tried to save theme without a local filesystem.");
|
||||
if errored {
|
||||
self.send_error_toast("Something went wrong".to_string(), ctx);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Writes a theme to the filesystem. Calls the success callback if successful.
|
||||
/// Note: the image option should be (original_theme_image_path, theme_name, image_extension).
|
||||
#[cfg(feature = "local_fs")]
|
||||
pub fn write_theme<T>(
|
||||
theme: &WarpTheme,
|
||||
dir: PathBuf,
|
||||
theme_yaml_file_name: String,
|
||||
image_option: Option<(PathBuf, String, &str)>,
|
||||
success_callback: impl FnOnce(PathBuf) -> T,
|
||||
) -> Option<T> {
|
||||
if let Ok(theme_yaml) = serde_yaml::to_string(theme) {
|
||||
let path = dir.join(theme_yaml_file_name);
|
||||
if let Ok(mut file) = crate::util::file::create_file(&path) {
|
||||
if write!(file, "{theme_yaml}").is_ok() {
|
||||
match image_option {
|
||||
Some((image_path, theme_name, image_extension)) => {
|
||||
if copy(
|
||||
image_path.clone(),
|
||||
dir.join(format!("{theme_name}.{image_extension}")),
|
||||
)
|
||||
.is_ok()
|
||||
{
|
||||
return Some((success_callback)(path));
|
||||
}
|
||||
}
|
||||
None => return Some((success_callback)(path)),
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
pub fn set_theme_from_image_path(&mut self, path: PathBuf, ctx: &mut ViewContext<Self>) {
|
||||
let file_stem_string = path
|
||||
.clone()
|
||||
.file_stem()
|
||||
.and_then(|stem| stem.to_str())
|
||||
.unwrap_or_default()
|
||||
.to_string();
|
||||
|
||||
ctx.spawn(
|
||||
InMemoryThemeOptions::new(file_stem_string.clone(), path.clone()),
|
||||
move |theme_creator_body, theme_options, ctx| {
|
||||
match theme_options {
|
||||
Ok(theme_options) => {
|
||||
AppearanceManager::handle(ctx).update(ctx, |appearance_manager, ctx| {
|
||||
appearance_manager.clear_transient_theme(ctx);
|
||||
});
|
||||
|
||||
theme_creator_body.theme_options = Some(theme_options);
|
||||
theme_creator_body.editor.update(ctx, |editor, ctx| {
|
||||
editor.set_buffer_text(&file_stem_string, ctx);
|
||||
});
|
||||
theme_creator_body.image_state = ThemeCreatorImageState::Uploaded;
|
||||
},
|
||||
Err(e) => {
|
||||
theme_creator_body.send_error_toast(
|
||||
format!("Failed to process selected image due to error: {e}. Please try again with a different image."),
|
||||
ctx,
|
||||
);
|
||||
}
|
||||
}
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
pub fn set_background_color(&mut self, index: usize, ctx: &mut ViewContext<Self>) {
|
||||
if let Some(theme_options) = &mut self.theme_options {
|
||||
theme_options.set_chosen_bg_color_index(index);
|
||||
|
||||
let theme_kind = ThemeKind::InMemory(theme_options.clone());
|
||||
AppearanceManager::handle(ctx).update(ctx, |appearance_manager, ctx| {
|
||||
appearance_manager.set_transient_theme(theme_kind, ctx);
|
||||
});
|
||||
}
|
||||
|
||||
ctx.notify();
|
||||
}
|
||||
|
||||
fn send_error_toast(&self, message: String, ctx: &mut ViewContext<Self>) {
|
||||
ctx.emit(ThemeCreatorBodyEvent::ShowErrorToast { message });
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for ThemeCreatorBody {
|
||||
type Event = ThemeCreatorBodyEvent;
|
||||
}
|
||||
|
||||
impl View for ThemeCreatorBody {
|
||||
fn ui_name() -> &'static str {
|
||||
"ThemeCreatorBody"
|
||||
}
|
||||
|
||||
fn render(&self, app: &AppContext) -> Box<dyn Element> {
|
||||
let appearance = Appearance::as_ref(app);
|
||||
|
||||
let default_button_styles = UiComponentStyles {
|
||||
font_size: Some(BUTTON_FONT_SIZE),
|
||||
font_family_id: Some(appearance.ui_font_family()),
|
||||
font_color: Some(
|
||||
appearance
|
||||
.theme()
|
||||
.main_text_color(appearance.theme().background())
|
||||
.into(),
|
||||
),
|
||||
font_weight: Some(Weight::Bold),
|
||||
border_radius: Some(CornerRadius::with_all(Radius::Pixels(BUTTON_BORDER_RADIUS))),
|
||||
border_color: Some(appearance.theme().outline().into()),
|
||||
border_width: Some(BORDER_WIDTH),
|
||||
padding: Some(Coords::uniform(BUTTON_PADDING)),
|
||||
background: Some(appearance.theme().surface_1().into()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let cancel_hovered_styles = UiComponentStyles {
|
||||
background: Some(appearance.theme().outline().into()),
|
||||
border_color: Some(appearance.theme().accent().into()),
|
||||
..default_button_styles
|
||||
};
|
||||
|
||||
let disabled_styles = UiComponentStyles {
|
||||
background: Some(appearance.theme().surface_3().into()),
|
||||
font_color: Some(appearance.theme().disabled_ui_text_color().into()),
|
||||
..default_button_styles
|
||||
};
|
||||
|
||||
let create_default_styles = UiComponentStyles {
|
||||
background: Some(appearance.theme().accent().into()),
|
||||
border_color: Some(appearance.theme().accent().into()),
|
||||
font_color: Some(
|
||||
appearance
|
||||
.theme()
|
||||
.main_text_color(appearance.theme().accent())
|
||||
.into(),
|
||||
),
|
||||
..default_button_styles
|
||||
};
|
||||
|
||||
let create_hovered_styles = UiComponentStyles {
|
||||
border_color: Some(
|
||||
appearance
|
||||
.theme()
|
||||
.main_text_color(appearance.theme().background())
|
||||
.into(),
|
||||
),
|
||||
..create_default_styles
|
||||
};
|
||||
|
||||
let image_picker_button_background = if self.theme_options.is_some() {
|
||||
appearance.theme().surface_1()
|
||||
} else {
|
||||
appearance.theme().accent()
|
||||
};
|
||||
|
||||
let image_picker_button_hovered_styles = if self.theme_options.is_some() {
|
||||
cancel_hovered_styles
|
||||
} else {
|
||||
UiComponentStyles {
|
||||
border_color: Some(appearance.theme().foreground().into()),
|
||||
background: Some(image_picker_button_background.into()),
|
||||
font_color: Some(
|
||||
appearance
|
||||
.theme()
|
||||
.main_text_color(image_picker_button_background)
|
||||
.into(),
|
||||
),
|
||||
..default_button_styles
|
||||
}
|
||||
};
|
||||
|
||||
let image_picker_button = appearance.ui_builder().button_with_custom_styles(
|
||||
ButtonVariant::Accent,
|
||||
self.button_mouse_states.image_picker_mouse_state.clone(),
|
||||
UiComponentStyles {
|
||||
background: Some(image_picker_button_background.into()),
|
||||
font_color: Some(
|
||||
appearance
|
||||
.theme()
|
||||
.main_text_color(image_picker_button_background)
|
||||
.into(),
|
||||
),
|
||||
..default_button_styles
|
||||
},
|
||||
Some(image_picker_button_hovered_styles),
|
||||
Some(image_picker_button_hovered_styles),
|
||||
Some(disabled_styles),
|
||||
);
|
||||
|
||||
let cancel_button = appearance
|
||||
.ui_builder()
|
||||
.button(
|
||||
ButtonVariant::Secondary,
|
||||
self.button_mouse_states.cancel_mouse_state.clone(),
|
||||
)
|
||||
.with_style(UiComponentStyles {
|
||||
font_size: Some(BUTTON_FONT_SIZE),
|
||||
font_weight: Some(Weight::Bold),
|
||||
padding: Some(Coords::uniform(BUTTON_PADDING)),
|
||||
..Default::default()
|
||||
})
|
||||
.with_centered_text_label(CANCEL_BUTTON_TEXT.into());
|
||||
|
||||
let mut create_button = appearance
|
||||
.ui_builder()
|
||||
.button_with_custom_styles(
|
||||
ButtonVariant::Basic,
|
||||
self.button_mouse_states.create_mouse_state.clone(),
|
||||
create_default_styles,
|
||||
Some(create_hovered_styles),
|
||||
Some(create_hovered_styles),
|
||||
Some(disabled_styles),
|
||||
)
|
||||
.with_centered_text_label(CREATE_BUTTON_TEXT.into());
|
||||
|
||||
let mut flex: Flex = Flex::column()
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Stretch)
|
||||
.with_child(
|
||||
Container::new(
|
||||
Text::new_inline(MODAL_SUBHEADER, appearance.ui_font_family(), 14.)
|
||||
.with_color(appearance.theme().active_ui_text_color().into())
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
);
|
||||
|
||||
if let Some(theme_options) = &self.theme_options {
|
||||
flex.add_child(
|
||||
Container::new(
|
||||
Text::new_inline("Theme name", appearance.ui_font_family(), 14.)
|
||||
.with_color(appearance.theme().active_ui_text_color().into())
|
||||
.finish(),
|
||||
)
|
||||
.with_margin_top(12.)
|
||||
.finish(),
|
||||
);
|
||||
flex.add_child(
|
||||
Container::new(
|
||||
TextInput::new(
|
||||
self.editor.clone(),
|
||||
UiComponentStyles::default()
|
||||
.set_border_color(appearance.theme().outline().into())
|
||||
.set_font_family_id(appearance.header_font_family())
|
||||
.set_font_size(14.)
|
||||
.set_background(Fill::None)
|
||||
.set_border_radius(CornerRadius::with_all(Radius::Pixels(4.)))
|
||||
.set_padding(Coords::uniform(20.).top(10.).bottom(10.))
|
||||
.set_border_width(2.),
|
||||
)
|
||||
.build()
|
||||
.finish(),
|
||||
)
|
||||
.with_margin_top(8.)
|
||||
.finish(),
|
||||
);
|
||||
|
||||
flex.add_child(
|
||||
Container::new(
|
||||
Text::new_inline("Background color", appearance.ui_font_family(), 14.)
|
||||
.with_color(appearance.theme().active_ui_text_color().into())
|
||||
.finish(),
|
||||
)
|
||||
.with_margin_top(24.)
|
||||
.finish(),
|
||||
);
|
||||
|
||||
let mut color_row = Flex::row().with_cross_axis_alignment(CrossAxisAlignment::Center);
|
||||
|
||||
for (bg_color_index, bg_color) in
|
||||
theme_options.possible_bg_colors().into_iter().enumerate()
|
||||
{
|
||||
// Add corner radius if the rect is the first or last one
|
||||
let corner_radius = if bg_color_index == 0 {
|
||||
CornerRadius::with_left(Radius::Pixels(8.))
|
||||
} else if bg_color_index == 4 {
|
||||
CornerRadius::with_right(Radius::Pixels(8.))
|
||||
} else {
|
||||
CornerRadius::with_all(Radius::Pixels(0.))
|
||||
};
|
||||
|
||||
// Add a border around the chosen background color
|
||||
let border_width = if bg_color_index == theme_options.chosen_bg_color_index() {
|
||||
3.
|
||||
} else {
|
||||
0.
|
||||
};
|
||||
|
||||
color_row.add_child(
|
||||
Flex::row()
|
||||
.with_child(
|
||||
EventHandler::new(
|
||||
ConstrainedBox::new(
|
||||
Rect::new()
|
||||
.with_background_color(bg_color)
|
||||
.with_corner_radius(corner_radius)
|
||||
.with_border(
|
||||
Border::all(border_width).with_border_fill(
|
||||
appearance.theme().main_text_color(
|
||||
appearance.theme().background(),
|
||||
),
|
||||
),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_width(110.)
|
||||
.with_height(40.)
|
||||
.finish(),
|
||||
)
|
||||
.on_left_mouse_down(move |ctx, _, _| {
|
||||
ctx.dispatch_typed_action(
|
||||
ThemeCreatorBodyAction::SetBackgroundColor(bg_color_index),
|
||||
);
|
||||
DispatchEventResult::StopPropagation
|
||||
})
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
);
|
||||
}
|
||||
|
||||
flex.add_child(
|
||||
Container::new(color_row.finish())
|
||||
.with_margin_top(8.)
|
||||
.finish(),
|
||||
);
|
||||
} else {
|
||||
create_button = create_button.disabled();
|
||||
}
|
||||
|
||||
flex.add_child(
|
||||
Container::new(
|
||||
if let ThemeCreatorImageState::Uploading = self.image_state {
|
||||
image_picker_button
|
||||
.with_centered_text_label(self.image_state.to_string())
|
||||
.disabled()
|
||||
.build()
|
||||
.finish()
|
||||
} else {
|
||||
image_picker_button
|
||||
.with_text_and_icon_label(
|
||||
TextAndIcon::new(
|
||||
TextAndIconAlignment::TextFirst,
|
||||
self.image_state.to_string(),
|
||||
Icon::new("bundled/svg/upload-01.svg", ColorU::white()),
|
||||
MainAxisSize::Max,
|
||||
MainAxisAlignment::Center,
|
||||
vec2f(16., 16.),
|
||||
)
|
||||
.with_inner_padding(4.),
|
||||
)
|
||||
.build()
|
||||
.with_cursor(Cursor::PointingHand)
|
||||
.on_click(move |ctx, _, _| {
|
||||
ctx.dispatch_typed_action(ThemeCreatorBodyAction::OpenFilePicker)
|
||||
})
|
||||
.finish()
|
||||
},
|
||||
)
|
||||
.with_margin_top(24.)
|
||||
.finish(),
|
||||
);
|
||||
|
||||
Flex::column()
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Stretch)
|
||||
.with_child(
|
||||
Container::new(
|
||||
flex.with_child(
|
||||
Container::new(
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Shrinkable::new(
|
||||
0.5,
|
||||
Container::new(
|
||||
SavePosition::new(
|
||||
cancel_button
|
||||
.build()
|
||||
.with_cursor(Cursor::PointingHand)
|
||||
.on_click(move |ctx, _, _| {
|
||||
ctx.dispatch_typed_action(
|
||||
ThemeCreatorBodyAction::Cancel,
|
||||
)
|
||||
})
|
||||
.finish(),
|
||||
"theme_creator_cancel_button",
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_margin_right(8.)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_child(
|
||||
Shrinkable::new(
|
||||
0.5,
|
||||
create_button
|
||||
.build()
|
||||
.with_cursor(Cursor::PointingHand)
|
||||
.on_click(move |ctx, _, _| {
|
||||
ctx.dispatch_typed_action(
|
||||
ThemeCreatorBodyAction::Create,
|
||||
)
|
||||
})
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_main_axis_size(MainAxisSize::Max)
|
||||
.finish(),
|
||||
)
|
||||
.with_margin_top(24.)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedActionView for ThemeCreatorBody {
|
||||
type Action = ThemeCreatorBodyAction;
|
||||
|
||||
fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext<Self>) {
|
||||
match action {
|
||||
ThemeCreatorBodyAction::Cancel => self.cancel(ctx),
|
||||
ThemeCreatorBodyAction::OpenFilePicker => self.open_file_picker(ctx),
|
||||
ThemeCreatorBodyAction::SetBackgroundColor(index) => {
|
||||
self.set_background_color(*index, ctx)
|
||||
}
|
||||
ThemeCreatorBodyAction::Create => self.create_theme(ctx),
|
||||
ThemeCreatorBodyAction::HandleImageSelected(path) => {
|
||||
self.set_theme_from_image_path(path.clone(), ctx);
|
||||
ctx.notify();
|
||||
}
|
||||
ThemeCreatorBodyAction::FilePickerCancelled => self.handle_file_picker_cancelled(ctx),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,202 @@
|
||||
use crate::modal::Modal;
|
||||
use crate::themes::theme::ThemeKind;
|
||||
use crate::themes::theme_creator_body::{
|
||||
ThemeCreatorBody, ThemeCreatorBodyAction, ThemeCreatorBodyEvent,
|
||||
};
|
||||
use crate::view_components::DismissibleToast;
|
||||
use crate::workspace::ToastStack;
|
||||
use std::default::Default;
|
||||
use std::path::PathBuf;
|
||||
use warpui::fonts::Weight;
|
||||
use warpui::keymap::FixedBinding;
|
||||
use warpui::platform::{FilePickerConfiguration, FileType};
|
||||
use warpui::presenter::ChildView;
|
||||
use warpui::ui_components::components::{Coords, UiComponentStyles};
|
||||
use warpui::ViewHandle;
|
||||
use warpui::{AppContext, SingletonEntity as _};
|
||||
use warpui::{Element, Entity, TypedActionView, View, ViewContext};
|
||||
|
||||
const THEME_CREATOR_MODAL_HEADER: &str = "Create new theme from image";
|
||||
|
||||
pub struct ThemeCreatorModal {
|
||||
theme_creator_modal: ViewHandle<Modal<ThemeCreatorBody>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ThemeCreatorModalAction {
|
||||
Cancel,
|
||||
}
|
||||
|
||||
pub enum ThemeCreatorModalEvent {
|
||||
Close,
|
||||
SetCustomTheme { theme: ThemeKind },
|
||||
ShowErrorToast { message: String },
|
||||
}
|
||||
|
||||
pub fn init(app: &mut AppContext) {
|
||||
use warpui::keymap::macros::*;
|
||||
|
||||
app.register_fixed_bindings([FixedBinding::new(
|
||||
"escape",
|
||||
ThemeCreatorModalAction::Cancel,
|
||||
id!("ThemeCreatorModal"),
|
||||
)]);
|
||||
}
|
||||
|
||||
impl ThemeCreatorModal {
|
||||
pub fn new(ctx: &mut ViewContext<Self>) -> Self {
|
||||
let theme_creator_body =
|
||||
ctx.add_typed_action_view(|ctx: &mut ViewContext<'_, ThemeCreatorBody>| {
|
||||
ThemeCreatorBody::new(ctx)
|
||||
});
|
||||
|
||||
ctx.subscribe_to_view(&theme_creator_body, move |me, _, event, ctx| {
|
||||
me.handle_theme_creator_body_event(event, ctx);
|
||||
});
|
||||
|
||||
let theme_creator_modal = ctx.add_typed_action_view(|ctx| {
|
||||
Modal::new(
|
||||
Some(THEME_CREATOR_MODAL_HEADER.to_string()),
|
||||
theme_creator_body,
|
||||
ctx,
|
||||
)
|
||||
.with_modal_style(UiComponentStyles {
|
||||
width: Some(600.),
|
||||
height: Some(300.),
|
||||
..Default::default()
|
||||
})
|
||||
.with_header_style(UiComponentStyles {
|
||||
padding: Some(Coords {
|
||||
top: 24.,
|
||||
bottom: 0.,
|
||||
left: 24.,
|
||||
right: 24.,
|
||||
}),
|
||||
font_size: Some(16.),
|
||||
font_weight: Some(Weight::Bold),
|
||||
..Default::default()
|
||||
})
|
||||
.with_body_style(UiComponentStyles {
|
||||
padding: Some(Coords {
|
||||
top: 0.,
|
||||
bottom: 24.,
|
||||
left: 24.,
|
||||
right: 24.,
|
||||
}),
|
||||
height: Some(0.),
|
||||
..Default::default()
|
||||
})
|
||||
.with_background_opacity(100)
|
||||
.with_dismiss_on_click()
|
||||
.close_modal_button_disabled()
|
||||
});
|
||||
|
||||
Self {
|
||||
theme_creator_modal,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn close(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
ctx.emit(ThemeCreatorModalEvent::Close);
|
||||
}
|
||||
|
||||
pub fn cancel(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
self.theme_creator_modal.update(ctx, |modal, ctx| {
|
||||
modal.body().update(ctx, |theme_creator_body, ctx| {
|
||||
theme_creator_body.cancel(ctx);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
pub fn open_file_picker(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
let window_id = ctx.window_id();
|
||||
let theme_creator_body_id = self
|
||||
.theme_creator_modal
|
||||
.read(ctx, |modal, _ctx| modal.body().id());
|
||||
ctx.open_file_picker(
|
||||
move |result, ctx| match result {
|
||||
Ok(paths) => {
|
||||
if let Some(path_string) = paths.into_iter().next() {
|
||||
ctx.dispatch_typed_action_for_view(
|
||||
window_id,
|
||||
theme_creator_body_id,
|
||||
&ThemeCreatorBodyAction::HandleImageSelected(PathBuf::from(
|
||||
path_string,
|
||||
)),
|
||||
);
|
||||
} else {
|
||||
ctx.dispatch_typed_action_for_view(
|
||||
window_id,
|
||||
theme_creator_body_id,
|
||||
&ThemeCreatorBodyAction::FilePickerCancelled,
|
||||
);
|
||||
}
|
||||
}
|
||||
Err(err) => {
|
||||
ToastStack::handle(ctx).update(ctx, |toast_stack, ctx| {
|
||||
toast_stack.add_ephemeral_toast(
|
||||
DismissibleToast::error(format!("{err}")),
|
||||
window_id,
|
||||
ctx,
|
||||
);
|
||||
});
|
||||
ctx.dispatch_typed_action_for_view(
|
||||
window_id,
|
||||
theme_creator_body_id,
|
||||
&ThemeCreatorBodyAction::FilePickerCancelled,
|
||||
);
|
||||
}
|
||||
},
|
||||
FilePickerConfiguration::new().set_allowed_file_types(vec![FileType::Image]),
|
||||
);
|
||||
}
|
||||
|
||||
fn handle_theme_creator_body_event(
|
||||
&mut self,
|
||||
event: &ThemeCreatorBodyEvent,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
ThemeCreatorBodyEvent::Close => {
|
||||
self.close(ctx);
|
||||
}
|
||||
ThemeCreatorBodyEvent::OpenFilePicker => {
|
||||
self.open_file_picker(ctx);
|
||||
}
|
||||
ThemeCreatorBodyEvent::SetCustomTheme { theme } => {
|
||||
ctx.emit(ThemeCreatorModalEvent::SetCustomTheme {
|
||||
theme: theme.clone(),
|
||||
});
|
||||
}
|
||||
ThemeCreatorBodyEvent::ShowErrorToast { message } => {
|
||||
ctx.emit(ThemeCreatorModalEvent::ShowErrorToast {
|
||||
message: message.clone(),
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for ThemeCreatorModal {
|
||||
type Event = ThemeCreatorModalEvent;
|
||||
}
|
||||
|
||||
impl View for ThemeCreatorModal {
|
||||
fn ui_name() -> &'static str {
|
||||
"ThemeCreatorModal"
|
||||
}
|
||||
|
||||
fn render(&self, _app: &AppContext) -> Box<dyn Element> {
|
||||
ChildView::new(&self.theme_creator_modal).finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedActionView for ThemeCreatorModal {
|
||||
type Action = ThemeCreatorModalAction;
|
||||
|
||||
fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext<Self>) {
|
||||
match action {
|
||||
ThemeCreatorModalAction::Cancel => self.cancel(ctx),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,59 @@
|
||||
use crate::util::color::OPAQUE;
|
||||
|
||||
use super::*;
|
||||
|
||||
// TODO(CORE-3626): figure out why the colors returned on Windows are slightly different.
|
||||
#[test]
|
||||
#[cfg(all(not(target_family = "wasm"), not(windows)))]
|
||||
fn top_colors_jellyfish_test() {
|
||||
let jellyfish_bg_path: PathBuf = [
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"assets",
|
||||
"async",
|
||||
"jpg",
|
||||
"jellyfish_bg.jpg",
|
||||
]
|
||||
.iter()
|
||||
.collect();
|
||||
|
||||
let colors = top_colors_for_image(jellyfish_bg_path)
|
||||
.expect("should be able to get colors from jellyfish bg");
|
||||
|
||||
assert_eq!(colors[0], ColorU::new(14, 13, 30, OPAQUE));
|
||||
assert_eq!(colors[1], ColorU::new(23, 22, 55, OPAQUE));
|
||||
assert_eq!(colors[2], ColorU::new(94, 38, 70, OPAQUE));
|
||||
assert_eq!(colors[3], ColorU::new(52, 68, 91, OPAQUE));
|
||||
assert_eq!(colors[4], ColorU::new(112, 118, 129, OPAQUE));
|
||||
}
|
||||
|
||||
#[test]
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn top_colors_invalid_image_test() {
|
||||
let invalid_image_path: PathBuf = [
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"assets",
|
||||
"async",
|
||||
"jpg",
|
||||
"this_doesnt_exist.jpg",
|
||||
]
|
||||
.iter()
|
||||
.collect();
|
||||
|
||||
let colors = top_colors_for_image(invalid_image_path);
|
||||
assert!(colors.is_err());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn accent_colors_contrast_test() {
|
||||
let foreground = ColorU::white();
|
||||
let background = ColorU::black();
|
||||
let accent_options = [
|
||||
ColorU::new(255, 0, 0, OPAQUE),
|
||||
ColorU::new(100, 0, 0, OPAQUE),
|
||||
ColorU::new(10, 0, 0, OPAQUE),
|
||||
];
|
||||
assert_eq!(
|
||||
accent_options[1],
|
||||
pick_accent_color_from_options(&[background, foreground], &accent_options)
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,281 @@
|
||||
use crate::appearance::Appearance;
|
||||
use crate::send_telemetry_from_ctx;
|
||||
use crate::server::telemetry::TelemetryEvent;
|
||||
use crate::settings::{active_theme_kind, ThemeSettings};
|
||||
use crate::themes::theme::{ThemeKind, WarpTheme};
|
||||
use crate::user_config;
|
||||
use crate::user_config::util::from_yaml;
|
||||
use std::default::Default;
|
||||
use std::fs;
|
||||
use std::fs::remove_file;
|
||||
use warpui::assets::asset_cache::AssetSource;
|
||||
use warpui::elements::{
|
||||
Container, CornerRadius, CrossAxisAlignment, Flex, MainAxisSize, MouseStateHandle,
|
||||
ParentElement, Radius, SavePosition, Shrinkable, Text,
|
||||
};
|
||||
use warpui::fonts::Weight;
|
||||
use warpui::ui_components::button::ButtonVariant;
|
||||
use warpui::ui_components::components::{Coords, UiComponent, UiComponentStyles};
|
||||
use warpui::{
|
||||
platform::Cursor, AppContext, Element, Entity, SingletonEntity, TypedActionView, View,
|
||||
ViewContext,
|
||||
};
|
||||
|
||||
const BUTTON_PADDING: f32 = 12.;
|
||||
const BUTTON_FONT_SIZE: f32 = 14.;
|
||||
const BUTTON_BORDER_RADIUS: f32 = 4.;
|
||||
const BORDER_WIDTH: f32 = 1.;
|
||||
|
||||
const MODAL_SUBHEADER: &str = "This will permanently delete the theme.";
|
||||
const CANCEL_BUTTON_TEXT: &str = "Cancel";
|
||||
const DELETE_BUTTON_TEXT: &str = "Delete theme";
|
||||
|
||||
#[derive(Default)]
|
||||
struct MouseStateHandles {
|
||||
cancel_mouse_state: MouseStateHandle,
|
||||
create_mouse_state: MouseStateHandle,
|
||||
}
|
||||
|
||||
pub struct ThemeDeletionBody {
|
||||
button_mouse_states: MouseStateHandles,
|
||||
theme_kind: Option<ThemeKind>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ThemeDeletionBodyAction {
|
||||
Delete,
|
||||
Cancel,
|
||||
}
|
||||
|
||||
pub enum ThemeDeletionBodyEvent {
|
||||
Close,
|
||||
ShowErrorToast { message: String },
|
||||
DeleteCurrentTheme,
|
||||
}
|
||||
|
||||
impl Default for ThemeDeletionBody {
|
||||
fn default() -> Self {
|
||||
Self::new()
|
||||
}
|
||||
}
|
||||
|
||||
impl ThemeDeletionBody {
|
||||
pub fn new() -> Self {
|
||||
Self {
|
||||
button_mouse_states: Default::default(),
|
||||
theme_kind: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn close(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
ctx.emit(ThemeDeletionBodyEvent::Close);
|
||||
}
|
||||
|
||||
pub fn delete_theme(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
let mut errored = true;
|
||||
let dir = user_config::themes_dir();
|
||||
// Check if the theme directory exists
|
||||
if fs::metadata(&dir).is_ok() {
|
||||
if let Some(ThemeKind::Custom(custom_theme)) = &self.theme_kind {
|
||||
if let Ok(theme_from_yaml) = from_yaml::<WarpTheme>(custom_theme.path()) {
|
||||
// If theme has an image
|
||||
if let Some(image) = theme_from_yaml.background_image() {
|
||||
// Only delete the image if it is in the ./warp/themes directory.
|
||||
// We don't want to delete images from other parts of the user's filesystem.
|
||||
if let AssetSource::LocalFile { path } = image.source() {
|
||||
let image_path_in_themes_dir = dir.join(path.as_str());
|
||||
let _ = remove_file(image_path_in_themes_dir);
|
||||
} else {
|
||||
log::warn!("Attempted to delete a custom theme image with an unexpected image source");
|
||||
}
|
||||
}
|
||||
|
||||
// Even if image can't be deleted, delete the theme .yaml file
|
||||
if remove_file(custom_theme.path()).is_ok() {
|
||||
let current_theme = active_theme_kind(ThemeSettings::as_ref(ctx), ctx);
|
||||
if matches!(current_theme, ThemeKind::Custom(theme) if &theme == custom_theme)
|
||||
{
|
||||
// Reset theme to Dark if we are deleting the current theme
|
||||
ctx.emit(ThemeDeletionBodyEvent::DeleteCurrentTheme)
|
||||
}
|
||||
errored = false;
|
||||
send_telemetry_from_ctx!(TelemetryEvent::DeleteCustomTheme, ctx);
|
||||
self.close(ctx);
|
||||
ctx.notify();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if errored {
|
||||
self.send_error_toast("Something went wrong", ctx);
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_theme_kind(&mut self, theme_kind: ThemeKind) {
|
||||
self.theme_kind = Some(theme_kind);
|
||||
}
|
||||
|
||||
fn send_error_toast(&self, message: &str, ctx: &mut ViewContext<Self>) {
|
||||
ctx.emit(ThemeDeletionBodyEvent::ShowErrorToast {
|
||||
message: message.to_string(),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for ThemeDeletionBody {
|
||||
type Event = ThemeDeletionBodyEvent;
|
||||
}
|
||||
|
||||
impl View for ThemeDeletionBody {
|
||||
fn ui_name() -> &'static str {
|
||||
"ThemeDeletionBody"
|
||||
}
|
||||
|
||||
fn render(&self, app: &AppContext) -> Box<dyn Element> {
|
||||
let appearance = Appearance::as_ref(app);
|
||||
|
||||
let default_button_styles = UiComponentStyles {
|
||||
font_size: Some(BUTTON_FONT_SIZE),
|
||||
font_family_id: Some(appearance.ui_font_family()),
|
||||
font_color: Some(
|
||||
appearance
|
||||
.theme()
|
||||
.main_text_color(appearance.theme().background())
|
||||
.into(),
|
||||
),
|
||||
font_weight: Some(Weight::Bold),
|
||||
border_radius: Some(CornerRadius::with_all(Radius::Pixels(BUTTON_BORDER_RADIUS))),
|
||||
border_color: Some(appearance.theme().outline().into()),
|
||||
border_width: Some(BORDER_WIDTH),
|
||||
padding: Some(Coords::uniform(BUTTON_PADDING)),
|
||||
background: Some(appearance.theme().surface_1().into()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let cancel_hovered_styles = UiComponentStyles {
|
||||
background: Some(appearance.theme().outline().into()),
|
||||
border_color: Some(appearance.theme().accent().into()),
|
||||
..default_button_styles
|
||||
};
|
||||
|
||||
let disabled_styles = UiComponentStyles {
|
||||
background: Some(appearance.theme().surface_3().into()),
|
||||
font_color: Some(appearance.theme().disabled_ui_text_color().into()),
|
||||
..default_button_styles
|
||||
};
|
||||
|
||||
let create_default_styles = UiComponentStyles {
|
||||
background: Some(appearance.theme().accent().into()),
|
||||
border_color: Some(appearance.theme().accent().into()),
|
||||
font_color: Some(
|
||||
appearance
|
||||
.theme()
|
||||
.main_text_color(appearance.theme().accent())
|
||||
.into(),
|
||||
),
|
||||
..default_button_styles
|
||||
};
|
||||
|
||||
let create_hovered_styles = UiComponentStyles {
|
||||
border_color: Some(
|
||||
appearance
|
||||
.theme()
|
||||
.main_text_color(appearance.theme().background())
|
||||
.into(),
|
||||
),
|
||||
..create_default_styles
|
||||
};
|
||||
|
||||
let cancel_button = appearance
|
||||
.ui_builder()
|
||||
.button_with_custom_styles(
|
||||
ButtonVariant::Basic,
|
||||
self.button_mouse_states.cancel_mouse_state.clone(),
|
||||
default_button_styles,
|
||||
Some(cancel_hovered_styles),
|
||||
Some(cancel_hovered_styles),
|
||||
Some(disabled_styles),
|
||||
)
|
||||
.with_centered_text_label(CANCEL_BUTTON_TEXT.into());
|
||||
|
||||
let create_button = appearance
|
||||
.ui_builder()
|
||||
.button_with_custom_styles(
|
||||
ButtonVariant::Basic,
|
||||
self.button_mouse_states.create_mouse_state.clone(),
|
||||
create_default_styles,
|
||||
Some(create_hovered_styles),
|
||||
Some(create_hovered_styles),
|
||||
Some(disabled_styles),
|
||||
)
|
||||
.with_centered_text_label(DELETE_BUTTON_TEXT.into());
|
||||
|
||||
Flex::column()
|
||||
.with_cross_axis_alignment(CrossAxisAlignment::Stretch)
|
||||
.with_child(
|
||||
Container::new(
|
||||
Text::new_inline(MODAL_SUBHEADER, appearance.ui_font_family(), 14.)
|
||||
.with_color(appearance.theme().active_ui_text_color().into())
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_child(
|
||||
Container::new(
|
||||
Flex::row()
|
||||
.with_child(
|
||||
Shrinkable::new(
|
||||
0.5,
|
||||
Container::new(
|
||||
SavePosition::new(
|
||||
cancel_button
|
||||
.build()
|
||||
.with_cursor(Cursor::PointingHand)
|
||||
.on_click(move |ctx, _, _| {
|
||||
ctx.dispatch_typed_action(
|
||||
ThemeDeletionBodyAction::Cancel,
|
||||
)
|
||||
})
|
||||
.finish(),
|
||||
"theme_deletion_cancel_button",
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_margin_right(8.)
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_child(
|
||||
Shrinkable::new(
|
||||
0.5,
|
||||
create_button
|
||||
.build()
|
||||
.with_cursor(Cursor::PointingHand)
|
||||
.on_click(move |ctx, _, _| {
|
||||
ctx.dispatch_typed_action(ThemeDeletionBodyAction::Delete)
|
||||
})
|
||||
.finish(),
|
||||
)
|
||||
.finish(),
|
||||
)
|
||||
.with_main_axis_size(MainAxisSize::Max)
|
||||
.finish(),
|
||||
)
|
||||
.with_margin_top(24.)
|
||||
.finish(),
|
||||
)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedActionView for ThemeDeletionBody {
|
||||
type Action = ThemeDeletionBodyAction;
|
||||
|
||||
fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext<Self>) {
|
||||
match action {
|
||||
ThemeDeletionBodyAction::Cancel => self.close(ctx),
|
||||
ThemeDeletionBodyAction::Delete => self.delete_theme(ctx),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,148 @@
|
||||
use crate::modal::Modal;
|
||||
use crate::themes::theme::ThemeKind;
|
||||
use crate::themes::theme_deletion_body::{ThemeDeletionBody, ThemeDeletionBodyEvent};
|
||||
use std::default::Default;
|
||||
use warpui::fonts::Weight;
|
||||
use warpui::keymap::FixedBinding;
|
||||
use warpui::presenter::ChildView;
|
||||
use warpui::ui_components::components::{Coords, UiComponentStyles};
|
||||
use warpui::AppContext;
|
||||
use warpui::ViewHandle;
|
||||
use warpui::{Element, Entity, TypedActionView, View, ViewContext};
|
||||
|
||||
const THEME_DELETION_MODAL_HEADER: &str = "Are you sure you want to delete this theme?";
|
||||
|
||||
pub struct ThemeDeletionModal {
|
||||
theme_deletion_modal: ViewHandle<Modal<ThemeDeletionBody>>,
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub enum ThemeDeletionModalAction {
|
||||
Cancel,
|
||||
}
|
||||
|
||||
pub enum ThemeDeletionModalEvent {
|
||||
Close,
|
||||
ShowErrorToast { message: String },
|
||||
DeleteCurrentTheme,
|
||||
}
|
||||
|
||||
pub fn init(app: &mut AppContext) {
|
||||
use warpui::keymap::macros::*;
|
||||
|
||||
app.register_fixed_bindings([FixedBinding::new(
|
||||
"escape",
|
||||
ThemeDeletionModalAction::Cancel,
|
||||
id!("ThemeDeletionModal"),
|
||||
)]);
|
||||
}
|
||||
|
||||
impl ThemeDeletionModal {
|
||||
pub fn new(ctx: &mut ViewContext<Self>) -> Self {
|
||||
let theme_deletion_body =
|
||||
ctx.add_typed_action_view(|_ctx: &mut ViewContext<'_, ThemeDeletionBody>| {
|
||||
ThemeDeletionBody::new()
|
||||
});
|
||||
|
||||
ctx.subscribe_to_view(&theme_deletion_body, move |me, _, event, ctx| {
|
||||
me.handle_theme_deletion_body_event(event, ctx);
|
||||
});
|
||||
|
||||
let theme_deletion_modal = ctx.add_typed_action_view(|ctx| {
|
||||
Modal::new(
|
||||
Some(THEME_DELETION_MODAL_HEADER.to_string()),
|
||||
theme_deletion_body,
|
||||
ctx,
|
||||
)
|
||||
.with_modal_style(UiComponentStyles {
|
||||
width: Some(460.),
|
||||
height: Some(132.),
|
||||
..Default::default()
|
||||
})
|
||||
.with_header_style(UiComponentStyles {
|
||||
padding: Some(Coords {
|
||||
top: 8.,
|
||||
bottom: 0.,
|
||||
left: 24.,
|
||||
right: 24.,
|
||||
}),
|
||||
font_size: Some(16.),
|
||||
font_weight: Some(Weight::Bold),
|
||||
..Default::default()
|
||||
})
|
||||
.with_body_style(UiComponentStyles {
|
||||
padding: Some(Coords {
|
||||
top: 0.,
|
||||
bottom: 24.,
|
||||
left: 24.,
|
||||
right: 24.,
|
||||
}),
|
||||
height: Some(0.),
|
||||
..Default::default()
|
||||
})
|
||||
.with_background_opacity(100)
|
||||
.with_dismiss_on_click()
|
||||
.close_modal_button_disabled()
|
||||
});
|
||||
|
||||
Self {
|
||||
theme_deletion_modal,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn close(&mut self, ctx: &mut ViewContext<Self>) {
|
||||
ctx.emit(ThemeDeletionModalEvent::Close);
|
||||
}
|
||||
|
||||
pub fn set_theme_kind(&mut self, theme_kind: ThemeKind, ctx: &mut ViewContext<Self>) {
|
||||
self.theme_deletion_modal.update(ctx, |modal, ctx| {
|
||||
modal.body().update(ctx, |theme_deletion_body, _ctx| {
|
||||
theme_deletion_body.set_theme_kind(theme_kind);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
fn handle_theme_deletion_body_event(
|
||||
&mut self,
|
||||
event: &ThemeDeletionBodyEvent,
|
||||
ctx: &mut ViewContext<Self>,
|
||||
) {
|
||||
match event {
|
||||
ThemeDeletionBodyEvent::Close => {
|
||||
self.close(ctx);
|
||||
}
|
||||
ThemeDeletionBodyEvent::ShowErrorToast { message } => {
|
||||
ctx.emit(ThemeDeletionModalEvent::ShowErrorToast {
|
||||
message: message.clone(),
|
||||
});
|
||||
}
|
||||
ThemeDeletionBodyEvent::DeleteCurrentTheme => {
|
||||
ctx.emit(ThemeDeletionModalEvent::DeleteCurrentTheme)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Entity for ThemeDeletionModal {
|
||||
type Event = ThemeDeletionModalEvent;
|
||||
}
|
||||
|
||||
impl View for ThemeDeletionModal {
|
||||
fn ui_name() -> &'static str {
|
||||
"ThemeDeletionModal"
|
||||
}
|
||||
|
||||
fn render(&self, _app: &AppContext) -> Box<dyn Element> {
|
||||
ChildView::new(&self.theme_deletion_modal).finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl TypedActionView for ThemeDeletionModal {
|
||||
type Action = ThemeDeletionModalAction;
|
||||
|
||||
fn handle_action(&mut self, action: &Self::Action, ctx: &mut ViewContext<Self>) {
|
||||
match action {
|
||||
ThemeDeletionModalAction::Cancel => self.close(ctx),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,69 @@
|
||||
use super::*;
|
||||
use crate::util::color::OPAQUE;
|
||||
|
||||
#[test]
|
||||
#[cfg(not(target_family = "wasm"))]
|
||||
fn in_memory_theme_generation_test() {
|
||||
let mountains_bg_path: PathBuf = [
|
||||
env!("CARGO_MANIFEST_DIR"),
|
||||
"assets",
|
||||
"async",
|
||||
"jpg",
|
||||
"mountains.jpg",
|
||||
]
|
||||
.iter()
|
||||
.collect();
|
||||
|
||||
let mut in_memory_theme = warpui::r#async::block_on(InMemoryThemeOptions::new(
|
||||
"mountains".to_string(),
|
||||
mountains_bg_path.clone(),
|
||||
))
|
||||
.unwrap();
|
||||
|
||||
let mountains_bg_path_string = mountains_bg_path.to_str().unwrap_or_default().to_owned();
|
||||
assert_eq!(
|
||||
in_memory_theme.theme(),
|
||||
WarpTheme::new(
|
||||
// the theme defaults to the 0th bg color
|
||||
ColorU::new(35, 31, 44, OPAQUE).into(),
|
||||
// this background color makes it a "dark" theme, so the foreground is white
|
||||
ColorU::white(),
|
||||
// the most distinct accent color is 3rd one
|
||||
ColorU::new(238, 203, 111, OPAQUE).into(),
|
||||
None,
|
||||
Some(Details::Darker),
|
||||
dark_mode_colors(),
|
||||
Some(Image {
|
||||
source: AssetSource::LocalFile {
|
||||
path: mountains_bg_path_string.clone()
|
||||
},
|
||||
opacity: 30,
|
||||
}),
|
||||
Some("mountains".to_string()),
|
||||
)
|
||||
);
|
||||
|
||||
in_memory_theme.chosen_bg_color_index = 2;
|
||||
|
||||
assert_eq!(
|
||||
in_memory_theme.theme(),
|
||||
WarpTheme::new(
|
||||
// now the background is the 2nd one
|
||||
ColorU::new(229, 142, 113, OPAQUE).into(),
|
||||
// changing the background color made this a light theme
|
||||
ColorU::black(),
|
||||
// now the 4th color is the most distinct color
|
||||
ColorU::new(193, 217, 212, OPAQUE).into(),
|
||||
None,
|
||||
Some(Details::Lighter),
|
||||
light_mode_colors(),
|
||||
Some(Image {
|
||||
source: AssetSource::LocalFile {
|
||||
path: mountains_bg_path_string
|
||||
},
|
||||
opacity: 30,
|
||||
}),
|
||||
Some("mountains".to_string()),
|
||||
)
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user