slint_sc/lib.rs
1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Software-3.0
3
4#![doc = include_str!("README.md")]
5#![no_std]
6#![forbid(unsafe_code)]
7#![forbid(missing_docs)]
8
9/// An RGBA color, as held by properties of the `color` type.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
11pub struct Color {
12 red: u8,
13 green: u8,
14 blue: u8,
15 alpha: u8,
16}
17
18impl Color {
19 /// Construct a color from its ARGB value, e.g. `0xff123456`.
20 pub const fn from_argb_encoded(argb: u32) -> Self {
21 let [alpha, red, green, blue] = argb.to_be_bytes();
22 Self { red, green, blue, alpha }
23 }
24
25 /// Construct a fully opaque color from its red, green, and blue channels.
26 pub const fn from_rgb_u8(red: u8, green: u8, blue: u8) -> Self {
27 Self { red, green, blue, alpha: 0xff }
28 }
29
30 /// The red channel, from 0 to 255.
31 pub const fn red(self) -> u8 {
32 self.red
33 }
34
35 /// The green channel, from 0 to 255.
36 pub const fn green(self) -> u8 {
37 self.green
38 }
39
40 /// The blue channel, from 0 to 255.
41 pub const fn blue(self) -> u8 {
42 self.blue
43 }
44
45 /// The alpha channel: the color's opacity, from 0 for a fully transparent
46 /// color to 255 for a fully opaque one.
47 pub const fn alpha(self) -> u8 {
48 self.alpha
49 }
50
51 /// Returns this color composited over `destination`, the Porter-Duff *over*
52 /// operation.
53 ///
54 /// ```
55 /// use slint_sc::Color;
56 ///
57 /// let red = Color::from_rgb_u8(0xff, 0, 0);
58 ///
59 /// // A fully transparent color leaves the destination as it was
60 /// assert_eq!(Color::default().composite_over(red), red);
61 ///
62 /// // A fully opaque one replaces it
63 /// let green = Color::from_rgb_u8(0, 0xff, 0);
64 /// assert_eq!(green.composite_over(red), green);
65 ///
66 /// // Half-transparent blue over opaque red keeps half of each, and the
67 /// // two halves round apart
68 /// let half_blue = Color::from_argb_encoded(0x800000ff);
69 /// assert_eq!(half_blue.composite_over(red), Color::from_rgb_u8(127, 0, 128));
70 ///
71 /// // Compositing two transparent colors has nothing to show
72 /// assert_eq!(Color::default().composite_over(Color::default()), Color::default());
73 /// ```
74 pub fn composite_over(self, destination: Self) -> Self {
75 let alpha = self.alpha as u32;
76 // How much each color contributes, both over a denominator of
77 // 255 * 255, which keeps the channels and the alpha exact over one
78 // common divisor instead of rounding the weights separately
79 let src_weight: u32 = alpha * 255;
80 let dst_weight: u32 = destination.alpha as u32 * (255 - alpha);
81 let total: u32 = src_weight + dst_weight;
82 // Neither color contributes anything, so there's nothing to weigh the
83 // channels by and the result is transparent
84 if total == 0 {
85 return Self::default();
86 }
87 // Both weights are at most 255 * 255 and so is their total, which
88 // bounds a channel's numerator by 255 * 255 * 255: well within a u32,
89 // and the quotient within a u8. Adding half the divisor first rounds
90 // to the nearest integer.
91 let channel = |src: u8, dst: u8| {
92 ((src as u32 * src_weight + dst as u32 * dst_weight + total / 2) / total) as u8
93 };
94 Self {
95 red: channel(self.red, destination.red),
96 green: channel(self.green, destination.green),
97 blue: channel(self.blue, destination.blue),
98 alpha: ((total + 127) / 255) as u8,
99 }
100 }
101}
102
103#[test]
104fn test_color() {
105 let c = Color::from_argb_encoded(0x87123456);
106 assert_eq!((c.red(), c.green(), c.blue(), c.alpha()), (0x12, 0x34, 0x56, 0x87));
107 assert_eq!(Color::from_rgb_u8(0x12, 0x34, 0x56).alpha(), 0xff);
108 // The default color is transparent
109 assert_eq!(Color::default().alpha(), 0);
110}
111
112#[test]
113fn test_composite_over() {
114 let destination = Color::from_rgb_u8(0, 255, 200);
115 // A fully opaque color is the result itself, a fully transparent one
116 // leaves the destination as it was: the rounding never drifts at either end
117 assert_eq!(
118 Color::from_argb_encoded(0xffff000a).composite_over(destination),
119 Color::from_rgb_u8(255, 0, 10)
120 );
121 assert_eq!(Color::from_argb_encoded(0x00ff000a).composite_over(destination), destination);
122 // Halfway between, each channel rounds to the nearest whole number, and
123 // the channels don't mix into one another
124 assert_eq!(
125 Color::from_argb_encoded(0x80ff000a).composite_over(destination),
126 Color::from_rgb_u8(128, 127, 105)
127 );
128 // The brightest possible result still fits in a u8
129 assert_eq!(
130 Color::from_argb_encoded(0x80ffffff).composite_over(Color::from_rgb_u8(255, 255, 255)),
131 Color::from_rgb_u8(255, 255, 255)
132 );
133 // Over a translucent destination the result keeps an alpha of its own: half
134 // over half leaves three quarters covered
135 assert_eq!(
136 Color::from_argb_encoded(0x80ff0000).composite_over(Color::from_argb_encoded(0x800000ff)),
137 Color::from_argb_encoded(0xc0aa0055)
138 );
139 // With nothing to composite, the result is transparent
140 assert_eq!(Color::default().composite_over(Color::default()), Color::default());
141}
142
143#[test]
144fn test_composite_over_matches_the_specified_formula() {
145 // Over an opaque destination, the case rendering is specified for, every
146 // channel comes out as the specified `(src * alpha + dst * (255 - alpha) +
147 // 127) / 255`, for every channel value and every alpha rather than only
148 // the ones the test cases happen to paint.
149 //#sls.paint.blend.formula
150 for alpha in 0..=255u32 {
151 for src in 0..=255u32 {
152 for dst in 0..=255u32 {
153 let color = Color::from_argb_encoded((alpha << 24) | (src << 16));
154 let got = color.composite_over(Color::from_rgb_u8(dst as u8, 0, 0)).red();
155 let weighted = src * alpha + dst * (255 - alpha);
156 let expected = ((weighted + 127) / 255) as u8;
157 assert_eq!(got, expected, "alpha {alpha}, src {src}, dst {dst}");
158 // And that is the nearest whole number, as specified: the
159 // remainder is itself a whole number, so it never lands on an
160 // exact half that could round either way
161 let nearest = (weighted / 255 + u32::from(weighted % 255 >= 128)) as u8;
162 assert_eq!(expected, nearest, "alpha {alpha}, src {src}, dst {dst}");
163 }
164 }
165 }
166}
167
168/// Error returned by the generated render functions.
169#[derive(Debug, Clone, PartialEq, Eq)]
170#[non_exhaustive]
171pub enum RenderError {
172 /// The frame buffer size doesn't match the requested width and height.
173 InvalidFrameBufferSize,
174}
175
176impl core::fmt::Display for RenderError {
177 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
178 match self {
179 Self::InvalidFrameBufferSize => {
180 f.write_str("the frame buffer size doesn't match the requested width and height")
181 }
182 }
183 }
184}
185
186impl core::error::Error for RenderError {}
187
188#[test]
189fn test_render_error_display() {
190 use core::fmt::Write;
191 // A no_std, no_alloc sink to capture the Display output.
192 struct Sink {
193 buf: [u8; 80],
194 len: usize,
195 }
196 impl Write for Sink {
197 fn write_str(&mut self, s: &str) -> core::fmt::Result {
198 let end = self.len + s.len();
199 self.buf[self.len..end].copy_from_slice(s.as_bytes());
200 self.len = end;
201 Ok(())
202 }
203 }
204 let mut sink = Sink { buf: [0; 80], len: 0 };
205 write!(sink, "{}", RenderError::InvalidFrameBufferSize).unwrap();
206 assert_eq!(
207 core::str::from_utf8(&sink.buf[..sink.len]).unwrap(),
208 "the frame buffer size doesn't match the requested width and height"
209 );
210}
211
212/// Module only meant to be used by the code generated by the Slint SC compiler.
213#[doc(hidden)]
214pub mod private_unstable_api {
215 /// Painting into a frame buffer.
216 pub mod renderer;
217}