use crate::primitives::layout_metrics::pixel_pipeline_view_layout;
use crate::primitives::pipeline_view::{PipelineView, PipelineViewLayout};
#[cfg(test)]
use crate::theme::Theme;
pub fn gtk_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::*;
use crate::event::Rect as QRect;
use crate::gtk::backend::GtkBackend;
use crate::primitives::pipeline_view::{PipelineStage, PipelineViewLayout, StageStatus};
use crate::types::WidgetId;
use crate::Backend;
use pangocairo::cairo::{Context, Format, ImageSurface};
const W: i32 = 240;
const H: i32 = 80;
const BOX_W: f64 = 200.0;
const BOX_H: f64 = 50.0;
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])
}
fn dist2(a: (u8, u8, u8), b: (u8, u8, u8)) -> i64 {
let dr = a.0 as i64 - b.0 as i64;
let dg = a.1 as i64 - b.1 as i64;
let db = a.2 as i64 - b.2 as i64;
dr * dr + dg * dg + db * db
}
fn most_chromatic(
data: &[u8],
stride: usize,
x0: i32,
y0: i32,
x1: i32,
y1: i32,
) -> (u8, u8, u8) {
let mut best = (0u8, 0u8, 0u8);
let mut best_chroma = -1i32;
for y in y0..=y1 {
for x in x0..=x1 {
let (r, g, b) = pixel(data, stride, x, y);
let chroma = r.max(g).max(b) as i32 - r.min(g).min(b) as i32;
if chroma > best_chroma {
best_chroma = chroma;
best = (r, g, b);
}
}
}
best
}
fn make_view() -> PipelineView {
PipelineView {
id: WidgetId::new("pipe"),
stages: vec![PipelineStage {
label: "Build".into(),
status: StageStatus::Done,
action: None,
}],
focused_stage: None,
}
}
fn paint(focused: Option<usize>) -> (ImageSurface, PipelineViewLayout) {
let surface = ImageSurface::create(Format::ARgb32, W, H).expect("create ImageSurface");
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 view = make_view();
view.focused_stage = focused;
let mut backend = GtkBackend::new();
let painted = std::cell::RefCell::new(None);
backend.enter_frame_scope(&cr, &pango_layout, |b| {
*painted.borrow_mut() = Some(
b.draw_pipeline_view(QRect::new(20.0, 20.0, BOX_W as f32, BOX_H as f32), &view),
);
});
layout = painted.into_inner().expect("layout captured");
}
(surface, layout)
}
fn caret_probe(layout: &PipelineViewLayout) -> (i32, i32) {
let bb = layout.stages[0].box_bounds;
let ind_x = bb.x + bb.width / 2.0;
let cx = (ind_x - 2.5).round() as i32;
let cy = (bb.y as f64 - 4.0).round() as i32;
(cx, cy)
}
#[test]
fn focused_done_stage_shows_indicator_and_retains_border() {
let (mut surface, layout) = paint(Some(0));
surface.flush();
let stride = surface.stride() as usize;
let data = surface.data().expect("surface data");
let theme = Theme::default();
let git_added = (theme.git_added.r, theme.git_added.g, theme.git_added.b);
let accent = (theme.accent_bg.r, theme.accent_bg.g, theme.accent_bg.b);
let muted = (theme.muted_fg.r, theme.muted_fg.g, theme.muted_fg.b);
let (cx, cy) = caret_probe(&layout);
let caret_px = pixel(&data, stride, cx, cy);
assert_ne!(
caret_px,
(255, 255, 255),
"focus chevron should paint above the box, got white"
);
assert!(
dist2(caret_px, muted) < 1500,
"chevron tip pixel {caret_px:?} should be ~muted_fg {muted:?}"
);
let bb = layout.stages[0].box_bounds;
let bx = bb.x.round() as i32;
let by = bb.y.round() as i32;
let bh = bb.height.round() as i32;
let border = most_chromatic(&data, stride, bx - 2, by + bh / 4, bx + 2, by + bh * 3 / 4);
assert!(
dist2(border, git_added) < dist2(border, accent),
"border {border:?} should be closer to git_added {git_added:?} \
than to accent_bg {accent:?}"
);
}
#[test]
fn no_indicator_when_not_focused() {
let (mut surface, layout) = paint(None);
surface.flush();
let stride = surface.stride() as usize;
let data = surface.data().expect("surface data");
let (cx, cy) = caret_probe(&layout);
let px = pixel(&data, stride, cx, cy);
assert_eq!(
px,
(255, 255, 255),
"no focus → reserved strip above the box must stay background white, got {px:?}"
);
}
fn hit_test_round_trip_at(x: f64, y: f64) {
use crate::primitives::pipeline_view::PipelineHit;
let mut view = make_view();
view.stages[0].action = Some("Retry".into());
let layout = gtk_pipeline_view_layout(&view, x, y, 200.0, 50.0);
let stage = &layout.stages[0];
assert_eq!(stage.box_bounds.x as f64, x);
let action = stage.action_bounds.expect("action bounds present");
let acx = action.x + action.width / 2.0;
let acy = action.y + action.height / 2.0;
assert_eq!(layout.hit_test(acx, acy), PipelineHit::Action(0));
let bcx = stage.box_bounds.x + 2.0;
let bcy = stage.box_bounds.y + 2.0;
assert_eq!(layout.hit_test(bcx, bcy), PipelineHit::Body(0));
}
#[test]
fn hit_test_round_trip() {
hit_test_round_trip_at(0.0, 0.0);
}
#[test]
fn hit_test_round_trip_at_nonzero_origin() {
hit_test_round_trip_at(7.0, 13.0);
}
}