// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0

// cSpell: ignore doesnt

use i_slint_renderer_skia::SkiaRenderer;
use i_slint_renderer_skia::SkiaSharedContext;
use i_slint_renderer_skia::skia_safe;
use slint::platform::software_renderer::{
    MinimalSoftwareWindow, PremultipliedRgbaColor, SoftwareRenderer, TargetPixel,
};
use slint::platform::{PlatformError, WindowAdapter};
use slint::{Model, PhysicalPosition, PhysicalSize, SharedPixelBuffer};
use std::cell::{Cell, RefCell};
use std::rc::{Rc, Weak};

thread_local! {
    static WINDOW: Rc<MinimalSoftwareWindow>  =
    MinimalSoftwareWindow::new(slint::platform::software_renderer::RepaintBufferType::ReusedBuffer);
    static SKIA_WINDOW: Rc<SkiaTestWindow> = SkiaTestWindow::new();
    static NEXT_WINDOW_CHOICE: Rc<RefCell<Option<Rc<dyn WindowAdapter>>>> = Rc::new(RefCell::new(None));
}

struct TestPlatform;
impl slint::platform::Platform for TestPlatform {
    fn create_window_adapter(&self) -> Result<Rc<dyn WindowAdapter>, PlatformError> {
        Ok(NEXT_WINDOW_CHOICE.with(|choice| {
            choice.borrow_mut().take().unwrap_or_else(|| WINDOW.with(|x| x.clone()))
        }))
    }
}

const BYTES_PER_PIXEL: usize = 4;

#[derive(Clone, Copy, Default)]
struct TestPixel(bool);

impl TargetPixel for TestPixel {
    fn blend(&mut self, _color: PremultipliedRgbaColor) {
        *self = Self(true);
    }

    fn from_rgb(_red: u8, _green: u8, _blue: u8) -> Self {
        Self(true)
    }
}

#[track_caller]
fn do_test_render_region(renderer: &SoftwareRenderer, x: i32, y: i32, x2: i32, y2: i32) {
    let mut buffer = vec![TestPixel(false); 500 * 500];
    let r = renderer.render(buffer.as_mut_slice(), 500);
    assert_eq!(r.bounding_box_size(), PhysicalSize { width: (x2 - x) as _, height: (y2 - y) as _ });
    assert_eq!(r.bounding_box_origin(), PhysicalPosition { x, y });

    let mut has_one_pixel = false;
    for py in 0..500 {
        for px in 0..500 {
            let in_bounding_box = (x..x2).contains(&(px as i32)) && (y..y2).contains(&(py as i32));
            if !in_bounding_box {
                assert!(
                    !buffer[py * 500 + px].0,
                    "Something written outside of bounding box in  {px},{py}   - (x={x},y={y},x2={x2},y2={y2})"
                )
            } else if buffer[py * 500 + px].0 {
                has_one_pixel = true;
            }
        }
    }
    assert!(has_one_pixel, "Nothing was rendered");
}

struct SkiaTestWindow {
    window: slint::Window,
    renderer: SkiaRenderer,
    needs_redraw: Cell<bool>,
    size: Cell<slint::PhysicalSize>,
    render_buffer: Rc<SkiaTestSoftwareBuffer>,
}

impl SkiaTestWindow {
    fn new() -> Rc<Self> {
        let render_buffer = Rc::new(SkiaTestSoftwareBuffer::default());
        let renderer = SkiaRenderer::new_with_surface(
            &SkiaSharedContext::default(),
            Box::new(i_slint_renderer_skia::software_surface::SoftwareSurface::from(
                render_buffer.clone(),
            )),
        );
        Rc::new_cyclic(|w: &Weak<Self>| Self {
            window: slint::Window::new(w.clone()),
            renderer,
            needs_redraw: Default::default(),
            size: Default::default(),
            render_buffer,
        })
    }

    fn draw_if_needed(&self) -> bool {
        if self.needs_redraw.replace(false) {
            let _ = self.renderer.render().unwrap();
            true
        } else {
            false
        }
    }

    fn last_dirty_region_bounding_box_size(&self) -> Option<slint::LogicalSize> {
        self.render_buffer.last_dirty_region.borrow().as_ref().map(|r| {
            let size = r.bounding_rect().size;
            slint::LogicalSize::new(size.width as _, size.height as _)
        })
    }
    fn last_dirty_region_bounding_box_origin(&self) -> Option<slint::LogicalPosition> {
        self.render_buffer.last_dirty_region.borrow().as_ref().map(|r| {
            let origin = r.bounding_rect().origin;
            slint::LogicalPosition::new(origin.x as _, origin.y as _)
        })
    }
}

impl WindowAdapter for SkiaTestWindow {
    fn window(&self) -> &slint::Window {
        &self.window
    }

    fn size(&self) -> PhysicalSize {
        self.size.get()
    }

    fn renderer(&self) -> &dyn slint::platform::Renderer {
        &self.renderer
    }

    fn set_size(&self, size: slint::WindowSize) {
        self.size.set(size.to_physical(1.));
        self.window
            .dispatch_event(slint::platform::WindowEvent::Resized { size: size.to_logical(1.) })
    }

    fn request_redraw(&self) {
        self.needs_redraw.set(true);
    }
}

#[derive(Default)]
struct SkiaTestSoftwareBuffer {
    pixels: RefCell<Option<SharedPixelBuffer<slint::Rgba8Pixel>>>,
    last_dirty_region: RefCell<Option<i_slint_core::partial_renderer::DirtyRegion>>,
}

impl i_slint_renderer_skia::software_surface::RenderBuffer for SkiaTestSoftwareBuffer {
    fn with_buffer(
        &self,
        _window: &slint::Window,
        size: PhysicalSize,
        render_callback: &mut dyn FnMut(
            std::num::NonZeroU32,
            std::num::NonZeroU32,
            i_slint_renderer_skia::skia_safe::ColorType,
            u8,
            &mut [u8],
        ) -> Result<
            Option<i_slint_core::partial_renderer::DirtyRegion>,
            i_slint_core::platform::PlatformError,
        >,
    ) -> Result<(), i_slint_core::platform::PlatformError> {
        let Some((width, height)): Option<(std::num::NonZeroU32, std::num::NonZeroU32)> =
            size.width.try_into().ok().zip(size.height.try_into().ok())
        else {
            // Nothing to render
            return Ok(());
        };

        let mut shared_pixel_buffer = self.pixels.borrow_mut().take();

        if shared_pixel_buffer.as_ref().is_some_and(|existing_buffer| {
            existing_buffer.width() != width.get() || existing_buffer.height() != height.get()
        }) {
            shared_pixel_buffer = None;
        }

        let mut age = 1;
        let pixels = shared_pixel_buffer.get_or_insert_with(|| {
            age = 0;
            SharedPixelBuffer::new(width.get(), height.get())
        });

        let bytes = bytemuck::cast_slice_mut(pixels.make_mut_slice());
        *self.last_dirty_region.borrow_mut() =
            render_callback(width, height, skia_safe::ColorType::RGBA8888, age, bytes)?;

        *self.pixels.borrow_mut() = shared_pixel_buffer;

        Ok(())
    }
}

