Files
oak-gpui/crates/gpui/src/color.rs
T
Mikayla Maki c0edb5bd6c GPUI custom window prompts (#8980)
This adds a GPUI fallback for window prompts. Linux does not support
this feature by default, so we have to implement it ourselves.

This implementation also makes it possible for GPUI clients to override
the platform prompts with their own implementations.

This is just a first pass. These alerts are not keyboard accessible yet,
does not reflect the prompt level, they're implemented in-window, rather
than as popups, and the whole feature need a pass from a designer.
Regardless, this gets us one step closer to Linux support :)

<img width="650" alt="Screenshot 2024-03-06 at 5 58 08 PM"
src="https://github.com/zed-industries/zed/assets/2280405/972ebb55-fd1f-4066-969c-a87f63b22a6f">

Release Notes:

- N/A
2024-03-06 18:15:06 -08:00

493 lines
13 KiB
Rust

use anyhow::bail;
use serde::de::{self, Deserialize, Deserializer, Visitor};
use std::fmt;
/// Convert an RGB hex color code number to a color type
pub fn rgb(hex: u32) -> Rgba {
let r = ((hex >> 16) & 0xFF) as f32 / 255.0;
let g = ((hex >> 8) & 0xFF) as f32 / 255.0;
let b = (hex & 0xFF) as f32 / 255.0;
Rgba { r, g, b, a: 1.0 }
}
/// Convert an RGBA hex color code number to [`Rgba`]
pub fn rgba(hex: u32) -> Rgba {
let r = ((hex >> 24) & 0xFF) as f32 / 255.0;
let g = ((hex >> 16) & 0xFF) as f32 / 255.0;
let b = ((hex >> 8) & 0xFF) as f32 / 255.0;
let a = (hex & 0xFF) as f32 / 255.0;
Rgba { r, g, b, a }
}
/// An RGBA color
#[derive(PartialEq, Clone, Copy, Default)]
pub struct Rgba {
/// The red component of the color, in the range 0.0 to 1.0
pub r: f32,
/// The green component of the color, in the range 0.0 to 1.0
pub g: f32,
/// The blue component of the color, in the range 0.0 to 1.0
pub b: f32,
/// The alpha component of the color, in the range 0.0 to 1.0
pub a: f32,
}
impl fmt::Debug for Rgba {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f, "rgba({:#010x})", u32::from(*self))
}
}
impl Rgba {
/// Create a new [`Rgba`] color by blending this and another color together
pub fn blend(&self, other: Rgba) -> Self {
if other.a >= 1.0 {
other
} else if other.a <= 0.0 {
return *self;
} else {
return Rgba {
r: (self.r * (1.0 - other.a)) + (other.r * other.a),
g: (self.g * (1.0 - other.a)) + (other.g * other.a),
b: (self.b * (1.0 - other.a)) + (other.b * other.a),
a: self.a,
};
}
}
}
impl From<Rgba> for u32 {
fn from(rgba: Rgba) -> Self {
let r = (rgba.r * 255.0) as u32;
let g = (rgba.g * 255.0) as u32;
let b = (rgba.b * 255.0) as u32;
let a = (rgba.a * 255.0) as u32;
(r << 24) | (g << 16) | (b << 8) | a
}
}
struct RgbaVisitor;
impl<'de> Visitor<'de> for RgbaVisitor {
type Value = Rgba;
fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
formatter.write_str("a string in the format #rrggbb or #rrggbbaa")
}
fn visit_str<E: de::Error>(self, value: &str) -> Result<Rgba, E> {
Rgba::try_from(value).map_err(E::custom)
}
}
impl<'de> Deserialize<'de> for Rgba {
fn deserialize<D: Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
deserializer.deserialize_str(RgbaVisitor)
}
}
impl From<Hsla> for Rgba {
fn from(color: Hsla) -> Self {
let h = color.h;
let s = color.s;
let l = color.l;
let c = (1.0 - (2.0 * l - 1.0).abs()) * s;
let x = c * (1.0 - ((h * 6.0) % 2.0 - 1.0).abs());
let m = l - c / 2.0;
let cm = c + m;
let xm = x + m;
let (r, g, b) = match (h * 6.0).floor() as i32 {
0 | 6 => (cm, xm, m),
1 => (xm, cm, m),
2 => (m, cm, xm),
3 => (m, xm, cm),
4 => (xm, m, cm),
_ => (cm, m, xm),
};
Rgba {
r,
g,
b,
a: color.a,
}
}
}
impl TryFrom<&'_ str> for Rgba {
type Error = anyhow::Error;
fn try_from(value: &'_ str) -> Result<Self, Self::Error> {
const RGB: usize = "rgb".len();
const RGBA: usize = "rgba".len();
const RRGGBB: usize = "rrggbb".len();
const RRGGBBAA: usize = "rrggbbaa".len();
const EXPECTED_FORMATS: &str = "Expected #rgb, #rgba, #rrggbb, or #rrggbbaa";
let Some(("", hex)) = value.trim().split_once('#') else {
bail!("invalid RGBA hex color: '{value}'. {EXPECTED_FORMATS}");
};
let (r, g, b, a) = match hex.len() {
RGB | RGBA => {
let r = u8::from_str_radix(&hex[0..1], 16)?;
let g = u8::from_str_radix(&hex[1..2], 16)?;
let b = u8::from_str_radix(&hex[2..3], 16)?;
let a = if hex.len() == RGBA {
u8::from_str_radix(&hex[3..4], 16)?
} else {
0xf
};
/// Duplicates a given hex digit.
/// E.g., `0xf` -> `0xff`.
const fn duplicate(value: u8) -> u8 {
value << 4 | value
}
(duplicate(r), duplicate(g), duplicate(b), duplicate(a))
}
RRGGBB | RRGGBBAA => {
let r = u8::from_str_radix(&hex[0..2], 16)?;
let g = u8::from_str_radix(&hex[2..4], 16)?;
let b = u8::from_str_radix(&hex[4..6], 16)?;
let a = if hex.len() == RRGGBBAA {
u8::from_str_radix(&hex[6..8], 16)?
} else {
0xff
};
(r, g, b, a)
}
_ => bail!("invalid RGBA hex color: '{value}'. {EXPECTED_FORMATS}"),
};
Ok(Rgba {
r: r as f32 / 255.,
g: g as f32 / 255.,
b: b as f32 / 255.,
a: a as f32 / 255.,
})
}
}
/// An HSLA color
#[derive(Default, Copy, Clone, Debug)]
#[repr(C)]
pub struct Hsla {
/// Hue, in a range from 0 to 1
pub h: f32,
/// Saturation, in a range from 0 to 1
pub s: f32,
/// Lightness, in a range from 0 to 1
pub l: f32,
/// Alpha, in a range from 0 to 1
pub a: f32,
}
impl PartialEq for Hsla {
fn eq(&self, other: &Self) -> bool {
self.h
.total_cmp(&other.h)
.then(self.s.total_cmp(&other.s))
.then(self.l.total_cmp(&other.l).then(self.a.total_cmp(&other.a)))
.is_eq()
}
}
impl PartialOrd for Hsla {
fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
Some(self.cmp(other))
}
}
impl Ord for Hsla {
fn cmp(&self, other: &Self) -> std::cmp::Ordering {
self.h
.total_cmp(&other.h)
.then(self.s.total_cmp(&other.s))
.then(self.l.total_cmp(&other.l).then(self.a.total_cmp(&other.a)))
}
}
impl Eq for Hsla {}
/// Construct an [`Hsla`] object from plain values
pub fn hsla(h: f32, s: f32, l: f32, a: f32) -> Hsla {
Hsla {
h: h.clamp(0., 1.),
s: s.clamp(0., 1.),
l: l.clamp(0., 1.),
a: a.clamp(0., 1.),
}
}
/// Pure black in [`Hsla`]
pub fn black() -> Hsla {
Hsla {
h: 0.,
s: 0.,
l: 0.,
a: 1.,
}
}
/// Transparent black in [`Hsla`]
pub fn transparent_black() -> Hsla {
Hsla {
h: 0.,
s: 0.,
l: 0.,
a: 0.,
}
}
/// Opaque grey in [`Hsla`], values will be clamped to the range [0, 1]
pub fn opaque_grey(lightness: f32, opacity: f32) -> Hsla {
Hsla {
h: 0.,
s: 0.,
l: lightness.clamp(0., 1.),
a: opacity.clamp(0., 1.),
}
}
/// Pure white in [`Hsla`]
pub fn white() -> Hsla {
Hsla {
h: 0.,
s: 0.,
l: 1.,
a: 1.,
}
}
/// The color red in [`Hsla`]
pub fn red() -> Hsla {
Hsla {
h: 0.,
s: 1.,
l: 0.5,
a: 1.,
}
}
/// The color blue in [`Hsla`]
pub fn blue() -> Hsla {
Hsla {
h: 0.6,
s: 1.,
l: 0.5,
a: 1.,
}
}
/// The color green in [`Hsla`]
pub fn green() -> Hsla {
Hsla {
h: 0.33,
s: 1.,
l: 0.5,
a: 1.,
}
}
/// The color yellow in [`Hsla`]
pub fn yellow() -> Hsla {
Hsla {
h: 0.16,
s: 1.,
l: 0.5,
a: 1.,
}
}
impl Hsla {
/// Converts this HSLA color to an RGBA color.
pub fn to_rgb(self) -> Rgba {
self.into()
}
/// The color red
pub fn red() -> Self {
red()
}
/// The color green
pub fn green() -> Self {
green()
}
/// The color blue
pub fn blue() -> Self {
blue()
}
/// The color black
pub fn black() -> Self {
black()
}
/// The color white
pub fn white() -> Self {
white()
}
/// The color transparent black
pub fn transparent_black() -> Self {
transparent_black()
}
/// Returns true if the HSLA color is fully transparent, false otherwise.
pub fn is_transparent(&self) -> bool {
self.a == 0.0
}
/// Blends `other` on top of `self` based on `other`'s alpha value. The resulting color is a combination of `self`'s and `other`'s colors.
///
/// If `other`'s alpha value is 1.0 or greater, `other` color is fully opaque, thus `other` is returned as the output color.
/// If `other`'s alpha value is 0.0 or less, `other` color is fully transparent, thus `self` is returned as the output color.
/// Else, the output color is calculated as a blend of `self` and `other` based on their weighted alpha values.
///
/// Assumptions:
/// - Alpha values are contained in the range [0, 1], with 1 as fully opaque and 0 as fully transparent.
/// - The relative contributions of `self` and `other` is based on `self`'s alpha value (`self.a`) and `other`'s alpha value (`other.a`), `self` contributing `self.a * (1.0 - other.a)` and `other` contributing its own alpha value.
/// - RGB color components are contained in the range [0, 1].
/// - If `self` and `other` colors are out of the valid range, the blend operation's output and behavior is undefined.
pub fn blend(self, other: Hsla) -> Hsla {
let alpha = other.a;
if alpha >= 1.0 {
other
} else if alpha <= 0.0 {
return self;
} else {
let converted_self = Rgba::from(self);
let converted_other = Rgba::from(other);
let blended_rgb = converted_self.blend(converted_other);
return Hsla::from(blended_rgb);
}
}
/// Returns a new HSLA color with the same hue, and lightness, but with no saturation.
pub fn grayscale(&self) -> Self {
Hsla {
h: self.h,
s: 0.,
l: self.l,
a: self.a,
}
}
/// Fade out the color by a given factor. This factor should be between 0.0 and 1.0.
/// Where 0.0 will leave the color unchanged, and 1.0 will completely fade out the color.
pub fn fade_out(&mut self, factor: f32) {
self.a *= 1.0 - factor.clamp(0., 1.);
}
}
impl From<Rgba> for Hsla {
fn from(color: Rgba) -> Self {
let r = color.r;
let g = color.g;
let b = color.b;
let max = r.max(g.max(b));
let min = r.min(g.min(b));
let delta = max - min;
let l = (max + min) / 2.0;
let s = if l == 0.0 || l == 1.0 {
0.0
} else if l < 0.5 {
delta / (2.0 * l)
} else {
delta / (2.0 - 2.0 * l)
};
let h = if delta == 0.0 {
0.0
} else if max == r {
((g - b) / delta).rem_euclid(6.0) / 6.0
} else if max == g {
((b - r) / delta + 2.0) / 6.0
} else {
((r - g) / delta + 4.0) / 6.0
};
Hsla {
h,
s,
l,
a: color.a,
}
}
}
impl<'de> Deserialize<'de> for Hsla {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
// First, deserialize it into Rgba
let rgba = Rgba::deserialize(deserializer)?;
// Then, use the From<Rgba> for Hsla implementation to convert it
Ok(Hsla::from(rgba))
}
}
#[cfg(test)]
mod tests {
use serde_json::json;
use super::*;
#[test]
fn test_deserialize_three_value_hex_to_rgba() {
let actual: Rgba = serde_json::from_value(json!("#f09")).unwrap();
assert_eq!(actual, rgba(0xff0099ff))
}
#[test]
fn test_deserialize_four_value_hex_to_rgba() {
let actual: Rgba = serde_json::from_value(json!("#f09f")).unwrap();
assert_eq!(actual, rgba(0xff0099ff))
}
#[test]
fn test_deserialize_six_value_hex_to_rgba() {
let actual: Rgba = serde_json::from_value(json!("#ff0099")).unwrap();
assert_eq!(actual, rgba(0xff0099ff))
}
#[test]
fn test_deserialize_eight_value_hex_to_rgba() {
let actual: Rgba = serde_json::from_value(json!("#ff0099ff")).unwrap();
assert_eq!(actual, rgba(0xff0099ff))
}
#[test]
fn test_deserialize_eight_value_hex_with_padding_to_rgba() {
let actual: Rgba = serde_json::from_value(json!(" #f5f5f5ff ")).unwrap();
assert_eq!(actual, rgba(0xf5f5f5ff))
}
#[test]
fn test_deserialize_eight_value_hex_with_mixed_case_to_rgba() {
let actual: Rgba = serde_json::from_value(json!("#DeAdbEeF")).unwrap();
assert_eq!(actual, rgba(0xdeadbeef))
}
}