Skip to main content

i_slint_renderer_software/
draw_functions.rs

1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4// cSpell: ignore premultiply
5#![allow(clippy::identity_op)] // We use x + 0 a lot here for symmetry
6
7//! This is the module for the functions that are drawing the pixels
8//! on the line buffer
9
10use super::{Fixed, PhysicalLength, PhysicalRect};
11use derive_more::{Add, Mul, Sub};
12use i_slint_core::Color;
13use i_slint_core::graphics::{Rgb8Pixel, TexturePixelFormat};
14use i_slint_core::lengths::{PointLengths, SizeLengths};
15use integer_sqrt::IntegerSquareRoot;
16#[allow(unused_imports)]
17use num_traits::Float;
18
19/// Draw one line of the texture in the line buffer
20///
21pub(super) fn draw_texture_line(
22    span: &PhysicalRect,
23    line: PhysicalLength,
24    texture: &super::SceneTexture,
25    line_buffer: &mut [impl TargetPixel],
26    extra_clip_begin: i16,
27    extra_clip_end: i16,
28) {
29    let super::SceneTexture {
30        data,
31        format,
32        pixel_stride,
33        extra: super::SceneTextureExtra { colorize, alpha, rotation, dx, dy, off_x, off_y },
34    } = *texture;
35
36    let source_size = texture.source_size().cast::<i32>();
37    let len = line_buffer.len();
38    let y = line - span.origin.y_length();
39    let y = if rotation.mirror_width() { span.size.height - y.get() - 1 } else { y.get() } as i32;
40
41    let off_y = Fixed::<i32, 8>::from_fixed(off_y);
42    let dx = Fixed::<i32, 8>::from_fixed(dx);
43    let dy = Fixed::<i32, 8>::from_fixed(dy);
44    let off_x = Fixed::<i32, 8>::from_fixed(off_x);
45
46    if !rotation.is_transpose() {
47        let mut delta = dx;
48        let row = off_y + dy * y;
49        // The position where to start in the image array for a this row
50        let row_offset = (row.truncate() % source_size.height) as usize * pixel_stride as usize;
51        let mut tile_start = 0;
52
53        // the size of the tile in physical pixels in the target
54        let tile_len = (Fixed::from_integer(source_size.width) / delta) as usize;
55        // the amount of missing image pixel on one tile
56        let mut remainder = Fixed::from_integer(source_size.width) % delta;
57        // The position in image pixel where to get the image
58        let mut pos;
59        // the end index in the target buffer
60        let mut end;
61        // the accumulated error in image pixels
62        let mut acc_err;
63        if rotation.mirror_height() {
64            let o = (off_x + (delta * (extra_clip_end as i32 + len as i32 - 1)))
65                % Fixed::from_integer(source_size.width);
66            pos = o;
67            tile_start = source_size.width;
68            end = (o / delta) as usize + 1;
69            acc_err = -delta + o % delta;
70            delta = -delta;
71            remainder = -remainder;
72        } else {
73            let o =
74                (off_x + delta * extra_clip_begin as i32) % Fixed::from_integer(source_size.width);
75            pos = o;
76            end = ((Fixed::from_integer(source_size.width) - o) / delta) as usize;
77            acc_err = (Fixed::from_integer(source_size.width) - o) % delta;
78            if acc_err != Fixed::default() {
79                acc_err = delta - acc_err;
80                end += 1;
81            }
82        }
83        end = end.min(len);
84        let mut begin = 0;
85        let row_fract = row.fract();
86        while begin < len {
87            fetch_blend_pixel(
88                &mut line_buffer[begin..end],
89                format,
90                data,
91                alpha,
92                colorize,
93                (pixel_stride as usize, dy),
94                #[inline(always)]
95                |bpp| {
96                    let p = ((row_offset + pos.truncate() as usize) * bpp, pos.fract(), row_fract);
97                    pos += delta;
98                    p
99                },
100            );
101            begin = end;
102            end += tile_len;
103            pos = acc_err + Fixed::from_integer(tile_start);
104            if remainder != Fixed::from_integer(0) {
105                acc_err -= remainder;
106                let wrap = if rotation.mirror_height() {
107                    acc_err >= Fixed::from_integer(0)
108                } else {
109                    acc_err < Fixed::from_integer(0)
110                };
111                if wrap {
112                    acc_err += delta;
113                    end += 1;
114                }
115            };
116            end = end.min(len);
117        }
118    } else {
119        let bpp = format.bpp();
120        let col = off_x + dx * y;
121        let col_fract = col.fract();
122        let col = (col.truncate() % source_size.width) as usize * bpp;
123        let stride = pixel_stride as usize * bpp;
124        let mut row_delta = dy;
125        let tile_len = (Fixed::from_integer(source_size.height) / row_delta) as usize;
126        let mut remainder = Fixed::from_integer(source_size.height) % row_delta;
127        let mut end;
128        let mut row_init = Fixed::default();
129        let mut row;
130        let mut acc_err;
131        if rotation.mirror_height() {
132            row_init = Fixed::from_integer(source_size.height);
133            row = (off_y + (row_delta * (extra_clip_end as i32 + len as i32 - 1)))
134                % Fixed::from_integer(source_size.height);
135            end = (row / row_delta) as usize + 1;
136            acc_err = -row_delta + row % row_delta;
137            row_delta = -row_delta;
138            remainder = -remainder;
139        } else {
140            row = (off_y + row_delta * extra_clip_begin as i32)
141                % Fixed::from_integer(source_size.height);
142            end = ((Fixed::from_integer(source_size.height) - row) / row_delta) as usize;
143            acc_err = (Fixed::from_integer(source_size.height) - row) % row_delta;
144            if acc_err != Fixed::default() {
145                acc_err = row_delta - acc_err;
146                end += 1;
147            }
148        };
149        end = end.min(len);
150        let mut begin = 0;
151        while begin < len {
152            fetch_blend_pixel(
153                &mut line_buffer[begin..end],
154                format,
155                data,
156                alpha,
157                colorize,
158                (stride, dy),
159                #[inline(always)]
160                |_| {
161                    let pos = (row.truncate() as usize * stride + col, col_fract, row.fract());
162                    row += row_delta;
163                    pos
164                },
165            );
166            begin = end;
167            end += tile_len;
168            row = row_init;
169            row += acc_err;
170            if remainder != Fixed::from_integer(0) {
171                acc_err -= remainder;
172                let wrap = if rotation.mirror_height() {
173                    acc_err >= Fixed::from_integer(0)
174                } else {
175                    acc_err < Fixed::from_integer(0)
176                };
177                if wrap {
178                    acc_err += row_delta;
179                    end += 1;
180                }
181            };
182            end = end.min(len);
183        }
184    };
185
186    fn fetch_blend_pixel(
187        line_buffer: &mut [impl TargetPixel],
188        format: TexturePixelFormat,
189        data: &[u8],
190        alpha: u8,
191        color: Color,
192        (stride, delta): (usize, Fixed<i32, 8>),
193        mut pos: impl FnMut(usize) -> (usize, u8, u8),
194    ) {
195        match format {
196            TexturePixelFormat::Rgb => {
197                for pix in line_buffer {
198                    let pos = pos(3).0;
199                    let p: &[u8] = &data[pos..pos + 3];
200                    if alpha == 0xff {
201                        *pix = TargetPixel::from_rgb(p[0], p[1], p[2]);
202                    } else {
203                        pix.blend(PremultipliedRgbaColor::premultiply(Color::from_argb_u8(
204                            alpha, p[0], p[1], p[2],
205                        )))
206                    }
207                }
208            }
209            TexturePixelFormat::Rgba => {
210                if color.alpha() == 0 {
211                    for pix in line_buffer {
212                        let pos = pos(4).0;
213                        let alpha = ((data[pos + 3] as u16 * alpha as u16) / 255) as u8;
214                        let c = PremultipliedRgbaColor::premultiply(Color::from_argb_u8(
215                            alpha,
216                            data[pos + 0],
217                            data[pos + 1],
218                            data[pos + 2],
219                        ));
220                        pix.blend(c);
221                    }
222                } else {
223                    for pix in line_buffer {
224                        let pos = pos(4).0;
225                        let alpha = ((data[pos + 3] as u16 * alpha as u16) / 255) as u8;
226                        let c = PremultipliedRgbaColor::premultiply(Color::from_argb_u8(
227                            alpha,
228                            color.red(),
229                            color.green(),
230                            color.blue(),
231                        ));
232                        pix.blend(c);
233                    }
234                }
235            }
236            TexturePixelFormat::RgbaPremultiplied => {
237                if color.alpha() > 0 {
238                    for pix in line_buffer {
239                        let pos = pos(4).0;
240                        let c = PremultipliedRgbaColor::premultiply(Color::from_argb_u8(
241                            ((data[pos + 3] as u16 * alpha as u16) / 255) as u8,
242                            color.red(),
243                            color.green(),
244                            color.blue(),
245                        ));
246                        pix.blend(c);
247                    }
248                } else if alpha == 0xff {
249                    for pix in line_buffer {
250                        let pos = pos(4).0;
251                        let c = PremultipliedRgbaColor {
252                            alpha: data[pos + 3],
253                            red: data[pos + 0],
254                            green: data[pos + 1],
255                            blue: data[pos + 2],
256                        };
257                        pix.blend(c);
258                    }
259                } else {
260                    for pix in line_buffer {
261                        let pos = pos(4).0;
262                        let c = PremultipliedRgbaColor {
263                            alpha: (data[pos + 3] as u16 * alpha as u16 / 255) as u8,
264                            red: (data[pos + 0] as u16 * alpha as u16 / 255) as u8,
265                            green: (data[pos + 1] as u16 * alpha as u16 / 255) as u8,
266                            blue: (data[pos + 2] as u16 * alpha as u16 / 255) as u8,
267                        };
268                        pix.blend(c);
269                    }
270                }
271            }
272            TexturePixelFormat::AlphaMap => {
273                for pix in line_buffer {
274                    let pos = pos(1).0;
275                    let c = PremultipliedRgbaColor::premultiply(Color::from_argb_u8(
276                        ((data[pos] as u16 * alpha as u16) / 255) as u8,
277                        color.red(),
278                        color.green(),
279                        color.blue(),
280                    ));
281                    pix.blend(c);
282                }
283            }
284            TexturePixelFormat::SignedDistanceField => {
285                const RANGE: i32 = 6;
286                let factor = (362 * 256 / delta.0) * RANGE; // 362 ≃ 255 * sqrt(2)
287                for pix in line_buffer {
288                    let (pos, col_f, row_f) = pos(1);
289                    let (col_f, row_f) = (col_f as i32, row_f as i32);
290                    let mut dist = ((data[pos] as i8 as i32) * (256 - col_f)
291                        + (data[pos + 1] as i8 as i32) * col_f)
292                        * (256 - row_f);
293                    if pos + stride + 1 < data.len() {
294                        dist += ((data[pos + stride] as i8 as i32) * (256 - col_f)
295                            + (data[pos + stride + 1] as i8 as i32) * col_f)
296                            * row_f
297                    } else {
298                        debug_assert_eq!(row_f, 0);
299                    }
300                    let a = ((((dist >> 8) * factor) >> 16) + 128).clamp(0, 255) * alpha as i32;
301                    let c = PremultipliedRgbaColor::premultiply(Color::from_argb_u8(
302                        (a / 255) as u8,
303                        color.red(),
304                        color.green(),
305                        color.blue(),
306                    ));
307                    pix.blend(c);
308                }
309            }
310        };
311    }
312}
313
314/// draw one line of the rounded rectangle in the line buffer
315#[allow(clippy::unnecessary_cast)] // Coord
316pub(super) fn draw_rounded_rectangle_line(
317    span: &PhysicalRect,
318    line: PhysicalLength,
319    rr: &super::RoundedRectangle,
320    line_buffer: &mut [impl TargetPixel],
321    extra_left_clip: i16,
322    extra_right_clip: i16,
323) {
324    /// This is an integer shifted by 4 bits.
325    /// Note: this is not a "fixed point" because multiplication and sqrt operation operate to
326    /// the shifted integer
327    #[derive(Clone, Copy, PartialEq, Ord, PartialOrd, Eq, Add, Sub, Mul)]
328    struct Shifted(u32);
329    impl Shifted {
330        const ONE: Self = Shifted(1 << 4);
331        #[track_caller]
332        #[inline]
333        pub fn new(value: impl TryInto<u32> + core::fmt::Debug + Copy) -> Self {
334            Self(value.try_into().unwrap_or_else(|_| panic!("Overflow {value:?}")) << 4)
335        }
336        #[inline(always)]
337        pub fn floor(self) -> u32 {
338            self.0 >> 4
339        }
340        #[inline(always)]
341        pub fn ceil(self) -> u32 {
342            (self.0 + Self::ONE.0 - 1) >> 4
343        }
344        #[inline(always)]
345        pub fn saturating_sub(self, other: Self) -> Self {
346            Self(self.0.saturating_sub(other.0))
347        }
348        #[inline(always)]
349        pub fn sqrt(self) -> Self {
350            Self(self.0.integer_sqrt())
351        }
352    }
353    impl core::ops::Mul for Shifted {
354        type Output = Shifted;
355        #[inline(always)]
356        fn mul(self, rhs: Self) -> Self::Output {
357            Self(self.0 * rhs.0)
358        }
359    }
360    let width = line_buffer.len();
361    let y1 = (line - span.origin.y_length()) + rr.top_clip;
362    let y2 = (span.origin.y_length() + span.size.height_length() - line) + rr.bottom_clip
363        - PhysicalLength::new(1);
364    let y = y1.min(y2);
365    debug_assert!(y.get() >= 0,);
366    let border = Shifted::new(rr.width.get());
367    const ONE: Shifted = Shifted::ONE;
368    const ZERO: Shifted = Shifted(0);
369    let anti_alias = |x1: Shifted, x2: Shifted, process_pixel: &mut dyn FnMut(usize, u32)| {
370        // x1 and x2 are the coordinate on the top and bottom of the intersection of the pixel
371        // line and the curve.
372        // `process_pixel` be called for the coordinate in the array and a coverage between 0..255
373        // This algorithm just go linearly which is not perfect, but good enough.
374        for x in x1.floor()..x2.ceil() {
375            // the coverage is basically how much of the pixel should be used
376            let cov = ((ONE + Shifted::new(x) - x1).0 << 8) / (ONE + x2 - x1).0;
377            process_pixel(x as usize, cov);
378        }
379    };
380    let rev = |x: Shifted| {
381        (Shifted::new(width) + Shifted::new(rr.right_clip.get() + extra_right_clip))
382            .saturating_sub(x)
383    };
384    let calculate_xxxx = |r: i16, y: i16| {
385        let r = Shifted::new(r);
386        // `y` is how far away from the center of the circle the current line is.
387        let y = r - Shifted::new(y);
388        // Circle equation: x = √(r² - y²)
389        // Coordinate from the left edge: x' = r - x
390        let x2 = r - (r * r).saturating_sub(y * y).sqrt();
391        let x1 = r - (r * r).saturating_sub((y - ONE) * (y - ONE)).sqrt();
392        let r2 = r.saturating_sub(border);
393        let x4 = r - (r2 * r2).saturating_sub(y * y).sqrt();
394        let x3 = r - (r2 * r2).saturating_sub((y - ONE) * (y - ONE)).sqrt();
395        (x1, x2, x3, x4)
396    };
397
398    let (x1, x2, x3, x4, x5, x6, x7, x8) = if let Some(r) = rr.radius.as_uniform() {
399        let (x1, x2, x3, x4) =
400            if y.get() < r { calculate_xxxx(r, y.get()) } else { (ZERO, ZERO, border, border) };
401        (x1, x2, x3, x4, rev(x4), rev(x3), rev(x2), rev(x1))
402    } else {
403        let (x1, x2, x3, x4) = if y1 < PhysicalLength::new(rr.radius.top_left) {
404            calculate_xxxx(rr.radius.top_left, y.get())
405        } else if y2 < PhysicalLength::new(rr.radius.bottom_left) {
406            calculate_xxxx(rr.radius.bottom_left, y.get())
407        } else {
408            (ZERO, ZERO, border, border)
409        };
410        let (x5, x6, x7, x8) = if y1 < PhysicalLength::new(rr.radius.top_right) {
411            let x = calculate_xxxx(rr.radius.top_right, y.get());
412            (x.3, x.2, x.1, x.0)
413        } else if y2 < PhysicalLength::new(rr.radius.bottom_right) {
414            let x = calculate_xxxx(rr.radius.bottom_right, y.get());
415            (x.3, x.2, x.1, x.0)
416        } else {
417            (border, border, ZERO, ZERO)
418        };
419        (x1, x2, x3, x4, rev(x5), rev(x6), rev(x7), rev(x8))
420    };
421    anti_alias(
422        x1.saturating_sub(Shifted::new(rr.left_clip.get() + extra_left_clip)),
423        x2.saturating_sub(Shifted::new(rr.left_clip.get() + extra_left_clip)),
424        &mut |x, cov| {
425            if x >= width {
426                return;
427            }
428            let c = if border == ZERO { rr.inner_color } else { rr.border_color };
429            let col = PremultipliedRgbaColor {
430                alpha: (((c.alpha as u32) * cov as u32) / 255) as u8,
431                red: (((c.red as u32) * cov as u32) / 255) as u8,
432                green: (((c.green as u32) * cov as u32) / 255) as u8,
433                blue: (((c.blue as u32) * cov as u32) / 255) as u8,
434            };
435            line_buffer[x].blend(col);
436        },
437    );
438    if y < rr.width {
439        // up or down border (x2 .. x7)
440        let l = x2
441            .ceil()
442            .saturating_sub((rr.left_clip.get() + extra_left_clip) as u32)
443            .min(width as u32) as usize;
444        let r = x7.floor().min(width as u32) as usize;
445        if l < r {
446            TargetPixel::blend_slice(&mut line_buffer[l..r], rr.border_color)
447        }
448    } else {
449        if border > ZERO {
450            // 3. draw the border (between x2 and x3)
451            if ONE + x2 <= x3 {
452                TargetPixel::blend_slice(
453                    &mut line_buffer[x2
454                        .ceil()
455                        .saturating_sub((rr.left_clip.get() + extra_left_clip) as u32)
456                        .min(width as u32) as usize
457                        ..x3.floor()
458                            .saturating_sub((rr.left_clip.get() + extra_left_clip) as u32)
459                            .min(width as u32) as usize],
460                    rr.border_color,
461                )
462            }
463            // 4. anti-aliasing for the contents (x3 .. x4)
464            anti_alias(
465                x3.saturating_sub(Shifted::new(rr.left_clip.get() + extra_left_clip)),
466                x4.saturating_sub(Shifted::new(rr.left_clip.get() + extra_left_clip)),
467                &mut |x, cov| {
468                    if x >= width {
469                        return;
470                    }
471                    let col = interpolate_color(cov, rr.border_color, rr.inner_color);
472                    line_buffer[x].blend(col);
473                },
474            );
475        }
476        if rr.inner_color.alpha > 0 {
477            // 5. inside (x4 .. x5)
478            let begin = x4
479                .ceil()
480                .saturating_sub((rr.left_clip.get() + extra_left_clip) as u32)
481                .min(width as u32);
482            let end = x5.floor().min(width as u32);
483            if begin < end {
484                TargetPixel::blend_slice(
485                    &mut line_buffer[begin as usize..end as usize],
486                    rr.inner_color,
487                )
488            }
489        }
490        if border > ZERO {
491            // 6. border anti-aliasing: x5..x6
492            anti_alias(x5, x6, &mut |x, cov| {
493                if x >= width {
494                    return;
495                }
496                let col = interpolate_color(cov, rr.inner_color, rr.border_color);
497                line_buffer[x].blend(col)
498            });
499            // 7. border x6 .. x7
500            if ONE + x6 <= x7 {
501                TargetPixel::blend_slice(
502                    &mut line_buffer[x6.ceil().min(width as u32) as usize
503                        ..x7.floor().min(width as u32) as usize],
504                    rr.border_color,
505                )
506            }
507        }
508    }
509    anti_alias(x7, x8, &mut |x, cov| {
510        if x >= width {
511            return;
512        }
513        let c = if border == ZERO { rr.inner_color } else { rr.border_color };
514        let col = PremultipliedRgbaColor {
515            alpha: (((c.alpha as u32) * (255 - cov) as u32) / 255) as u8,
516            red: (((c.red as u32) * (255 - cov) as u32) / 255) as u8,
517            green: (((c.green as u32) * (255 - cov) as u32) / 255) as u8,
518            blue: (((c.blue as u32) * (255 - cov) as u32) / 255) as u8,
519        };
520        line_buffer[x].blend(col);
521    });
522}
523
524// a is between 0 and 255. When 0, we get color1, when 255 we get color2
525fn interpolate_color(
526    a: u32,
527    color1: PremultipliedRgbaColor,
528    color2: PremultipliedRgbaColor,
529) -> PremultipliedRgbaColor {
530    let b = 255 - a;
531
532    let al1 = color1.alpha as u32;
533    let al2 = color2.alpha as u32;
534
535    let a_ = a * al2;
536    let b_ = b * al1;
537    let m = a_ + b_;
538
539    if m == 0 {
540        return PremultipliedRgbaColor::default();
541    }
542
543    PremultipliedRgbaColor {
544        alpha: (m / 255) as u8,
545        red: ((b * color1.red as u32 + a * color2.red as u32) / 255) as u8,
546        green: ((b * color1.green as u32 + a * color2.green as u32) / 255) as u8,
547        blue: ((b * color1.blue as u32 + a * color2.blue as u32) / 255) as u8,
548    }
549}
550
551pub(super) fn draw_linear_gradient(
552    rect: &PhysicalRect,
553    line: PhysicalLength,
554    g: &super::LinearGradientCommand,
555    mut buffer: &mut [impl TargetPixel],
556    extra_left_clip: i16,
557) {
558    let fill_col1 = g.flags & 0b010 != 0;
559    let fill_col2 = g.flags & 0b100 != 0;
560    let invert_slope = g.flags & 0b1 != 0;
561
562    let y = (line.get() - rect.min_y() + g.top_clip.get()) as i32;
563    let size_y = (rect.height() + g.top_clip.get() + g.bottom_clip.get()) as i32;
564    let start = g.start as i32;
565
566    let (mut color1, mut color2) = (g.color1, g.color2);
567
568    if g.start == 0 {
569        let p = if invert_slope {
570            (255 - start) * y / size_y
571        } else {
572            start + (255 - start) * y / size_y
573        };
574        if (fill_col1 || p >= 0) && (fill_col2 || p < 255) {
575            let col = interpolate_color(p.clamp(0, 255) as u32, color1, color2);
576            TargetPixel::blend_slice(buffer, col);
577        }
578        return;
579    }
580
581    let size_x = (rect.width() + g.left_clip.get() + g.right_clip.get()) as i32;
582
583    let mut x = if invert_slope {
584        (y * size_x * (255 - start)) / (size_y * start)
585    } else {
586        (size_y - y) * size_x * (255 - start) / (size_y * start)
587    } + g.left_clip.get() as i32
588        + extra_left_clip as i32;
589
590    let len = ((255 * size_x) / start) as usize;
591
592    if x < 0 {
593        let l = (-x as usize).min(buffer.len());
594        if invert_slope {
595            if fill_col1 {
596                TargetPixel::blend_slice(&mut buffer[..l], g.color1);
597            }
598        } else if fill_col2 {
599            TargetPixel::blend_slice(&mut buffer[..l], g.color2);
600        }
601        buffer = &mut buffer[l..];
602        x = 0;
603    }
604
605    if buffer.len() + x as usize > len {
606        let l = len.saturating_sub(x as usize);
607        if invert_slope {
608            if fill_col2 {
609                TargetPixel::blend_slice(&mut buffer[l..], g.color2);
610            }
611        } else if fill_col1 {
612            TargetPixel::blend_slice(&mut buffer[l..], g.color1);
613        }
614        buffer = &mut buffer[..l];
615    }
616
617    if buffer.is_empty() {
618        return;
619    }
620
621    if !invert_slope {
622        core::mem::swap(&mut color1, &mut color2);
623    }
624
625    let dr = (((color2.red as i32 - color1.red as i32) * start) << 15) / (255 * size_x);
626    let dg = (((color2.green as i32 - color1.green as i32) * start) << 15) / (255 * size_x);
627    let db = (((color2.blue as i32 - color1.blue as i32) * start) << 15) / (255 * size_x);
628    let da = (((color2.alpha as i32 - color1.alpha as i32) * start) << 15) / (255 * size_x);
629
630    let mut r = ((color1.red as u32) << 15).wrapping_add((x * dr) as _);
631    let mut g = ((color1.green as u32) << 15).wrapping_add((x * dg) as _);
632    let mut b = ((color1.blue as u32) << 15).wrapping_add((x * db) as _);
633    let mut a = ((color1.alpha as u32) << 15).wrapping_add((x * da) as _);
634
635    if color1.alpha == 255 && color2.alpha == 255 {
636        buffer.fill_with(|| {
637            let pix = TargetPixel::from_rgb((r >> 15) as u8, (g >> 15) as u8, (b >> 15) as u8);
638            r = r.wrapping_add(dr as _);
639            g = g.wrapping_add(dg as _);
640            b = b.wrapping_add(db as _);
641            pix
642        })
643    } else {
644        for pix in buffer {
645            pix.blend(PremultipliedRgbaColor {
646                red: (r >> 15) as u8,
647                green: (g >> 15) as u8,
648                blue: (b >> 15) as u8,
649                alpha: (a >> 15) as u8,
650            });
651            r = r.wrapping_add(dr as _);
652            g = g.wrapping_add(dg as _);
653            b = b.wrapping_add(db as _);
654            a = a.wrapping_add(da as _);
655        }
656    }
657}
658
659/// Draw a radial gradient on a line
660pub(super) fn draw_radial_gradient(
661    rect: &PhysicalRect,
662    line: PhysicalLength,
663    g: &super::RadialGradientCommand,
664    buffer: &mut [impl TargetPixel],
665    extra_left_clip: i16,
666    _extra_right_clip: i16,
667) {
668    if g.stops.is_empty() {
669        return;
670    }
671
672    let center_x = rect.min_x() as f32 + g.center_x;
673    let center_y = rect.min_y() as f32 + g.center_y;
674
675    debug_assert!(
676        g.radius >= 0.0,
677        "radius must be resolved before constructing RadialGradientCommand"
678    );
679    let max_radius = g.radius.max(f32::EPSILON);
680
681    let start_x = rect.min_x() + extra_left_clip;
682    let dy = line.get() as f32 - center_y;
683    let dy_squared = dy * dy;
684
685    for (i, pixel) in buffer.iter_mut().enumerate() {
686        let x = start_x + i as i16;
687        let dx = x as f32 - center_x;
688        let distance = (dx * dx + dy_squared).sqrt();
689        let position = (distance / max_radius).clamp(0.0, 1.0);
690
691        // Find the two gradient stops to interpolate between
692        let mut color = g.stops.first().map(|s| s.color).unwrap_or_default();
693
694        for window in g.stops.windows(2) {
695            let stop1 = &window[0];
696            let stop2 = &window[1];
697
698            if position >= stop1.position && position <= stop2.position {
699                // Interpolate between the two stops
700                let t = if stop2.position == stop1.position {
701                    0.0
702                } else {
703                    (position - stop1.position) / (stop2.position - stop1.position)
704                };
705
706                let c1 = stop1.color.to_argb_u8();
707                let c2 = stop2.color.to_argb_u8();
708
709                let alpha = ((1.0 - t) * c1.alpha as f32 + t * c2.alpha as f32) as u8;
710                let red = ((1.0 - t) * c1.red as f32 + t * c2.red as f32) as u8;
711                let green = ((1.0 - t) * c1.green as f32 + t * c2.green as f32) as u8;
712                let blue = ((1.0 - t) * c1.blue as f32 + t * c2.blue as f32) as u8;
713
714                color = Color::from_argb_u8(alpha, red, green, blue);
715                break;
716            } else if position > stop2.position {
717                color = stop2.color;
718            }
719        }
720
721        pixel.blend(super::PremultipliedRgbaColor::from(color));
722    }
723}
724
725/// Draw a conic gradient on a line
726pub(super) fn draw_conic_gradient(
727    rect: &PhysicalRect,
728    line: PhysicalLength,
729    g: &super::ConicGradientCommand,
730    buffer: &mut [impl TargetPixel],
731    extra_left_clip: i16,
732    _extra_right_clip: i16,
733) {
734    if g.stops.is_empty() {
735        return;
736    }
737
738    let center_x = rect.min_x() as f32 + g.center_x;
739    let center_y = rect.min_y() as f32 + g.center_y;
740
741    let start_x = rect.min_x() + extra_left_clip;
742    let y = line.get() as f32;
743
744    for (i, pixel) in buffer.iter_mut().enumerate() {
745        let x = (start_x + i as i16) as f32;
746
747        // Calculate angle from center to current pixel
748        let dx = x - center_x;
749        let dy = y - center_y;
750
751        // atan2 returns angle in radians from -π to π
752        // For 0deg at north (12 o'clock), we need to rotate by -90 degrees
753        let mut angle = dy.atan2(dx) + core::f32::consts::FRAC_PI_2;
754
755        // Normalize angle to [0, 2π]
756        while angle < 0.0 {
757            angle += 2.0 * core::f32::consts::PI;
758        }
759        while angle >= 2.0 * core::f32::consts::PI {
760            angle -= 2.0 * core::f32::consts::PI;
761        }
762
763        // Convert to position in [0, 1]
764        let position = angle / (2.0 * core::f32::consts::PI);
765
766        // Find the two gradient stops to interpolate between
767        let mut color = g.stops.first().map(|s| s.color).unwrap_or_default();
768
769        for window in g.stops.windows(2) {
770            let stop1 = &window[0];
771            let stop2 = &window[1];
772
773            if position >= stop1.position && position <= stop2.position {
774                // Interpolate between the two stops
775                let t = if stop2.position == stop1.position {
776                    0.0
777                } else {
778                    (position - stop1.position) / (stop2.position - stop1.position)
779                };
780
781                let c1 = stop1.color.to_argb_u8();
782                let c2 = stop2.color.to_argb_u8();
783
784                let alpha = ((1.0 - t) * c1.alpha as f32 + t * c2.alpha as f32) as u8;
785                let red = ((1.0 - t) * c1.red as f32 + t * c2.red as f32) as u8;
786                let green = ((1.0 - t) * c1.green as f32 + t * c2.green as f32) as u8;
787                let blue = ((1.0 - t) * c1.blue as f32 + t * c2.blue as f32) as u8;
788
789                color = Color::from_argb_u8(alpha, red, green, blue);
790                break;
791            } else if position > stop2.position {
792                color = stop2.color;
793            }
794        }
795
796        pixel.blend(super::PremultipliedRgbaColor::from(color));
797    }
798}
799
800/// A color whose component have been pre-multiplied by alpha
801///
802/// The renderer operates faster on pre-multiplied color since it
803/// caches the multiplication of its component
804///
805/// PremultipliedRgbaColor can be constructed from a [`Color`] with
806/// the [`From`] trait. This conversion will pre-multiply the color
807/// components
808#[allow(missing_docs)]
809#[derive(Clone, Copy, Debug, Default, bytemuck::Pod, bytemuck::Zeroable)]
810#[repr(C)]
811pub struct PremultipliedRgbaColor {
812    pub red: u8,
813    pub green: u8,
814    pub blue: u8,
815    pub alpha: u8,
816}
817
818/// Convert a non-premultiplied color to a premultiplied one
819impl From<Color> for PremultipliedRgbaColor {
820    fn from(col: Color) -> Self {
821        Self::premultiply(col)
822    }
823}
824
825impl PremultipliedRgbaColor {
826    /// Convert a non premultiplied color to a premultiplied one
827    fn premultiply(col: Color) -> Self {
828        let a = col.alpha() as u16;
829        Self {
830            alpha: col.alpha(),
831            red: (col.red() as u16 * a / 255) as u8,
832            green: (col.green() as u16 * a / 255) as u8,
833            blue: (col.blue() as u16 * a / 255) as u8,
834        }
835    }
836}
837
838/// Trait for the pixels in the buffer
839pub trait TargetPixel: Sized + Copy {
840    /// Blend a single pixel with a color
841    fn blend(&mut self, color: PremultipliedRgbaColor);
842    /// Blend a color to all the pixel in the slice.
843    fn blend_slice(slice: &mut [Self], color: PremultipliedRgbaColor) {
844        if color.alpha == u8::MAX {
845            slice.fill(Self::from_rgb(color.red, color.green, color.blue))
846        } else {
847            for x in slice {
848                Self::blend(x, color);
849            }
850        }
851    }
852    /// Create a pixel from the red, gree, blue component in the range 0..=255
853    fn from_rgb(red: u8, green: u8, blue: u8) -> Self;
854
855    /// Pixel which will be filled as the background in case the slint view has transparency
856    fn background() -> Self {
857        Self::from_rgb(0, 0, 0)
858    }
859}
860
861impl TargetPixel for Rgb8Pixel {
862    fn blend(&mut self, color: PremultipliedRgbaColor) {
863        let a = (u8::MAX - color.alpha) as u16;
864        self.r = (self.r as u16 * a / 255) as u8 + color.red;
865        self.g = (self.g as u16 * a / 255) as u8 + color.green;
866        self.b = (self.b as u16 * a / 255) as u8 + color.blue;
867    }
868
869    fn from_rgb(r: u8, g: u8, b: u8) -> Self {
870        Self::new(r, g, b)
871    }
872}
873
874impl TargetPixel for PremultipliedRgbaColor {
875    fn blend(&mut self, color: PremultipliedRgbaColor) {
876        let a = (u8::MAX - color.alpha) as u16;
877        self.red = (self.red as u16 * a / 255) as u8 + color.red;
878        self.green = (self.green as u16 * a / 255) as u8 + color.green;
879        self.blue = (self.blue as u16 * a / 255) as u8 + color.blue;
880        self.alpha = (self.alpha as u16 + color.alpha as u16
881            - (self.alpha as u16 * color.alpha as u16) / 255) as u8;
882    }
883
884    fn from_rgb(r: u8, g: u8, b: u8) -> Self {
885        Self { red: r, green: g, blue: b, alpha: 255 }
886    }
887
888    fn background() -> Self {
889        Self { red: 0, green: 0, blue: 0, alpha: 0 }
890    }
891}
892
893/// A 16bit pixel that has 5 red bits, 6 green bits and  5 blue bits
894#[repr(transparent)]
895#[derive(Copy, Clone, Debug, PartialEq, Eq, Default, bytemuck::Pod, bytemuck::Zeroable)]
896pub struct Rgb565Pixel(pub u16);
897
898impl Rgb565Pixel {
899    const R_MASK: u16 = 0b1111_1000_0000_0000;
900    const G_MASK: u16 = 0b0000_0111_1110_0000;
901    const B_MASK: u16 = 0b0000_0000_0001_1111;
902
903    /// Return the red component as a u8.
904    ///
905    /// The bits are shifted so that the result is between 0 and 255
906    fn red(self) -> u8 {
907        ((self.0 & Self::R_MASK) >> 8) as u8
908    }
909    /// Return the green component as a u8.
910    ///
911    /// The bits are shifted so that the result is between 0 and 255
912    fn green(self) -> u8 {
913        ((self.0 & Self::G_MASK) >> 3) as u8
914    }
915    /// Return the blue component as a u8.
916    ///
917    /// The bits are shifted so that the result is between 0 and 255
918    fn blue(self) -> u8 {
919        ((self.0 & Self::B_MASK) << 3) as u8
920    }
921}
922
923impl TargetPixel for Rgb565Pixel {
924    fn blend(&mut self, color: PremultipliedRgbaColor) {
925        let a = (u8::MAX - color.alpha) as u32;
926        // convert to 5 bits
927        let a = (a + 4) >> 3;
928
929        // 00000ggg_ggg00000_rrrrr000_000bbbbb
930        let expanded = (self.0 & (Self::R_MASK | Self::B_MASK)) as u32
931            | (((self.0 & Self::G_MASK) as u32) << 16);
932
933        // gggggggg_000rrrrr_rrr000bb_bbbbbb00
934        let c =
935            ((color.red as u32) << 13) | ((color.green as u32) << 24) | ((color.blue as u32) << 2);
936        // gggggg00_000rrrrr_000000bb_bbb00000
937        let c = c & 0b11111100_00011111_00000011_11100000;
938
939        let res = expanded * a + c;
940
941        self.0 = ((res >> 21) as u16 & Self::G_MASK)
942            | ((res >> 5) as u16 & (Self::R_MASK | Self::B_MASK));
943    }
944
945    fn from_rgb(r: u8, g: u8, b: u8) -> Self {
946        Self(((r as u16 & 0b11111000) << 8) | ((g as u16 & 0b11111100) << 3) | (b as u16 >> 3))
947    }
948}
949
950impl From<Rgb8Pixel> for Rgb565Pixel {
951    fn from(p: Rgb8Pixel) -> Self {
952        Self::from_rgb(p.r, p.g, p.b)
953    }
954}
955
956impl From<Rgb565Pixel> for Rgb8Pixel {
957    fn from(p: Rgb565Pixel) -> Self {
958        Rgb8Pixel { r: p.red(), g: p.green(), b: p.blue() }
959    }
960}
961
962#[test]
963fn rgb565() {
964    let pix565 = Rgb565Pixel::from_rgb(0xff, 0x25, 0);
965    let pix888: Rgb8Pixel = pix565.into();
966    assert_eq!(pix565, pix888.into());
967
968    let pix565 = Rgb565Pixel::from_rgb(0x56, 0x42, 0xe3);
969    let pix888: Rgb8Pixel = pix565.into();
970    assert_eq!(pix565, pix888.into());
971}