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