1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
//! Direct2D / DirectWrite rasteriser for [`crate::Panel`].
//!
//! Painting is implemented in the shared
//! [`crate::primitives::panel::native_surface_paint::paint`] — see that fn's doc
//! for details. This module carries [`win_panel_layout`] for pure layout
//! queries needed by `WinBackend::panel_layout`.
//!
//! Only compiled on `target_os = "windows"` — see `super::mod`'s
//! `#[cfg(target_os = "windows")] mod 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.
//!
//! # Theme
//!
//! `WinBackend::draw_panel` passes `&self.current_theme` to the shared
//! [`crate::primitives::panel::native_surface_paint::paint`] — the same
//! live theme `Backend::set_theme` writes, the same shape
//! `win::status_bar`'s module doc documents.
#[cfg(test)]
use super::text::DWrite;
use crate::event::Rect;
use crate::primitives::layout_metrics::pixel_panel_layout;
use crate::primitives::panel::{Panel, PanelLayout};
#[cfg(test)]
use crate::theme::Theme;
/// Compute a [`Panel`]'s layout without painting — used by
/// `WinBackend::panel_layout` for no-paint hit-test queries.
/// Shares its action-button width with `gtk_panel_layout` / `mac_panel_layout` via
/// [`pixel_panel_layout`].
pub fn win_panel_layout(rect: Rect, panel: &Panel, line_height: f32) -> PanelLayout {
pixel_panel_layout(panel, rect.x, rect.y, rect.width, rect.height, line_height)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::primitives::panel::{PanelAction, PanelHit};
use crate::types::{StyledText, WidgetId};
use crate::win::testing::HeadlessSurface;
const W: u32 = 200;
const H: u32 = 100;
const LINE_HEIGHT: f32 = 20.0;
fn panel() -> Panel {
Panel {
id: WidgetId::new("p"),
title: Some(StyledText::plain("Terminal")),
actions: vec![PanelAction {
id: WidgetId::new("p:close"),
icon: "\u{d7}".into(),
tooltip: "Close".into(),
is_active: false,
}],
accent: None,
collapsed: false,
}
}
/// Paint `panel` via the shared
/// [`crate::primitives::panel::native_surface_paint::paint`] through
/// a [`super::super::surface::D2dSurface`] over `surface`'s headless target — the same
/// adapter the now-removed `draw_panel` shim used,
/// exercised here directly.
fn paint(surface: &HeadlessSurface, dwrite: &DWrite, rect: Rect, panel: &Panel) -> PanelLayout {
let layout = win_panel_layout(rect, panel, LINE_HEIGHT);
surface
.paint(|target| {
let mut raw = super::super::surface::D2dSurface {
target,
dwrite: Some(dwrite),
};
crate::primitives::panel::native_surface_paint::paint(
panel,
&layout,
&mut raw,
&Theme::default(),
);
})
.expect("paint panel");
layout
}
/// Paint↔click round trip: title bar, action button, and content
/// area each paint their own bg (or, for content, are simply left
/// unpainted chrome) at the bounds `hit_test` resolves to the
/// matching `PanelHit` variant.
#[test]
fn paint_and_hit_test_round_trip() {
let surface = HeadlessSurface::new(W, H).expect("create surface");
let (dwrite, _, _) = DWrite::new("Segoe UI", 10.0, None).expect("create DWrite");
let panel = panel();
let rect = Rect::new(0.0, 0.0, W as f32, H as f32);
let layout = paint(&surface, &dwrite, rect, &panel);
let theme = Theme::default();
// Title bar: painted bg matches theme.separator (no accent
// override), and a click on the body (left of the action
// button) resolves to `TitleBar`.
let tb = layout.title_bar_bounds.expect("title bar present");
let tb_px = surface.pixel_at(2, (tb.y + tb.height / 2.0) as u32);
assert_eq!(
(tb_px.r, tb_px.g, tb_px.b),
(theme.separator.r, theme.separator.g, theme.separator.b)
);
let title_hit = layout.hit_test(2.0, tb.y + tb.height / 2.0);
assert_eq!(title_hit, PanelHit::TitleBar(WidgetId::new("p")));
// Action button: painted at its own bounds, hit-tests to
// `Action`.
let va = &layout.visible_actions[0];
let action_hit = layout.hit_test(
va.bounds.x + va.bounds.width / 2.0,
va.bounds.y + va.bounds.height / 2.0,
);
assert_eq!(action_hit, PanelHit::Action(WidgetId::new("p:close")));
// Content area: below the title bar, hit-tests to `Content`.
let cb = layout.content_bounds;
assert!(cb.height > 0.0, "content area should be non-empty");
let content_hit = layout.hit_test(cb.x + 2.0, cb.y + 2.0);
assert_eq!(content_hit, PanelHit::Content(WidgetId::new("p")));
}
/// A no-title panel has no title bar and the content area fills the
/// whole bounds.
#[test]
fn no_title_panel_has_no_title_bar() {
let mut panel = panel();
panel.title = None;
panel.actions = Vec::new();
let surface = HeadlessSurface::new(W, H).expect("create surface");
let (dwrite, _, _) = DWrite::new("Segoe UI", 10.0, None).expect("create DWrite");
let rect = Rect::new(0.0, 0.0, W as f32, H as f32);
let layout = paint(&surface, &dwrite, rect, &panel);
assert!(layout.title_bar_bounds.is_none());
assert_eq!(layout.content_bounds.height, H as f32);
}
/// `win_panel_layout` (no-paint) must produce the same layout
/// as `paint` — same panel, same rect, same line height.
#[test]
fn no_paint_layout_matches_paint_layout() {
let panel = panel();
let rect = Rect::new(0.0, 0.0, W as f32, H as f32);
let surface = HeadlessSurface::new(W, H).expect("create surface");
let (dwrite, _, _) = DWrite::new("Segoe UI", 10.0, None).expect("create DWrite");
let painted = paint(&surface, &dwrite, rect, &panel);
let no_paint = win_panel_layout(rect, &panel, LINE_HEIGHT);
assert_eq!(painted, no_paint);
}
}