#[test]
fn simple() {
    slint::slint! {
        export component Ui inherits Window {
            in property <color> c: yellow;
            background: black;
            Rectangle {
                x: 1phx;
                y: 80phx;
                width: 15phx;
                height: 17phx;
                background: red;
            }
            Rectangle {
                x: 10phx;
                y: 19phx;
                Rectangle {
                    x: 5phx;
                    y: 80phx;
                    width: 12phx;
                    height: 13phx;
                    background: c;
                }
                Rectangle {
                    x: 50phx;
                    y: 8phx;
                    width: 15phx;
                    height: 17phx;
                    background: c;
                }
            }
        }
    }

    slint::platform::set_platform(Box::new(TestPlatform)).ok();
    let ui = Ui::new().unwrap();
    let window = WINDOW.with(|x| x.clone());
    window.set_size(slint::PhysicalSize::new(180, 260));
    ui.show().unwrap();
    assert!(window.draw_if_needed(|renderer| {
        do_test_render_region(renderer, 0, 0, 180, 260);
    }));
    assert!(!window.draw_if_needed(|_| { unreachable!() }));
    ui.set_c(slint::Color::from_rgb_u8(45, 12, 13));
    assert!(window.draw_if_needed(|renderer| {
        do_test_render_region(renderer, 10 + 5, 19 + 8, 10 + 50 + 15, 19 + 80 + 13);
    }));
    ui.set_c(slint::Color::from_rgb_u8(45, 12, 13));
    assert!(!window.draw_if_needed(|_| { unreachable!() }));
}

#[test]
fn visibility() {
    slint::slint! {
        export component Ui inherits Window {
            in property <bool> c : true;
            in property <bool> another: false;
            background: black;
            Rectangle {
                x: 10phx;
                y: 19phx;
                Rectangle {
                    x: 5phx;
                    y: 80phx;
                    width: 12phx;
                    height: 13phx;
                    background: red;
                    visible: c;
                    Rectangle {
                        x: 3phx;
                        y: 3phx;
                        width: 3phx;
                        height: 3phx;
                        background: another ? blue: green;
                    }
                }
                Rectangle {
                    x: 50phx;
                    y: 8phx;
                    width: 15phx;
                    height: 17phx;
                    background: gray;
                    visible: !c;
                    Rectangle {
                        x: 3phx;
                        y: 3phx;
                        width: 3phx;
                        height: 3phx;
                        background: another ? blue: green;
                    }
                }
            }
        }
    }

    slint::platform::set_platform(Box::new(TestPlatform)).ok();
    let ui = Ui::new().unwrap();
    let window = WINDOW.with(|x| x.clone());
    window.set_size(slint::PhysicalSize::new(180, 260));
    ui.show().unwrap();
    assert!(window.draw_if_needed(|renderer| {
        do_test_render_region(renderer, 0, 0, 180, 260);
    }));
    assert!(!window.draw_if_needed(|_| { unreachable!() }));

    ui.set_c(false);
    assert!(window.draw_if_needed(|renderer| {
        do_test_render_region(renderer, 10 + 5, 19 + 8, 10 + 50 + 15, 19 + 80 + 13);
    }));
    assert!(!window.draw_if_needed(|_| { unreachable!() }));
    ui.set_another(true);
    assert!(window.draw_if_needed(|renderer| {
        do_test_render_region(renderer, 10 + 50 + 3, 19 + 8 + 3, 10 + 50 + 6, 19 + 8 + 6);
    }));
    assert!(!window.draw_if_needed(|_| { unreachable!() }));

    ui.set_c(true);
    assert!(window.draw_if_needed(|renderer| {
        do_test_render_region(renderer, 10 + 5, 19 + 8, 10 + 50 + 15, 19 + 80 + 13);
    }));
    assert!(!window.draw_if_needed(|_| { unreachable!() }));
    ui.set_another(false);
    assert!(window.draw_if_needed(|renderer| {
        do_test_render_region(renderer, 10 + 5 + 3, 19 + 80 + 3, 10 + 5 + 6, 19 + 80 + 6);
    }));
    assert!(!window.draw_if_needed(|_| { unreachable!() }));
}

#[test]
fn if_condition() {
    slint::slint! {
        export component Ui inherits Window {
            in property <bool> c : true;
            background: black;
            if c: Rectangle {
                x: 45px;
                y: 45px;
                background: pink;
                width: 32px;
                height: 3px;
            }
        }
    }

    slint::platform::set_platform(Box::new(TestPlatform)).ok();
    let ui = Ui::new().unwrap();
    let window = WINDOW.with(|x| x.clone());
    window.set_size(slint::PhysicalSize::new(180, 260));
    ui.show().unwrap();
    assert!(window.draw_if_needed(|renderer| {
        do_test_render_region(renderer, 0, 0, 180, 260);
    }));
    assert!(!window.draw_if_needed(|_| { unreachable!() }));
    ui.set_c(false);
    assert!(window.draw_if_needed(|renderer| {
        // Currently we redraw when a condition becomes false because we don't track the position otherwise
        do_test_render_region(renderer, 0, 0, 180, 260);
    }));
    assert!(!window.draw_if_needed(|_| { unreachable!() }));
    ui.set_c(true);
    assert!(window.draw_if_needed(|renderer| {
        do_test_render_region(renderer, 45, 45, 45 + 32, 45 + 3);
    }));
    assert!(!window.draw_if_needed(|_| { unreachable!() }));
}

