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/// The size of a window, in pixels.
10#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
11pub struct Size {
12 /// The width in pixels.
13 pub width: u32,
14 /// The height in pixels.
15 pub height: u32,
16}
17
18impl Size {
19 /// Construct a size from a width and a height.
20 pub const fn new(width: u32, height: u32) -> Self {
21 Self { width, height }
22 }
23}
24
25#[test]
26fn test_size() {
27 let size = Size::new(320, 240);
28 assert_eq!((size.width, size.height), (320, 240));
29 assert_eq!(size, Size { width: 320, height: 240 });
30 assert_ne!(size, Size::new(240, 320));
31 // The default size is empty
32 assert_eq!(Size::default(), Size::new(0, 0));
33 assert_eq!(Sink::format(format_args!("{size:?}")).as_str(), "Size { width: 320, height: 240 }");
34}
35
36/// An RGBA color, as held by properties of the `color` type.
37#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
38pub struct Color {
39 red: u8,
40 green: u8,
41 blue: u8,
42 alpha: u8,
43}
44
45impl Color {
46 /// Construct a color from its ARGB value, e.g. `0xff123456`.
47 pub const fn from_argb_encoded(argb: u32) -> Self {
48 let [alpha, red, green, blue] = argb.to_be_bytes();
49 Self { red, green, blue, alpha }
50 }
51
52 /// Construct a fully opaque color from its red, green, and blue channels.
53 pub const fn from_rgb_u8(red: u8, green: u8, blue: u8) -> Self {
54 Self { red, green, blue, alpha: 0xff }
55 }
56
57 /// The red channel, from 0 to 255.
58 pub const fn red(self) -> u8 {
59 self.red
60 }
61
62 /// The green channel, from 0 to 255.
63 pub const fn green(self) -> u8 {
64 self.green
65 }
66
67 /// The blue channel, from 0 to 255.
68 pub const fn blue(self) -> u8 {
69 self.blue
70 }
71
72 /// The alpha channel: the color's opacity, from 0 for a fully transparent
73 /// color to 255 for a fully opaque one.
74 pub const fn alpha(self) -> u8 {
75 self.alpha
76 }
77
78 /// Returns this color composited over `destination`, the Porter-Duff *over*
79 /// operation.
80 ///
81 /// ```
82 /// use slint_sc::Color;
83 ///
84 /// let red = Color::from_rgb_u8(0xff, 0, 0);
85 ///
86 /// // A fully transparent color leaves the destination as it was
87 /// assert_eq!(Color::default().composite_over(red), red);
88 ///
89 /// // A fully opaque one replaces it
90 /// let green = Color::from_rgb_u8(0, 0xff, 0);
91 /// assert_eq!(green.composite_over(red), green);
92 ///
93 /// // Half-transparent blue over opaque red keeps half of each, and the
94 /// // two halves round apart
95 /// let half_blue = Color::from_argb_encoded(0x800000ff);
96 /// assert_eq!(half_blue.composite_over(red), Color::from_rgb_u8(127, 0, 128));
97 ///
98 /// // Compositing two transparent colors has nothing to show
99 /// assert_eq!(Color::default().composite_over(Color::default()), Color::default());
100 /// ```
101 pub fn composite_over(self, destination: Self) -> Self {
102 let alpha = self.alpha as u32;
103 // How much each color contributes, both over a denominator of
104 // 255 * 255, which keeps the channels and the alpha exact over one
105 // common divisor instead of rounding the weights separately
106 let src_weight: u32 = alpha * 255;
107 let dst_weight: u32 = destination.alpha as u32 * (255 - alpha);
108 let total: u32 = src_weight + dst_weight;
109 // Neither color contributes anything, so there's nothing to weigh the
110 // channels by and the result is transparent
111 if total == 0 {
112 return Self::default();
113 }
114 // Both weights are at most 255 * 255 and so is their total, which
115 // bounds a channel's numerator by 255 * 255 * 255: well within a u32,
116 // and the quotient within a u8. Adding half the divisor first rounds
117 // to the nearest integer.
118 let channel = |src: u8, dst: u8| {
119 ((src as u32 * src_weight + dst as u32 * dst_weight + total / 2) / total) as u8
120 };
121 Self {
122 red: channel(self.red, destination.red),
123 green: channel(self.green, destination.green),
124 blue: channel(self.blue, destination.blue),
125 alpha: ((total + 127) / 255) as u8,
126 }
127 }
128}
129
130#[test]
131fn test_color() {
132 let c = Color::from_argb_encoded(0x87123456);
133 assert_eq!((c.red(), c.green(), c.blue(), c.alpha()), (0x12, 0x34, 0x56, 0x87));
134 assert_eq!(Color::from_rgb_u8(0x12, 0x34, 0x56).alpha(), 0xff);
135 // The default color is transparent
136 assert_eq!(Color::default().alpha(), 0);
137}
138
139#[test]
140fn test_composite_over() {
141 let destination = Color::from_rgb_u8(0, 255, 200);
142 // A fully opaque color is the result itself, a fully transparent one
143 // leaves the destination as it was: the rounding never drifts at either end
144 assert_eq!(
145 Color::from_argb_encoded(0xffff000a).composite_over(destination),
146 Color::from_rgb_u8(255, 0, 10)
147 );
148 assert_eq!(Color::from_argb_encoded(0x00ff000a).composite_over(destination), destination);
149 // Halfway between, each channel rounds to the nearest whole number, and
150 // the channels don't mix into one another
151 assert_eq!(
152 Color::from_argb_encoded(0x80ff000a).composite_over(destination),
153 Color::from_rgb_u8(128, 127, 105)
154 );
155 // The brightest possible result still fits in a u8
156 assert_eq!(
157 Color::from_argb_encoded(0x80ffffff).composite_over(Color::from_rgb_u8(255, 255, 255)),
158 Color::from_rgb_u8(255, 255, 255)
159 );
160 // Over a translucent destination the result keeps an alpha of its own: half
161 // over half leaves three quarters covered
162 assert_eq!(
163 Color::from_argb_encoded(0x80ff0000).composite_over(Color::from_argb_encoded(0x800000ff)),
164 Color::from_argb_encoded(0xc0aa0055)
165 );
166 // With nothing to composite, the result is transparent
167 assert_eq!(Color::default().composite_over(Color::default()), Color::default());
168}
169
170#[test]
171fn test_composite_over_matches_the_specified_formula() {
172 // Over an opaque destination, the case rendering is specified for, every
173 // channel comes out as the specified `(src * alpha + dst * (255 - alpha) +
174 // 127) / 255`, for every channel value and every alpha rather than only
175 // the ones the test cases happen to paint.
176 //#sls.paint.blend.formula
177 for alpha in 0..=255u32 {
178 for src in 0..=255u32 {
179 for dst in 0..=255u32 {
180 let color = Color::from_argb_encoded((alpha << 24) | (src << 16));
181 let got = color.composite_over(Color::from_rgb_u8(dst as u8, 0, 0)).red();
182 let weighted = src * alpha + dst * (255 - alpha);
183 let expected = ((weighted + 127) / 255) as u8;
184 assert_eq!(got, expected, "alpha {alpha}, src {src}, dst {dst}");
185 // And that is the nearest whole number, as specified: the
186 // remainder is itself a whole number, so it never lands on an
187 // exact half that could round either way
188 let nearest = (weighted / 255 + u32::from(weighted % 255 >= 128)) as u8;
189 assert_eq!(expected, nearest, "alpha {alpha}, src {src}, dst {dst}");
190 }
191 }
192 }
193}
194
195/// A position in the window, in pixels: the origin is the top-left corner of
196/// the window, x grows to the right, and y grows downwards.
197#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
198pub struct Point {
199 /// The distance from the left edge of the window.
200 pub x: i32,
201 /// The distance from the top edge of the window.
202 pub y: i32,
203}
204
205impl Point {
206 /// Construct a position from its distance to the left and top edges.
207 pub const fn new(x: i32, y: i32) -> Self {
208 Self { x, y }
209 }
210}
211
212/// A touch of the display, delivered to the generated `dispatch_touch_event`.
213#[derive(Debug, Clone, Copy, PartialEq, Eq)]
214#[non_exhaustive]
215pub enum TouchEvent {
216 /// A finger touched the display.
217 #[non_exhaustive]
218 Pressed {
219 /// Where the finger touched.
220 position: Point,
221 },
222 /// The finger lifted off the display.
223 #[non_exhaustive]
224 Released {
225 /// Where the finger lifted off.
226 position: Point,
227 },
228}
229
230impl TouchEvent {
231 /// A finger touching the display at `position`.
232 pub const fn pressed(position: Point) -> Self {
233 Self::Pressed { position }
234 }
235
236 /// A finger lifting off the display at `position`.
237 pub const fn released(position: Point) -> Self {
238 Self::Released { position }
239 }
240}
241
242/// An image, as held by properties of the `image` type.
243#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
244#[non_exhaustive]
245pub enum Image {
246 /// No image, the value of an `image` property without one.
247 #[default]
248 None,
249 /// An image decoded at compile time into a static array of packed bytes.
250 /// Bytes rather than [`Color`] values so that the generated code can
251 /// carry the image in a byte-string literal, which parses much faster
252 /// than an array of per-pixel constructor calls.
253 StaticArgb {
254 /// Four bytes per pixel, alpha, red, green, and blue, the pixels row
255 /// by row from the top-left corner.
256 argb: &'static [u8],
257 /// The number of pixels in each row.
258 width: usize,
259 },
260}
261
262impl Image {
263 /// The width in pixels. An [`Image::None`] has a width of 0.
264 ///
265 /// ```
266 /// use slint_sc::Image;
267 ///
268 /// assert_eq!(Image::None.width(), 0);
269 ///
270 /// let image = Image::StaticArgb { argb: &[0x80; 24], width: 3 };
271 /// assert_eq!(image.width(), 3);
272 /// ```
273 pub const fn width(self) -> usize {
274 match self {
275 Self::None => 0,
276 Self::StaticArgb { width, .. } => width,
277 }
278 }
279
280 /// The height in pixels, derived from the pixel count and the width: an
281 /// incomplete last pixel or row is not counted. An [`Image::None`], and
282 /// an [`Image::StaticArgb`] with a width of 0, have a height of 0.
283 ///
284 /// ```
285 /// use slint_sc::Image;
286 ///
287 /// assert_eq!(Image::None.height(), 0);
288 ///
289 /// let image = Image::StaticArgb { argb: &[0x80; 24], width: 3 };
290 /// assert_eq!(image.height(), 2);
291 /// ```
292 pub const fn height(self) -> usize {
293 match self {
294 Self::None => 0,
295 Self::StaticArgb { width: 0, .. } => 0,
296 Self::StaticArgb { argb, width } => argb.len() / 4 / width,
297 }
298 }
299}
300
301#[test]
302fn test_touch_event() {
303 let pressed = TouchEvent::pressed(Point::new(3, -4));
304 assert_eq!(pressed, TouchEvent::Pressed { position: Point { x: 3, y: -4 } });
305 // A press and a release of the same position are different events
306 assert_ne!(pressed, TouchEvent::released(Point::new(3, -4)));
307 // The default position is the origin
308 assert_eq!(Point::default(), Point::new(0, 0));
309 assert_eq!(
310 Sink::format(format_args!("{pressed:?}")).as_str(),
311 "Pressed { position: Point { x: 3, y: -4 } }"
312 );
313}
314
315#[test]
316fn test_image() {
317 // The default image is no image
318 //#sls.gen.prop.types.image
319 assert_eq!(Image::default(), Image::None);
320 assert_eq!((Image::None.width(), Image::None.height()), (0, 0));
321
322 // Four bytes per pixel, so six pixels of bytes make a 2x3 image
323 let image = Image::StaticArgb { argb: &[0x80; 24], width: 2 };
324 assert_eq!((image.width(), image.height()), (2, 3));
325
326 // Incomplete trailing pixels and rows don't count towards the height,
327 // and a width of 0 derives a height of 0 rather than dividing by it
328 assert_eq!(Image::StaticArgb { argb: &[0x80; 23], width: 2 }.height(), 2);
329 assert_eq!(Image::StaticArgb { argb: &[0x80; 24], width: 4 }.height(), 1);
330 assert_eq!(Image::StaticArgb { argb: &[0x80; 24], width: 0 }.height(), 0);
331
332 // The image is Copy and compares by its parts
333 let copy = image;
334 assert_eq!(copy, image);
335 assert_ne!(copy, Image::None);
336}
337
338/// Error returned by the generated render functions.
339#[derive(Debug, Clone, PartialEq, Eq)]
340#[non_exhaustive]
341pub enum RenderError {
342 /// The frame buffer size doesn't match the size of the window.
343 InvalidFrameBufferSize,
344}
345
346impl core::fmt::Display for RenderError {
347 fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
348 match self {
349 Self::InvalidFrameBufferSize => {
350 f.write_str("the frame buffer size doesn't match the size of the window")
351 }
352 }
353 }
354}
355
356impl core::error::Error for RenderError {}
357
358#[test]
359fn test_render_error_display() {
360 assert_eq!(
361 Sink::format(format_args!("{}", RenderError::InvalidFrameBufferSize)).as_str(),
362 "the frame buffer size doesn't match the size of the window"
363 );
364}
365
366/// Module only meant to be used by the code generated by the Slint SC compiler.
367#[doc(hidden)]
368pub mod private_unstable_api {
369 /// Painting into a frame buffer.
370 pub mod renderer;
371}
372
373/// A sink that captures formatted output in a fixed buffer, so that a test can
374/// assert on a `Debug` or `Display` implementation without an allocator.
375#[cfg(test)]
376struct Sink {
377 buf: [u8; 128],
378 len: usize,
379}
380
381#[cfg(test)]
382impl core::fmt::Write for Sink {
383 fn write_str(&mut self, s: &str) -> core::fmt::Result {
384 let end = self.len + s.len();
385 self.buf[self.len..end].copy_from_slice(s.as_bytes());
386 self.len = end;
387 Ok(())
388 }
389}
390
391#[cfg(test)]
392impl Sink {
393 /// The formatted arguments, held by the returned sink.
394 fn format(args: core::fmt::Arguments<'_>) -> Self {
395 use core::fmt::Write;
396 let mut sink = Self { buf: [0; 128], len: 0 };
397 sink.write_fmt(args).unwrap();
398 sink
399 }
400
401 fn as_str(&self) -> &str {
402 core::str::from_utf8(&self.buf[..self.len]).unwrap()
403 }
404}