quadraui 0.1.0

Cross-platform Rust UI toolkit: write one AppLogic and run it unmodified on TUI, GTK4 (Linux), macOS, and Windows backends
Documentation
//! GTK rasteriser for [`crate::ToastOverlay`].
//!
//! Painting moved to the shared
//! [`crate::primitives::toast::native_surface_paint::paint`] (#861,
//! `PaintSurface` Phase 2d slice 4/9) — see that fn's doc for the named
//! divergences (Win never took a live theme; Win's dismiss/action text
//! wasn't centred in its sub-region) found while unifying
//! `gtk::draw_toast_stack`, `macos::toast::draw_toast_stack` and
//! `win::toast::draw_toast_stack` into one implementation. This module
//! now only carries [`gtk_toast_stack_layout`] (pure layout, still needed
//! by `GtkDriver`/downstream callers for no-paint hit-test queries); the
//! deprecated `draw_toast_stack` compatibility shim over the shared
//! [`super::surface::CairoSurface`] adapter was removed in issue #1109
//! (zero uses in coord-tui's `main` and vimcode's `develop`).

use gtk4::pango;

use crate::primitives::layout_metrics::pixel_toast_stack_layout;
use crate::primitives::toast::{ToastOverlay, ToastStackLayout};
#[cfg(test)]
use crate::theme::Theme;

