use core_text::font::CTFont;
use crate::primitives::sidebar_panel::{SidebarPanel, SidebarPanelLayout, SidebarPanelMeasure};
use crate::primitives::toolbar::{measure_button, ToolbarItemMeasure};
pub fn mac_sidebar_panel_layout(
panel: &SidebarPanel,
font: &CTFont,
line_height: f64,
x: f64,
y: f64,
w: f64,
h: f64,
) -> SidebarPanelLayout {
let bounds = crate::event::Rect::new(x as f32, y as f32, w as f32, h as f32);
panel.layout(
bounds,
SidebarPanelMeasure::new(line_height as f32, 8.0),
|btn| ToolbarItemMeasure::new(measure_button(font, btn)),
)
}
#[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::sidebar_panel::SidebarPanelHit;
use crate::primitives::toolbar::{Toolbar, ToolbarButton};
use crate::types::WidgetId;
use crate::Backend;
const W: u32 = 240;
const H: u32 = 200;
fn font() -> CTFont {
make_font("Menlo", 14.0).expect("Menlo installed")
}
fn panel_with_toolbar() -> SidebarPanel {
SidebarPanel {
id: WidgetId::new("sb"),
toolbar: Some(Toolbar {
id: WidgetId::new("sb:toolbar"),
buttons: vec![ToolbarButton::Action {
id: WidgetId::new("refine"),
label: "Refine".into(),
icon: None,
key_hint: None,
enabled: true,
is_active: false,
tooltip: String::new(),
}],
bg: None,
focused_index: None,
}),
toolbar_height: None,
}
}
fn paint_via_backend(panel: &SidebarPanel) -> (BitmapSurface, SidebarPanelLayout) {
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_sidebar_panel_interactive(
QRect::new(0.0, 0.0, W as f32, H as f32),
panel,
&crate::InteractionState::new(),
);
*layout.borrow_mut() = Some(l);
});
backend.end_frame();
(surface, layout.into_inner().unwrap())
}
#[test]
fn toolbar_reserves_header_slot_content_starts_below() {
let panel = panel_with_toolbar();
let (_surface, layout) = paint_via_backend(&panel);
let tb = layout.toolbar_bounds.expect("toolbar bounds present");
assert_eq!(tb.y, 0.0);
assert!(
layout.content_bounds.y >= tb.y + tb.height,
"content should start below the toolbar slot: tb.bottom={}, content.y={}",
tb.y + tb.height,
layout.content_bounds.y,
);
}
#[test]
fn no_toolbar_gives_full_rect_to_content() {
let panel = SidebarPanel {
id: WidgetId::new("sb"),
toolbar: None,
toolbar_height: None,
};
let (_surface, layout) = paint_via_backend(&panel);
assert!(layout.toolbar_bounds.is_none());
assert_eq!(layout.content_bounds.y, 0.0);
assert_eq!(layout.content_bounds.height, H as f32);
}
fn click_in_header_round_trip_at(origin_x: f64, origin_y: f64) {
let panel = panel_with_toolbar();
let f = font();
let layout =
mac_sidebar_panel_layout(&panel, &f, 16.0, origin_x, origin_y, W as f64, H as f64);
match layout.hit_test(origin_x as f32 + 2.0, origin_y as f32) {
SidebarPanelHit::ToolbarButton(id) => assert_eq!(id.as_str(), "refine"),
other => panic!("expected ToolbarButton, got {other:?}"),
}
}
#[test]
fn click_in_header_resolves_to_toolbar_button() {
click_in_header_round_trip_at(0.0, 0.0);
}
#[test]
fn click_in_header_resolves_to_toolbar_button_at_nonzero_origin() {
click_in_header_round_trip_at(7.0, 13.0);
}
#[test]
fn toolbar_header_height_tracks_chrome_font_not_editor_font() {
use crate::Backend;
fn toolbar_height_for(editor: (&str, f32), chrome_desc: &str) -> f32 {
let surface = BitmapSurface::new(W, H);
surface.fill(0.0, 0.0, 0.0, 0.0);
let mut backend = MacBackend::new();
Backend::set_editor_font(&mut backend, editor.0, editor.1);
Backend::set_ui_font(&mut backend, chrome_desc);
backend.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
let panel = panel_with_toolbar();
let layout = std::cell::RefCell::new(None);
backend.enter_frame_scope(surface.context_ptr(), |b| {
let l = b.draw_sidebar_panel_interactive(
QRect::new(0.0, 0.0, W as f32, H as f32),
&panel,
&crate::InteractionState::new(),
);
*layout.borrow_mut() = Some(l);
});
backend.end_frame();
layout
.into_inner()
.unwrap()
.toolbar_bounds
.expect("toolbar bounds present")
.height
}
let h_small_chrome = toolbar_height_for(("Menlo", 9.0), "Menlo 9");
let h_huge_chrome = toolbar_height_for(("Menlo", 9.0), "Menlo 60");
assert!(
h_huge_chrome > h_small_chrome * 2.0,
"sidebar header slot height must grow with the chrome font, not stay \
pinned to the editor's: {h_small_chrome} vs {h_huge_chrome}"
);
}
#[test]
fn toolbar_icon_glyph_is_vertically_centered_in_header_band() {
let panel = SidebarPanel {
id: WidgetId::new("sb"),
toolbar: Some(Toolbar {
id: WidgetId::new("sb:toolbar"),
buttons: vec![ToolbarButton::Action {
id: WidgetId::new("new-file"),
label: String::new(),
icon: Some("\u{EAF0}".into()),
key_hint: None,
enabled: true,
is_active: false,
tooltip: String::new(),
}],
bg: None,
focused_index: None,
}),
toolbar_height: None,
};
let (surface, layout) = paint_via_backend(&panel);
let tb = layout.toolbar_bounds.expect("toolbar bounds present");
let item = layout
.toolbar_layout
.expect("toolbar layout present")
.visible_items[0]
.bounds;
let bg_x = (item.x as u32).min(W - 1);
let bg_y = (tb.y as u32).min(H - 1);
let bg = surface.pixel(bg_x, bg_y);
let x0 = item.x.max(0.0) as u32;
let x1 = ((item.x + item.width).min(W as f32)) as u32;
let y0 = tb.y.max(0.0) as u32;
let y1 = ((tb.y + tb.height).min(H as f32)) as u32;
let mut min_y: Option<u32> = None;
let mut max_y: Option<u32> = None;
for y in y0..y1 {
for x in x0..x1 {
if surface.pixel(x, y) != bg {
min_y = Some(min_y.map_or(y, |m| m.min(y)));
max_y = Some(max_y.map_or(y, |m| m.max(y)));
}
}
}
let min_y = min_y.expect("icon glyph should paint at least one pixel") as f32;
let max_y = max_y.expect("icon glyph should paint at least one pixel") as f32;
assert!(
min_y >= tb.y - 0.5,
"icon glyph top ({min_y}) sits above the header band (tb.y={})",
tb.y,
);
assert!(
max_y + 1.0 <= tb.y + tb.height + 0.5,
"icon glyph bottom ({}) is clipped by the header band's bottom edge \
({})",
max_y + 1.0,
tb.y + tb.height,
);
let glyph_center = (min_y + max_y + 1.0) / 2.0;
let band_center = tb.y + tb.height / 2.0;
assert!(
(glyph_center - band_center).abs() <= 1.0,
"icon glyph vertical centre ({glyph_center}) is not within 1px of the \
header band's centre ({band_center})",
);
}
}