#[test]
fn list_view() {
    slint::slint! {
        // We can't rely on the style as they are all different so implement our own in very basic terms
        component ListView inherits Flickable {
            out property <length> visible-width <=> self.width;
            out property <length> visible-height <=> self.height;
            @children
        }
        export component Ui inherits Window {
            width: 300px;
            height: 300px;
            in property <[int]> model;
            ListView {
                x: 20px; y: 10px; width: 100px; height: 90px;
                for x in model: Rectangle {
                    background: x == 1 ? red : blue;
                    height: 10px;
                    width: 25px;
                }

            }
        }
    }

    slint::platform::set_platform(Box::new(TestPlatform)).ok();
    let ui = Ui::new().unwrap();
    let window = WINDOW.with(|x| x.clone());
    window.set_size(slint::PhysicalSize::new(300, 300));
    ui.show().unwrap();
    assert!(window.draw_if_needed(|renderer| {
        do_test_render_region(renderer, 0, 0, 300, 300);
    }));
    assert!(!window.draw_if_needed(|_| { unreachable!() }));
    let model = std::rc::Rc::new(slint::VecModel::from(vec![0]));
    ui.set_model(model.clone().into());

    const LV_X: i32 = 20;
    const LV_Y: i32 = 10;

    assert!(window.draw_if_needed(|renderer| {
        do_test_render_region(renderer, LV_X, LV_Y, LV_X + 25, LV_Y + 10);
    }));
    assert!(!window.draw_if_needed(|_| { unreachable!() }));

    model.insert(0, 1);
    assert!(window.draw_if_needed(|renderer| {
        do_test_render_region(renderer, LV_X, LV_Y, LV_X + 25, LV_Y + 20);
    }));
    assert!(!window.draw_if_needed(|_| { unreachable!() }));
    model.set_row_data(1, 1);
    assert!(window.draw_if_needed(|renderer| {
        do_test_render_region(renderer, LV_X, LV_Y + 10, LV_X + 25, LV_Y + 20);
    }));
    assert!(!window.draw_if_needed(|_| { unreachable!() }));
    model.set_vec(vec![0, 0]);
    assert!(window.draw_if_needed(|renderer| {
        // Currently, when ItemTree are removed, we redraw the whole window.
        do_test_render_region(renderer, 0, 0, 300, 300);
    }));
    assert!(!window.draw_if_needed(|_| { unreachable!() }));
    model.remove(1);
    assert!(window.draw_if_needed(|renderer| {
        // Currently, when ItemTree are removed, we redraw the whole window.
        do_test_render_region(renderer, 0, 0, 300, 300);
    }));
    assert!(!window.draw_if_needed(|_| { unreachable!() }));
}

#[test]
/// test for #6932
fn scale_factor() {
    slint::slint! {
        export component Ui inherits Window {
        }
    }

    slint::platform::set_platform(Box::new(TestPlatform)).ok();
    let ui = Ui::new().unwrap();
    let window = WINDOW.with(|x| x.clone());
    window.set_size(slint::PhysicalSize::new(500, 500));
    window.dispatch_event(slint::platform::WindowEvent::ScaleFactorChanged { scale_factor: 1.33 });
    ui.show().unwrap();
    assert!(window.draw_if_needed(|renderer| {
        do_test_render_region(renderer, 0, 0, 500, 500);
    }));
}

#[test]
fn rotated_image() {
    slint::slint! {
        export component Ui inherits Window {
            in property <angle> rotation <=> i.transform-rotation;
            in property <length> x-pos <=> i.x;
            background: black;
            i := Image {
                x: 50px;
                y: 50px;
                width: 50px;
                height: 150px;
                source: @image-url("../../../logo/slint-logo-full-dark.png");
            }
        }
    }

    slint::platform::set_platform(Box::new(TestPlatform)).ok();

    let window = SKIA_WINDOW.with(|w| w.clone());
    NEXT_WINDOW_CHOICE.with(|choice| {
        *choice.borrow_mut() = Some(window.clone());
    });
    let ui = Ui::new().unwrap();
    window.set_size(slint::PhysicalSize::new(250, 250).into());
    ui.show().unwrap();

    assert!(window.draw_if_needed());
    assert_eq!(
        window.last_dirty_region_bounding_box_size(),
        Some(slint::LogicalSize { width: 250., height: 250. })
    );
    assert_eq!(
        window.last_dirty_region_bounding_box_origin(),
        Some(slint::LogicalPosition { x: 0., y: 0. })
    );

    assert!(!window.draw_if_needed());

    ui.set_x_pos(51.);

    assert!(window.draw_if_needed());
    assert_eq!(
        window.last_dirty_region_bounding_box_size(),
        Some(slint::LogicalSize { width: 51., height: 150. })
    );
    assert_eq!(
        window.last_dirty_region_bounding_box_origin(),
        Some(slint::LogicalPosition { x: 50., y: 50. })
    );

    ui.set_rotation(90.);

    assert!(window.draw_if_needed());
    assert_eq!(
        window.last_dirty_region_bounding_box_size(),
        Some(slint::LogicalSize { width: 150., height: 150. })
    );
    assert_eq!(
        window.last_dirty_region_bounding_box_origin(),
        Some(slint::LogicalPosition { x: 1., y: 50. })
    );
}

#[test]
fn window_background() {
    slint::slint! {
        export component Ui inherits Window {
            in property <color> c: yellow;
            background: c;
        }
    }

    slint::platform::set_platform(Box::new(TestPlatform)).ok();
    let ui = Ui::new().unwrap();
    let window = WINDOW.with(|x| x.clone());
    window.set_size(slint::PhysicalSize::new(180, 260));
    ui.show().unwrap();
    assert!(window.draw_if_needed(|renderer| {
        do_test_render_region(renderer, 0, 0, 180, 260);
    }));
    assert!(!window.draw_if_needed(|_| { unreachable!() }));
    ui.set_c(slint::Color::from_rgb_u8(45, 12, 13));
    assert!(window.draw_if_needed(|renderer| {
        do_test_render_region(renderer, 0, 0, 180, 260);
    }));
    ui.set_c(slint::Color::from_rgb_u8(45, 12, 13));
    assert!(!window.draw_if_needed(|_| { unreachable!() }));
}

#[test]
fn touch_area_doesnt_cause_redraw() {
    slint::slint! {
        export component Ui inherits Window {
            in property <color> c: yellow;
            in property <length> touch-area-1-x <=> ta1.x;
            in property <length> touch-area-2-x <=> ta2.x;
            in property <color> sole-pixel-color: red;
            background: black;
            ta1 := TouchArea {
                x: 10px;
                y: 0px;
                width: 20px;
                height: 40px;
                Rectangle {
                    x: 1phx;
                    y: 20phx;
                    width: 15phx;
                    height: 17phx;
                    background: c;
                }
            }
            ta2 := TouchArea {
                x: 10px;
                y: 0px;
                width: 20px;
                height: 40px;
            }
            sole-pixel := Rectangle {
                x: 60px;
                y: 0px;
                width: 1px;
                height: 1px;
                background: sole-pixel-color;
            }
        }
    }

    slint::platform::set_platform(Box::new(TestPlatform)).ok();
    let ui = Ui::new().unwrap();
    let window = WINDOW.with(|x| x.clone());
    window.set_size(slint::PhysicalSize::new(180, 260));
    ui.show().unwrap();
    assert!(window.draw_if_needed(|renderer| {
        do_test_render_region(renderer, 0, 0, 180, 260);
    }));
    assert!(!window.draw_if_needed(|_| { unreachable!() }));
    ui.set_c(slint::Color::from_rgb_u8(45, 12, 13));
    assert!(window.draw_if_needed(|renderer| {
        do_test_render_region(renderer, 10 + 1, 20, 10 + 1 + 15, 20 + 17);
    }));
    assert!(!window.draw_if_needed(|_| { unreachable!() }));
    ui.set_touch_area_1_x(20.);
    assert!(window.draw_if_needed(|renderer| {
        do_test_render_region(renderer, 10 + 1, 20, 10 + 1 + 15 + 10, 20 + 17);
    }));
    assert!(!window.draw_if_needed(|_| { unreachable!() }));
    ui.set_touch_area_2_x(20.);
    ui.set_sole_pixel_color(slint::Color::from_rgb_u8(45, 12, 13));
    // Moving the touch area should not cause it to be redrawn.
    assert!(window.draw_if_needed(|renderer| {
        do_test_render_region(renderer, 60, 0, 61, 1);
    }));
}

