use gtk4::cairo::Context;
use gtk4::pango;
use crate::primitives::tooltip::{native_surface_paint, Tooltip, TooltipChrome, TooltipLayout};
use crate::theme::Theme;
pub fn draw_tooltip(
cr: &Context,
layout: &pango::Layout,
tooltip: &Tooltip,
tooltip_layout: &TooltipLayout,
line_height: f64,
padding_x: f64,
theme: &Theme,
) {
draw_tooltip_with_chrome(
cr,
layout,
tooltip,
tooltip_layout,
&TooltipChrome::default(),
line_height,
padding_x,
theme,
);
}
#[allow(clippy::too_many_arguments)]
pub fn draw_tooltip_with_chrome(
cr: &Context,
layout: &pango::Layout,
tooltip: &Tooltip,
tooltip_layout: &TooltipLayout,
chrome: &TooltipChrome,
line_height: f64,
padding_x: f64,
theme: &Theme,
) {
let mut surface = super::surface::CairoSurface {
cr,
layout: Some(layout),
translucent_fill: true,
};
native_surface_paint::paint(
tooltip,
tooltip_layout,
chrome,
line_height as f32,
padding_x as f32,
&mut surface,
theme,
&crate::style::Style::default(),
);
layout.set_attributes(None);
}
#[cfg(test)]
mod tests {
use gtk4::cairo::{Context, Format, ImageSurface};
use super::*;
use crate::event::Rect as QRect;
use crate::primitives::tooltip::{ResolvedPlacement, TooltipBorder, TooltipPlacement};
use crate::types::WidgetId;
const W: i32 = 200;
const H: i32 = 80;
const LINE_H: f64 = 16.0;
const PAD_X: f64 = 8.0;
fn pixel(data: &[u8], stride: usize, x: i32, y: i32) -> (u8, u8, u8) {
let off = y as usize * stride + x as usize * 4;
(data[off + 2], data[off + 1], data[off])
}
fn bg_reference(data: &[u8], stride: usize, bounds: QRect) -> (u8, u8, u8) {
pixel(
data,
stride,
(bounds.x + bounds.width) as i32 - 4,
(bounds.y + bounds.height) as i32 - 4,
)
}
fn sample_tooltip() -> Tooltip {
Tooltip {
id: WidgetId::new("tip"),
text: "Hover hint".into(),
styled_lines: None,
placement: TooltipPlacement::Bottom,
fg: None,
bg: None,
}
}
fn sample_layout(border: TooltipBorder, title: Option<&str>) -> (TooltipLayout, TooltipChrome) {
let layout = TooltipLayout {
bounds: QRect::new(20.0, 20.0, 120.0, 24.0),
resolved_placement: ResolvedPlacement::Bottom,
};
let mut chrome = TooltipChrome::new(border);
chrome.title = title.map(str::to_string);
(layout, chrome)
}
fn paint(
tooltip: &Tooltip,
layout: &TooltipLayout,
chrome: &TooltipChrome,
) -> (Vec<u8>, usize) {
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_tooltip_with_chrome(
&cr,
&pango_layout,
tooltip,
layout,
chrome,
LINE_H,
PAD_X,
&Theme::default(),
);
}
surface.flush();
let stride = surface.stride() as usize;
let data = surface.data().expect("surface data").to_vec();
(data, stride)
}
#[test]
fn full_border_strokes_all_four_edges() {
let tooltip = sample_tooltip();
let (layout, chrome) = sample_layout(TooltipBorder::Full, None);
let (data, stride) = paint(&tooltip, &layout, &chrome);
let b = layout.bounds;
let (bx, by, bw, bh) = (b.x as i32, b.y as i32, b.width as i32, b.height as i32);
let inner = bg_reference(&data, stride, b);
for (name, edge) in [
("top", pixel(&data, stride, bx + bw / 2, by)),
("bottom", pixel(&data, stride, bx + bw / 2, by + bh - 1)),
("left", pixel(&data, stride, bx, by + bh / 2)),
("right", pixel(&data, stride, bx + bw - 1, by + bh / 2)),
] {
assert_ne!(
edge, inner,
"TooltipBorder::Full: {name} edge should show border ink, distinct from the \
interior background (edge={edge:?}, inner={inner:?})"
);
}
}
#[test]
fn sides_border_only_strokes_left_and_right() {
let tooltip = sample_tooltip();
let (layout, chrome) = sample_layout(TooltipBorder::Sides, None);
let (data, stride) = paint(&tooltip, &layout, &chrome);
let b = layout.bounds;
let (bx, by, bw, bh) = (b.x as i32, b.y as i32, b.width as i32, b.height as i32);
let inner = bg_reference(&data, stride, b);
let top = pixel(&data, stride, bx + bw / 2, by);
let bottom = pixel(&data, stride, bx + bw / 2, by + bh - 1);
let left = pixel(&data, stride, bx, by + bh / 2);
let right = pixel(&data, stride, bx + bw - 1, by + bh / 2);
assert_eq!(
top, inner,
"TooltipBorder::Sides must not stroke the top edge — no top/bottom rule, \
regardless of box height (top={top:?}, inner={inner:?})"
);
assert_eq!(
bottom, inner,
"TooltipBorder::Sides must not stroke the bottom edge (bottom={bottom:?}, \
inner={inner:?})"
);
assert_ne!(
left, inner,
"TooltipBorder::Sides must still stroke the left edge (left={left:?}, \
inner={inner:?})"
);
assert_ne!(
right, inner,
"TooltipBorder::Sides must still stroke the right edge (right={right:?}, \
inner={inner:?})"
);
}
#[test]
fn none_border_strokes_nothing() {
let tooltip = sample_tooltip();
let (layout, chrome) = sample_layout(TooltipBorder::None, None);
let (data, stride) = paint(&tooltip, &layout, &chrome);
let b = layout.bounds;
let (bx, by, bw, bh) = (b.x as i32, b.y as i32, b.width as i32, b.height as i32);
let inner = bg_reference(&data, stride, b);
for (name, edge) in [
("top", pixel(&data, stride, bx + bw / 2, by)),
("bottom", pixel(&data, stride, bx + bw / 2, by + bh - 1)),
("left", pixel(&data, stride, bx, by + bh / 2)),
("right", pixel(&data, stride, bx + bw - 1, by + bh / 2)),
] {
assert_eq!(
edge, inner,
"TooltipBorder::None must not stroke {name} — no chrome at all \
(edge={edge:?}, inner={inner:?})"
);
}
}
#[test]
fn full_border_title_punches_a_gap_but_leaves_the_rest_of_the_top_edge_stroked() {
let tooltip = sample_tooltip();
let (layout, chrome) = sample_layout(TooltipBorder::Full, Some("Hi"));
let (data, stride) = paint(&tooltip, &layout, &chrome);
let b = layout.bounds;
let (bx, by, bw) = (b.x as i32, b.y as i32, b.width as i32);
let inner = bg_reference(&data, stride, b);
let margin = 3; let interior: Vec<(u8, u8, u8)> = (bx + margin..bx + bw - margin)
.map(|x| pixel(&data, stride, x, by))
.collect();
assert!(
interior.contains(&inner),
"the title's short label + padding should punch at least one background-\
coloured pixel into the top edge somewhere in its interior (inner={inner:?}, \
top-edge interior samples={interior:?})"
);
let top_near_corner = pixel(&data, stride, bx + 1, by);
assert_ne!(
top_near_corner, inner,
"the top edge right at the corner should still show the plain border stroke, \
i.e. the punch must not swallow the whole edge \
(top_near_corner={top_near_corner:?}, inner={inner:?})"
);
}
#[test]
fn title_is_ignored_when_border_is_sides_or_none() {
for border in [TooltipBorder::Sides, TooltipBorder::None] {
let with_title = sample_tooltip();
let without_title = sample_tooltip();
let (with_layout, with_chrome) = sample_layout(border, Some("Ignored"));
let (without_layout, without_chrome) = sample_layout(border, None);
let (with_data, stride) = paint(&with_title, &with_layout, &with_chrome);
let (without_data, _) = paint(&without_title, &without_layout, &without_chrome);
let b = with_layout.bounds;
let (bx, by, bw) = (b.x as i32, b.y as i32, b.width as i32);
let top_with = pixel(&with_data, stride, bx + bw / 2, by);
let top_without = pixel(&without_data, stride, bx + bw / 2, by);
assert_eq!(
top_with, top_without,
"{border:?}: setting `title` must not change what's painted at the top \
edge (top_with={top_with:?}, top_without={top_without:?})"
);
}
}
#[test]
fn styled_line_bold_span_paints_wider_ink_than_regular() {
use crate::types::{StyledSpan, StyledText};
fn last_ink_x(
data: &[u8],
stride: usize,
bg: (u8, u8, u8),
x_start: i32,
x_end: i32,
y_start: i32,
y_end: i32,
) -> i32 {
let mut last = x_start;
for y in y_start..y_end {
for x in x_start..x_end {
if pixel(data, stride, x, y) != bg {
last = last.max(x);
}
}
}
last
}
let (layout, chrome) = sample_layout(TooltipBorder::None, None);
let b = layout.bounds;
let mut bold_tip = sample_tooltip();
bold_tip.styled_lines = Some(vec![StyledText {
spans: vec![StyledSpan {
bold: true,
..StyledSpan::plain("MMM")
}],
}]);
let mut regular_tip = sample_tooltip();
regular_tip.styled_lines = Some(vec![StyledText {
spans: vec![StyledSpan::plain("MMM")],
}]);
let (bold_data, stride) = paint(&bold_tip, &layout, &chrome);
let (regular_data, _) = paint(®ular_tip, &layout, &chrome);
let x_start = b.x as i32;
let x_end = (b.x + b.width) as i32;
let y_start = b.y as i32 + 2; let y_end = y_start + LINE_H as i32;
let bg = bg_reference(&bold_data, stride, b);
let bold_ink_x = last_ink_x(&bold_data, stride, bg, x_start, x_end, y_start, y_end);
let regular_ink_x = last_ink_x(®ular_data, stride, bg, x_start, x_end, y_start, y_end);
assert!(
bold_ink_x > regular_ink_x,
"bold span should paint wider than the same text at regular weight \
(bold_ink_x={bold_ink_x}, regular_ink_x={regular_ink_x})"
);
}
}