/// Compute the GTK pixel-unit layout for a [`ToastOverlay`] without painting.
///
/// `(origin_x, origin_y)` is baked into the returned bounds (absolute
/// window coordinates, matching `gtk_menu_bar_layout` / `gtk_panel_layout`)
/// — hosts call `layout.hit_test(x, y)` with raw click coordinates, no
/// localisation needed.
///
/// Still its own pango-based measurer, independent of the shared paint's
/// internal layout computation (which measures via
/// [`PaintSurface::surface_measure_text`]) — same "no-paint layout stays
/// put" posture as `GtkBackend::status_bar_layout` (#860). Shares its
/// geometry with `mac_toast_stack_layout` / `win_toast_stack_layout` via
/// [`pixel_toast_stack_layout`] (issue #1079); `pango_layout`'s
/// `set_attributes(None)` reset (previously done per-column inside the
/// measure closure) happens once up front instead, since
/// `super::toolbar::PangoMeasure` never touches attributes itself.
#[allow(clippy::too_many_arguments)]
pub fn gtk_toast_stack_layout(
    stack: &ToastOverlay,
    pango_layout: &pango::Layout,
    origin_x: f32,
    origin_y: f32,
    viewport_width: f32,
    viewport_height: f32,
    line_height: f64,
) -> ToastStackLayout {
    pango_layout.set_attributes(None);
    let measure = super::toolbar::PangoMeasure {
        pango_layout: Some(pango_layout),
        char_width: 0.0,
    };
    pixel_toast_stack_layout(
        stack,
        &measure,
        origin_x,
        origin_y,
        viewport_width,
        viewport_height,
        line_height as f32,
    )
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::primitives::toast::{Toast, ToastCorner, ToastHit, ToastOverlay, ToastSeverity};
    use crate::types::{Color, WidgetId};
    use pangocairo::cairo::{Context, Format, ImageSurface};

    // Fixed overlay size, independent of the test's origin — this file
    // had no test module at all before quadraui#494 (the reviewed gap:
    // `gtk_toast_stack_layout`/`draw_toast_stack` never received the
    // overlay's origin, so a non-zero-origin toast stack — e.g.
    // coord-tui's `main_content_bounds`, which sits right of a sidebar —
    // painted at the wrong screen position and hit-tested against the
    // wrong coordinates).
    const VIEW_W: f64 = 340.0;
    const VIEW_H: f64 = 200.0;
    const LINE_HEIGHT: f64 = 16.0;
    const BOX_COLOR: Color = Color::rgb(10, 20, 30);

    /// A toast with a distinct `accent` fill (overrides the severity
    /// tint) so its painted box is trivially distinguishable from the
    /// white canvas background by colour, without scanning for glyphs.
    fn colored_toast(id: &str, title: &str) -> Toast {
        Toast {
            id: WidgetId::new(id),
            title: title.into(),
            body: String::new(),
            severity: ToastSeverity::Info,
            actions: Vec::new(),
            accent: Some(BOX_COLOR),
        }
    }

    fn stack_br(toasts: Vec<Toast>) -> ToastOverlay {
        ToastOverlay {
            id: WidgetId::new("toasts"),
            corner: ToastCorner::BottomRight,
            toasts,
            focus: None,
        }
    }

    fn pixel(data: &[u8], stride: usize, x: i32, y: i32) -> (u8, u8, u8) {
        let off = y as usize * stride + x as usize * 4;
        (data[off + 2], data[off + 1], data[off])
    }

    /// Paint→click round trip at `(origin_x, origin_y)`: paints a single
    /// toast through [`super::super::surface::CairoSurface`] and the shared
    /// `primitives::toast::native_surface_paint::paint` directly — the
    /// same adapter the now-removed `draw_toast_stack` shim used (issue
    /// #1109) — confirms the box's fill colour lands at the
    /// origin-shifted *absolute*
    /// position — not the viewport-local one `gtk_toast_stack_layout`
    /// used to compute internally before shifting — and that `hit_test`
    /// resolves clicks at that same absolute position through Dismiss
    /// and Body.
    ///
    /// Canvas grows with the origin (`VIEW_W`/`VIEW_H` stay fixed) so a
    /// dropped-origin regression shows up as a shifted absolute paint
    /// position rather than being masked by a shrinking viewport.
    fn paint_and_click_round_trip_at(origin_x: f64, origin_y: f64) {
        let canvas_w = (origin_x + VIEW_W).ceil() as i32;
        let canvas_h = (origin_y + VIEW_H).ceil() as i32;
        let mut surface =
            ImageSurface::create(Format::ARgb32, canvas_w, canvas_h).expect("create ImageSurface");
        let stack = stack_br(vec![colored_toast("t1", "Hello")]);

        let layout = {
            let cr = Context::new(&surface).expect("Context::new");
            cr.set_source_rgb(1.0, 1.0, 1.0);
            cr.paint().ok();
            let pango_layout = pangocairo::functions::create_layout(&cr);
            let mut raw = crate::gtk::surface::CairoSurface {
                cr: &cr,
                layout: Some(&pango_layout),
                translucent_fill: true,
            };
            crate::primitives::toast::native_surface_paint::paint(
                &stack,
                &mut raw,
                &Theme::default(),
                origin_x as f32,
                origin_y as f32,
                VIEW_W as f32,
                VIEW_H as f32,
                LINE_HEIGHT as f32,
            )
        };
        surface.flush();
        let stride = surface.stride() as usize;
        let data = surface.data().expect("surface data");

        assert_eq!(layout.visible_toasts.len(), 1);
        let vt = &layout.visible_toasts[0];

        // Probe near the box's own bottom-left corner — inset, away
        // from glyphs/dismiss — must be the toast's fill colour at the
        // *absolute* bounds `draw_toast_stack` painted into.
        let probe_x = (vt.bounds.x + 2.0) as i32;
        let probe_y = (vt.bounds.y + vt.bounds.height - 2.0) as i32;
        assert_eq!(
            pixel(&data, stride, probe_x, probe_y),
            (BOX_COLOR.r, BOX_COLOR.g, BOX_COLOR.b),
            "toast box should be painted at its own absolute bounds \
             (origin=({origin_x}, {origin_y}), bounds={:?})",
            vt.bounds,
        );

        // Round trip: absolute clicks at the dismiss and body positions
        // resolve through hit_test.
        let db = vt.dismiss_bounds.expect("dismiss bounds present");
        let hit = layout.hit_test(db.x + db.width * 0.5, db.y + db.height * 0.5);
        assert_eq!(hit, ToastHit::Dismiss(WidgetId::new("t1")));

        let body_hit = layout.hit_test(vt.bounds.x + 5.0, vt.bounds.y + vt.bounds.height * 0.5);
        assert_eq!(body_hit, ToastHit::Body(WidgetId::new("t1")));
    }

    #[test]
    fn paint_and_click_round_trip() {
        paint_and_click_round_trip_at(0.0, 0.0);
    }

    /// Non-zero-origin regression guard (quadraui#494 / LESSONS.md
    /// "Layout helpers must return coords in the same frame across
    /// backends"): before this fix `gtk_toast_stack_layout` had no
    /// origin parameter at all, so `draw_toast_stack` could only ever
    /// paint correctly when the overlay's own rect started at `(0, 0)`.
    #[test]
    fn paint_and_click_round_trip_at_nonzero_origin() {
        paint_and_click_round_trip_at(7.0, 13.0);
    }
}