Skip to content

Commit 042e0d3

Browse files
authored
Add oklch() color function and to-oklch() method (#10431)
* Add oklch() color function and to-oklch() method Add support for the Oklch perceptually uniform color space: - Add oklch(lightness, chroma, hue, alpha?) global function - Add color.to-oklch() method returning {lightness, chroma, hue, alpha} - Add C++ API: OklchColor struct, Color::from_oklch(), Color::to_oklch() - Add FFI functions: slint_color_from_oklch, slint_color_to_oklch - Add interpreter and LSP preview support - Add documentation and tests * Add Wikipedia link for Oklch color space in documentation * Add percentage and angle support for oklch() parameters - Lightness can be specified as percentage (0% to 100%) - Chroma can be specified as percentage (0% to 100%, where 100% = 0.4) - Hue can be specified as angle (e.g., 180deg, 0.5turn) * Add angle support for hsv() hue parameter The hue parameter can now be specified as an angle (e.g., 240deg, 1turn) in addition to a plain float value. * Fix alignment in C++ slint_color.h to_oklch method
1 parent 9c3d001 commit 042e0d3

File tree

14 files changed

+545
-5
lines changed

14 files changed

+545
-5
lines changed

api/cpp/cbindgen.rs

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -508,7 +508,9 @@ fn gen_corelib(
508508
"slint_color_mix",
509509
"slint_color_with_alpha",
510510
"slint_color_to_hsva",
511-
"slint_color_from_hsva",],
511+
"slint_color_from_hsva",
512+
"slint_color_from_oklch",
513+
"slint_color_to_oklch",],
512514
"slint_color_internal.h",
513515
"",
514516
),

api/cpp/include/slint_color.h

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,20 @@ struct HsvaColor
4949
float alpha;
5050
};
5151

52+
/// OklchColor stores the lightness, chroma, hue, and alpha components of a color in the Oklch
53+
/// color space (a perceptually uniform color space).
54+
struct OklchColor
55+
{
56+
/// The lightness component, between 0 (black) and 1 (white).
57+
float lightness;
58+
/// The chroma component (saturation), typically between 0 (grayscale) and ~0.4 (vivid).
59+
float chroma;
60+
/// The hue component in degrees between 0 and 360.
61+
float hue;
62+
/// The alpha component, between 0 and 1.
63+
float alpha;
64+
};
65+
5266
/// Color represents a color in the Slint run-time, represented using 8-bit channels for
5367
/// red, green, blue and the alpha (opacity).
5468
class Color
@@ -154,6 +168,28 @@ class Color
154168
return hsv;
155169
}
156170

171+
/// Construct a color from the Oklch color space components.
172+
/// Oklch is a perceptually uniform color space.
173+
/// The lightness is expected to be in the range between 0 and 1,
174+
/// chroma typically between 0 and 0.4, hue between 0 and 360,
175+
/// and alpha between 0 and 1.
176+
[[nodiscard]] static Color from_oklch(float l, float c, float h, float a)
177+
{
178+
Color ret;
179+
ret.inner = cbindgen_private::types::slint_color_from_oklch(l, c, h, a);
180+
return ret;
181+
}
182+
183+
/// Convert this color to the Oklch color space.
184+
/// @returns a new OklchColor.
185+
[[nodiscard]] OklchColor to_oklch() const
186+
{
187+
OklchColor oklch {};
188+
cbindgen_private::types::slint_color_to_oklch(&inner, &oklch.lightness, &oklch.chroma,
189+
&oklch.hue, &oklch.alpha);
190+
return oklch;
191+
}
192+
157193
/// Returns the red channel of the color as u8 in the range 0..255.
158194
[[nodiscard]] uint8_t red() const { return inner.red; }
159195

docs/astro/src/content/docs/reference/colors-and-brushes.mdx

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,16 @@ Returns a color using HSV (Hue, Saturation, Value) coordinates. The hue paramete
6767
representing degrees (0-360) and wraps around (e.g., 480 becomes 120).
6868
The saturation, value, and optional alpha parameter are expected to be within the range of 0 and 1.
6969

