quadraui 0.1.1

Cross-platform Rust UI toolkit: write one AppLogic and run it unmodified on TUI, GTK4 (Linux), macOS, and Windows backends
Documentation
//! macOS rasteriser for [`crate::Panel`].
//!
//! Painting moved to the shared
//! [`crate::primitives::panel::native_surface_paint::paint`] (#859,
//! `PaintSurface` Phase 2d slice 2/9) — see that fn's doc for the named
//! divergences (unclamped glyph centring vs. win's `.max(0.0)`; GTK's
//! `set_source` alpha-drop, fixed at the source by #811) found while
//! unifying `gtk::draw_panel`, `macos::panel::draw_panel` and
//! `win::panel::draw_panel` into one implementation. This module now
//! only carries [`mac_panel_layout`] (pure layout, still needed by
//! `MacBackend::panel_layout` for no-paint hit-test queries); the
//! deprecated `draw_panel` compatibility shim over the shared
//! [`super::surface::CgSurface`] adapter was removed in issue #1109
//! (zero uses in coord-tui's `main` and vimcode's `develop`).

#[cfg(test)]
use core_text::font::CTFont;

use crate::primitives::layout_metrics::pixel_panel_layout;
use crate::primitives::panel::{Panel, PanelLayout};
#[cfg(test)]
use crate::theme::Theme;

/// Compute the macOS pixel-unit layout for a [`Panel`] without painting.
/// Shares its action-button width with `gtk_panel_layout` /
/// `win_panel_layout` via [`pixel_panel_layout`] (issue #1079).
pub fn mac_panel_layout(
    panel: &Panel,
    x: f64,
    y: f64,
    w: f64,
    h: f64,
    line_height: f64,
) -> PanelLayout {
    pixel_panel_layout(
        panel,
        x as f32,
        y as f32,
        w as f32,
        h as f32,
        line_height as f32,
    )
}

#[cfg(test)]
mod tests {
    use super::super::headless::BitmapSurface;
    use super::super::text::make_font;
    use super::super::MacBackend;
    use super::*;
    use crate::event::{Rect as QRect, Viewport};
    use crate::primitives::panel::{PanelAction, PanelHit};
    use crate::types::{StyledText, WidgetId};
    use crate::Backend;

    const W: u32 = 240;
    const H: u32 = 160;

    fn font() -> CTFont {
        make_font("Menlo", 14.0).expect("Menlo installed")
    }

    fn sample_panel() -> Panel {
        Panel {
            id: WidgetId::new("panel"),
            title: Some(StyledText::plain("Terminal")),
            actions: vec![
                PanelAction {
                    id: WidgetId::new("panel:close"),
                    icon: "×".into(),
                    tooltip: "Close".into(),
                    is_active: false,
                },
                PanelAction {
                    id: WidgetId::new("panel:max"),
                    icon: "□".into(),
                    tooltip: "Maximize".into(),
                    is_active: false,
                },
            ],
            accent: None,
            collapsed: false,
        }
    }

    fn paint_via_backend(panel: &Panel) -> (BitmapSurface, PanelLayout) {
        let surface = BitmapSurface::new(W, H);
        surface.fill(0.0, 0.0, 0.0, 0.0);
        let mut backend = MacBackend::new();
        backend.set_current_font(font());
        backend.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
        let layout = std::cell::RefCell::new(None);
        backend.enter_frame_scope(surface.context_ptr(), |b| {
            let l = b.draw_panel(QRect::new(0.0, 0.0, W as f32, H as f32), panel);
            *layout.borrow_mut() = Some(l);
        });
        backend.end_frame();
        (surface, layout.into_inner().unwrap())
    }

    #[test]
    fn title_bar_paints_separator_bg_by_default() {
        let panel = sample_panel();
        let (surface, layout) = paint_via_backend(&panel);
        let theme = Theme::default();
        let tb = layout.title_bar_bounds.expect("title bar present");
        // Probe right side of the title bar, left of the action buttons.
        // First action starts at W - 24; sample around W/2.
        let px = (tb.x + tb.width / 2.0) as u32;
        let py = (tb.y + tb.height - 1.0) as u32;
        let (r, g, b, _) = surface.pixel(px, py);
        assert_eq!(
            (r, g, b),
            (theme.separator.r, theme.separator.g, theme.separator.b),
        );
    }

