use core_text::font::CTFont;
use crate::primitives::status_bar::{
StatusSegmentMeasure, PIXEL_EDGE_INSET, PIXEL_SEGMENT_PADDING,
};
use crate::{StatusBar, StatusBarLayout};
pub(crate) const MIN_GAP_PX: f32 = 16.0;
pub fn mac_status_bar_layout(
font: &CTFont,
width: f64,
line_height: f64,
bar: &StatusBar,
) -> StatusBarLayout {
if width <= 0.0 || line_height <= 0.0 {
return bar.layout_padded(
width.max(0.0) as f32,
line_height.max(0.0) as f32,
MIN_GAP_PX,
PIXEL_EDGE_INSET,
PIXEL_SEGMENT_PADDING,
|_| StatusSegmentMeasure::new(0.0),
);
}
let measure = |seg: &crate::primitives::status_bar::StatusBarSegment| -> StatusSegmentMeasure {
let (w, _) = super::text::measure_text(font, &seg.text);
StatusSegmentMeasure::new(w as f32)
};
bar.layout_padded(
width as f32,
line_height as f32,
MIN_GAP_PX,
PIXEL_EDGE_INSET,
PIXEL_SEGMENT_PADDING,
measure,
)
}
#[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::status_bar::StatusBarHit;
use crate::primitives::status_bar::StatusBarSegment;
use crate::primitives::status_bar::StatusSegmentSide;
use crate::theme::Theme;
use crate::types::{Color, WidgetId};
use crate::Backend;
const W: u32 = 320;
const H: u32 = 24;
const FONT_SIZE: f64 = 14.0;
fn font() -> CTFont {
make_font("Menlo", FONT_SIZE).expect("Menlo installed on every macOS host")
}
fn backend_with_test_fonts() -> MacBackend {
let mut backend = MacBackend::new();
backend.set_current_font(font());
backend.set_chrome_font(font());
backend
}
fn sample_bar() -> StatusBar {
StatusBar {
id: WidgetId::new("status"),
left_segments: vec![
StatusBarSegment {
text: "(L) ".into(),
fg: Color::rgb(255, 255, 255),
bg: Color::rgb(10, 20, 30),
bold: false,
action_id: None,
},
StatusBarSegment {
text: " Save ".into(),
fg: Color::rgb(255, 255, 255),
bg: Color::rgb(40, 80, 120),
bold: false,
action_id: Some(WidgetId::new("status:save")),
},
],
right_segments: vec![StatusBarSegment {
text: " 1:1 ".into(),
fg: Color::rgb(255, 255, 255),
bg: Color::rgb(40, 80, 120),
bold: false,
action_id: None,
}],
}
}
fn paint_via_backend(bar: &StatusBar) -> (BitmapSurface, StatusBarLayout) {
paint_via_backend_at(bar, 0.0, 0.0)
}
fn paint_via_backend_at(bar: &StatusBar, x: f32, y: f32) -> (BitmapSurface, StatusBarLayout) {
let surface = BitmapSurface::new(W, H);
surface.fill(0.0, 0.0, 0.0, 0.0);
let mut backend = backend_with_test_fonts();
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_status_bar_interactive(
QRect::new(x, y, W as f32 - x, H as f32 - y),
bar,
&crate::InteractionState::new(),
);
*layout.borrow_mut() = Some(l);
});
backend.end_frame();
(surface, layout.into_inner().unwrap())
}
fn probe_save_segment_bg(surface: &BitmapSurface, layout: &StatusBarLayout) -> (u8, u8, u8) {
let save = layout
.visible_segments
.iter()
.filter(|vs| vs.side == StatusSegmentSide::Left)
.nth(1)
.expect("save segment visible");
let probe_x = (save.bounds.x as u32) + 1;
let probe_y = 2;
let (r, g, b, _) = surface.pixel(probe_x, probe_y);
(r, g, b)
}
#[test]
fn paints_segment_backgrounds() {
let bar = sample_bar();
let (surface, layout) = paint_via_backend(&bar);
assert_eq!(
probe_save_segment_bg(&surface, &layout),
(40, 80, 120),
"left-segment bg should match StatusBarSegment.bg",
);
}
fn round_trip_click_hits_clickable_segment_at(origin_x: f32, origin_y: f32) {
let bar = sample_bar();
let (surface, layout) = paint_via_backend_at(&bar, origin_x, origin_y);
let save = layout
.visible_segments
.iter()
.filter(|vs| vs.side == StatusSegmentSide::Left)
.nth(1)
.expect("save segment visible");
let probe_x = (origin_x + save.bounds.x) as u32 + 1;
let probe_y = origin_y as u32 + 2;
let (r, g, b, _) = surface.pixel(probe_x, probe_y);
assert_eq!(
(r, g, b),
(40, 80, 120),
"painted Save segment bg should be at absolute x = origin_x + local bounds.x",
);
let abs_x = origin_x + save.bounds.x + save.bounds.width * 0.5;
let abs_y = origin_y + save.bounds.y + save.bounds.height * 0.5;
let local_x = abs_x - origin_x;
let local_y = abs_y - origin_y;
let hit = layout.hit_test(local_x, local_y);
assert_eq!(
hit,
StatusBarHit::Segment(WidgetId::new("status:save")),
"expected clickable Save segment hit at local ({}, {})",
local_x,
local_y,
);
let right = layout
.visible_segments
.iter()
.find(|vs| vs.side == StatusSegmentSide::Right)
.expect("right segment visible");
let right_hit = layout.hit_test(
right.bounds.x + right.bounds.width * 0.5,
right.bounds.y + right.bounds.height * 0.5,
);
assert_eq!(
right_hit,
StatusBarHit::Empty,
"non-clickable right segment must hit Empty",
);
}
#[test]
fn round_trip_click_hits_clickable_segment() {
round_trip_click_hits_clickable_segment_at(0.0, 0.0);
}
#[test]
fn round_trip_click_hits_clickable_segment_at_nonzero_origin() {
round_trip_click_hits_clickable_segment_at(7.0, 4.0);
}
#[test]
fn empty_bar_falls_back_to_theme_background() {
let bar = StatusBar {
id: WidgetId::new("empty"),
left_segments: vec![],
right_segments: vec![],
};
let surface = BitmapSurface::new(W, H);
surface.fill(0.0, 0.0, 0.0, 0.0);
let mut backend = backend_with_test_fonts();
backend.set_current_theme(Theme {
background: Color::rgb(1, 2, 3),
..Theme::default()
});
backend.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
backend.enter_frame_scope(surface.context_ptr(), |b| {
b.draw_status_bar_interactive(
QRect::new(0.0, 0.0, W as f32, H as f32),
&bar,
&crate::InteractionState::new(),
);
});
backend.end_frame();
let (r, g, b, _) = surface.pixel(W / 2, H / 2);
assert_eq!(
(r, g, b),
(1, 2, 3),
"empty bar should be filled with theme.background",
);
}
#[test]
fn hover_tint_lightens_clickable_segment_bg() {
let bar = sample_bar();
let mut backend = backend_with_test_fonts();
backend.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
let surface = BitmapSurface::new(W, H);
surface.fill(0.0, 0.0, 0.0, 0.0);
let hovered = WidgetId::new("status:save");
let layout = std::cell::RefCell::new(None);
backend.enter_frame_scope(surface.context_ptr(), |b| {
let l = b.draw_status_bar_interactive(
QRect::new(0.0, 0.0, W as f32, H as f32),
&bar,
&crate::InteractionState::from_parts(Some(hovered.clone()), None),
);
*layout.borrow_mut() = Some(l);
});
backend.end_frame();
let layout = layout.into_inner().unwrap();
let (r, g, b) = probe_save_segment_bg(&surface, &layout);
assert!(
r > 40 && g > 80 && b > 120,
"hover-tinted bg ({}, {}, {}) should be lighter than base (40, 80, 120)",
r,
g,
b,
);
}
#[test]
fn layout_twin_matches_the_painted_layout() {
let bar = sample_bar();
let (_surface, painted) = paint_via_backend(&bar);
let backend = backend_with_test_fonts();
let computed = backend.status_bar_layout(QRect::new(0.0, 0.0, W as f32, H as f32), &bar);
assert_eq!(
painted.visible_segments.len(),
computed.visible_segments.len(),
);
for (p, c) in painted
.visible_segments
.iter()
.zip(computed.visible_segments.iter())
{
assert_eq!(p.segment_idx, c.segment_idx);
assert_eq!(p.side, c.side);
assert!((p.bounds.x - c.bounds.x).abs() < 0.001);
assert!((p.bounds.width - c.bounds.width).abs() < 0.001);
}
}
#[test]
fn layout_twin_is_bar_local_at_a_nonzero_origin() {
let bar = sample_bar();
let (_surface, painted) = paint_via_backend_at(&bar, 0.0, 0.0);
let backend = backend_with_test_fonts();
let computed = backend.status_bar_layout(QRect::new(17.0, 3.0, W as f32, H as f32), &bar);
for (p, c) in painted
.visible_segments
.iter()
.zip(computed.visible_segments.iter())
{
assert!(
(p.bounds.x - c.bounds.x).abs() < 0.001,
"segment x moved with the origin: {} vs {}",
p.bounds.x,
c.bounds.x,
);
}
}
}