use windows::Win32::Graphics::Direct2D::ID2D1RenderTarget;
use super::text::DWrite;
use crate::event::Rect;
use crate::primitives::toolbar::{
measure_button, native_surface_paint, Toolbar, ToolbarItemMeasure, ToolbarLayout,
ToolbarPaintOptions,
};
use crate::theme::Theme;
use crate::types::WidgetId;
pub fn win_toolbar_layout(dwrite: &DWrite, rect: Rect, bar: &Toolbar) -> ToolbarLayout {
bar.layout(rect.x, rect.y, rect.width, rect.height, |btn| {
ToolbarItemMeasure::new(measure_button(dwrite, btn))
})
}
pub fn draw_toolbar(
target: &ID2D1RenderTarget,
dwrite: &DWrite,
rect: Rect,
bar: &Toolbar,
hovered_id: Option<&WidgetId>,
pressed_id: Option<&WidgetId>,
) -> ToolbarLayout {
draw_toolbar_with_options(
target,
dwrite,
rect,
bar,
hovered_id,
pressed_id,
ToolbarPaintOptions::default(),
)
}
#[allow(clippy::too_many_arguments)]
pub fn draw_toolbar_with_options(
target: &ID2D1RenderTarget,
dwrite: &DWrite,
rect: Rect,
bar: &Toolbar,
hovered_id: Option<&WidgetId>,
pressed_id: Option<&WidgetId>,
options: ToolbarPaintOptions,
) -> ToolbarLayout {
let layout = win_toolbar_layout(dwrite, rect, bar);
if rect.width <= 0.0 || rect.height <= 0.0 {
return layout;
}
let theme = Theme::default();
let mut surface = super::surface::D2dSurface {
target,
dwrite: Some(dwrite),
};
native_surface_paint::paint(
bar,
&layout,
&mut surface,
&theme,
hovered_id,
pressed_id,
options,
);
layout
}
#[cfg(test)]
mod tests {
use super::*;
use crate::primitives::toolbar::{ToolbarButton, ToolbarHit};
use crate::win::testing::HeadlessSurface;
const W: f32 = 240.0;
const H: f32 = 40.0;
fn mk_action(id: &str, label: &str, enabled: bool) -> ToolbarButton {
ToolbarButton::Action {
id: WidgetId::new(id),
label: label.into(),
icon: None,
key_hint: None,
enabled,
is_active: false,
tooltip: String::new(),
}
}
fn sample_bar() -> Toolbar {
Toolbar {
id: WidgetId::new("tb"),
buttons: vec![
mk_action("tb:refine", "Refine", true),
mk_action("tb:drop", "Drop", true),
],
bg: None,
focused_index: None,
}
}
fn paint_via_backend_at(bar: &Toolbar, x: f32, y: f32) -> ToolbarLayout {
let surface = HeadlessSurface::new(W as u32, H as u32).expect("create surface");
let (dwrite, _, _) = DWrite::new("Segoe UI", 10.0, None).expect("create DWrite");
let rect = Rect::new(x, y, W - x, H - y);
surface
.paint(|target| {
draw_toolbar_with_options(
target,
&dwrite,
rect,
bar,
None,
None,
ToolbarPaintOptions::default(),
);
})
.map(|_| win_toolbar_layout(&dwrite, rect, bar))
.expect("paint toolbar")
}
#[test]
fn round_trip_click_hits_enabled_button() {
let bar = sample_bar();
let layout = paint_via_backend_at(&bar, 0.0, 0.0);
assert_eq!(layout.visible_items.len(), 2);
let refine = &layout.visible_items[0];
let hit = layout.hit_test(
refine.bounds.x + refine.bounds.width * 0.5,
refine.bounds.y + refine.bounds.height * 0.5,
);
assert_eq!(
hit,
ToolbarHit::Button(WidgetId::new("tb:refine")),
"expected Refine button hit",
);
}
#[test]
fn round_trip_click_hits_enabled_button_at_nonzero_origin() {
let bar = sample_bar();
let origin_x = 7.0_f32;
let origin_y = 13.0_f32;
let layout = paint_via_backend_at(&bar, origin_x, origin_y);
let refine = &layout.visible_items[0];
assert!(
(refine.bounds.x - origin_x).abs() < 0.01,
"first button should start at origin_x={}, got {}",
origin_x,
refine.bounds.x,
);
assert!(
(refine.bounds.y - origin_y).abs() < 0.01,
"first button should start at origin_y={}, got {}",
origin_y,
refine.bounds.y,
);
let hit = layout.hit_test(
refine.bounds.x + refine.bounds.width * 0.5,
refine.bounds.y + refine.bounds.height * 0.5,
);
assert_eq!(
hit,
ToolbarHit::Button(WidgetId::new("tb:refine")),
"expected Refine button hit at non-zero origin",
);
}
#[test]
fn disabled_action_not_clickable_after_paint() {
let bar = Toolbar {
id: WidgetId::new("tb"),
buttons: vec![mk_action("a", "Refine", false)],
bg: None,
focused_index: None,
};
let layout = paint_via_backend_at(&bar, 0.0, 0.0);
let r = layout.visible_items[0].bounds;
assert_eq!(
layout.hit_test(r.x + 1.0, r.y + 1.0),
ToolbarHit::Empty,
"disabled action must not be clickable"
);
}
#[test]
fn separator_and_label_are_not_clickable() {
let bar = Toolbar {
id: WidgetId::new("tb"),
buttons: vec![
ToolbarButton::Separator,
ToolbarButton::Label {
text: "2 of 5".into(),
fg: None,
},
],
bg: None,
focused_index: None,
};
let layout = paint_via_backend_at(&bar, 0.0, 0.0);
assert!(!layout.visible_items[0].clickable);
assert!(!layout.visible_items[1].clickable);
let r = layout.visible_items[1].bounds;
assert_eq!(layout.hit_test(r.x, r.y), ToolbarHit::Empty);
}
#[test]
fn no_paint_layout_matches_paint_layout() {
let bar = sample_bar();
let rect = Rect::new(0.0, 0.0, W, H);
let (dwrite, _, _) = DWrite::new("Segoe UI", 10.0, None).expect("create DWrite");
let surface = HeadlessSurface::new(W as u32, H as u32).expect("create surface");
let painted = surface
.paint(|target| {
draw_toolbar_with_options(
target,
&dwrite,
rect,
&bar,
None,
None,
ToolbarPaintOptions::default(),
);
})
.map(|_| win_toolbar_layout(&dwrite, rect, &bar))
.expect("paint");
let no_paint = win_toolbar_layout(&dwrite, rect, &bar);
assert_eq!(painted, no_paint);
}
#[test]
fn active_button_highlight_has_rounded_corners_not_square() {
let bar = Toolbar {
id: WidgetId::new("tb"),
buttons: vec![ToolbarButton::Action {
id: WidgetId::new("tb:on"),
label: "On".into(),
icon: None,
key_hint: None,
enabled: true,
is_active: true,
tooltip: String::new(),
}],
bg: None,
focused_index: None,
};
let rect = Rect::new(0.0, 0.0, W, H);
let (dwrite, _, _) = DWrite::new("Segoe UI", 10.0, None).expect("create DWrite");
let surface = HeadlessSurface::new(W as u32, H as u32).expect("create surface");
surface
.paint(|target| {
draw_toolbar_with_options(
target,
&dwrite,
rect,
&bar,
None,
None,
ToolbarPaintOptions::default(),
);
})
.expect("paint toolbar");
let layout = win_toolbar_layout(&dwrite, rect, &bar);
let theme = Theme::default();
let item = &layout.visible_items[0].bounds;
let corner_x = (item.x + 2.0) as u32;
let corner_y = (item.y + 2.0) as u32;
let corner = surface.pixel_at(corner_x, corner_y);
assert_ne!(
(corner.r, corner.g, corner.b),
(
theme.selected_bg.r,
theme.selected_bg.g,
theme.selected_bg.b
),
"highlight corner pixel should NOT be filled — rounded corner (quadraui#1081), not square",
);
let cx = (item.x + 5.0) as u32;
let cy = (item.y + item.height / 2.0) as u32;
let centre = surface.pixel_at(cx, cy);
assert_eq!(
(centre.r, centre.g, centre.b),
(
theme.selected_bg.r,
theme.selected_bg.g,
theme.selected_bg.b
),
"highlight fill should be solid away from the corners and the label text",
);
}
#[test]
fn multi_row_valign_moves_painted_text_vertically() {
use crate::primitives::toolbar::ToolbarVAlign;
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 theme = Theme::default();
let rect = Rect::new(0.0, 0.0, W, H);
let (dwrite, _, _) = DWrite::new("Segoe UI", 10.0, None).expect("create DWrite");
let first_ink_row = |valign: ToolbarVAlign| -> u32 {
let surface = HeadlessSurface::new(W as u32, H as u32).expect("create surface");
surface
.paint(|target| {
draw_toolbar_with_options(
target,
&dwrite,
rect,
&bar,
None,
None,
ToolbarPaintOptions { valign },
);
})
.expect("paint toolbar");
for y in 0..H as u32 {
for x in 0..W as u32 {
let px = surface.pixel_at(x, y);
if (px.r, px.g, px.b)
!= (theme.header_bg.r, theme.header_bg.g, theme.header_bg.b)
{
return y;
}
}
}
panic!("no ink painted for valign {valign:?}");
};
let top_row = first_ink_row(ToolbarVAlign::Top);
let bottom_row = first_ink_row(ToolbarVAlign::Bottom);
assert!(
top_row < bottom_row,
"Top should paint higher than Bottom (top={top_row}, bottom={bottom_row})"
);
}
}