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 / DirectWrite rasteriser for
//! [`crate::primitives::sidebar_panel::SidebarPanel`].
//!
//! This module contains [`win_sidebar_panel_layout`] for pure layout
//! calculations needed by `WinBackend::sidebar_panel_layout` for
//! hit-test queries. Painting has moved to the shared
//! [`crate::primitives::sidebar_panel::native_surface_paint::paint`].
//!
//! Only compiled on `target_os = "windows"` — see `super::mod`'s
//! `#[cfg(target_os = "windows")] mod sidebar_panel;` and `backend.rs`'s
//! module docs for why the rest of this repo's `--features win` compile
//! gate stays meaningful without a Windows host.

use super::text::DWrite;
use crate::event::Rect;
use crate::primitives::sidebar_panel::{SidebarPanel, SidebarPanelLayout, SidebarPanelMeasure};
use crate::primitives::toolbar::{measure_button, ToolbarItemMeasure};
#[cfg(test)]
use crate::theme::Theme;
#[cfg(test)]
use crate::types::WidgetId;

/// Compute the Win-GUI pixel/DIP layout for a [`SidebarPanel`] without
/// painting. No geometry is re-derived here: this delegates
/// entirely to [`SidebarPanel::layout`].
///
/// Coordinate frame: **ABSOLUTE** (`rect.x`/`rect.y` baked into
/// `content_bounds` / `toolbar_bounds`), matching
/// [`crate::Backend::sidebar_panel_layout`]'s documented contract and
/// `gtk_sidebar_panel_layout` / the TUI/macOS twins.
pub fn win_sidebar_panel_layout(
    dwrite: &DWrite,
    line_height: f32,
    rect: Rect,
    panel: &SidebarPanel,
) -> SidebarPanelLayout {
    panel.layout(rect, SidebarPanelMeasure::new(line_height, 0.0), |btn| {
        ToolbarItemMeasure::new(measure_button(dwrite, btn))
    })
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::primitives::sidebar_panel::SidebarPanelHit;
    use crate::primitives::toolbar::{Toolbar, ToolbarButton};
    use crate::win::testing::HeadlessSurface;

    const W: f32 = 240.0;
    const H: f32 = 120.0;

    /// Paint `panel` via the shared
    /// [`crate::primitives::sidebar_panel::native_surface_paint::paint`]
    /// through a [`super::super::surface::D2dSurface`] over `surface`'s headless
    /// target — the same adapter the now-removed `draw_sidebar_panel`
    /// shim used, exercised here directly.
    fn paint(
        surface: &HeadlessSurface,
        dwrite: &DWrite,
        line_height: f32,
        rect: Rect,
        panel: &SidebarPanel,
        hovered_toolbar_id: Option<&WidgetId>,
        pressed_toolbar_id: Option<&WidgetId>,
    ) -> SidebarPanelLayout {
        let theme = Theme::default();
        surface
            .paint(|target| {
                let mut raw = super::super::surface::D2dSurface {
                    target,
                    dwrite: Some(dwrite),
                };
                crate::primitives::sidebar_panel::native_surface_paint::paint(
                    panel,
                    &mut raw,
                    &theme,
                    rect,
                    line_height,
                    hovered_toolbar_id,
                    pressed_toolbar_id,
                );
            })
            .map(|_| win_sidebar_panel_layout(dwrite, line_height, rect, panel))
            .expect("paint sidebar panel")
    }

    fn mk_action(id: &str, label: &str) -> ToolbarButton {
        ToolbarButton::Action {
            id: WidgetId::new(id),
            label: label.into(),
            icon: None,
            key_hint: None,
            enabled: true,
            is_active: false,
            tooltip: String::new(),
        }
    }

    fn panel_with_toolbar() -> SidebarPanel {
        SidebarPanel {
            id: WidgetId::new("sb"),
            toolbar: Some(Toolbar {
                id: WidgetId::new("sb:toolbar"),
                buttons: vec![mk_action("a", "Refine"), mk_action("b", "Drop")],
                bg: None,
                focused_index: None,
            }),
            toolbar_height: None,
        }
    }

    fn panel_without_toolbar() -> SidebarPanel {
        SidebarPanel {
            id: WidgetId::new("sb"),
            toolbar: None,
            toolbar_height: None,
        }
    }

    /// Smoke test: `draw_sidebar_panel` must paint and return a
    /// click-routable layout.
    #[test]
    fn text_ok_round_trip_click_hits_toolbar_button() {
        let panel = panel_with_toolbar();
        let surface = HeadlessSurface::new(W as u32, H as u32).expect("create surface");
        let (dwrite, _, line_height) = DWrite::new("Segoe UI", 10.0, None).expect("create DWrite");
        let rect = Rect::new(0.0, 0.0, W, H);

        let layout = paint(&surface, &dwrite, line_height, rect, &panel, None, None);

        let tb = layout.toolbar_bounds.expect("toolbar slot reserved");
        let hit = layout.hit_test(tb.x + 2.0, tb.y + tb.height / 2.0);
        assert_eq!(
            hit,
            SidebarPanelHit::ToolbarButton(WidgetId::new("a")),
            "expected first toolbar button hit"
        );
    }

    /// No-toolbar case: content gets the full rect, and a click inside
    /// it round-trips to content-local coordinates.
    #[test]
    fn no_toolbar_click_hits_content_local_coords() {
        let panel = panel_without_toolbar();
        let surface = HeadlessSurface::new(W as u32, H as u32).expect("create surface");
        let (dwrite, _, line_height) = DWrite::new("Segoe UI", 10.0, None).expect("create DWrite");
        let rect = Rect::new(10.0, 5.0, W - 10.0, H - 5.0);

        let layout = paint(&surface, &dwrite, line_height, rect, &panel, None, None);

        assert!(layout.toolbar_bounds.is_none());
        assert_eq!(layout.content_bounds, rect);

        match layout.hit_test(rect.x + 3.0, rect.y + 4.0) {
            SidebarPanelHit::Content { x, y } => {
                assert!((x - 3.0).abs() < 0.01 && (y - 4.0).abs() < 0.01);
            }
            other => panic!("expected Content hit, got {other:?}"),
        }
    }

    /// No-paint layout must agree byte-for-byte with what
    /// `draw_sidebar_panel` painted — same contract every other `win::`
    /// rasteriser's `no_paint_layout_matches_paint_layout` test proves
    /// (see `win::toolbar`, `win::form`).
    #[test]
    fn no_paint_layout_matches_paint_layout() {
        let panel = panel_with_toolbar();
        let rect = Rect::new(0.0, 0.0, W, H);
        let (dwrite, _, line_height) = DWrite::new("Segoe UI", 10.0, None).expect("create DWrite");
        let surface = HeadlessSurface::new(W as u32, H as u32).expect("create surface");

        let painted = paint(&surface, &dwrite, line_height, rect, &panel, None, None);
        let no_paint = win_sidebar_panel_layout(&dwrite, line_height, rect, &panel);
        assert_eq!(painted, no_paint);
    }
}