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
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
//! macOS rasteriser for [`crate::primitives::pipeline_view::PipelineView`].
//!
//! Painting is implemented in the shared
//! [`crate::primitives::pipeline_view::native_surface_paint::paint`].
//! This module carries [`mac_pipeline_view_layout`], the backend-specific
//! pure geometry for pipeline view layout.
#[cfg(test)]
use core_text::font::CTFont;
use crate::primitives::layout_metrics::pixel_pipeline_view_layout;
use crate::primitives::pipeline_view::{PipelineView, PipelineViewLayout};
#[cfg(test)]
use crate::theme::Theme;
/// Compute the macOS pixel-unit layout for a [`PipelineView`]. Shares its
/// geometry with `gtk_pipeline_view_layout` / `win_pipeline_view_layout`
/// via [`pixel_pipeline_view_layout`].
pub fn mac_pipeline_view_layout(
view: &PipelineView,
x: f64,
y: f64,
w: f64,
h: f64,
) -> PipelineViewLayout {
pixel_pipeline_view_layout(view, x as f32, y as f32, w as f32, h 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::pipeline_view::{PipelineHit, PipelineStage, StageStatus};
use crate::types::WidgetId;
use crate::Backend;
const W: u32 = 300;
const H: u32 = 80;
fn font() -> CTFont {
make_font("Menlo", 14.0).expect("Menlo installed")
}
fn make_view() -> PipelineView {
PipelineView {
id: WidgetId::new("pipe"),
stages: vec![
PipelineStage {
label: "Build".into(),
status: StageStatus::Done,
action: None,
},
PipelineStage {
label: "Test".into(),
status: StageStatus::Active,
action: Some("Retry".into()),
},
],
focused_stage: None,
}
}
/// Paint via the real `Backend::draw_pipeline_view` path — which now
/// routes through the shared `native_surface_paint::paint` — avoids
/// tripping the `-D warnings`-denied `deprecated` lint (CLAUDE.md
/// rule 3) by not calling the deprecated free-function shim above.
fn paint_via_backend(view: &PipelineView) -> (BitmapSurface, PipelineViewLayout) {
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_pipeline_view(QRect::new(0.0, 0.0, W as f32, H as f32), view);
*layout.borrow_mut() = Some(l);
});
backend.end_frame();
(surface, layout.into_inner().unwrap())
}
#[test]
fn draws_without_panic_and_has_two_stages() {
let view = make_view();
let (_surface, layout) = paint_via_backend(&view);
assert_eq!(layout.stages.len(), 2);
}
/// When a stage has an action button, its `action_bounds` displays a
/// visible `accent_bg` tint rather than the plain box background.
#[test]
fn action_button_paints_a_translucent_accent_tint() {
let view = make_view();
let (surface, layout) = paint_via_backend(&view);
let ab = layout.stages[1]
.action_bounds
.expect("stage 1 has an action button");
// Probe near the tint's edge, clear of the button's own glyphs.
let px = (ab.x + 2.0) as u32;
let py = (ab.y + ab.height - 2.0) as u32;
let (r, g, b, _a) = surface.pixel(px, py);
assert_ne!(
(r, g, b),
(0, 0, 0),
"action button area must show the accent tint, not the plain (black) box fill",
);
}
/// The action-button label centres in its button like GTK/Windows
/// rather than painting flush to `ab.y`. The button carries a 1px
/// `accent_bg` border on its outer rows/columns, so the "no glyph
/// ink near the top" probe starts two rows inside the border and
/// skips the side-border columns; the border itself is asserted
/// separately on the top row.
#[test]
fn action_button_label_is_vertically_centred() {
let view = make_view();
let (surface, layout) = paint_via_backend(&view);
let ab = layout.stages[1]
.action_bounds
.expect("stage 1 has an action button");
let theme = Theme::default();
let accent = (theme.accent_bg.r, theme.accent_bg.g, theme.accent_bg.b);
let is_accent = |x: u32, y: u32| {
let (r, g, b, _) = surface.pixel(x, y);
(r, g, b) == accent
};
let top = ab.y as u32;
let x0 = ab.x as u32;
let x1 = (ab.x + ab.width) as u32;
assert!(
(x0..x1).any(|x| is_accent(x, top)),
"the action button's top row should carry its accent border",
);
// Glyph ink is painted in `accent_bg`; the tint alone is a much
// darker blend toward the black background. A near-pure
// accent_bg pixel just inside the border means the label is
// hugging the top of the button.
let probe_y = top + 2;
let top_rows_have_glyph_ink =
((x0 + 2)..x1.saturating_sub(2)).any(|x| is_accent(x, probe_y));
assert!(
!top_rows_have_glyph_ink,
"action label should be vertically centred in its button, not flush to the top row",
);
}
/// Shared body for the action↔click round trip, run at both the
/// origin and a non-zero origin. `mac_pipeline_view_layout` bakes
/// `x`/`y` straight into the returned bounds (absolute frame, matching
/// the GTK/TUI twins) and also adds
/// [`crate::primitives::layout_metrics::pixel::PIPELINE_FOCUS_INDICATOR_H`]
/// to `y` itself before laying out. Deriving `ab`/`bb` from the layout
/// (not hardcoding them) exercises the origin math across backends.
/// Calls `mac_pipeline_view_layout` directly (pure fn, no font/paint
/// dependency — it forwards to `PipelineView::layout`, which is
/// plain geometry).
fn layout_hit_test_action_round_trip_at(origin_x: f64, origin_y: f64) {
let view = make_view();
let layout = mac_pipeline_view_layout(&view, origin_x, origin_y, 300.0, 80.0);
// Box top must sit exactly at origin_y + PIPELINE_FOCUS_INDICATOR_H,
// not a hardcoded absolute value — pins the offset math
// independently of the hit_test round trip below.
let bb0 = layout.stages[0].box_bounds;
assert!(
(bb0.y as f64
- (origin_y
+ (crate::primitives::layout_metrics::pixel::PIPELINE_FOCUS_INDICATOR_H
as f64)))
.abs()
< 0.001,
"stage box top should be origin_y + PIPELINE_FOCUS_INDICATOR_H, got {}",
bb0.y,
);
// Stage 1 has action bounds.
let ab = layout.stages[1]
.action_bounds
.expect("action bounds for stage 1");
let hit = layout.hit_test(ab.x + ab.width / 2.0, ab.y + ab.height / 2.0);
assert_eq!(hit, PipelineHit::Action(1));
}
#[test]
fn layout_hit_test_action_round_trip() {
layout_hit_test_action_round_trip_at(0.0, 0.0);
}
/// Non-zero-origin regression guard.
#[test]
fn layout_hit_test_action_round_trip_at_nonzero_origin() {
layout_hit_test_action_round_trip_at(7.0, 13.0);
}
fn layout_hit_test_body_round_trip_at(origin_x: f64, origin_y: f64) {
let view = make_view();
let layout = mac_pipeline_view_layout(&view, origin_x, origin_y, 300.0, 80.0);
let bb = layout.stages[0].box_bounds;
assert!(
(bb.y as f64
- (origin_y
+ (crate::primitives::layout_metrics::pixel::PIPELINE_FOCUS_INDICATOR_H
as f64)))
.abs()
< 0.001,
"stage box top should be origin_y + PIPELINE_FOCUS_INDICATOR_H, got {}",
bb.y,
);
// Stage 0 has no action, so a click inside its box resolves to Body.
let hit = layout.hit_test(bb.x + 1.0, bb.y + 1.0);
assert_eq!(hit, PipelineHit::Body(0));
}
#[test]
fn layout_hit_test_body_round_trip() {
layout_hit_test_body_round_trip_at(0.0, 0.0);
}
/// Non-zero-origin regression guard.
#[test]
fn layout_hit_test_body_round_trip_at_nonzero_origin() {
layout_hit_test_body_round_trip_at(7.0, 13.0);
}
}