1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
//! GTK rasteriser for [`crate::primitives::command_line::CommandLine`].
//!
//! Painting is implemented via the shared
//! [`crate::primitives::command_line::native_surface_paint::paint`].
//! This module carries [`gtk_command_line_layout`] (pure measurement,
//! used directly by `GtkBackend::command_line_layout`) and thin
//! wrappers over the shared paint, using
//! [`crate::gtk::surface::CairoSurface`] as the `PaintSurface` adapter.
use gtk4::cairo::Context;
use gtk4::pango;
use crate::primitives::command_line::{CommandLine, CommandLineLayout, CommandLineMeasure};
use crate::theme::Theme;
/// Compute [`CommandLineLayout`] for `cmd` painted at
/// `(x, y, width, line_height)`, using the backend's monospace
/// `char_width`.
pub fn gtk_command_line_layout(
cmd: &CommandLine,
x: f64,
y: f64,
width: f64,
line_height: f64,
char_width: f32,
) -> CommandLineLayout {
let rect = crate::event::Rect::new(x as f32, y as f32, width as f32, line_height as f32);
cmd.layout(rect, CommandLineMeasure::new(char_width))
}
/// Paint `cmd` into `cr` at `(x, y, width, line_height)` and return the
/// [`CommandLineLayout`] used to place its glyphs — the same value
/// `gtk_command_line_layout` would compute for the same arguments, handed
/// back so callers (and tests) can rely on a consistent layout without
/// recomputing it.
#[allow(clippy::too_many_arguments)]
pub fn draw_command_line(
cr: &Context,
layout: &pango::Layout,
cmd: &CommandLine,
theme: &Theme,
x: f64,
y: f64,
width: f64,
line_height: f64,
char_width: f32,
) -> CommandLineLayout {
draw_command_line_selection(
cr,
layout,
cmd,
theme,
x,
y,
width,
line_height,
char_width,
None,
)
}
/// Paint `cmd` exactly like [`draw_command_line`], additionally painting
/// a selection highlight behind the text for `selection`.
///
/// A sibling function, not a new parameter on `draw_command_line` — that
/// keeps `draw_command_line`'s signature stable for any existing caller
/// (`CommandLine` itself gained no new field, for the same reason; see
/// `crate::primitives::command_line`'s module doc and
/// `crate::primitives::text_input`'s "Why the editing state is a
/// wrapper" section for the precedent this follows).
#[allow(clippy::too_many_arguments)]
pub fn draw_command_line_selection(
cr: &Context,
layout: &pango::Layout,
cmd: &CommandLine,
theme: &Theme,
x: f64,
y: f64,
width: f64,
line_height: f64,
char_width: f32,
selection: Option<(usize, usize)>,
) -> CommandLineLayout {
let mut surface = super::surface::CairoSurface {
cr,
layout: Some(layout),
translucent_fill: true,
};
let rect = crate::event::Rect::new(x as f32, y as f32, width as f32, line_height as f32);
crate::primitives::command_line::native_surface_paint::paint(
cmd,
&mut surface,
theme,
rect,
char_width,
selection,
)
}
#[cfg(test)]
mod tests {
use super::*;
use crate::types::{Color, WidgetId};
use pangocairo::cairo::{Context, Format, ImageSurface};
/// Regression test: typing a command with a multibyte character left of
/// the cursor used to panic when `cursor_offset` landed mid-character.
#[test]
fn draw_command_line_with_multibyte_cursor_does_not_panic() {
let surface = ImageSurface::create(Format::ARgb32, 400, 30).expect("create ImageSurface");
let cr = Context::new(&surface).expect("Context::new");
let pango_layout = pangocairo::functions::create_layout(&cr);
let theme = Theme::default();
// ":éditer" — byte 2 sits inside the 2-byte 'é' (starts at byte 1).
let text = ":éditer";
assert!(!text.is_char_boundary(2));
let cmd = CommandLine {
id: WidgetId::new("cmdline"),
text: text.into(),
cursor_offset: Some(2),
right_align: false,
};
// Must not panic.
draw_command_line(&cr, &pango_layout, &cmd, &theme, 0.0, 0.0, 400.0, 20.0, 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;
// Cairo ARGB32 byte order on little-endian is BGRA.
(data[off + 2], data[off + 1], data[off])
}
fn is_painted(data: &[u8], stride: usize, x: i32, y: i32) -> bool {
let (r, g, b) = pixel(data, stride, x, y);
!(r == 255 && g == 255 && b == 255)
}
/// Find any painted pixel within (x_range, y_range), row-major.
fn first_painted_in(
data: &[u8],
stride: usize,
w: i32,
h: i32,
x_range: (i32, i32),
y_range: (i32, i32),
) -> Option<(i32, i32)> {
for y in y_range.0..y_range.1 {
for x in x_range.0..x_range.1 {
if x < 0 || y < 0 || x >= w || y >= h {
continue;
}
if is_painted(data, stride, x, y) {
return Some((x, y));
}
}
}
None
}
/// Paint/click round-trip: paint via `draw_command_line` into a headless
/// Cairo surface, find the actual painted (non-background) pixel for
/// each of two characters, then `hit_test` those exact pixels and
/// assert they resolve to the correct byte offsets. This catches
/// `draw_command_line`'s real Pango glyph placement matching
/// `CommandLine::layout`'s fixed-advance column formula.
///
/// A monospace font is forced so the fixed-advance grid
/// `CommandLine::layout` assumes actually matches what Pango paints;
/// `char_width` is measured from that same font rather than assumed.
///
/// A LOCAL/ABSOLUTE mixup is invisible at `x == 0`, so this is
/// exercised at a nonzero origin too.
#[test]
fn gtk_command_line_paint_and_click_round_trip_at_nonzero_origin() {
const W: i32 = 400;
const H: i32 = 40;
let mut surface = ImageSurface::create(Format::ARgb32, W, H).expect("create ImageSurface");
let font = pango::FontDescription::from_string("Monospace 12");
let theme = Theme {
command_line_bg: Color::rgb(255, 255, 255),
command_line_fg: Color::rgb(0, 0, 0),
..Theme::default()
};
let cmd = CommandLine {
id: WidgetId::new("cmdline"),
text: ":wq".into(),
cursor_offset: None,
right_align: false,
};
let (x, y, width, line_height) = (40.0, 12.0, 200.0, 20.0_f64);
// All painting happens in this block, so `cr` (and the
// `pango::Layout`s it owns a reference through) is dropped
// before `surface.data()` needs exclusive access below.
let (char_width, layout) = {
let cr = Context::new(&surface).expect("Context::new");
// White background so any non-white pixel uniquely identifies
// painted ink (mirrors `gtk::tree`'s round-trip harness).
cr.set_source_rgb(1.0, 1.0, 1.0);
cr.paint().ok();
// Measure the font's real per-character advance with a
// throwaway layout, distinct from the one `draw_command_line`
// paints with — so a bug that corrupts the paint layout's
// state can't accidentally make the "expected" and "actual"
// values agree for the wrong reason (mirrors `gtk::editor`'s
// truth/probe-layout split).
let measure_layout = pangocairo::functions::create_layout(&cr);
measure_layout.set_font_description(Some(&font));
measure_layout.set_text(":wq");
let char_width = measure_layout.index_to_pos(0).width() as f32 / pango::SCALE as f32;
assert!(
char_width > 1.0,
"monospace char_width should be several px"
);
let pango_layout = pangocairo::functions::create_layout(&cr);
pango_layout.set_font_description(Some(&font));
let layout = draw_command_line(
&cr,
&pango_layout,
&cmd,
&theme,
x,
y,
width,
line_height,
char_width,
);
(char_width, layout)
};
let stride = surface.stride() as usize;
let data = surface.data().expect("surface data");
// Column 0 (':') interior — inset 1px from each edge to dodge
// antialiasing at the cell boundary (see `gtk::multi_section_view`'s
// header round-trip for the same AA rationale).
let col_y_range = (y as i32 + 1, (y + line_height) as i32 - 1);
let col0_x_range = (x as i32 + 1, (x + char_width as f64).floor() as i32);
let (px0, _) = first_painted_in(&data, stride, W, H, col0_x_range, col_y_range)
.unwrap_or_else(|| panic!("column 0 (':') painted no pixel in {col0_x_range:?}"));
assert_eq!(layout.hit_test(px0 as f32), 0);
// Column 1 ('w').
let col1_x_range = (
(x + char_width as f64).ceil() as i32 + 1,
(x + 2.0 * char_width as f64).floor() as i32,
);
let (px1, _) = first_painted_in(&data, stride, W, H, col1_x_range, col_y_range)
.unwrap_or_else(|| panic!("column 1 ('w') painted no pixel in {col1_x_range:?}"));
assert_eq!(layout.hit_test(px1 as f32), 1);
// A click left of the bar clamps to the first column's byte offset.
assert_eq!(layout.hit_test(0.0), 0);
assert_eq!(layout.hit_test(x as f32), 0);
}
/// `draw_command_line_selection` actually paints the highlight rect
/// `CommandLineLayout::selection_bounds` computes — the
/// gap this issue closes: the geometry existed but nothing painted
/// it. Selects a *space* character deliberately, so the probed pixel
/// carries no glyph ink and the highlight colour (alpha 1.0, so an
/// exact replace over the white background) can be asserted exactly
/// instead of guessing around antialiased text edges.
#[test]
fn gtk_command_line_selection_paints_highlight_behind_text() {
const W: i32 = 400;
const H: i32 = 40;
let mut surface = ImageSurface::create(Format::ARgb32, W, H).expect("create ImageSurface");
let font = pango::FontDescription::from_string("Monospace 12");
let theme = Theme {
command_line_bg: Color::rgb(255, 255, 255),
command_line_fg: Color::rgb(0, 0, 0),
selection: Color::rgb(0, 0, 255),
selection_alpha: 1.0,
..Theme::default()
};
// Two leading spaces then a letter: selecting byte 0..1 highlights
// a space (no ink), and byte 1..2 (the second space) stays an
// unselected but equally ink-free control pixel.
let cmd = CommandLine {
id: WidgetId::new("cmdline"),
text: " x".into(),
cursor_offset: None,
right_align: false,
};
let (x, y, width, line_height) = (40.0, 12.0, 200.0, 20.0_f64);
let (char_width, cmd_layout) = {
let cr = Context::new(&surface).expect("Context::new");
cr.set_source_rgb(1.0, 1.0, 1.0);
cr.paint().ok();
let measure_layout = pangocairo::functions::create_layout(&cr);
measure_layout.set_font_description(Some(&font));
measure_layout.set_text(" x");
let char_width = measure_layout.index_to_pos(0).width() as f32 / pango::SCALE as f32;
assert!(
char_width > 1.0,
"monospace char_width should be several px"
);
let pango_layout = pangocairo::functions::create_layout(&cr);
pango_layout.set_font_description(Some(&font));
let cmd_layout = draw_command_line_selection(
&cr,
&pango_layout,
&cmd,
&theme,
x,
y,
width,
line_height,
char_width,
Some((0, 1)),
);
(char_width, cmd_layout)
};
// Sanity: the layout agrees the highlight should cover column 0
// only.
let sel_rect = cmd_layout
.selection_bounds((0, 1))
.expect("non-empty selection should produce a rect");
assert_eq!(
(sel_rect.x as f64, sel_rect.width as f64),
(x, char_width as f64)
);
let stride = surface.stride() as usize;
let data = surface.data().expect("surface data");
let row = (y + line_height / 2.0) as i32;
// Selected column (first space): exact highlight colour, alpha
// 1.0 over white is an opaque replace.
let sel_x = x as i32 + (char_width / 2.0) as i32;
assert_eq!(
pixel(&data, stride, sel_x, row),
(0, 0, 255),
"selected space column should paint the highlight colour"
);
// Unselected column (second space): still plain background.
let unsel_x = (x + char_width as f64) as i32 + (char_width / 2.0) as i32;
assert_eq!(
pixel(&data, stride, unsel_x, row),
(255, 255, 255),
"unselected column should not paint the highlight"
);
// `draw_command_line` (no selection) must remain unaffected —
// same pixel stays plain background when selection is `None`.
let mut surface2 = ImageSurface::create(Format::ARgb32, W, H).expect("create ImageSurface");
{
let cr = Context::new(&surface2).expect("Context::new");
cr.set_source_rgb(1.0, 1.0, 1.0);
cr.paint().ok();
let pango_layout = pangocairo::functions::create_layout(&cr);
pango_layout.set_font_description(Some(&font));
draw_command_line(
&cr,
&pango_layout,
&cmd,
&theme,
x,
y,
width,
line_height,
char_width,
);
}
let stride2 = surface2.stride() as usize;
let data2 = surface2.data().expect("surface data");
assert_eq!(
pixel(&data2, stride2, sel_x, row),
(255, 255, 255),
"draw_command_line (no selection arg) must not paint a highlight"
);
}
}