#[test]
/// Regression test for #12173: when an item is rendering-dirty (here a color
/// change) but keeps its geometry while its parent moves, the old position must
/// still be cleared. The child is wider than its parent, so its protruding part
/// is not covered by the parent's repaint and would otherwise leave a ghost.
fn moving_rendering_dirty_item_clears_old_position() {
    slint::slint! {
        export component Ui inherits Window {
            in property <length> pos;
            in property <color> c: red;
            background: black;
            Rectangle {
                x: root.pos;
                y: 50px;
                width: 20px;
                height: 20px;
                background: skyblue;
                Rectangle {
                    // Same geometry every frame, but wider than the parent so
                    // it protrudes onto the window background.
                    x: 0px;
                    y: 0px;
                    width: 100px;
                    height: 10px;
                    background: root.c;
                }
            }
        }
    }

    slint::platform::set_platform(Box::new(TestPlatform)).ok();
    let ui = Ui::new().unwrap();
    let window = WINDOW.with(|x| x.clone());
    window.set_size(slint::PhysicalSize::new(400, 200));
    ui.set_pos(200.);
    ui.show().unwrap();
    assert!(window.draw_if_needed(|renderer| {
        do_test_render_region(renderer, 0, 0, 400, 200);
    }));
    assert!(!window.draw_if_needed(|_| { unreachable!() }));

    // Move the rectangle left and recolor the child. The color change makes the
    // child rendering-dirty; the move changes its transform. The dirty region
    // must reach the child's old right edge (200 + 100 = 300), not stop at the
    // parent's old geometry (220).
    ui.set_pos(20.);
    ui.set_c(slint::Color::from_rgb_u8(0, 0, 255));
    assert!(window.draw_if_needed(|renderer| {
        do_test_render_region(renderer, 20, 50, 300, 70);
    }));
}

#[test]
fn shadow_redraw_beyond_geometry() {
    slint::slint! {
        export component Ui inherits Window {
            in property <length> x-pos: 10px;
            Rectangle {
                x: root.x-pos;
                y: 10px;
                width: 20px;
                height: 20px;
                drop-shadow-blur: 5px;
                drop-shadow-offset-x: 15px;
                drop-shadow-offset-y: 5px;
                drop-shadow-color: red;
            }
        }
    }

    slint::platform::set_platform(Box::new(TestPlatform)).ok();

    let window = SKIA_WINDOW.with(|w| w.clone());
    NEXT_WINDOW_CHOICE.with(|choice| {
        *choice.borrow_mut() = Some(window.clone());
    });
    let ui = Ui::new().unwrap();
    window.set_size(slint::PhysicalSize::new(250, 250).into());
    ui.show().unwrap();

    assert!(window.draw_if_needed());
    assert_eq!(
        window.last_dirty_region_bounding_box_size(),
        Some(slint::LogicalSize { width: 250., height: 250. })
    );
    assert_eq!(
        window.last_dirty_region_bounding_box_origin(),
        Some(slint::LogicalPosition { x: 0., y: 0. })
    );

    assert!(!window.draw_if_needed());

    ui.set_x_pos(20.);

    assert!(window.draw_if_needed());

    let shadow_width = /* rect width */ 20. + 2. * /* blur */ 5.;
    let move_delta = 10.;
    let shadow_height = /* rect height */ 20. + 2. * /*blur */ 5.;

    let old_shadow_x = /* rect x */ 10. + /* shadow offset */ 15. - /* blur */ 5.;
    let old_shadow_y = /* rect y */ 10. + /* shadow offset */ 5. - /* blur */ 5.;

    assert_eq!(
        window.last_dirty_region_bounding_box_size(),
        Some(slint::LogicalSize { width: shadow_width + move_delta, height: shadow_height })
    );
    assert_eq!(
        window.last_dirty_region_bounding_box_origin(),
        Some(slint::LogicalPosition { x: old_shadow_x, y: old_shadow_y })
    );
}

#[test]
fn text_alignment() {
    slint::slint! {
        export component Ui inherits Window {
            in property <color> c: green;
            Text {
                x: 10px;
                y: 10px;
                width: 200px;
                height: 50px;
                text: "Ok";
                color: c;
            }
        }
    }

    slint::platform::set_platform(Box::new(TestPlatform)).ok();

    let window = SKIA_WINDOW.with(|w| w.clone());
    NEXT_WINDOW_CHOICE.with(|choice| {
        *choice.borrow_mut() = Some(window.clone());
    });
    let ui = Ui::new().unwrap();
    window.set_size(slint::PhysicalSize::new(250, 250).into());
    ui.show().unwrap();

    assert!(window.draw_if_needed());
    assert_eq!(
        window.last_dirty_region_bounding_box_size(),
        Some(slint::LogicalSize { width: 250., height: 250. })
    );
    assert_eq!(
        window.last_dirty_region_bounding_box_origin(),
        Some(slint::LogicalPosition { x: 0., y: 0. })
    );

    assert!(!window.draw_if_needed());

    ui.set_c(slint::Color::from_rgb_u8(45, 12, 13));

    assert!(window.draw_if_needed());
    assert_eq!(
        window.last_dirty_region_bounding_box_size(),
        Some(slint::LogicalSize { width: 200., height: 50. })
    );
    assert_eq!(
        window.last_dirty_region_bounding_box_origin(),
        Some(slint::LogicalPosition { x: 10., y: 10. })
    );
}

