quadraui 0.6.0

Cross-platform Rust UI toolkit: write one AppLogic and run it unmodified on TUI, GTK4 (Linux), macOS, and Windows backends
Documentation
//! Direct2D rasteriser for [`crate::Scrollbar`].
//!
//! Painting is implemented in the shared
//! [`crate::primitives::scrollbar::native_surface_paint::paint`] — see that
//! function's documentation for implementation details.
//!
//! `super::multi_section_view`'s embedded scrollbar uses its own
//! CPU-premix convention — see that module's documentation.
//!
//! Only compiled on `target_os = "windows"` — see `super::mod`'s
//! `#[cfg(target_os = "windows")] mod scrollbar;` and `backend.rs`'s
//! module docs for why the rest of this repo's `--features win` compile
//! gate stays meaningful without a Windows host.

#[cfg(test)]
use crate::primitives::scrollbar::Scrollbar;
#[cfg(test)]
use crate::theme::Theme;

#[cfg(test)]
mod tests {
    use super::*;
    use crate::event::Rect;
    use crate::types::{Color, WidgetId};
    use crate::win::testing::HeadlessSurface;

    const W: u32 = 80;
    const H: u32 = 200;

    /// Paint `scrollbar` via the shared
    /// [`crate::primitives::scrollbar::native_surface_paint::paint`]
    /// through a [`super::super::surface::D2dSurface`] over `surface`'s
    /// headless target — the same adapter the now-removed
    /// `draw_scrollbar` shim used, exercised here
    /// directly.
    fn paint(scrollbar: &Scrollbar) -> HeadlessSurface {
        let surface = HeadlessSurface::new(W, H).expect("create surface");
        // Fill with a known background so blended track/thumb colours
        // are deterministic to probe.
        surface
            .fill_rect(
                Rect::new(0.0, 0.0, W as f32, H as f32),
                Theme::default().background,
            )
            .expect("fill bg");
        surface
            .paint(|target| {
                let mut raw = super::super::surface::D2dSurface {
                    target,
                    dwrite: None,
                };
                crate::primitives::scrollbar::native_surface_paint::paint(
                    scrollbar,
                    &mut raw,
                    &Theme::default(),
                );
            })
            .expect("paint scrollbar");
        surface
    }

    fn vertical_bar(scroll: f32, total: f32, visible: f32) -> Scrollbar {
        Scrollbar::vertical(
            WidgetId::new("sb"),
            Rect::new(0.0, 0.0, 8.0, H as f32),
            scroll,
            total,
            visible,
            20.0,
        )
    }

    /// Reference alpha blend, independent of production code, for
    /// computing the expected pixel colour in
    /// [`track_blends_with_real_background_not_theme_background`].
    fn expected_alpha_blend(base: Color, over: Color, alpha: f32) -> Color {
        let mix =
            |b: u8, o: u8| -> u8 { (b as f32 * (1.0 - alpha) + o as f32 * alpha).round() as u8 };
        Color::rgb(
            mix(base.r, over.r),
            mix(base.g, over.g),
            mix(base.b, over.b),
        )
    }

    /// The scrollbar track blends correctly with the actual background
    /// beneath it, not with the theme background. This ensures the track
    /// color is alpha-blended with whatever content is actually underneath
    /// (editor, terminal, panel header, etc.), not with a premixed theme
    /// background color.
    #[test]
    fn track_blends_with_real_background_not_theme_background() {
        let sb = vertical_bar(0.0, 200.0, 50.0);
        let theme = Theme::default();
        let real_bg = Color::rgb(200, 30, 30);
        assert_ne!(
            (real_bg.r, real_bg.g, real_bg.b),
            (theme.background.r, theme.background.g, theme.background.b),
            "test fixture must differ from theme.background to be a meaningful probe",
        );

        let surface = HeadlessSurface::new(W, H).expect("create surface");
        surface
            .fill_rect(Rect::new(0.0, 0.0, W as f32, H as f32), real_bg)
            .expect("fill bg");
        surface
            .paint(|target| {
                let mut raw = super::super::surface::D2dSurface {
                    target,
                    dwrite: None,
                };
                crate::primitives::scrollbar::native_surface_paint::paint(&sb, &mut raw, &theme);
            })
            .expect("paint scrollbar");

        // Not hovered/dragging: track_alpha = 0.20 (see the shared `paint`).
        let track_alpha = 0.20;
        let expected = expected_alpha_blend(real_bg, theme.scrollbar_track, track_alpha);
        let old_halo = expected_alpha_blend(theme.background, theme.scrollbar_track, track_alpha);

        // Probe mid-track, below the thumb (thumb_len ≈ 50px at scroll=0).
        let c = surface.pixel_at(4, 120);
        assert_eq!(
            (c.r, c.g, c.b),
            (expected.r, expected.g, expected.b),
            "track should alpha-blend with the real background beneath it",
        );
        assert_ne!(
            (c.r, c.g, c.b),
            (old_halo.r, old_halo.g, old_halo.b),
            "track must not premix against theme.background regardless of what's painted underneath",
        );
    }