70+
### oklch(l: float, c: float, h: float) -> color
71+
### oklch(l: float, c: float, h: float, a: float) -> color
72+
73+
Returns a color using the [Oklch color space](https://en.wikipedia.org/wiki/Oklab_color_space) (a perceptually uniform color space).
74+
75+
- `l` (lightness): 0 (black) to 1 (white), or 0% to 100%
76+
- `c` (chroma): 0 (grayscale) to ~0.4 (vivid), or 0% to 100% (where 100% = 0.4)
77+
- `h` (hue): 0-360 degrees, or as an angle (e.g., `180deg`, `0.5turn`)
78+
- `a` (alpha): 0-1, defaults to 1
79+
7080
## Color Methods
7181
All colors and brushes define the following methods:
7282

@@ -105,6 +115,12 @@ Returns a new color with the alpha value set to `alpha` (between 0 and 1)
105115
Converts this color to the HSV color space and returns a struct with the `hue`, `saturation`, `value`,
106116
and `alpha` fields. `hue` is between 0 and 360 while `saturation`, `value`, and `alpha` are between 0 and 1.
107117

118+
### to-oklch() -> \{ lightness: float, chroma: float, hue: float, alpha: float }
119+
120+
Converts this color to the [Oklch color space](https://en.wikipedia.org/wiki/Oklab_color_space) and returns a struct with the `lightness`, `chroma`, `hue`,
121+
and `alpha` fields. `lightness` is between 0 and 1, `chroma` is typically between 0 and ~0.4,
122+
`hue` is between 0 and 360, and `alpha` is between 0 and 1.
123+
108124
## Linear Gradients
109125

110126
Linear gradients describe smooth, colorful surfaces. They're specified using an angle and a series of

internal/compiler/builtin_macros.rs

Lines changed: 62 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,7 @@ pub fn lower_macro(
8787
}
8888
BuiltinMacroFunction::Rgb => rgb_macro(n, sub_expr.collect(), diag),
8989
BuiltinMacroFunction::Hsv => hsv_macro(n, sub_expr.collect(), diag),
90+
BuiltinMacroFunction::Oklch => oklch_macro(n, sub_expr.collect(), diag),
9091
}
9192
}
9293

@@ -277,8 +278,22 @@ fn hsv_macro(
277278
);
278279
return Expression::Invalid;
279280
}
280-
let mut arguments: Vec<_> =
281-
args.into_iter().map(|(expr, n)| expr.maybe_convert_to(Type::Float32, &n, diag)).collect();
281+
let mut arguments: Vec<_> = args
282+
.into_iter()
283+
.enumerate()
284+
.map(|(i, (expr, n))| {
285+
// For hue (index 0), convert angle to degrees
286+
if i == 0 && expr.ty() == Type::Angle {
287+
Expression::BinaryExpression {
288+
lhs: Box::new(expr),
289+
rhs: Box::new(Expression::NumberLiteral(1., Unit::Deg)),
290+
op: '/',
291+
}
292+
} else {
293+
expr.maybe_convert_to(Type::Float32, &n, diag)
294+
}
295+
})
296+
.collect();
282297
if arguments.len() < 4 {
283298
arguments.push(Expression::NumberLiteral(1., Unit::None))
284299
}
@@ -289,6 +304,51 @@ fn hsv_macro(
289304
}
290305
}
291306