#[test]
fn nowrap_text_change_doesnt_change_height() {
    slint::slint! {
        export component Ui inherits Window {
            in property <string> first-text: "First text";
            out property <length> first-label-width: first-label.width;
            out property <length> first-label-height: first-label.height;
            background: black;
            VerticalLayout {
                first-label := Text {
                    text: root.first-text;
                }
                Text {
                    text: "Second text";
                }
            }
        }
    }

    slint::platform::set_platform(Box::new(TestPlatform)).ok();
    let ui = Ui::new().unwrap();
    let window = WINDOW.with(|x| x.clone());
    window.set_size(slint::PhysicalSize::new(180, 260));
    ui.show().unwrap();
    assert!(window.draw_if_needed(|renderer| {
        do_test_render_region(renderer, 0, 0, 180, 260);
    }));
    assert!(!window.draw_if_needed(|_| { unreachable!() }));
    ui.set_first_text("Hello World longer".into());

    let expected_width = ui.get_first_label_width().ceil() as _;
    let expected_height = ui.get_first_label_height().ceil() as _;

    assert!(window.draw_if_needed(|renderer| {
        do_test_render_region(renderer, 0, 0, expected_width, expected_height);
    }));
    assert!(!window.draw_if_needed(|_| { unreachable!() }));
}

#[test]
fn create_item_tree_during_rendering() {
    // This test has `init` callbacks that cascade: cond1's init sets cond2=true,
    // cond2-red's init sets cond3=true. The ensure_tree_instantiated loop
    // materializes all three levels before rendering, so every rectangle
    // lands in the first draw's dirty region.
    slint::slint! {
        export component Ui inherits Window {
            in property <bool> cond1: false;
            property <bool> cond2: false;
            property <bool> cond3: false;

            in property <length> foo: 5px;

            if cond3: Rectangle {
                x: 12px;
                y: foo;
                width: 10px;
                height: 10px;
                background: yellow;
            }
            if cond2: Rectangle {
                x: 10px;
                y: 10px;
                width: 10px;
                height: 10px;
                background: red;
                init => { cond3=true; }
            }
            if cond1: Rectangle {
                x: 10px;
                y: 15px;
                width: 10px;
                height: 10px;
                background: blue;
                init => { cond2=true; }
            }
            if cond2: Rectangle {
                x: 12px;
                y: 10px + foo;
                width: 10px;
                height: 10px;
                background: green;
            }
        }
    }

    slint::platform::set_platform(Box::new(TestPlatform)).ok();
    let ui = Ui::new().unwrap();
    let window = WINDOW.with(|x| x.clone());
    window.set_size(slint::PhysicalSize::new(180, 260));
    ui.show().unwrap();
    assert!(window.draw_if_needed(|renderer| {
        do_test_render_region(renderer, 0, 0, 180, 260);
    }));
    assert!(!window.draw_if_needed(|_| { unreachable!() }));
    ui.set_cond1(true);

    assert!(window.draw_if_needed(|renderer| {
        // All three cascaded conditionals are instantiated before rendering:
        // cond3's rect is at y=5 (foo), so the region starts at y=5.
        do_test_render_region(renderer, 10, 5, 22, 25);
    }));

    assert!(!window.draw_if_needed(|_| { unreachable!() }));

    ui.set_foo(4.0);

    assert!(window.draw_if_needed(|renderer| {
        do_test_render_region(renderer, 12, 4, 22, 25);
    }));

    assert!(!window.draw_if_needed(|_| { unreachable!() }));

    ui.set_foo(3.0);
    assert!(window.draw_if_needed(|renderer| {
        do_test_render_region(renderer, 12, 3, 22, 24);
    }));
}

#[test]
fn init_property_read_does_not_trigger_redraw() {
    slint::slint! {
        export component Ui inherits Window {
            width: 100px;
            height: 100px;

            in property <bool> cond: false;
            in property <length> unrelated: 10px;
            property <length> stash;

            if cond: Rectangle {
                x: 5px;
                y: 5px;
                width: 20px;
                height: 20px;
                background: red;
                // The init callback reads `unrelated`.  That read must NOT
                // register as a dependency of the redraw tracker.
                init => { stash = unrelated; }
            }
        }
    }

    slint::platform::set_platform(Box::new(TestPlatform)).ok();
    let ui = Ui::new().unwrap();
    let window = WINDOW.with(|x| x.clone());
    window.set_size(slint::PhysicalSize::new(100, 100));
    ui.show().unwrap();
    assert!(window.draw_if_needed(|renderer| {
        do_test_render_region(renderer, 0, 0, 100, 100);
    }));
    assert!(!window.draw_if_needed(|_| { unreachable!() }));

    // Activate the conditional — the rectangle appears and init runs.
    ui.set_cond(true);
    assert!(window.draw_if_needed(|renderer| {
        do_test_render_region(renderer, 5, 5, 25, 25);
    }));
    assert!(!window.draw_if_needed(|_| { unreachable!() }));

    // Change the property that the init callback read.
    // This must NOT trigger a redraw because init reads should be untracked.
    ui.set_unrelated(42.0);
    assert!(!window.draw_if_needed(|_| {
        unreachable!("changing a property only read by init must not trigger a redraw")
    }));
}

#[test]
fn issue_9882_borrow_mut() {
    slint::slint! {
        export component App inherits Window {
            in-out property <[string]> strings: ["Home"];
            VerticalLayout {
                VerticalLayout {
                    for str in root.strings: Text {
                        text: str;
                    }
                }
            }
        }
    }

    slint::platform::set_platform(Box::new(TestPlatform)).ok();
    let ui = App::new().unwrap();
    let window = WINDOW.with(|x| x.clone());
    window.set_size(slint::PhysicalSize::new(180, 260));
    ui.show().unwrap();
    assert!(window.draw_if_needed(|renderer| {
        do_test_render_region(renderer, 0, 0, 180, 260);
    }));
    assert!(!window.draw_if_needed(|_| { unreachable!() }));
    ui.set_strings(slint::ModelRc::new(slint::VecModel::from_iter([slint::SharedString::from(
        "Hello",
    )])));

    assert!(window.draw_if_needed(|renderer| {
        do_test_render_region(renderer, 0, 0, 180, 260);
    }));

    ui.set_strings(slint::ModelRc::new(slint::VecModel::from_iter([slint::SharedString::from(
        "World",
    )])));

    assert!(window.draw_if_needed(|renderer| {
        do_test_render_region(renderer, 0, 0, 180, 260);
    }));
}

