use core_graphics::sys::CGContextRef;
use core_text::font::CTFont;
use crate::primitives::toolbar::{
measure_button, native_surface_paint, Toolbar, ToolbarItemMeasure, ToolbarLayout,
ToolbarPaintOptions,
};
use crate::theme::Theme;
use crate::types::WidgetId;
pub fn mac_toolbar_layout(
bar: &Toolbar,
font: &CTFont,
x: f64,
y: f64,
w: f64,
h: f64,
) -> ToolbarLayout {
bar.layout(x as f32, y as f32, w as f32, h as f32, |btn| {
ToolbarItemMeasure::new(measure_button(font, btn))
})
}
#[allow(clippy::too_many_arguments)]
pub unsafe fn draw_toolbar(
ctx: CGContextRef,
font: &CTFont,
x: f64,
y: f64,
w: f64,
h: f64,
bar: &Toolbar,
theme: &Theme,
hovered_id: Option<&WidgetId>,
pressed_id: Option<&WidgetId>,
) -> ToolbarLayout {
draw_toolbar_with_options(
ctx,
font,
x,
y,
w,
h,
bar,
theme,
hovered_id,
pressed_id,
ToolbarPaintOptions::default(),
)
}
#[allow(clippy::too_many_arguments)]
pub unsafe fn draw_toolbar_with_options(
ctx: CGContextRef,
font: &CTFont,
x: f64,
y: f64,
w: f64,
h: f64,
bar: &Toolbar,
theme: &Theme,
hovered_id: Option<&WidgetId>,
pressed_id: Option<&WidgetId>,
options: ToolbarPaintOptions,
) -> ToolbarLayout {
let layout = mac_toolbar_layout(bar, font, x, y, w, h);
if w <= 0.0 || h <= 0.0 {
return layout;
}
super::backend::ns_push_clip(
ctx,
crate::event::Rect::new(x as f32, y as f32, w as f32, h as f32),
);
let mut surface = super::surface::CgSurface {
ctx,
font: Some(font),
};
native_surface_paint::paint(
bar,
&layout,
&mut surface,
theme,
hovered_id,
pressed_id,
options,
);
super::backend::ns_pop_clip(ctx);
layout
}
#[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::toolbar::{ToolbarButton, ToolbarHit};
use crate::types::WidgetId;
use crate::Backend;
const W: u32 = 240;
const H: u32 = 40;
fn font() -> CTFont {
make_font("Menlo", 14.0).expect("Menlo installed on every macOS host")
}
fn mk_action(id: &str, label: &str, enabled: bool) -> ToolbarButton {
ToolbarButton::Action {
id: WidgetId::new(id),
label: label.into(),
icon: None,
key_hint: None,
enabled,
is_active: false,
tooltip: String::new(),
}
}
fn sample_bar() -> Toolbar {
Toolbar {
id: WidgetId::new("tb"),
buttons: vec![
mk_action("tb:refine", "Refine", true),
mk_action("tb:drop", "Drop", true),
],
bg: None,
focused_index: None,
}
}
fn paint_via_backend(bar: &Toolbar) -> (BitmapSurface, ToolbarLayout) {
paint_via_backend_at(bar, 0.0, 0.0)
}
fn paint_via_backend_at(bar: &Toolbar, x: f32, y: f32) -> (BitmapSurface, ToolbarLayout) {
paint_via_backend_with_options(
bar,
x,
y,
crate::primitives::toolbar::ToolbarPaintOptions::default(),
)
}
fn paint_via_backend_with_options(
bar: &Toolbar,
x: f32,
y: f32,
options: crate::primitives::toolbar::ToolbarPaintOptions,
) -> (BitmapSurface, ToolbarLayout) {
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_toolbar_with_options(
QRect::new(x, y, W as f32 - x, H as f32 - y),
bar,
&crate::InteractionState::new(),
options,
);
*layout.borrow_mut() = Some(l);
});
backend.end_frame();
(surface, layout.into_inner().unwrap())
}
#[test]
fn round_trip_click_hits_enabled_button() {
let bar = sample_bar();
let (_surface, layout) = paint_via_backend(&bar);
assert_eq!(layout.visible_items.len(), 2);
let refine = &layout.visible_items[0];
let hit = layout.hit_test(
refine.bounds.x + refine.bounds.width * 0.5,
refine.bounds.y + refine.bounds.height * 0.5,
);
assert_eq!(
hit,
ToolbarHit::Button(WidgetId::new("tb:refine")),
"expected Refine button hit",
);
}
#[test]
fn round_trip_click_hits_enabled_button_at_nonzero_origin() {
let bar = sample_bar();
let origin_x = 7.0_f32;
let origin_y = 13.0_f32;
let (_surface, layout) = paint_via_backend_at(&bar, origin_x, origin_y);
let refine = &layout.visible_items[0];
assert!(
(refine.bounds.x - origin_x).abs() < 0.01,
"first button should start at origin_x={}, got {}",
origin_x,
refine.bounds.x,
);
assert!(
(refine.bounds.y - origin_y).abs() < 0.01,
"first button should start at origin_y={}, got {}",
origin_y,
refine.bounds.y,
);
let hit = layout.hit_test(
refine.bounds.x + refine.bounds.width * 0.5,
refine.bounds.y + refine.bounds.height * 0.5,
);
assert_eq!(
hit,
ToolbarHit::Button(WidgetId::new("tb:refine")),
"expected Refine button hit at non-zero origin",
);
}
#[test]
fn active_button_highlight_has_rounded_corners_not_square() {
let bar = Toolbar {
id: WidgetId::new("tb"),
buttons: vec![ToolbarButton::Action {
id: WidgetId::new("tb:on"),
label: "On".into(),
icon: None,
key_hint: None,
enabled: true,
is_active: true,
tooltip: String::new(),
}],
bg: None,
focused_index: None,
};
let (surface, layout) = paint_via_backend(&bar);
let theme = Theme::default();
let item = &layout.visible_items[0];
let corner_x = (item.bounds.x + 2.0) as u32;
let corner_y = (item.bounds.y + 2.0) as u32;
let (r, g, b, _) = surface.pixel(corner_x, corner_y);
assert_ne!(
(r, g, b),
(
theme.selected_bg.r,
theme.selected_bg.g,
theme.selected_bg.b
),
"highlight corner pixel should NOT be filled — rounded corner (quadraui#1081), not square",
);
let cx = (item.bounds.x + 5.0) as u32;
let cy = (item.bounds.y + item.bounds.height / 2.0) as u32;
let (r, g, b, _) = surface.pixel(cx, cy);
assert_eq!(
(r, g, b),
(
theme.selected_bg.r,
theme.selected_bg.g,
theme.selected_bg.b
),
"highlight fill should be solid away from the corners and the label text",
);
}
#[test]
fn multi_row_valign_moves_painted_text_vertically() {
use crate::primitives::toolbar::ToolbarVAlign;
let bar = Toolbar {
id: WidgetId::new("tb"),
buttons: vec![ToolbarButton::Action {
id: WidgetId::new("tb:go"),
label: "Go".into(),
icon: None,
key_hint: None,
enabled: true,
is_active: false,
tooltip: String::new(),
}],
bg: None,
focused_index: None,
};
let theme = Theme::default();
let first_ink_row = |valign: ToolbarVAlign| -> u32 {
let (surface, _layout) = paint_via_backend_with_options(
&bar,
0.0,
0.0,
crate::primitives::toolbar::ToolbarPaintOptions { valign },
);
for y in 0..H {
for x in 0..W {
let (r, g, b, _) = surface.pixel(x, y);
if (r, g, b) != (theme.header_bg.r, theme.header_bg.g, theme.header_bg.b) {
return y;
}
}
}
panic!("no ink painted for valign {valign:?}");
};
let top_row = first_ink_row(ToolbarVAlign::Top);
let bottom_row = first_ink_row(ToolbarVAlign::Bottom);
assert!(
top_row < bottom_row,
"Top should paint higher than Bottom (top={top_row}, bottom={bottom_row})"
);
}
}