use gtk4::cairo::Context;
use gtk4::pango;
use crate::primitives::layout_metrics::TextMeasure;
use crate::primitives::toolbar::{
measure_button, native_surface_paint, Toolbar, ToolbarItemMeasure, ToolbarLayout,
ToolbarPaintOptions,
};
use crate::theme::Theme;
use crate::types::WidgetId;
pub(crate) struct PangoMeasure<'a> {
pub(crate) pango_layout: Option<&'a pango::Layout>,
pub(crate) char_width: f64,
}
impl TextMeasure for PangoMeasure<'_> {
fn width_of(&self, text: &str) -> f32 {
if let Some(pl) = self.pango_layout {
pl.set_text(text);
pl.pixel_size().0.max(0) as f32
} else {
(text.chars().count() as f64 * self.char_width).ceil() as f32
}
}
}
pub fn gtk_toolbar_layout(
bar: &Toolbar,
pango_layout: Option<&pango::Layout>,
char_width: f64,
x: f64,
y: f64,
w: f64,
h: f64,
) -> ToolbarLayout {
let measure = PangoMeasure {
pango_layout,
char_width,
};
bar.layout(x as f32, y as f32, w as f32, h as f32, |btn| {
ToolbarItemMeasure::new(measure_button(&measure, btn))
})
}
#[allow(clippy::too_many_arguments)]
pub fn draw_toolbar(
cr: &Context,
pango_layout: &pango::Layout,
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(
cr,
pango_layout,
x,
y,
w,
h,
bar,
theme,
hovered_id,
pressed_id,
ToolbarPaintOptions::default(),
)
}
#[allow(clippy::too_many_arguments)]
pub fn draw_toolbar_with_options(
cr: &Context,
pango_layout: &pango::Layout,
x: f64,
y: f64,
w: f64,
h: f64,
bar: &Toolbar,
theme: &Theme,
hovered_id: Option<&WidgetId>,
pressed_id: Option<&WidgetId>,
options: ToolbarPaintOptions,
) -> ToolbarLayout {
pango_layout.set_attributes(None);
pango_layout.set_width(-1);
pango_layout.set_ellipsize(pango::EllipsizeMode::None);
let toolbar_layout = gtk_toolbar_layout(bar, Some(pango_layout), 8.0, x, y, w, h);
if w <= 0.0 || h <= 0.0 {
return toolbar_layout;
}
cr.save().ok();
cr.rectangle(x, y, w, h);
cr.clip();
let mut surface = super::surface::CairoSurface {
cr,
layout: Some(pango_layout),
translucent_fill: true,
};
native_surface_paint::paint(
bar,
&toolbar_layout,
&mut surface,
theme,
hovered_id,
pressed_id,
options,
);
pango_layout.set_attributes(None);
cr.restore().ok();
toolbar_layout
}
#[cfg(test)]
mod tests {
use super::*;
use crate::primitives::toolbar::{ToolbarButton, ToolbarHit};
use crate::types::WidgetId;
fn test_toolbar() -> Toolbar {
Toolbar {
id: WidgetId::new("tb"),
buttons: vec![ToolbarButton::Action {
id: WidgetId::new("tb:action"),
label: "Refine".into(),
icon: None,
key_hint: None,
enabled: true,
is_active: false,
tooltip: String::new(),
}],
bg: None,
focused_index: None,
}
}
fn round_trip_at(x: f64, y: f64) {
let bar = test_toolbar();
let layout = gtk_toolbar_layout(&bar, None, 8.0, x, y, 100.0, 20.0);
let vis = &layout.visible_items[0];
assert_eq!(vis.bounds.x as f64, x);
assert_eq!(vis.bounds.y as f64, y);
let ccx = vis.bounds.x + 1.0;
let ccy = vis.bounds.y + 1.0;
assert_eq!(
layout.hit_test(ccx, ccy),
ToolbarHit::Button(WidgetId::new("tb:action"))
);
}
#[test]
fn paint_and_click_round_trip() {
round_trip_at(0.0, 0.0);
}
#[test]
fn paint_and_click_round_trip_at_nonzero_origin() {
round_trip_at(7.0, 13.0);
}
#[test]
fn multi_row_valign_moves_painted_text_vertically() {
use crate::primitives::toolbar::ToolbarVAlign;
use pangocairo::cairo::{Context, Format, ImageSurface};
const W: i32 = 200;
const H: i32 = 60;
let theme = Theme::default();
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 first_ink_row = |valign: ToolbarVAlign| -> i32 {
let mut surface =
ImageSurface::create(Format::ARgb32, W, H).expect("create ImageSurface");
{
let cr = Context::new(&surface).expect("Context::new");
let pango_layout = pangocairo::functions::create_layout(&cr);
draw_toolbar_with_options(
&cr,
&pango_layout,
0.0,
0.0,
W as f64,
H as f64,
&bar,
&theme,
None,
None,
ToolbarPaintOptions { valign },
);
}
let stride = surface.stride() as usize;
let data = surface.data().expect("surface data");
let bg = theme.header_bg;
for y in 0..H {
for x in 0..W {
let off = y as usize * stride + x as usize * 4;
let (b, g, r) = (data[off], data[off + 1], data[off + 2]);
if (r, g, b) != (bg.r, bg.g, bg.b) {
return y;
}
}
}
panic!("no ink painted for valign {valign:?}");
};
let top_row = first_ink_row(ToolbarVAlign::Top);
let center_row = first_ink_row(ToolbarVAlign::Center);
let bottom_row = first_ink_row(ToolbarVAlign::Bottom);
assert!(
top_row < center_row,
"Top should paint higher than Center (top={top_row}, center={center_row})"
);
assert!(
center_row < bottom_row,
"Center should paint higher than Bottom (center={center_row}, bottom={bottom_row})"
);
}
}