#[test]
/// Check that changing the position of a rectangle is properly tracked when RepaintBufferType::NewBuffer is used.
fn position_tracking_without_partial_rendering() {
    slint::slint! {
        export component App inherits Window {
            width: 180px;
            height: 260px;

            in property <bool> cond: true;
            in property <length> rect-x: 10px;

            Rectangle {
                background: red;
                width: 40px;
                height: 80px;
                x: rect-x;
            }

            Rectangle {
                width: 100px;
                height: 100px;
                x: 1000px;
                background: cond ? green : blue;
            }
        }
    }

    struct TestPlatform2(Rc<MinimalSoftwareWindow>);
    impl slint::platform::Platform for TestPlatform2 {
        fn create_window_adapter(&self) -> Result<Rc<dyn WindowAdapter>, PlatformError> {
            Ok(self.0.clone())
        }
    }
    let window = MinimalSoftwareWindow::new(
        slint::platform::software_renderer::RepaintBufferType::NewBuffer,
    );
    slint::platform::set_platform(Box::new(TestPlatform2(window.clone()))).ok();
    let ui = App::new().unwrap();
    window.set_size(slint::PhysicalSize::new(180, 260));
    ui.show().unwrap();
    assert!(window.draw_if_needed(|renderer| {
        do_test_render_region(renderer, 0, 0, 180, 260);
    }));
    assert!(!window.draw_if_needed(|_| { unreachable!() }));
    ui.set_cond(false);
    assert!(!window.draw_if_needed(|_| {
        unreachable!("the rectangle is outside the window so it should not be drawn")
    }));

    ui.set_rect_x(20.);
    assert!(window.draw_if_needed(|renderer| {
        do_test_render_region(renderer, 0, 0, 180, 260);
    }));
    assert!(!window.draw_if_needed(|_| { unreachable!() }));

    ui.set_rect_x(-500.);
    assert!(window.draw_if_needed(|renderer| {
        do_test_render_region(renderer, 0, 0, 180, 260);
    }));
    assert!(!window.draw_if_needed(|_| { unreachable!() }));

    ui.set_rect_x(20.);
    assert!(window.draw_if_needed(|renderer| {
        do_test_render_region(renderer, 0, 0, 180, 260);
    }));
    assert!(!window.draw_if_needed(|_| { unreachable!() }));
}

/// Items under a `transform-scale` parent at large local coordinates must not
/// be culled when their screen-space position (after scaling) is within the
/// viewport.
#[test]
fn partial_rendering_does_not_cull_scaled_items() {
    slint::slint! {
        export component Ui inherits Window {
            width: 300px;
            height: 300px;
            background: white;
            in property <float> scale-val: 0.5;

            Rectangle {
                x: 0px; y: 0px;
                width: 1000px; height: 1000px;
                transform-origin: { x: 0px, y: 0px };
                transform-scale-x: scale-val;
                transform-scale-y: scale-val;

                Rectangle {
                    x: 400px; y: 400px;
                    width: 100px; height: 100px;
                    background: red;
                }
            }
        }
    }

    slint::platform::set_platform(Box::new(TestPlatform)).ok();
    let window = SKIA_WINDOW.with(|w| w.clone());
    NEXT_WINDOW_CHOICE.with(|choice| {
        *choice.borrow_mut() = Some(window.clone());
    });
    let ui = Ui::new().unwrap();
    window.set_size(slint::PhysicalSize::new(300, 300).into());
    ui.show().unwrap();

    // Frame 1: full repaint.
    assert!(window.draw_if_needed());

    // Check that the scaled rectangle is visible via pixel data.
    // At scale 0.5 from origin (0,0), rect at local (400,400) size 100
    // → screen (200,200) size 50.
    {
        let pixels = window.render_buffer.pixels.borrow();
        let buf = pixels.as_ref().expect("render buffer should contain pixels");
        {
            let data = buf.as_bytes();
            // RGBA8888, pixel at (220, 220)
            let offset = ((220 * 300 + 220) * 4) as usize;
            let (r, g, b) = (data[offset], data[offset + 1], data[offset + 2]);
            assert!(
                r > 200 && g < 50 && b < 50,
                "Frame 1: pixel at (220,220) should be red, got rgb=({r},{g},{b})"
            );
        }
    }

    assert!(!window.draw_if_needed());

    // Change scale to trigger a partial repaint.
    ui.set_scale_val(0.5001);

    // Frame 2: partial repaint. The item must not be culled.
    assert!(window.draw_if_needed());

    {
        let pixels = window.render_buffer.pixels.borrow();
        let buf = pixels.as_ref().expect("render buffer should contain pixels");
        {
            let data = buf.as_bytes();
            let offset = ((220 * 300 + 220) * 4) as usize;
            let (r, g, b) = (data[offset], data[offset + 1], data[offset + 2]);
            assert!(
                r > 200 && g < 50 && b < 50,
                "Frame 2: pixel at (220,220) should still be red after partial repaint, \
                 got rgb=({r},{g},{b})"
            );
        }
    }
}

/// Items under a `transform-rotation` parent must not be culled when their
/// screen-space position (after rotation) is within the viewport.
#[test]
fn partial_rendering_does_not_cull_rotated_items() {
    slint::slint! {
        export component Ui inherits Window {
            width: 300px;
            height: 300px;
            background: white;
            in property <angle> rot: 90deg;

            Rectangle {
                x: 0px; y: 0px;
                width: 600px; height: 600px;
                transform-origin: { x: 150px, y: 150px };
                transform-rotation: rot;

                Rectangle {
                    x: 250px; y: 150px;
                    width: 50px; height: 50px;
                    background: red;
                }
            }
        }
    }

    slint::platform::set_platform(Box::new(TestPlatform)).ok();
    let window = SKIA_WINDOW.with(|w| w.clone());
    NEXT_WINDOW_CHOICE.with(|choice| {
        *choice.borrow_mut() = Some(window.clone());
    });
    let ui = Ui::new().unwrap();
    window.set_size(slint::PhysicalSize::new(300, 300).into());
    ui.show().unwrap();

    assert!(window.draw_if_needed());

    // 90° clockwise rotation around (150,150):
    // local (250,150) → screen (100,250). Check center at (125, 275).
    {
        let pixels = window.render_buffer.pixels.borrow();
        let buf = pixels.as_ref().expect("render buffer should contain pixels");
        {
            let data = buf.as_bytes();
            let offset = ((275 * 300 + 125) * 4) as usize;
            let (r, g, b) = (data[offset], data[offset + 1], data[offset + 2]);
            assert!(
                r > 200 && g < 50 && b < 50,
                "Rotated rectangle should be visible at (125,275), got rgb=({r},{g},{b})"
            );
        }
    }
}