    #[test]
    fn content_bounds_excludes_title_bar() {
        let panel = sample_panel();
        let (_surface, layout) = paint_via_backend(&panel);
        let tb = layout.title_bar_bounds.expect("title bar present");
        let cb = layout.content_bounds;
        assert!(
            cb.y >= tb.y + tb.height,
            "content should start below title bar: tb.bottom={}, content.y={}",
            tb.y + tb.height,
            cb.y,
        );
    }

    #[test]
    fn action_buttons_right_aligned() {
        let panel = sample_panel();
        let (_surface, layout) = paint_via_backend(&panel);
        // Two actions, each 24px wide. Right-most at (W-24..W).
        assert_eq!(layout.visible_actions.len(), 2);
        let first = &layout.visible_actions[0];
        // First action_idx=0 painted right-most.
        assert!(
            (first.bounds.x
                - (W as f32 - crate::primitives::layout_metrics::pixel::PANEL_ACTION_BUTTON))
                .abs()
                < 0.5
        );
    }

    #[test]
    fn hit_test_resolves_action_vs_title_vs_content() {
        let panel = sample_panel();
        let (_surface, layout) = paint_via_backend(&panel);
        let first_action = &layout.visible_actions[0];
        let hit = layout.hit_test(
            first_action.bounds.x + first_action.bounds.width * 0.5,
            first_action.bounds.y + first_action.bounds.height * 0.5,
        );
        assert!(matches!(hit, PanelHit::Action(_)));

        // Title body (left of actions).
        let tb = layout.title_bar_bounds.unwrap();
        let hit = layout.hit_test(tb.x + 10.0, tb.y + tb.height * 0.5);
        assert!(matches!(hit, PanelHit::TitleBar(_)), "hit was {:?}", hit);

        // Content area.
        let cb = layout.content_bounds;
        let hit = layout.hit_test(cb.x + cb.width * 0.5, cb.y + cb.height * 0.5);
        assert!(matches!(hit, PanelHit::Content(_)));
    }

    #[test]
    fn hit_test_resolves_action_vs_title_vs_content_at_nonzero_origin() {
        // Regression for quadraui#494 / LESSONS.md "Layout helpers must
        // return coords in the same frame across backends": every other
        // test in this file lays out at origin (0, 0). `mac_panel_
        // layout` bakes `x`/`y` straight into `bounds` (absolute frame,
        // matching the GTK/TUI twins) — call it directly (pure fn, no
        // paint needed) at a non-zero origin and prove action/title/
        // content clicks still resolve through `hit_test`.
        let panel = sample_panel();
        let layout = mac_panel_layout(&panel, 7.0, 13.0, W as f64, H as f64, 16.0);

        let first_action = &layout.visible_actions[0];
        let hit = layout.hit_test(
            first_action.bounds.x + first_action.bounds.width * 0.5,
            first_action.bounds.y + first_action.bounds.height * 0.5,
        );
        assert!(matches!(hit, PanelHit::Action(_)));

        // Title body (left of actions).
        let tb = layout.title_bar_bounds.unwrap();
        let hit = layout.hit_test(tb.x + 10.0, tb.y + tb.height * 0.5);
        assert!(matches!(hit, PanelHit::TitleBar(_)), "hit was {:?}", hit);

        // Content area.
        let cb = layout.content_bounds;
        let hit = layout.hit_test(cb.x + cb.width * 0.5, cb.y + cb.height * 0.5);
        assert!(matches!(hit, PanelHit::Content(_)));
    }

    #[test]
    fn collapsed_panel_zero_content_height() {
        let mut panel = sample_panel();
        panel.collapsed = true;
        let (_surface, layout) = paint_via_backend(&panel);
        assert_eq!(layout.content_bounds.height, 0.0);
    }
}