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/// 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}