/// Nested scale transforms must compose correctly across save/restore.
#[test]
fn partial_rendering_nested_scales() {
    slint::slint! {
        export component Ui inherits Window {
            width: 300px;
            height: 300px;
            background: white;

            Rectangle {
                x: 0px; y: 0px;
                width: 2000px; height: 2000px;
                transform-origin: { x: 0px, y: 0px };
                transform-scale-x: 0.5;
                transform-scale-y: 0.5;

                Rectangle {
                    x: 0px; y: 0px;
                    width: 2000px; height: 2000px;
                    transform-origin: { x: 0px, y: 0px };
                    transform-scale-x: 0.5;
                    transform-scale-y: 0.5;

                    Rectangle {
                        x: 800px; y: 800px;
                        width: 200px; height: 200px;
                        background: red;
                    }
                }
            }
        }
    }

    slint::platform::set_platform(Box::new(TestPlatform)).ok();
    let window = SKIA_WINDOW.with(|w| w.clone());
    NEXT_WINDOW_CHOICE.with(|choice| {
        *choice.borrow_mut() = Some(window.clone());
    });
    let ui = Ui::new().unwrap();
    window.set_size(slint::PhysicalSize::new(300, 300).into());
    ui.show().unwrap();

    assert!(window.draw_if_needed());

    // Combined scale 0.25: local (800,800) size 200 → screen (200,200) size 50.
    {
        let pixels = window.render_buffer.pixels.borrow();
        let buf = pixels.as_ref().expect("render buffer should contain pixels");
        {
            let data = buf.as_bytes();
            // Inside at (220,220)
            let offset = ((220 * 300 + 220) * 4) as usize;
            let (r, g, b) = (data[offset], data[offset + 1], data[offset + 2]);
            assert!(
                r > 200 && g < 50 && b < 50,
                "Nested scale: pixel at (220,220) should be red, got rgb=({r},{g},{b})"
            );
            // Outside at (260,260)
            let offset2 = ((260 * 300 + 260) * 4) as usize;
            let (r2, g2, b2) = (data[offset2], data[offset2 + 1], data[offset2 + 2]);
            assert!(
                r2 > 200 && g2 > 200 && b2 > 200,
                "Nested scale: pixel at (260,260) should be white, got rgb=({r2},{g2},{b2})"
            );
        }
    }
}

/// Regression test for https://github.com/slint-ui/slint/issues/11431.
///
/// A cached Layer (`cache-rendering-hint: true`) that starts zero-sized must
/// re-render when its size later becomes non-zero. Without tracking the
/// bounds-closure's dependencies on the zero-size path, the cache stores
/// `None` with an empty dependency tracker and never reruns, so the layer
/// stays invisible even after its size grows.
#[test]
fn layer_visible_after_becoming_non_zero_sized() {
    slint::slint! {
        export component Ui inherits Window {
            width: 32px;
            height: 32px;
            background: white;
            in-out property <length> content-height: 0px;
            Rectangle {
                cache-rendering-hint: true;
                x: 4px;
                y: 4px;
                width: 24px;
                height: root.content-height;
                background: red;
            }
        }
    }

    slint::platform::set_platform(Box::new(TestPlatform)).ok();
    let window = SKIA_WINDOW.with(|w| w.clone());
    NEXT_WINDOW_CHOICE.with(|choice| {
        *choice.borrow_mut() = Some(window.clone());
    });
    let ui = Ui::new().unwrap();
    window.set_size(slint::PhysicalSize::new(32, 32).into());
    ui.show().unwrap();

    // Frame 1: the layer is 0-height, so the cache update closure returns
    // None. The fix re-invokes the bounds closure with tracking so the
    // dependency on `content-height` gets registered.
    assert!(window.draw_if_needed());

    ui.set_content_height(24.);

    // Frame 2: the tracked dependency is now dirty, the layer cache reruns,
    // and the red rectangle is drawn. Without the fix the cache stays on
    // the stale None and the pixel below remains white.
    assert!(window.draw_if_needed());

    let pixels = window.render_buffer.pixels.borrow();
    let buf = pixels.as_ref().expect("render buffer should contain pixels");
    let data = buf.as_bytes();
    let offset = ((16 * 32 + 16) * 4) as usize;
    let (r, g, b) = (data[offset], data[offset + 1], data[offset + 2]);
    assert!(
        r > 200 && g < 50 && b < 50,
        "Layer pixel at (16,16) should be red after content-height grew, got rgb=({r},{g},{b})"
    );
}

#[test]
fn layer_rendered_at_correct_position() {
    // Regression test for #11763: render_layer's compute_bounds used
    // item_rc.geometry() (parent coordinates) instead of a local-coords
    // rect. Inside a Flickable the parent offset is the content position
    // (large x), while the clip is in item-local coords. The union mixes
    // coordinate systems, producing a wrong layer origin when the item is
    // partially scrolled out of the visible area. This makes the layer
    // texture miss the left portion of the item.
    slint::slint! {
        export component Ui inherits Window {
            width: 64px;
            height: 64px;
            background: white;
            in-out property <length> vpx: 0px;
            Flickable {
                width: 64px;
                height: 64px;
                content-width: 200px;
                content-x <=> root.vpx;
                interactive: false;

                Rectangle {
                    cache-rendering-hint: true;
                    x: 100px;
                    y: 0px;
                    width: 40px;
                    height: 40px;
                    Rectangle {
                        background: red;
                        width: 100%;
                        height: 100%;
                    }
                }
            }
        }
    }

    slint::platform::set_platform(Box::new(TestPlatform)).ok();
    let window = SKIA_WINDOW.with(|w| w.clone());
    NEXT_WINDOW_CHOICE.with(|choice| {
        *choice.borrow_mut() = Some(window.clone());
    });
    let ui = Ui::new().unwrap();
    window.set_size(slint::PhysicalSize::new(64, 64).into());
    ui.show().unwrap();

    // Frame 1: item partially scrolled off the left edge of the Flickable.
    // content-x = -120 → visible range 120..184. Item at 100..140.
    // Item left edge (100) is 20px left of visible start (120).
    // Only the right 20px are visible in the Flickable.
    // The layer cache is created with the clip starting at x=20 in local
    // coords. With the bug, geometry (100,..) makes the union start at 20
    // instead of 0, so only the right 20px are captured.
    ui.set_vpx(-120.);
    assert!(window.draw_if_needed());

    // Frame 2: scroll so the item is fully visible. Do NOT change any
    // child property, so the layer cache stays valid and reuses the stale
    // texture from frame 1. With the bug, the stale texture has origin
    // (20, 0) instead of (0, 0), so 20px on the left are missing.
    window.render_buffer.pixels.borrow_mut().take(); // force full redraw
    ui.set_vpx(-60.);
    assert!(window.draw_if_needed());

    let pixels = window.render_buffer.pixels.borrow();
    let buf = pixels.as_ref().expect("render buffer should contain pixels");
    let data = buf.as_bytes();
    let stride = 64;

    // Item at vpx=-60: window x = 100 + (-60) = 40. Spans 40..80, clipped
    // at 64, so visible 40..63.
    // With the bug: stale cache has origin=(20,0), size=(20,40). Drawn at
    // (40+20, 0) = (60, 0), only 4px visible (60..63). Gap at 40..59.
    // With the fix: cache has origin=(0,0), size=(40,40). Drawn at (40, 0),
    // visible 40..63. Correct.
    // The item should start at window x=40.
    let off = ((5 * stride + 40) * 4) as usize;
    let (r, g, b) = (data[off], data[off + 1], data[off + 2]);
    assert!(
        r > 200 && g < 50 && b < 50,
        "Pixel at (40,5) should be red (left edge of cached layer), got rgb=({r},{g},{b})"
    );
}

