use gtk4::cairo::Context;
use gtk4::pango;
use pangocairo::functions as pcfn;
use crate::Rect;
pub(crate) fn show_layout(cr: &Context, layout: &pango::Layout) {
if crate::testing::text_run_sink_active() {
if let Ok((ux, uy)) = cr.current_point() {
let text = layout.text();
if !text.trim().is_empty() {
let (w_px, h_px) = layout.pixel_size();
let (dx, dy) = cr.user_to_device(ux, uy);
let (dw, dh) = cr
.user_to_device_distance(w_px as f64, h_px as f64)
.unwrap_or((w_px as f64, h_px as f64));
crate::testing::record_text_run(
text.as_str(),
Rect::new(dx as f32, dy as f32, dw as f32, dh as f32),
);
}
}
}
pcfn::show_layout(cr, layout);
}
#[cfg(test)]
mod tests {
use crate::backend::Backend;
use crate::gtk::backend::GtkBackend;
use crate::gtk::testing::GtkDriver;
use crate::primitives::status_bar::{StatusBar, StatusBarSegment};
use crate::runner::{AppLogic, Reaction};
use crate::types::{Color, WidgetId};
use crate::{InteractionState, Rect, UiEvent};
use pangocairo::cairo::{Context, Format, ImageSurface};
const W: i32 = 200;
const H: i32 = 60;
const ROW_H: f32 = 20.0;
struct StackedBarsApp;
impl AppLogic for StackedBarsApp {
type AreaId = ();
fn render(&self, backend: &mut dyn Backend, _area: ()) {
for (i, label) in ["row zero", "row one"].into_iter().enumerate() {
backend.draw_status_bar_interactive(
Rect::new(0.0, i as f32 * ROW_H, W as f32, ROW_H),
&StatusBar {
id: WidgetId::new(format!("row-{i}")),
left_segments: vec![StatusBarSegment {
text: label.to_string(),
fg: Color::rgb(255, 255, 255),
bg: Color::rgb(20, 20, 20),
bold: false,
action_id: None,
}],
right_segments: vec![],
},
&InteractionState::new(),
);
}
}
fn handle(&mut self, _event: UiEvent, _backend: &mut dyn Backend) -> Reaction {
Reaction::Continue
}
}
#[test]
fn recording_is_off_unless_explicitly_enabled() {
let surface = ImageSurface::create(Format::ARgb32, W, H).expect("surface");
let cr = Context::new(&surface).expect("context");
let mut backend = GtkBackend::new();
crate::gtk::run::render_frame(&mut backend, &StackedBarsApp, &cr, W, H);
let without = backend.painted_text_for_test().len();
backend.set_painted_text_recording(true);
crate::gtk::run::render_frame(&mut backend, &StackedBarsApp, &cr, W, H);
let with = backend.painted_text_for_test().len();
assert_eq!(
without, 2,
"with recording off only the trait method's own records should land"
);
assert!(
with > without,
"enabling recording should add the frame's painted glyph runs \
({with} vs {without})"
);
}
#[test]
fn logical_records_precede_glyph_runs_in_the_map() {
let driver = GtkDriver::new(StackedBarsApp, W, H);
let row0 = driver.find_bounds("row zero").expect("row zero painted");
let row1 = driver.find_bounds("row one").expect("row one painted");
assert_eq!(
row0.y, 0.0,
"row 0 should resolve to `draw_status_bar`'s own record at the bar's y"
);
assert_eq!(
row1.y, ROW_H,
"row 1 should resolve to its own bar's y, not row 0's"
);
assert_eq!(
driver
.painted_texts()
.iter()
.filter(|t| t.contains("row zero"))
.count(),
2,
"expected one logical record plus one glyph run: {:?}",
driver.painted_texts()
);
}
#[test]
fn translated_paints_record_absolute_coordinates() {
let surface = ImageSurface::create(Format::ARgb32, W, H).expect("surface");
let cr = Context::new(&surface).expect("context");
let layout = pangocairo::functions::create_layout(&cr);
layout.set_text("shifted");
let prev = crate::testing::install_text_run_sink();
cr.save().ok();
cr.translate(30.0, 12.0);
cr.move_to(5.0, 3.0);
super::show_layout(&cr, &layout);
cr.restore().ok();
let runs = crate::testing::take_text_run_sink(prev);
assert_eq!(runs.len(), 1, "one run should have been recorded");
let run = &runs[0];
assert_eq!(run.text, "shifted");
assert_eq!(
(run.bounds.x, run.bounds.y),
(35.0, 15.0),
"translate must be folded into the recorded origin"
);
assert!(run.bounds.width > 0.0 && run.bounds.height > 0.0);
}
#[test]
fn whitespace_only_runs_are_not_recorded() {
let surface = ImageSurface::create(Format::ARgb32, W, H).expect("surface");
let cr = Context::new(&surface).expect("context");
let layout = pangocairo::functions::create_layout(&cr);
let prev = crate::testing::install_text_run_sink();
for text in ["", " ", "real"] {
layout.set_text(text);
cr.move_to(0.0, 0.0);
super::show_layout(&cr, &layout);
}
let runs = crate::testing::take_text_run_sink(prev);
assert_eq!(runs.len(), 1, "only the non-blank run should record");
assert_eq!(runs[0].text, "real");
}
#[test]
fn painting_with_no_sink_installed_is_a_no_op_for_recording() {
let surface = ImageSurface::create(Format::ARgb32, W, H).expect("surface");
let cr = Context::new(&surface).expect("context");
let layout = pangocairo::functions::create_layout(&cr);
layout.set_text("unrecorded");
cr.move_to(0.0, 0.0);
super::show_layout(&cr, &layout);
let prev = crate::testing::install_text_run_sink();
let runs = crate::testing::take_text_run_sink(prev);
assert!(
runs.is_empty(),
"a run painted before the sink existed must not leak into it: {runs:?}"
);
}
}