feat: color: add HSV methods to slint

This commit is contained in:
Luke D. Jones 2024-03-05 17:37:22 +13:00 committed by Olivier Goffart
parent ae2e0197f8
commit d4a3f77877
11 changed files with 236 additions and 1 deletions

View file

@ -47,6 +47,8 @@ All notable changes to this project are documented in this file.
- Image: Added `horizontal-` and `vertical-tiling`
- Flickable: Added `flicked` callback
- Slint: Expose `.red`, `.green`, `.blue`, and `.alpha` properties on `color`
- Slint: Expose `.hue()`, `.saturation()`, `.brightness()` methods on `color`
- Slint: Add `hsv` and `hsva` as method to create colors
### Widgets

View file

@ -81,6 +81,11 @@ All properties are in the range 0-255.
All colors and brushes define the following methods:
- **`linear-mix(other: brush, factor: float) -> brush`**
Returns a new color that is a mix of this color and `other`, with a proportion
factor given by \a factor (which will be clamped to be between `0.0` and `1.0`).
- **`brighter(factor: float) -> brush`**
Returns a new color derived from this color but has its brightness increased by the specified factor.

View file

@ -63,6 +63,7 @@ pub fn lower_macro(
expr
}
BuiltinMacroFunction::Rgb => rgb_macro(n, sub_expr.collect(), diag),
BuiltinMacroFunction::Hsv => hsv_macro(n, sub_expr.collect(), diag),
}
}
@ -226,6 +227,39 @@ fn rgb_macro(
}
}
fn hsv_macro(
node: Option<NodeOrToken>,
args: Vec<(Expression, Option<NodeOrToken>)>,
diag: &mut BuildDiagnostics,
) -> Expression {
if args.len() < 3 {
diag.push_error("Needs 3 or 4 argument".into(), &node);
return Expression::Invalid;
}
let mut arguments: Vec<_> = args
.into_iter()
.enumerate()
.map(|(i, (expr, n))| {
if i < 3 {
expr.maybe_convert_to(Type::Float32, &n, diag)
} else {
expr.maybe_convert_to(Type::Float32, &n, diag)
}
})
.collect();
if arguments.len() < 4 {
arguments.push(Expression::NumberLiteral(1., Unit::None))
}
Expression::FunctionCall {
function: Box::new(Expression::BuiltinFunctionReference(
BuiltinFunction::Hsv,
node.as_ref().map(|t| t.to_source_location()),
)),
arguments,
source_location: Some(node.to_source_location()),
}
}
fn debug_macro(
node: Option<NodeOrToken>,
args: Vec<(Expression, Option<NodeOrToken>)>,

View file

@ -48,6 +48,9 @@ pub enum BuiltinFunction {
/// the "42".is_float()
StringIsFloat,
ColorRgbaStruct,
ColorHue,
ColorSaturation,
ColorBrightness,
ColorBrighter,
ColorDarker,
ColorTransparentize,
@ -56,6 +59,7 @@ pub enum BuiltinFunction {
ImageSize,
ArrayLength,
Rgb,
Hsv,
ColorScheme,
TextInputFocused,
SetTextInputFocused,
@ -86,6 +90,7 @@ pub enum BuiltinMacroFunction {
/// The argument can be r,g,b,a or r,g,b and they can be percentages or integer.
/// transform the argument so it is always rgb(r, g, b, a) with r, g, b between 0 and 255.
Rgb,
Hsv,
/// transform `debug(a, b, c)` into debug `a + " " + b + " " + c`
Debug,
}
@ -169,6 +174,18 @@ impl BuiltinFunction {
}),
args: vec![Type::Color],
},
BuiltinFunction::ColorHue => Type::Function {
return_type: Box::new(Type::Float32),
args: vec![Type::Color],
},
BuiltinFunction::ColorSaturation => Type::Function {
return_type: Box::new(Type::Float32),
args: vec![Type::Color],
},
BuiltinFunction::ColorBrightness => Type::Function {
return_type: Box::new(Type::Float32),
args: vec![Type::Color],
},
BuiltinFunction::ColorBrighter => Type::Function {
return_type: Box::new(Type::Brush),
args: vec![Type::Brush, Type::Float32],
@ -209,6 +226,10 @@ impl BuiltinFunction {
return_type: Box::new(Type::Color),
args: vec![Type::Int32, Type::Int32, Type::Int32, Type::Float32],
},
BuiltinFunction::Hsv => Type::Function {
return_type: Box::new(Type::Color),
args: vec![Type::Float32, Type::Float32, Type::Float32, Type::Float32],
},
BuiltinFunction::ColorScheme => Type::Function {
return_type: Box::new(Type::Enumeration(
crate::typeregister::BUILTIN_ENUMS.with(|e| e.ColorScheme.clone()),
@ -276,6 +297,9 @@ impl BuiltinFunction {
BuiltinFunction::ItemMemberFunction(..) => false,
BuiltinFunction::StringToFloat | BuiltinFunction::StringIsFloat => true,
BuiltinFunction::ColorRgbaStruct
| BuiltinFunction::ColorHue
| BuiltinFunction::ColorSaturation
| BuiltinFunction::ColorBrightness
| BuiltinFunction::ColorBrighter
| BuiltinFunction::ColorDarker
| BuiltinFunction::ColorTransparentize
@ -291,6 +315,7 @@ impl BuiltinFunction {
BuiltinFunction::ImageSize => false,
BuiltinFunction::ArrayLength => true,
BuiltinFunction::Rgb => true,
BuiltinFunction::Hsv => true,
BuiltinFunction::SetTextInputFocused => false,
BuiltinFunction::TextInputFocused => false,
BuiltinFunction::ImplicitLayoutInfo(_) => false,
@ -331,6 +356,9 @@ impl BuiltinFunction {
BuiltinFunction::ItemMemberFunction(..) => false,
BuiltinFunction::StringToFloat | BuiltinFunction::StringIsFloat => true,
BuiltinFunction::ColorRgbaStruct
| BuiltinFunction::ColorHue
| BuiltinFunction::ColorSaturation
| BuiltinFunction::ColorBrightness
| BuiltinFunction::ColorBrighter
| BuiltinFunction::ColorDarker
| BuiltinFunction::ColorTransparentize
@ -339,6 +367,7 @@ impl BuiltinFunction {
BuiltinFunction::ImageSize => true,
BuiltinFunction::ArrayLength => true,
BuiltinFunction::Rgb => true,
BuiltinFunction::Hsv => true,
BuiltinFunction::ImplicitLayoutInfo(_) => true,
BuiltinFunction::ItemAbsolutePosition => true,
BuiltinFunction::SetTextInputFocused => false,

View file

@ -3051,6 +3051,15 @@ fn compile_builtin_function_call(
BuiltinFunction::ColorRgbaStruct => {
format!("{}.to_argb_uint()", a.next().unwrap())
}
BuiltinFunction::ColorHue => {
format!("{}.hue()", a.next().unwrap())
}
BuiltinFunction::ColorSaturation => {
format!("{}.saturation()", a.next().unwrap())
}
BuiltinFunction::ColorBrightness => {
format!("{}.brightness()", a.next().unwrap())
}
BuiltinFunction::ColorBrighter => {
format!("{}.brighter({})", a.next().unwrap(), a.next().unwrap())
}
@ -3080,6 +3089,14 @@ fn compile_builtin_function_call(
a = a.next().unwrap(),
)
}
BuiltinFunction::Hsv => {
format!("slint::Color::from_hsva(std::clamp(static_cast<float>({h}), 0., 360.), std::clamp(static_cast<float>({s}), 0., 1.), std::clamp(static_cast<float>({v}), 0., 1.), std::clamp(static_cast<float>({a}) * 1., 0., 1.))",
h = a.next().unwrap(),
s = a.next().unwrap(),
v = a.next().unwrap(),
a = a.next().unwrap(),
)
}
BuiltinFunction::ColorScheme => {
format!("{}.color_scheme()", access_window_field(ctx))
}

View file

@ -2494,6 +2494,18 @@ fn compile_builtin_function_call(
}
BuiltinFunction::StringIsFloat => quote!(#(#a)*.as_str().parse::<f64>().is_ok()),
BuiltinFunction::ColorRgbaStruct => quote!( #(#a)*.to_argb_u8()),
BuiltinFunction::ColorHue => {
let x = a.next().unwrap();
quote!(#x.hue())
}
BuiltinFunction::ColorSaturation => {
let x = a.next().unwrap();
quote!(#x.saturation())
}
BuiltinFunction::ColorBrightness => {
let x = a.next().unwrap();
quote!(#x.brightness())
}
BuiltinFunction::ColorBrighter => {
let x = a.next().unwrap();
let factor = a.next().unwrap();
@ -2539,6 +2551,17 @@ fn compile_builtin_function_call(
sp::Color::from_argb_u8(a, r, g, b)
})
}
BuiltinFunction::Hsv => {
let (h, s, v, a) =
(a.next().unwrap(), a.next().unwrap(), a.next().unwrap(), a.next().unwrap());
quote!({
let h: f32 = (#h as f32).max(0.).min(360.) as f32;
let s: f32 = (#s as f32).max(0.).min(1.) as f32;
let v: f32 = (#v as f32).max(0.).min(1.) as f32;
let a: f32 = (1. * (#a as f32)).max(0.).min(1.) as f32;
sp::Color::from_hsva(h, s, v, a)
})
}
BuiltinFunction::ColorScheme => {
let window_adapter_tokens = access_window_adapter_field(ctx);
quote!(sp::WindowInner::from_pub(#window_adapter_tokens.window()).color_scheme())

View file

@ -88,6 +88,9 @@ fn builtin_function_cost(function: &BuiltinFunction) -> isize {
BuiltinFunction::StringToFloat => 50,
BuiltinFunction::StringIsFloat => 50,
BuiltinFunction::ColorRgbaStruct => 50,
BuiltinFunction::ColorHue => 50,
BuiltinFunction::ColorSaturation => 50,
BuiltinFunction::ColorBrightness => 50,
BuiltinFunction::ColorBrighter => 50,
BuiltinFunction::ColorDarker => 50,
BuiltinFunction::ColorTransparentize => 50,
@ -96,6 +99,7 @@ fn builtin_function_cost(function: &BuiltinFunction) -> isize {
BuiltinFunction::ImageSize => 50,
BuiltinFunction::ArrayLength => 50,
BuiltinFunction::Rgb => 50,
BuiltinFunction::Hsv => 50,
BuiltinFunction::ImplicitLayoutInfo(_) => isize::MAX,
BuiltinFunction::ItemAbsolutePosition => isize::MAX,
BuiltinFunction::RegisterCustomFontByPath => isize::MAX,

View file

@ -833,6 +833,8 @@ impl LookupObject for ColorFunctions {
let mut f = |n, e: Expression| f(n, e.into());
None.or_else(|| f("rgb", BuiltinMacroReference(BuiltinMacroFunction::Rgb, t.clone())))
.or_else(|| f("rgba", BuiltinMacroReference(BuiltinMacroFunction::Rgb, t.clone())))
.or_else(|| f("hsv", BuiltinMacroReference(BuiltinMacroFunction::Hsv, t.clone())))
.or_else(|| f("hsva", BuiltinMacroReference(BuiltinMacroFunction::Hsv, t.clone())))
}
}
@ -1020,6 +1022,9 @@ impl<'a> LookupObject for ColorExpression<'a> {
.or_else(|| f("green", field_access("green")))
.or_else(|| f("blue", field_access("blue")))
.or_else(|| f("alpha", field_access("alpha")))
.or_else(|| f("hue", member_function(BuiltinFunction::ColorHue)))
.or_else(|| f("saturation", member_function(BuiltinFunction::ColorSaturation)))
.or_else(|| f("brightness", member_function(BuiltinFunction::ColorBrightness)))
.or_else(|| f("brighter", member_function(BuiltinFunction::ColorBrighter)))
.or_else(|| f("darker", member_function(BuiltinFunction::ColorDarker)))
.or_else(|| f("transparentize", member_function(BuiltinFunction::ColorTransparentize)))

View file

@ -145,6 +145,31 @@ impl Color {
RgbaColor::from(*self)
}
/// Converts this color to a tuple of `(hue, saturation, value)`
pub fn to_hsv(&mut self) -> (f32, f32, f32) {
let hsva = self.hsva();
(hsva.h, hsva.s, hsva.v)
}
/// Converts this color to a tuple of `(hue, saturation, value, alpha)`
pub fn to_hsva(&mut self) -> (f32, f32, f32, f32) {
let hsva = self.hsva();
(hsva.h, hsva.s, hsva.v, hsva.alpha)
}
/// Construct a color from the hue, saturation, and value HSV color parameters. The alpha
/// channel will have the value 1.0.
pub fn from_hsv(hue: f32, saturation: f32, value: f32) -> Self {
let hsva = HsvaColor { h: hue, s: saturation, v: value, alpha: 1.0 };
<RgbaColor<f32>>::from(hsva).into()
}
/// Construct a color from the hue, saturation, and value HSV color parameters.
pub fn from_hsva(hue: f32, saturation: f32, value: f32, alpha: f32) -> Self {
let hsva = HsvaColor { h: hue, s: saturation, v: value, alpha };
<RgbaColor<f32>>::from(hsva).into()
}
/// Returns the red channel of the color as u8 in the range 0..255.
#[inline(always)]
pub fn red(self) -> u8 {
@ -169,6 +194,29 @@ impl Color {
self.alpha
}
fn hsva(&mut self) -> HsvaColor {
let rgba: RgbaColor<f32> = (*self).into();
rgba.into()
}
/// Returns the hue channel of the color as f32 in degrees 0..PI.
#[inline(always)]
pub fn hue(mut self) -> f32 {
self.hsva().h
}
/// Returns the saturation of the color as u8 in the range 0..255.
#[inline(always)]
pub fn saturation(mut self) -> f32 {
self.hsva().s
}
/// Returns the brightness of the color as u8 in the range 0..255.
#[inline(always)]
pub fn brightness(mut self) -> f32 {
self.hsva().v
}
/// Returns a new version of this color that has the brightness increased
/// by the specified factor. This is done by converting the color to the HSV
/// color space and multiplying the brightness (value) with (1 + factor).

View file

@ -785,6 +785,36 @@ fn call_builtin_function(
panic!("First argument not a color");
}
}
BuiltinFunction::ColorHue => {
if arguments.len() != 1 {
panic!("internal error: incorrect argument count to ColorHue")
}
if let Value::Brush(brush) = eval_expression(&arguments[0], local_context) {
(brush.color().hue() as f32).into()
} else {
panic!("First argument not a color");
}
}
BuiltinFunction::ColorSaturation => {
if arguments.len() != 1 {
panic!("internal error: incorrect argument count to ColorSaturation")
}
if let Value::Brush(brush) = eval_expression(&arguments[0], local_context) {
(brush.color().saturation() as f32).into()
} else {
panic!("First argument not a color");
}
}
BuiltinFunction::ColorBrightness => {
if arguments.len() != 1 {
panic!("internal error: incorrect argument count to ColorBrightness")
}
if let Value::Brush(brush) = eval_expression(&arguments[0], local_context) {
(brush.color().brightness() as f32).into()
} else {
panic!("First argument not a color");
}
}
BuiltinFunction::ColorTransparentize => {
if arguments.len() != 2 {
panic!("internal error: incorrect argument count to ColorFaded")
@ -884,6 +914,14 @@ fn call_builtin_function(
let a: u8 = (255. * a).max(0.).min(255.) as u8;
Value::Brush(Brush::SolidColor(Color::from_argb_u8(a, r, g, b)))
}
BuiltinFunction::Hsv => {
let h: f32 = eval_expression(&arguments[0], local_context).try_into().unwrap();
let s: f32 = eval_expression(&arguments[1], local_context).try_into().unwrap();
let v: f32 = eval_expression(&arguments[2], local_context).try_into().unwrap();
let a: f32 = eval_expression(&arguments[3], local_context).try_into().unwrap();
let a = (1. * a).max(0.).min(1.);
Value::Brush(Brush::SolidColor(Color::from_hsva(h, s, v, a)))
}
BuiltinFunction::ColorScheme => match local_context.component_instance {
ComponentInstance::InstanceRef(component) => component
.window_adapter()

View file

@ -29,11 +29,41 @@ Test := Rectangle {
// allow to use `with_alpha` on colors
property<brush> invisible: b1.with-alpha(0%);
property<float> b1hue: 240.0;
property<float> b1sat: 1.0;
property<float> b1bri: 1.0;
property<float> r1hue: 0.0;
property<float> r1sat: 1.0;
property<float> r1bri: 1.0;
property<float> y1hue: 60.0;
property<float> y1sat: 1.0;
property<float> y1bri: 1.0;
property <color> gr1: green;
property<float> gr1hue: 120.0;
property<float> gr1sat: 1.0;
property<float> gr1bri: 0.501960813999176;
property <color> new_green: hsv(120.0, 1.0, 0.501960813999176);
// burlywood
property<color> bwood: Colors.burlywood;
property<float> bwood_hue: 33.79310607910156;
property<float> bwood_sat: 0.39189186692237854;
property<float> bwood_bri: 0.8705882430076599;
out property <bool> test_rgb: Colors.blue.blue == 255 && Colors.blue.red == 0 && Colors.blue.green == 0 && Colors.blue.alpha == 255
&& Colors.rgb(45, 12, 78).red == 45 && Colors.rgb(45, 12, 78).green == 12 && Colors.rgba(45, 12, 78, 12/255).alpha == 12 && Colors.rgba(145, 112, 178, 85%).alpha == floor(85% * 255)
&& #abc.green == (11 * 16 + 11) && #abcd.alpha == (13 * 16 + 13) && #abcdef.red == (10 * 16 + 11);
property<bool> test: b1 == b2 && b2 == b5 && b3 == Colors.blue && Colors.red == r4 && y1 == Colors.rgba(255, 100%, 0, 100%) && test_rgb;
out property <bool> test_hsv: gr1.hue() == new_green.hue() && gr1.saturation() == new_green.saturation() && gr1.brightness() == new_green.brightness();
out property <bool> test_hsv_hue: b1.hue() == b1hue && r1.hue() == r1hue && y1.hue() == y1hue && gr1.hue() == gr1hue && bwood.hue() == bwood_hue;
out property <bool> test_hsv_sat: b1.saturation() == b1sat && r1.saturation() == r1sat && y1.saturation() == y1sat && gr1.saturation() == gr1sat && bwood.saturation() == bwood_sat;
out property <bool> test_hsv_bri: b1.brightness() == b1bri && r1.brightness() == r1bri && y1.brightness() == y1bri && gr1.brightness() == gr1bri && bwood.brightness() == bwood_bri;
property<bool> test: b1 == b2 && b2 == b5 && b3 == Colors.blue && Colors.red == r4 && y1 == Colors.rgba(255, 100%, 0, 100%)
&& test_rgb && test_hsv && test_hsv_hue && test_hsv_sat && test_hsv_bri;
}
/*