#[test]
fn partial_rendering_popup_position_size_change() {
    slint::slint! {
        global MyProperty {
            in-out property<length> popup-x: 0px;
            in-out property<length> popup-y: 0px;
            in-out property<length> popup-width: 100px;
            in-out property<length> popup-height: 200px;
        }

        export component Ui inherits Window {
            width: 600px;
            height: 600px;
            background: red;

            TouchArea {
                property<bool> was-clicked: false;
                clicked => {
                    if !was-clicked {
                        was-clicked = true;
                        show_popup();
                    }
                }
            }

            callback show_popup();
            show_popup() => {
                popup.show();
            }

            callback change_popup();
            change_popup() => {
                debug("Changing popup properties");
                MyProperty.popup-x = 10px;
                MyProperty.popup-y = 20px;
                MyProperty.popup-width = 150px;
                MyProperty.popup-height = 30px;
            }

            popup:= PopupWindow {
                x: MyProperty.popup-x;
                y: MyProperty.popup-y;
                width: MyProperty.popup-width;
                height: MyProperty.popup-height;
                close-policy: PopupClosePolicy.no-auto-close;

                TouchArea {
                    clicked => {
                        change_popup();
                    }
                }

                changed width => {
                    debug("Width changed");
                }

                Rectangle {
                    background: blue;
                }
            }
        }
    }

    slint::platform::set_platform(Box::new(TestPlatform)).ok();
    let window = SKIA_WINDOW.with(|w| w.clone());
    NEXT_WINDOW_CHOICE.with(|choice| {
        *choice.borrow_mut() = Some(window.clone());
    });
    const WINDOW_WIDTH: usize = 600;
    const WINDOW_HEIGHT: usize = 600;
    const POPUP_WIDTH: usize = 100;
    const POPUP_HEIGHT: usize = 200;
    const RGB_COLOR_WINDOW: (u8, u8, u8) = (255, 0, 0);
    const RGB_COLOR_POPUP: (u8, u8, u8) = (0, 0, 255);

    let ui = Ui::new().unwrap();
    // Required otherwise the buffer gets not initialized
    window.set_size(slint::PhysicalSize::new(WINDOW_WIDTH as u32, WINDOW_HEIGHT as u32).into());
    ui.show().unwrap();

    assert!(window.draw_if_needed());

    let get_pixel_values = |pixel_index: usize| {
        let pixels = window.render_buffer.pixels.borrow();
        let buf = pixels.as_ref().expect("render buffer should contain pixels");
        let data = buf.as_bytes();
        let offset = pixel_index * BYTES_PER_PIXEL;
        (data[offset], data[offset + 1], data[offset + 2])
    };

    // For debugging. Dump pixels
    let _dump_pixels = || {
        for v_pixel in 0..WINDOW_HEIGHT {
            for h_pixel in 0..WINDOW_WIDTH {
                let pixel_idx = WINDOW_WIDTH * v_pixel + h_pixel;
                let rgb = get_pixel_values(pixel_idx);
                // print!("{r:02x}{g:02x}{b:02x} ", r = rgb.0, g = rgb.1, b = rgb.2);
                if rgb.0 > 0 {
                    print!("r");
                } else if rgb.2 > 0 {
                    print!("b");
                } else {
                    print!("g")
                }
            }
            print!("\n");
        }
    };

    {
        let pixels = window.render_buffer.pixels.borrow();
        let buf = pixels.as_ref().expect("render buffer should contain pixels");
        assert_eq!(buf.as_bytes().len(), WINDOW_WIDTH * WINDOW_HEIGHT * BYTES_PER_PIXEL);
        for pixel_idx in 0..(WINDOW_WIDTH * WINDOW_HEIGHT) {
            let rgb = get_pixel_values(pixel_idx);
            assert_eq!(rgb, RGB_COLOR_WINDOW, "Wrong color at pixel index pixel_idx");
        }
    }

    ui.invoke_show_popup();
    assert!(window.draw_if_needed());

    {
        for v_pixel in 0..WINDOW_HEIGHT {
            for h_pixel in 0..WINDOW_WIDTH {
                let pixel_idx = WINDOW_WIDTH * v_pixel + h_pixel;

                if h_pixel < POPUP_WIDTH && v_pixel < POPUP_HEIGHT {
                    let rgb = get_pixel_values(pixel_idx);
                    assert_eq!(rgb, RGB_COLOR_POPUP, "Wrong color at pixel ({h_pixel}, {v_pixel})");
                } else {
                    let rgb = get_pixel_values(pixel_idx);
                    assert_eq!(
                        rgb, RGB_COLOR_WINDOW,
                        "Wrong color at pixel ({h_pixel}, {v_pixel})"
                    );
                }
            }
        }
    }

    ui.invoke_change_popup();
    // The popup properties change trigger a tracker with a timer we have to process before the next draw.
    slint::platform::update_timers_and_animations();

    assert!(window.draw_if_needed());

    {
        // New popup properties
        const POPUP_POS_X: usize = 10;
        const POPUP_POS_Y: usize = 20;
        const POPUP_WIDTH: usize = 150;
        const POPUP_HEIGHT: usize = 30;
        for v_pixel in 0..WINDOW_HEIGHT {
            for h_pixel in 0..WINDOW_WIDTH {
                let pixel_idx = WINDOW_WIDTH * v_pixel + h_pixel;

                if h_pixel >= POPUP_POS_X
                    && h_pixel < POPUP_POS_X + POPUP_WIDTH
                    && v_pixel >= POPUP_POS_Y
                    && v_pixel < POPUP_POS_Y + POPUP_HEIGHT
                {
                    let rgb = get_pixel_values(pixel_idx);
                    assert_eq!(
                        rgb, RGB_COLOR_POPUP,
                        "Wrong color at pixel ({h_pixel}, {v_pixel}). Expected popup color."
                    );
                } else {
                    let rgb = get_pixel_values(pixel_idx);
                    assert_eq!(
                        rgb, RGB_COLOR_WINDOW,
                        "Wrong color at pixel ({h_pixel}, {v_pixel}). Expected window color"
                    );
                }
            }
        }
    }
}