    #[test]
    fn track_paints_over_background() {
        let sb = vertical_bar(0.0, 200.0, 50.0);
        let surface = paint(&sb);
        let theme = Theme::default();
        // Probe mid-track at a y the thumb does NOT cover (thumb sits
        // at the top with thumb_len ≈ 50px when scroll=0).
        let c = surface.pixel_at(4, 120);
        assert_ne!(
            (c.r, c.g, c.b),
            (theme.background.r, theme.background.g, theme.background.b),
            "track should paint something other than plain background",
        );
    }

    #[test]
    fn thumb_paints_differently_than_track_only_zone() {
        let sb = vertical_bar(0.0, 200.0, 50.0);
        let surface = paint(&sb);
        // Thumb at top: probe inside thumb_len.
        let thumb_px = surface.pixel_at(4, 5);
        // Track only: probe well below the thumb.
        let track_px = surface.pixel_at(4, 150);
        assert_ne!(
            (thumb_px.r, thumb_px.g, thumb_px.b),
            (track_px.r, track_px.g, track_px.b),
            "thumb zone should differ from a track-only zone",
        );
    }

    #[test]
    fn dragging_makes_thumb_more_opaque() {
        let mut sb = vertical_bar(0.0, 200.0, 50.0);
        let normal = paint(&sb).pixel_at(4, 5);
        sb.dragging = true;
        let dragging = paint(&sb).pixel_at(4, 5);
        assert_ne!(
            (normal.r, normal.g, normal.b),
            (dragging.r, dragging.g, dragging.b),
            "dragging should change the thumb's blended colour",
        );
    }

    #[test]
    fn full_scroll_lands_thumb_at_track_bottom() {
        // scroll = total - visible should align the thumb's bottom edge
        // to the track end.
        let sb = vertical_bar(150.0, 200.0, 50.0);
        let surface = paint(&sb);
        let bottom = surface.pixel_at(4, H - 4);
        let mid = surface.pixel_at(4, 80);
        assert_ne!(
            (bottom.r, bottom.g, bottom.b),
            (mid.r, mid.g, mid.b),
            "full-scroll: the bottom probe (now inside the thumb) should differ from mid-track",
        );
    }

    #[test]
    fn horizontal_orientation_uses_width() {
        let track = Rect::new(0.0, 50.0, W as f32, 8.0);
        let sb = Scrollbar::horizontal(WidgetId::new("h"), track, 0.0, 200.0, 40.0, 10.0);
        let surface = paint(&sb);
        // Thumb at left: x ∈ [0, thumb_len) should show thumb;
        // x past thumb should show track only.
        let left = surface.pixel_at(2, 54);
        let right = surface.pixel_at(W - 4, 54);
        assert_ne!(
            (left.r, left.g, left.b),
            (right.r, right.g, right.b),
            "horizontal: thumb zone (left) should differ from track-only zone (right)",
        );
    }

    #[test]
    fn zero_size_track_is_a_no_op() {
        let sb = Scrollbar::vertical(
            WidgetId::new("sb"),
            Rect::new(0.0, 0.0, 0.0, 0.0),
            0.0,
            200.0,
            50.0,
            20.0,
        );
        // Must not panic (a zero-size fill_rect is a no-op D2D call), and
        // must genuinely paint nothing: probe a few points across the
        // surface and confirm they're all still the plain background
        // `paint()` filled before invoking the scrollbar rasteriser.
        let surface = paint(&sb);
        let theme = Theme::default();
        for (x, y) in [(4, 4), (4, H / 2), (4, H - 4)] {
            let c = surface.pixel_at(x, y);
            assert_eq!(
                (c.r, c.g, c.b),
                (theme.background.r, theme.background.g, theme.background.b),
                "a zero-size track should paint nothing, leaving the background untouched at ({x}, {y})",
            );
        }
    }
}