307+
fn oklch_macro(
308+
node: &dyn Spanned,
309+
args: Vec<(Expression, Option<NodeOrToken>)>,
310+
diag: &mut BuildDiagnostics,
311+
) -> Expression {
312+
if args.len() < 3 || args.len() > 4 {
313+
diag.push_error(
314+
format!("This function needs 3 or 4 arguments, but {} were provided", args.len()),
315+
node,
316+
);
317+
return Expression::Invalid;
318+
}
319+
let mut arguments: Vec<_> = args
320+
.into_iter()
321+
.enumerate()
322+
.map(|(i, (expr, n))| {
323+
// For chroma (index 1), 100% should equal 0.4, not 1.0
324+
if i == 1 && expr.ty() == Type::Percent {
325+
Expression::BinaryExpression {
326+
lhs: Box::new(expr),
327+
rhs: Box::new(Expression::NumberLiteral(0.004, Unit::None)),
328+
op: '*',
329+
}
330+
// For hue (index 2), convert angle to degrees
331+
} else if i == 2 && expr.ty() == Type::Angle {
332+
Expression::BinaryExpression {
333+
lhs: Box::new(expr),
334+
rhs: Box::new(Expression::NumberLiteral(1., Unit::Deg)),
335+
op: '/',
336+
}
337+
} else {
338+
expr.maybe_convert_to(Type::Float32, &n, diag)
339+
}
340+
})
341+
.collect();
342+
if arguments.len() < 4 {
343+
arguments.push(Expression::NumberLiteral(1., Unit::None))
344+
}
345+
Expression::FunctionCall {
346+
function: BuiltinFunction::Oklch.into(),
347+
arguments,
348+
source_location: Some(node.to_source_location()),
349+
}
350+
}
351+
292352
fn debug_macro(
293353
node: &dyn Spanned,
294354
args: Vec<(Expression, Option<NodeOrToken>)>,

internal/compiler/expression_tree.rs

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,7 @@ pub enum BuiltinFunction {
7575
StringToUppercase,
7676
ColorRgbaStruct,
7777
ColorHsvaStruct,
78+
ColorOklchStruct,
7879
ColorBrighter,
7980
ColorDarker,
8081
ColorTransparentize,
@@ -84,6 +85,7 @@ pub enum BuiltinFunction {
8485
ArrayLength,
8586
Rgb,
8687
Hsv,
88+
Oklch,
8789
ColorScheme,
8890
SupportsNativeMenuBar,
8991
/// Setup the menu bar
@@ -142,6 +144,7 @@ pub enum BuiltinMacroFunction {
142144
/// transform the argument so it is always rgb(r, g, b, a) with r, g, b between 0 and 255.
143145
Rgb,
144146
Hsv,
147+
Oklch,
145148
/// transform `debug(a, b, c)` into debug `a + " " + b + " " + c`
146149
Debug,
147150
}
@@ -231,6 +234,16 @@ declare_builtin_function_types!(
231234
.collect(),
232235
name: BuiltinPublicStruct::Color.into(),
233236
})),
237+
ColorOklchStruct: (Type::Color) -> Type::Struct(Rc::new(Struct {
238+
fields: IntoIterator::into_iter([
239+
(SmolStr::new_static("lightness"), Type::Float32),
240+
(SmolStr::new_static("chroma"), Type::Float32),
241+
(SmolStr::new_static("hue"), Type::Float32),
242+
(SmolStr::new_static("alpha"), Type::Float32),
243+
])
244+
.collect(),
245+
name: BuiltinPublicStruct::Color.into(),
246+
})),
234247
ColorBrighter: (Type::Brush, Type::Float32) -> Type::Brush,
235248
ColorDarker: (Type::Brush, Type::Float32) -> Type::Brush,
236249
ColorTransparentize: (Type::Brush, Type::Float32) -> Type::Brush,
@@ -247,6 +260,7 @@ declare_builtin_function_types!(
247260
ArrayLength: (Type::Model) -> Type::Int32,
248261
Rgb: (Type::Int32, Type::Int32, Type::Int32, Type::Float32) -> Type::Color,
249262
Hsv: (Type::Float32, Type::Float32, Type::Float32, Type::Float32) -> Type::Color,
263+
Oklch: (Type::Float32, Type::Float32, Type::Float32, Type::Float32) -> Type::Color,
250264
ColorScheme: () -> Type::Enumeration(
251265
typeregister::BUILTIN.with(|e| e.enums.ColorScheme.clone()),
252266
),
@@ -343,6 +357,7 @@ impl BuiltinFunction {
343357
| BuiltinFunction::StringToUppercase => true,
344358
BuiltinFunction::ColorRgbaStruct
345359
| BuiltinFunction::ColorHsvaStruct
360+
| BuiltinFunction::ColorOklchStruct
346361
| BuiltinFunction::ColorBrighter
347362
| BuiltinFunction::ColorDarker
348363
| BuiltinFunction::ColorTransparentize
@@ -359,6 +374,7 @@ impl BuiltinFunction {
359374
BuiltinFunction::ArrayLength => true,
360375
BuiltinFunction::Rgb => true,
361376
BuiltinFunction::Hsv => true,
377+
BuiltinFunction::Oklch => true,
362378
BuiltinFunction::SetTextInputFocused => false,
363379
BuiltinFunction::TextInputFocused => false,
364380
BuiltinFunction::ImplicitLayoutInfo(_) => false,
@@ -429,6 +445,7 @@ impl BuiltinFunction {
429445
| BuiltinFunction::StringToUppercase => true,
430446
BuiltinFunction::ColorRgbaStruct
431447
| BuiltinFunction::ColorHsvaStruct
448+
| BuiltinFunction::ColorOklchStruct
432449
| BuiltinFunction::ColorBrighter
433450
| BuiltinFunction::ColorDarker
434451
| BuiltinFunction::ColorTransparentize
@@ -438,6 +455,7 @@ impl BuiltinFunction {
438455
BuiltinFunction::ArrayLength => true,
439456
BuiltinFunction::Rgb => true,
440457
BuiltinFunction::Hsv => true,
458+
BuiltinFunction::Oklch => true,
441459
BuiltinFunction::ImplicitLayoutInfo(_) => true,
442460
BuiltinFunction::ItemAbsolutePosition => true,
443461
BuiltinFunction::SetTextInputFocused => false,

internal/compiler/generator/cpp.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3981,6 +3981,9 @@ fn compile_builtin_function_call(
39813981
BuiltinFunction::ColorHsvaStruct => {
39823982
format!("{}.to_hsva()", a.next().unwrap())
39833983
}
3984+
BuiltinFunction::ColorOklchStruct => {
3985+
format!("{}.to_oklch()", a.next().unwrap())
3986+
}
39843987
BuiltinFunction::ColorBrighter => {
39853988
format!("{}.brighter({})", a.next().unwrap(), a.next().unwrap())
39863989
}
@@ -4018,6 +4021,14 @@ fn compile_builtin_function_call(
40184021
a = a.next().unwrap(),
40194022
)
40204023
}
4024+
BuiltinFunction::Oklch => {
4025+
format!("slint::Color::from_oklch(std::clamp(static_cast<float>({l}), 0.f, 1.f), std::max(static_cast<float>({c}), 0.f), static_cast<float>({h}), std::clamp(static_cast<float>({alpha}), 0.f, 1.f))",
4026+
l = a.next().unwrap(),
4027+
c = a.next().unwrap(),
4028+
h = a.next().unwrap(),
4029+
alpha = a.next().unwrap(),
4030+
)
4031+
}
40214032
BuiltinFunction::ColorScheme => {
40224033
format!("{}.color_scheme()", access_window_field(ctx))
40234034
}

internal/compiler/generator/rust.rs

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3232,6 +3232,7 @@ fn compile_builtin_function_call(
32323232
BuiltinFunction::StringToUppercase => quote!(sp::SharedString::from(#(#a)*.to_uppercase())),
32333233
BuiltinFunction::ColorRgbaStruct => quote!( #(#a)*.to_argb_u8()),
32343234
BuiltinFunction::ColorHsvaStruct => quote!( #(#a)*.to_hsva()),
3235+
BuiltinFunction::ColorOklchStruct => quote!( #(#a)*.to_oklch()),
32353236
BuiltinFunction::ColorBrighter => {
32363237
let x = a.next().unwrap();
32373238
let factor = a.next().unwrap();
@@ -3287,6 +3288,16 @@ fn compile_builtin_function_call(
32873288
sp::Color::from_hsva(#h as f32, s, v, a)
32883289
})
32893290
}
3291+
BuiltinFunction::Oklch => {
3292+
let (l, c, h, alpha) =
3293+
(a.next().unwrap(), a.next().unwrap(), a.next().unwrap(), a.next().unwrap());
3294+
quote!({
3295+
let l: f32 = (#l as f32).max(0.).min(1.) as f32;
3296+
let c: f32 = (#c as f32).max(0.) as f32;
3297+
let alpha: f32 = (#alpha as f32).max(0.).min(1.) as f32;
3298+
sp::Color::from_oklch(l, c, #h as f32, alpha)
3299+
})
3300+
}
32903301
BuiltinFunction::ColorScheme => {
32913302
let window_adapter_tokens = access_window_adapter_field(ctx);
32923303
quote!(sp::WindowInner::from_pub(#window_adapter_tokens.window()).color_scheme())

internal/compiler/llr/optim_passes/inline_expressions.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,7 @@ fn builtin_function_cost(function: &BuiltinFunction) -> isize {
123123
BuiltinFunction::StringToUppercase => ALLOC_COST,
124124
BuiltinFunction::ColorRgbaStruct => 50,
125125
BuiltinFunction::ColorHsvaStruct => 50,
126+
BuiltinFunction::ColorOklchStruct => 50,
126127
BuiltinFunction::ColorBrighter => 50,
127128
BuiltinFunction::ColorDarker => 50,
128129
BuiltinFunction::ColorTransparentize => 50,
@@ -132,6 +133,7 @@ fn builtin_function_cost(function: &BuiltinFunction) -> isize {
132133
BuiltinFunction::ArrayLength => 50,
133134
BuiltinFunction::Rgb => 50,
134135
BuiltinFunction::Hsv => 50,
136+
BuiltinFunction::Oklch => 50,
135137
BuiltinFunction::ImplicitLayoutInfo(_) => isize::MAX,
136138
BuiltinFunction::ItemAbsolutePosition => isize::MAX,
137139
BuiltinFunction::RegisterCustomFontByPath => isize::MAX,

internal/compiler/lookup.rs

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -819,6 +819,7 @@ impl LookupObject for ColorFunctions {
819819
None.or_else(|| f("rgb", BuiltinMacroFunction::Rgb))
820820
.or_else(|| f("rgba", BuiltinMacroFunction::Rgb))
821821
.or_else(|| f("hsv", BuiltinMacroFunction::Hsv))
822+
.or_else(|| f("oklch", BuiltinMacroFunction::Oklch))
822823
}
823824
}
824825

@@ -989,7 +990,10 @@ impl LookupObject for ColorExpression<'_> {
989990
f: &mut impl FnMut(&SmolStr, LookupResult) -> Option<R>,
990991
) -> Option<R> {
991992
let member_function = |f: BuiltinFunction| {
992-
let base = if f == BuiltinFunction::ColorHsvaStruct && self.0.ty() == Type::Brush {
993+
let base = if (f == BuiltinFunction::ColorHsvaStruct
994+
|| f == BuiltinFunction::ColorOklchStruct)
995+
&& self.0.ty() == Type::Brush
996+
{
993997
Expression::Cast { from: Box::new(self.0.clone()), to: Type::Color }
994998
} else {
995999
self.0.clone()
@@ -1022,6 +1026,7 @@ impl LookupObject for ColorExpression<'_> {
10221026
.or_else(|| f("blue", field_access("blue")))
10231027
.or_else(|| f("alpha", field_access("alpha")))
10241028
.or_else(|| f("to-hsv", member_function(BuiltinFunction::ColorHsvaStruct)))
1029+
.or_else(|| f("to-oklch", member_function(BuiltinFunction::ColorOklchStruct)))
10251030
.or_else(|| f("brighter", member_function(BuiltinFunction::ColorBrighter)))
10261031
.or_else(|| f("darker", member_function(BuiltinFunction::ColorDarker)))
10271032
.or_else(|| f("transparentize", member_function(BuiltinFunction::ColorTransparentize)))

internal/compiler/tests/syntax/lookup/color.slint

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,7 +50,13 @@ export X := Rectangle {
5050
// > <error{This function needs 3 or 4 arguments, but 5 were provided}
5151
property<color> c5: Colors.hsv(1,2,3,4,5);
5252
// > <error{This function needs 3 or 4 arguments, but 5 were provided}
53+
property<color> c6: Colors.oklch(1,2,3,4,5);
54+
// > <error{This function needs 3 or 4 arguments, but 5 were provided}
55+
property<color> c7: oklch(0.5, 0.1, 180);
56+
property<color> c8: oklch(0.5, 0.1, 180, 0.8);
5357

58+
// Test to-oklch() method
59+
property<{ lightness: float, chroma: float, hue: float, alpha: float }> oklch-result: Colors.blue.to-oklch();
5460

5561
}
5662

0 commit comments

Comments
 (0)