use gtk4::cairo::Context;
use gtk4::pango;
use crate::accelerator::Platform;
use crate::primitives::context_menu::{native_surface_paint, ContextMenu, ContextMenuLayout};
use crate::theme::Theme;
use crate::types::WidgetId;
#[allow(clippy::too_many_arguments)]
pub fn draw_context_menu(
cr: &Context,
layout: &pango::Layout,
menu: &ContextMenu,
menu_layout: &ContextMenuLayout,
line_height: f64,
theme: &Theme,
) -> Vec<(f64, f64, f64, f64, WidgetId)> {
let _ = line_height;
let mut surface = super::surface::CairoSurface {
cr,
layout: Some(layout),
translucent_fill: true,
};
let hits = native_surface_paint::paint(
menu,
menu_layout,
Platform::Linux,
&mut surface,
theme,
&crate::style::Style::default(),
);
layout.set_attributes(None);
hits.into_iter()
.map(|(r, id)| (r.x as f64, r.y as f64, r.width as f64, r.height as f64, id))
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::event::Rect;
use crate::primitives::context_menu::{
ContextMenuHit, ContextMenuItem, ContextMenuItemMeasure,
};
use crate::types::StyledText;
use pangocairo::cairo::{Context, Format, ImageSurface};
fn action(id: &str) -> ContextMenuItem {
ContextMenuItem {
id: Some(WidgetId::new(id)),
label: StyledText::plain(id),
..Default::default()
}
}
fn menu() -> ContextMenu {
ContextMenu {
id: WidgetId::new("m"),
items: vec![action("cut"), ContextMenuItem::default(), action("paste")],
selected_idx: 0,
bg: None,
placement: crate::primitives::context_menu::ContextMenuPlacement::AnchorPoint,
}
}
#[test]
fn draw_context_menu_returns_hits_for_clickable_items_only() {
let surface = ImageSurface::create(Format::ARgb32, 200, 120).expect("create ImageSurface");
let cr = Context::new(&surface).expect("Context::new");
let pango_layout = pangocairo::functions::create_layout(&cr);
let m = menu();
let viewport = Rect::new(0.0, 0.0, 200.0, 120.0);
let layout = m.layout(10.0, 10.0, viewport, 120.0, |i| {
if m.items[i].is_separator() {
ContextMenuItemMeasure::new(6.0)
} else {
ContextMenuItemMeasure::new(20.0)
}
});
let hits = draw_context_menu(&cr, &pango_layout, &m, &layout, 20.0, &Theme::default());
assert_eq!(hits.len(), 2, "cut + paste are clickable, separator is not");
assert_eq!(hits[0].4, WidgetId::new("cut"));
assert_eq!(hits[1].4, WidgetId::new("paste"));
let (x, y, _, _, _) = hits[0];
let hit = layout.hit_test(x as f32 + 4.0, y as f32 + 2.0);
assert_eq!(hit, ContextMenuHit::Item(WidgetId::new("cut")));
}
fn probe_pixel(data: &[u8], stride: usize, x: i32, y: i32) -> (u8, u8, u8) {
let off = y as usize * stride + x as usize * 4;
(data[off], data[off + 1], data[off + 2])
}
fn submenu_parent_menu() -> ContextMenu {
use crate::primitives::context_menu::ContextMenuItem;
ContextMenu {
id: WidgetId::new("m"),
items: vec![
ContextMenuItem {
id: Some(WidgetId::new("parent")),
label: StyledText::plain("Parent"),
submenu: Some(vec![action("child")]),
..Default::default()
},
action("leaf"),
action("decoy"),
],
selected_idx: 2,
bg: None,
placement: crate::primitives::context_menu::ContextMenuPlacement::AnchorPoint,
}
}
#[test]
fn draw_context_menu_paints_submenu_arrow_not_shown_on_plain_leaf() {
use pangocairo::cairo::{Context, Format, ImageSurface};
let mut surface = ImageSurface::create(Format::ARgb32, 200, 120).expect("create surface");
let (parent_row, leaf_row) = {
let cr = Context::new(&surface).expect("Context::new");
let pango_layout = pangocairo::functions::create_layout(&cr);
let m = submenu_parent_menu();
let viewport = Rect::new(0.0, 0.0, 200.0, 120.0);
let layout = m.layout(10.0, 10.0, viewport, 120.0, |_| {
ContextMenuItemMeasure::new(24.0)
});
let parent_row = layout.visible_items[0].bounds;
let leaf_row = layout.visible_items[1].bounds;
let _ = draw_context_menu(&cr, &pango_layout, &m, &layout, 24.0, &Theme::default());
(parent_row, leaf_row)
};
surface.flush();
let stride = surface.stride() as usize;
let data = surface.data().expect("surface data");
let bg_x = (parent_row.x + parent_row.width * 0.4) as i32;
let band_x0 = (parent_row.x + parent_row.width * 0.6) as i32;
let band_x1 = (parent_row.x + parent_row.width - 3.0) as i32;
let parent_y = (parent_row.y + parent_row.height / 2.0) as i32;
let leaf_y = (leaf_row.y + leaf_row.height / 2.0) as i32;
let parent_bg = probe_pixel(&data, stride, bg_x, parent_y);
let leaf_bg = probe_pixel(&data, stride, bg_x, leaf_y);
let band_differs = |cy: i32, bg: (u8, u8, u8)| {
(cy - 4..=cy + 4)
.any(|y| (band_x0..band_x1).any(|x| probe_pixel(&data, stride, x, y) != bg))
};
assert!(
band_differs(parent_y, parent_bg),
"submenu-parent item should paint a ▶ affordance near the right edge"
);
assert!(
!band_differs(leaf_y, leaf_bg),
"plain leaf item (no submenu, no shortcut) must not paint anything there"
);
}
fn checked_item_menu() -> ContextMenu {
use crate::primitives::context_menu::ContextMenuItem;
ContextMenu {
id: WidgetId::new("m"),
items: vec![
ContextMenuItem {
id: Some(WidgetId::new("checked")),
label: StyledText::plain("Item"),
checked: Some(true),
..Default::default()
},
ContextMenuItem {
id: Some(WidgetId::new("unchecked")),
label: StyledText::plain("Item"),
checked: Some(false),
..Default::default()
},
],
selected_idx: 2,
bg: None,
placement: crate::primitives::context_menu::ContextMenuPlacement::AnchorPoint,
}
}
#[test]
fn draw_context_menu_paints_check_glyph_only_on_checked_item() {
use pangocairo::cairo::{Context, Format, ImageSurface};
let mut surface = ImageSurface::create(Format::ARgb32, 200, 80).expect("create surface");
let (checked_row, unchecked_row) = {
let cr = Context::new(&surface).expect("Context::new");
let pango_layout = pangocairo::functions::create_layout(&cr);
let m = checked_item_menu();
let viewport = Rect::new(0.0, 0.0, 200.0, 80.0);
let layout = m.layout(10.0, 10.0, viewport, 80.0, |_| {
ContextMenuItemMeasure::new(24.0)
});
let checked_row = layout.visible_items[0].bounds;
let unchecked_row = layout.visible_items[1].bounds;
let _ = draw_context_menu(&cr, &pango_layout, &m, &layout, 24.0, &Theme::default());
(checked_row, unchecked_row)
};
surface.flush();
let stride = surface.stride() as usize;
let data = surface.data().expect("surface data");
let dy_offset = (checked_row.height / 2.0 - 4.0) as i32;
let band_x0 = 2_i32;
let band_x1 = (checked_row.width * 0.5) as i32;
let differs = (0..=8).any(|dy| {
(band_x0..band_x1).any(|dx| {
let x = (checked_row.x as i32) + dx;
let cy = (checked_row.y as i32) + dy_offset + dy;
let uy = (unchecked_row.y as i32) + dy_offset + dy;
probe_pixel(&data, stride, x, cy) != probe_pixel(&data, stride, x, uy)
})
});
assert!(
differs,
"a checked item must paint something in its leading slot that an \
otherwise-identical unchecked item does not"
);
}
}