Skip to main content

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}