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
//! Direct2D / DirectWrite rasteriser for [`crate::Palette`].
//!
//! Content painting (background, border, title/query/item rows,
//! scrollbar, create row, preview pane) uses the shared
//! [`crate::primitives::palette::native_surface_paint::paint`]
//! function — see that fn's module doc for what's shared, including
//! geometry handling and per-backend feature coverage. Windows gains
//! icon rendering, a query cursor, and `PaletteMode::Input` handling.
//!
//! Only compiled on `target_os = "windows"` — see `super::mod`'s
//! `#[cfg(target_os = "windows")] mod palette;` and `backend.rs`'s
//! module docs.
use windows::Win32::Graphics::Direct2D::ID2D1RenderTarget;
use super::text::DWrite;
use crate::event::Rect;
use crate::primitives::palette::{native_surface_paint, Palette, PaletteLayout};
/// Compute a [`Palette`]'s layout at `(rect.x, rect.y)` without
/// painting. Thin wrapper over
/// [`crate::primitives::palette::native_surface_paint::layout`]
/// — the canonical geometry formula every backend now shares. See that
/// fn's doc for the three-way drift this closes (`query_height`, row
/// flooring, scrollbar width). `Palette::layout` keeps the selected row
/// visible internally — no backend-side scroll clamp needed
/// here.
pub fn win_palette_layout(rect: Rect, palette: &Palette, line_height: f32) -> PaletteLayout {
native_surface_paint::layout(rect.width, rect.height, palette, line_height).0
}
/// Draw a [`Palette`] modal into `rect`. Backend-internal layout (see
/// [`win_palette_layout`]) — `Palette` has no layout-passthrough trait
/// method (unlike `ContextMenu`/`Dialog`), matching
/// [`crate::Backend::draw_palette`]'s signature.
///
/// `nerd_fonts_enabled` selects between item icons' Nerd-Font glyph and
/// ASCII fallback, matching every other backend's
/// `draw_palette`. Colours come from `Theme::default()` rather than a
/// live `WinBackend` theme field — same convention `win::status_bar`
/// and every other pre-`PaintSurface` Win-GUI rasteriser use.
pub fn draw_palette(
target: &ID2D1RenderTarget,
dwrite: &DWrite,
rect: Rect,
palette: &Palette,
line_height: f32,
nerd_fonts_enabled: bool,
) {
if rect.width < 20.0 || rect.height < line_height * 4.0 {
return;
}
let theme = crate::theme::Theme::default();
let (palette_layout, rows_h) =
native_surface_paint::layout(rect.width, rect.height, palette, line_height);
let mut surface = super::surface::D2dSurface {
target,
dwrite: Some(dwrite),
};
native_surface_paint::paint(
palette,
rect,
&palette_layout,
rows_h,
line_height,
nerd_fonts_enabled,
&mut surface,
&theme,
&crate::style::Style::default(),
);
}
#[cfg(test)]
mod tests {
use super::*;
use crate::primitives::palette::PaletteMode;
use crate::theme::Theme;
use crate::types::{Color, StyledText, WidgetId};
use crate::win::testing::HeadlessSurface;
fn item(text: &str, match_positions: Vec<usize>) -> crate::primitives::palette::PaletteItem {
crate::primitives::palette::PaletteItem {
text: StyledText::plain(text),
detail: None,
icon: None,
match_positions,
depth: 0,
expandable: false,
expanded: false,
}
}
fn palette() -> Palette {
Palette {
id: WidgetId::new("pal"),
title: "Commands".into(),
query: "op".into(),
query_cursor: 2,
items: vec![item("open file", vec![0, 1]), item("close", vec![])],
selected_idx: 0,
scroll_offset: 0,
total_count: 0,
has_focus: true,
show_query: true,
create_label: None,
preview: None,
mode: PaletteMode::List,
}
}
#[test]
fn renders_and_highlights_match_positions() {
let surface = HeadlessSurface::new(300, 200).expect("create surface");
let (dwrite, _, _) = DWrite::new("Segoe UI", 10.0, None).expect("create DWrite");
let p = palette();
let rect = Rect::new(0.0, 0.0, 300.0, 200.0);
let line_height = 18.0;
surface
.paint(|target| {
draw_palette(target, &dwrite, rect, &p, line_height, false);
})
.expect("paint palette");
let layout = win_palette_layout(rect, &p, line_height);
let first_item = layout.visible_items[0].bounds;
let theme = Theme::default();
// Scan the whole first-item row box for match-highlighted ink —
// matched runs ('o', 'p' at byte offsets 0, 1 of "open file")
// paint in `match_fg`, warm and far from every other colour on
// that row. Two deliberate weakenings versus an exact-equality
// probe at a guessed coordinate: the scan covers the row's full
// height (real DirectWrite glyph placement inside the row box
// isn't predictable ahead of a live Windows font pass), and a
// pixel counts when it is *nearer* `match_fg` than the row's
// other two colours rather than exactly equal to it, since
// glyph edges are antialiased blends over the selection fill.
let dist2 = |px: Color, c: Color| {
let dr = px.r as i32 - c.r as i32;
let dg = px.g as i32 - c.g as i32;
let db = px.b as i32 - c.b as i32;
dr * dr + dg * dg + db * db
};
let found = (first_item.y as u32..(first_item.y + first_item.height) as u32).any(|y| {
(first_item.x as u32..(first_item.x + first_item.width) as u32).any(|x| {
let px = surface.pixel_at(x, y);
let d_match = dist2(px, theme.match_fg);
d_match < dist2(px, theme.surface_fg) && d_match < dist2(px, theme.selected_bg)
})
});
assert!(
found,
"expected to find match_fg-coloured pixels on the matched item's row"
);
}
/// The shared `paint` now fills a cursor block in `query_fg` on
/// every backend, including Windows.
#[test]
fn query_row_paints_a_cursor_block() {
let surface = HeadlessSurface::new(300, 200).expect("create surface");
let (dwrite, _, _) = DWrite::new("Segoe UI", 10.0, None).expect("create DWrite");
let p = palette();
let rect = Rect::new(0.0, 0.0, 300.0, 200.0);
let line_height = 18.0;
surface
.paint(|target| {
draw_palette(target, &dwrite, rect, &p, line_height, false);
})
.expect("paint palette");
let layout = win_palette_layout(rect, &p, line_height);
let qb = layout.query_bounds.expect("query bounds present");
let theme = Theme::default();
// A cursor is a *solid fill* spanning the row's full height —
// unlike glyph ink (which never covers a whole text-row column
// solidly, since real fonts leave leading above ascenders and
// below descenders), so scan for the column with the highest
// fraction of query_fg-near pixels and require it to be nearly
// fully covered.
let dist2 = |px: Color, c: Color| {
let dr = px.r as i32 - c.r as i32;
let dg = px.g as i32 - c.g as i32;
let db = px.b as i32 - c.b as i32;
dr * dr + dg * dg + db * db
};
let rows: Vec<u32> = (qb.y as u32..(qb.y + qb.height) as u32).collect();
let best_coverage = (qb.x as u32..(qb.x + qb.width) as u32)
.map(|x| {
let covered = rows
.iter()
.filter(|&&y| dist2(surface.pixel_at(x, y), theme.query_fg) < 400)
.count();
covered as f32 / rows.len().max(1) as f32
})
.fold(0.0_f32, f32::max);
assert!(
best_coverage > 0.9,
"expected a full-row-height query_fg cursor block; best column coverage was {best_coverage}"
);
}
/// The query row height uses a formula that reserves space for a
/// separator pixel at the bottom: `line_height + 1.0`. The separator
/// stroke sits in the query band's last pixel, and the first item row
/// moves down by 1.0 pixel accordingly.
///
/// Paints through the real Direct2D `HeadlessSurface` and pins all
/// three rows: the reserved separator pixel carries border ink, the
/// query text row above it does not, and the first item row below it
/// carries the selected row's fill — i.e. the separator neither
/// climbs into the text row nor steals (and get overpainted by) the
/// first item row.
#[test]
fn separator_paints_at_corrected_row_not_drifted_one() {
let surface = HeadlessSurface::new(300, 200).expect("create surface");
let (dwrite, _, _) = DWrite::new("Segoe UI", 10.0, None).expect("create DWrite");
let p = palette();
let rect = Rect::new(0.0, 0.0, 300.0, 200.0);
let line_height = 18.0_f32;
surface
.paint(|target| {
draw_palette(target, &dwrite, rect, &p, line_height, false);
})
.expect("paint palette");
let theme = Theme::default();
let layout = win_palette_layout(rect, &p, line_height);
let qb = layout.query_bounds.expect("query bounds present");
assert_eq!(
qb.height,
line_height + 1.0,
"query band must reserve line_height + 1.0 (the +1 is the separator)",
);
let title_h = line_height; // native_surface_paint::layout: title_h = line_height
let sep_y = (qb.y + qb.height - 1.0) as u32; // corrected: the reserved pixel
let first_row_y = (qb.y + qb.height) as u32; // This was sep_y
assert_eq!(sep_y, (title_h + line_height) as u32);
assert_eq!(first_row_y, (title_h + line_height + 1.0) as u32);
assert_eq!(
layout.visible_items[0].bounds.y,
qb.y + qb.height,
"first item row starts immediately below the query band",
);
// `p`'s row 0 is the selected one, so the first item row paints
// `selected_bg` — the fill that must not overwrite the separator.
let probe_x = 150_u32;
let is_border = |y: u32| {
let px = surface.pixel_at(probe_x, y);
(px.r, px.g, px.b) == (theme.border_fg.r, theme.border_fg.g, theme.border_fg.b)
};
assert!(
is_border(sep_y),
"expected separator ink in the query band's reserved pixel, row {sep_y}",
);
assert!(
!is_border(sep_y - 1),
"separator climbed into the query *text* row {} — it belongs in the \
single pixel query_bounds reserves for it",
sep_y - 1,
);
let first_row_px = surface.pixel_at(probe_x, first_row_y);
assert_eq!(
(first_row_px.r, first_row_px.g, first_row_px.b),
(
theme.selected_bg.r,
theme.selected_bg.g,
theme.selected_bg.b
),
"row {first_row_y} is the first item row (selected), not the separator's",
);
}
/// The item list flooring: when available item-list height is not an
/// exact multiple of `line_height`, the row count is floored and a
/// reduced `viewport_height` is used to prevent partial-height last
/// rows. The rect height (200) and `line_height` (18) are chosen so
/// the available item area (159px after title/query/bottom-inset) is
/// not an exact multiple of 18 — the condition that produces a partial
/// last row without flooring.
#[test]
fn item_list_never_shows_a_partial_last_row() {
let surface = HeadlessSurface::new(300, 200).expect("create surface");
let (dwrite, _, _) = DWrite::new("Segoe UI", 10.0, None).expect("create DWrite");
let mut p = palette();
p.items = (0..40)
.map(|i| item(&format!("item {i}"), vec![]))
.collect();
let rect = Rect::new(0.0, 0.0, 300.0, 200.0);
let line_height = 18.0_f32;
surface
.paint(|target| {
draw_palette(target, &dwrite, rect, &p, line_height, false);
})
.expect("paint palette");
let layout = win_palette_layout(rect, &p, line_height);
let last = layout
.visible_items
.last()
.expect("some items should be visible");
assert_eq!(
last.bounds.height, line_height,
"last visible row must be full height — a partial row means the \
pre-#1076 unfloored-viewport drift regressed",
);
// Driver-tier: paint through the real Direct2D backend and
// confirm no ink bleeds into the row immediately below the
// floored boundary — a partial 9th row would have painted glyph
// ink there.
let theme = Theme::default();
let boundary_y = (last.bounds.y + last.bounds.height + 1.0) as u32;
let probe_x = (last.bounds.x + 4.0) as u32;
let px = surface.pixel_at(probe_x, boundary_y);
assert_eq!(
(px.r, px.g, px.b),
(theme.surface_bg.r, theme.surface_bg.g, theme.surface_bg.b),
"expected plain surface_bg just below the floored last row, not a \
partially-clipped row's content",
);
}
#[test]
fn selected_row_paints_selection_bg() {
let surface = HeadlessSurface::new(300, 200).expect("create surface");
let (dwrite, _, _) = DWrite::new("Segoe UI", 10.0, None).expect("create DWrite");
let p = palette();
let rect = Rect::new(0.0, 0.0, 300.0, 200.0);
let line_height = 18.0;
surface
.paint(|target| {
draw_palette(target, &dwrite, rect, &p, line_height, false);
})
.expect("paint palette");
let layout = win_palette_layout(rect, &p, line_height);
let first_item = layout.visible_items[0].bounds;
let sel = Theme::default().selected_bg;
let px = surface.pixel_at(
(first_item.x + first_item.width - 2.0) as u32,
(first_item.y + 1.0) as u32,
);
assert_eq!((px.r, px.g, px.b), (sel.r, sel.g, sel.b));
}
}