use std::cell::{Cell, RefCell};
use std::rc::Rc;
use gtk4::prelude::*;
use gtk4::{pango, DrawingArea, EventControllerMotion, GestureClick};
use pangocairo::functions as pcfn;
use super::backend::GtkBackend;
use crate::backend::Backend;
use crate::compose::menu_system::{MenuEvent, MenuSystem};
use crate::event::{ButtonMask, MouseButton, Point, Rect, UiEvent, Viewport};
use crate::types::Modifiers;
pub struct MenuOverlay {
da: DrawingArea,
}
impl Default for MenuOverlay {
fn default() -> Self {
Self::new()
}
}
impl MenuOverlay {
pub fn new() -> Self {
let da = DrawingArea::new();
da.set_hexpand(true);
da.set_vexpand(true);
da.set_can_target(false);
da.set_focusable(false);
Self { da }
}
pub fn drawing_area(&self) -> &DrawingArea {
&self.da
}
pub fn bar_rect_in_overlay(bar_rect: Rect) -> Rect {
Rect::new(
bar_rect.x,
-bar_rect.height,
bar_rect.width,
bar_rect.height,
)
}
pub fn sync(&self, is_open: bool, bar_da: Option<&DrawingArea>) {
self.da.set_can_target(is_open);
self.da.queue_draw();
if let Some(bar) = bar_da {
bar.queue_draw();
}
}
pub fn connect(
&self,
menu_system: Rc<RefCell<MenuSystem>>,
backend: Rc<RefCell<GtkBackend>>,
bar_rect: Rc<Cell<Rect>>,
font: &str,
on_event: impl Fn(MenuEvent) + 'static + Clone,
) {
self.wire_draw(menu_system.clone(), backend.clone(), bar_rect.clone(), font);
self.wire_click(
menu_system.clone(),
backend.clone(),
bar_rect.clone(),
on_event.clone(),
);
self.wire_motion(menu_system, backend, bar_rect, on_event);
}
fn wire_draw(
&self,
menu_system: Rc<RefCell<MenuSystem>>,
backend: Rc<RefCell<GtkBackend>>,
bar_rect: Rc<Cell<Rect>>,
font: &str,
) {
let font_str = font.to_string();
self.da.set_draw_func(move |_da, cr, w, h| {
if !menu_system.borrow().is_open() {
return;
}
let b_ref = backend.borrow();
let pango_layout = match b_ref.create_stable_pango_layout() {
Some(pl) => pl,
None => {
let ctx = pcfn::create_context(cr);
pango::Layout::new(&ctx)
}
};
pcfn::update_layout(cr, &pango_layout);
let font_desc = pango::FontDescription::from_string(&font_str);
pango_layout.set_font_description(Some(&font_desc));
let pango_ctx = pango_layout.context();
let metrics = pango_ctx.metrics(Some(&font_desc), None);
let lh = (metrics.ascent() + metrics.descent()) as f64 / pango::SCALE as f64;
let cw = super::backend::measure_char_width_px(&pango_layout);
drop(b_ref);
let overlay_rect = Self::bar_rect_in_overlay(bar_rect.get());
let mut b = backend.borrow_mut();
b.begin_frame(Viewport::new(w as f32, h as f32, 1.0));
b.enter_frame_scope(cr, &pango_layout, |b| {
b.set_current_line_height(lh);
b.set_current_char_width(cw);
menu_system.borrow().render(b, overlay_rect);
});
});
}
fn wire_click(
&self,
menu_system: Rc<RefCell<MenuSystem>>,
backend: Rc<RefCell<GtkBackend>>,
bar_rect: Rc<Cell<Rect>>,
on_event: impl Fn(MenuEvent) + 'static,
) {
let gesture = GestureClick::new();
gesture.set_button(1);
gesture.connect_pressed(move |g, _, x, y| {
let widget = g
.widget()
.expect("event controller has a widget while its callback runs");
let w = widget.width() as f32;
let h = widget.height() as f32;
let mut b = backend.borrow_mut();
b.begin_frame(Viewport::new(w, h, 1.0));
let overlay_rect = Self::bar_rect_in_overlay(bar_rect.get());
let ev = UiEvent::MouseDown {
widget: None,
button: MouseButton::Left,
position: Point::new(x as f32, y as f32),
modifiers: Modifiers::default(),
};
let menu_event = menu_system.borrow_mut().handle(&ev, &mut *b, overlay_rect);
drop(b);
on_event(menu_event);
});
self.da.add_controller(gesture);
}
fn wire_motion(
&self,
menu_system: Rc<RefCell<MenuSystem>>,
backend: Rc<RefCell<GtkBackend>>,
bar_rect: Rc<Cell<Rect>>,
on_event: impl Fn(MenuEvent) + 'static,
) {
let motion = EventControllerMotion::new();
motion.connect_motion(move |m, x, y| {
if !menu_system.borrow().is_open() {
return;
}
let widget = m
.widget()
.expect("event controller has a widget while its callback runs");
let w = widget.width() as f32;
let h = widget.height() as f32;
let mut b = backend.borrow_mut();
b.begin_frame(Viewport::new(w, h, 1.0));
let overlay_rect = Self::bar_rect_in_overlay(bar_rect.get());
let ev = UiEvent::MouseMoved {
position: Point::new(x as f32, y as f32),
buttons: ButtonMask::default(),
};
let menu_event = menu_system.borrow_mut().handle(&ev, &mut *b, overlay_rect);
drop(b);
on_event(menu_event);
});
self.da.add_controller(motion);
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn bar_rect_in_overlay_applies_negative_y() {
let stored = Rect::new(0.0, 0.0, 800.0, 24.0);
let transformed = MenuOverlay::bar_rect_in_overlay(stored);
assert_eq!(transformed.x, 0.0);
assert_eq!(transformed.y, -24.0);
assert_eq!(transformed.width, 800.0);
assert_eq!(transformed.height, 24.0);
}
#[test]
fn bar_rect_in_overlay_preserves_x_offset() {
let stored = Rect::new(10.0, 0.0, 780.0, 20.0);
let transformed = MenuOverlay::bar_rect_in_overlay(stored);
assert_eq!(transformed.x, 10.0);
assert_eq!(transformed.y, -20.0);
assert_eq!(transformed.width, 780.0);
assert_eq!(transformed.height, 20.0);
}
}