use core_graphics::sys::CGContextRef;
use core_text::font::CTFont;
use crate::accelerator::Platform;
use crate::event::Rect as QRect;
use crate::primitives::context_menu::{native_surface_paint, ContextMenu, ContextMenuLayout};
use crate::theme::Theme;
use crate::types::WidgetId;
pub unsafe fn draw_context_menu(
ctx: CGContextRef,
font: &CTFont,
menu: &ContextMenu,
menu_layout: &ContextMenuLayout,
theme: &Theme,
) -> Vec<(QRect, WidgetId)> {
let mut surface = super::surface::CgSurface {
ctx,
font: Some(font),
};
native_surface_paint::paint(
menu,
menu_layout,
Platform::Macos,
&mut surface,
theme,
&crate::style::Style::default(),
)
}
#[cfg(test)]
mod tests {
use super::super::headless::BitmapSurface;
use super::super::text::make_font;
use super::super::MacBackend;
use super::*;
use crate::event::Viewport;
use crate::primitives::context_menu::{
ContextMenuItem, ContextMenuItemMeasure, ContextMenuPlacement,
};
use crate::types::StyledText;
use crate::Backend;
const W: u32 = 200;
const H: u32 = 120;
fn font() -> CTFont {
make_font("Menlo", 14.0).expect("Menlo installed")
}
fn item(id: &str, label: &str) -> ContextMenuItem {
ContextMenuItem {
id: Some(WidgetId::new(id)),
label: StyledText::plain(label),
..Default::default()
}
}
fn separator() -> ContextMenuItem {
ContextMenuItem::default()
}
fn sample_menu() -> ContextMenu {
ContextMenu {
id: WidgetId::new("menu"),
items: vec![
item("cut", "Cut"),
item("copy", "Copy"),
separator(),
item("paste", "Paste"),
],
selected_idx: 1,
placement: ContextMenuPlacement::Below,
bg: None,
}
}
fn layout_for(menu: &ContextMenu, viewport: QRect, item_h: f32) -> ContextMenuLayout {
menu.layout(20.0, 20.0, viewport, 120.0, |_| {
ContextMenuItemMeasure::new(item_h)
})
}
fn paint_via_backend(
menu: &ContextMenu,
layout: &ContextMenuLayout,
) -> (BitmapSurface, Vec<(QRect, WidgetId)>) {
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 hits = std::cell::RefCell::new(Vec::new());
backend.enter_frame_scope(surface.context_ptr(), |b| {
*hits.borrow_mut() = b.draw_context_menu(menu, layout);
});
backend.end_frame();
(surface, hits.into_inner())
}
#[test]
fn menu_paints_bg_inside_bounds() {
let menu = sample_menu();
let viewport = QRect::new(0.0, 0.0, W as f32, H as f32);
let layout = layout_for(&menu, viewport, 20.0);
let (surface, _hits) = paint_via_backend(&menu, &layout);
let theme = Theme::default();
let b = layout.bounds;
let (r, g, bp, _) =
surface.pixel((b.x + b.width - 8.0) as u32, (b.y + b.height - 8.0) as u32);
assert_eq!(
(r, g, bp),
(theme.hover_bg.r, theme.hover_bg.g, theme.hover_bg.b),
);
}
#[test]
fn selected_row_paints_selected_bg() {
let menu = sample_menu();
let viewport = QRect::new(0.0, 0.0, W as f32, H as f32);
let layout = layout_for(&menu, viewport, 20.0);
let (surface, _hits) = paint_via_backend(&menu, &layout);
let theme = Theme::default();
let row = layout
.visible_items
.iter()
.find(|v| v.item_idx == 1 && v.clickable)
.expect("copy row visible");
let px = (row.bounds.x + row.bounds.width - 4.0) as u32;
let py = (row.bounds.y + row.bounds.height / 2.0) as u32;
let (r, g, b, _) = surface.pixel(px, py);
assert_eq!(
(r, g, b),
(
theme.selected_bg.r,
theme.selected_bg.g,
theme.selected_bg.b
),
);
}
#[test]
fn key_equivalent_renders_via_platform_macos() {
use crate::accelerator::{
render_accelerator, Accelerator, AcceleratorId, AcceleratorScope, KeyBinding, Platform,
};
let acc = Accelerator {
id: AcceleratorId::new("editor.save"),
binding: KeyBinding::Save,
scope: AcceleratorScope::Global,
label: None,
};
let shortcut = render_accelerator(&acc, Platform::Macos);
assert!(
shortcut.contains('⌘'),
"macOS shortcut should contain ⌘, got {shortcut:?}",
);
assert!(
shortcut.contains('S'),
"macOS shortcut should contain S, got {shortcut:?}",
);
}
#[test]
fn checked_submenu_glyph_override_paints_app_glyph_default_paints_codicon() {
use crate::primitives::context_menu::ContextMenuGlyphs;
let mut menu = sample_menu();
menu.items[0].checked = Some(true);
menu.items[3].submenu = Some(vec![item("paste-plain", "Paste as Plain Text")]);
let viewport = QRect::new(0.0, 0.0, W as f32, H as f32);
let layout = layout_for(&menu, viewport, 20.0);
let glyphs = ContextMenuGlyphs::new()
.with_checked_glyph("V")
.with_submenu_arrow_glyph(">");
let paint = |glyphs: Option<&ContextMenuGlyphs>| -> Vec<crate::testing::TextRun> {
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.set_painted_text_recording(true);
backend.begin_frame(Viewport::new(W as f32, H as f32, 1.0));
backend.enter_frame_scope(surface.context_ptr(), |b| {
let _ = b.draw_context_menu_with_glyphs(&menu, &layout, glyphs);
});
let runs = backend.text_runs().to_vec();
backend.end_frame();
runs
};
let overridden = paint(Some(&glyphs));
assert!(
overridden.iter().any(|r| r.text == "V"),
"expected the overridden checked glyph among painted runs, got {:?}",
overridden
.iter()
.map(|r| r.text.as_str())
.collect::<Vec<_>>()
);
assert!(!overridden
.iter()
.any(|r| r.text.contains(crate::codicon::CHECK)));
assert!(
overridden.iter().any(|r| r.text == ">"),
"expected the overridden submenu-arrow glyph among painted runs, got {:?}",
overridden
.iter()
.map(|r| r.text.as_str())
.collect::<Vec<_>>()
);
assert!(!overridden
.iter()
.any(|r| r.text.contains(crate::codicon::CHEVRON_RIGHT)));
let default_runs = paint(None);
assert!(
default_runs
.iter()
.any(|r| r.text.contains(crate::codicon::CHECK)),
"expected the default codicon check glyph among painted runs, got {:?}",
default_runs
.iter()
.map(|r| r.text.as_str())
.collect::<Vec<_>>()
);
assert!(
default_runs
.iter()
.any(|r| r.text.contains(crate::codicon::CHEVRON_RIGHT)),
"expected the default codicon submenu-arrow glyph among painted runs, got {:?}",
default_runs
.iter()
.map(|r| r.text.as_str())
.collect::<Vec<_>>()
);
}
#[test]
fn hits_returned_for_clickable_items_only() {
let menu = sample_menu();
let viewport = QRect::new(0.0, 0.0, W as f32, H as f32);
let layout = layout_for(&menu, viewport, 20.0);
let (_surface, hits) = paint_via_backend(&menu, &layout);
assert_eq!(hits.len(), 3);
let ids: Vec<&str> = hits.iter().map(|(_, id)| id.as_str()).collect();
assert!(ids.contains(&"cut"));
assert!(ids.contains(&"copy"));
assert!(ids.contains(&"paste"));
}
}