use windows::Win32::Graphics::Direct2D::ID2D1RenderTarget;
use super::text::DWrite;
use crate::event::Rect;
use crate::primitives::activity_bar::{native_surface_paint, ActivityBarStyle};
use crate::theme::Theme;
use crate::{ActivityBar, ActivityBarLayout, ActivityBarRowHit};
pub const ACTIVITY_ROW_DIP: f32 = 48.0;
pub fn win_activity_bar_layout(rect: Rect, bar: &ActivityBar) -> ActivityBarLayout {
bar.layout(rect.width, rect.height, ACTIVITY_ROW_DIP)
}
pub fn draw_activity_bar(
target: &ID2D1RenderTarget,
dwrite: &DWrite,
rect: Rect,
bar: &ActivityBar,
hovered_idx: Option<usize>,
theme: &Theme,
) -> Vec<ActivityBarRowHit> {
let layout = win_activity_bar_layout(rect, bar);
let style = ActivityBarStyle::default();
let icon_size_pt = style.resolved_icon_size_px() * 72.0 / 96.0;
let icon_dwrite = dwrite.with_size(icon_size_pt);
let icon_dwrite = icon_dwrite.as_ref().unwrap_or(dwrite);
let mut surface = super::surface::D2dSurface {
target,
dwrite: Some(icon_dwrite),
};
super::text::with_translation(target, rect.x, rect.y, || {
native_surface_paint::paint(
bar,
&layout,
&style,
&mut surface,
theme,
hovered_idx,
false,
)
})
}
#[cfg(test)]
mod tests {
use super::*;
use crate::primitives::activity_bar::{ActivityBarHit, ActivityItem};
use crate::types::{Color, WidgetId};
use crate::win::testing::HeadlessSurface;
use crate::ActivitySide;
const W: f32 = 48.0;
const H: f32 = 200.0;
fn bar() -> ActivityBar {
ActivityBar {
id: WidgetId::new("activity"),
top_items: vec![
ActivityItem {
id: WidgetId::new("activity:explorer"),
icon: "E".into(),
tooltip: "Explorer".into(),
is_active: true,
is_keyboard_selected: false,
},
ActivityItem {
id: WidgetId::new("activity:search"),
icon: "S".into(),
tooltip: "Search".into(),
is_active: false,
is_keyboard_selected: false,
},
],
bottom_items: vec![ActivityItem {
id: WidgetId::new("activity:settings"),
icon: "G".into(),
tooltip: "Settings".into(),
is_active: false,
is_keyboard_selected: false,
}],
active_accent: Some(Color::rgb(80, 140, 255)),
selection_bg: None,
is_keyboard_focused: false,
}
}
#[test]
fn paint_and_hit_test_round_trip() {
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 bar = bar();
let rect = Rect::new(0.0, 0.0, W, H);
surface
.paint(|target| {
draw_activity_bar(target, &dwrite, rect, &bar, None, &Theme::default());
})
.expect("paint activity bar");
let layout = win_activity_bar_layout(rect, &bar);
let accent = Color::rgb(80, 140, 255);
let mid_y = (ACTIVITY_ROW_DIP / 2.0) as u32;
let px = surface.pixel_at(0, mid_y);
assert_eq!(
(px.r, px.g, px.b),
(accent.r, accent.g, accent.b),
"active row's accent strip should be visible at x=0"
);
for item in bar.top_items.iter().chain(bar.bottom_items.iter()) {
let hit = layout
.visible_items
.iter()
.find(|vi| {
let candidate = match vi.side {
ActivitySide::Top => &bar.top_items[vi.item_idx],
ActivitySide::Bottom => &bar.bottom_items[vi.item_idx],
};
candidate.id == item.id
})
.expect("item is visible");
let cy = hit.bounds.y + hit.bounds.height / 2.0;
assert_eq!(
layout.hit_test(1.0, cy),
ActivityBarHit::Item(item.id),
"row centre for {:?} should hit-test back to itself",
item.id,
);
}
}
#[test]
fn hit_regions_are_bar_relative_not_absolute() {
let (dwrite, _, _) = DWrite::new("Segoe UI", 10.0, None).expect("create DWrite");
let bar = bar();
let surface = HeadlessSurface::new(W as u32, (H + 40.0) as u32).expect("create surface");
let rect = Rect::new(0.0, 40.0, W, H);
let mut hits = None;
surface
.paint(|target| {
hits = Some(draw_activity_bar(
target,
&dwrite,
rect,
&bar,
None,
&Theme::default(),
));
})
.expect("paint");
let hits = hits.expect("draw_activity_bar ran");
let explorer_id = bar.top_items[0].id;
let explorer_hit = hits
.iter()
.find(|h| h.id == explorer_id)
.expect("explorer row is visible");
assert_eq!(
explorer_hit.y_start, 0.0,
"the topmost row must start at bar-relative y=0 regardless of rect.y"
);
}
#[test]
fn right_edge_has_separator_pixel() {
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 bar = bar();
let rect = Rect::new(0.0, 0.0, W, H);
let theme = Theme::default();
surface
.paint(|target| {
draw_activity_bar(target, &dwrite, rect, &bar, None, &theme);
})
.expect("paint activity bar");
let px = surface.pixel_at(W as u32 - 1, H as u32 / 2);
assert_eq!(
(px.r, px.g, px.b),
(theme.separator.r, theme.separator.g, theme.separator.b),
"right-edge column should paint theme.separator (quadraui#1081 \
— this used to be a no-op on Windows)",
);
}
#[test]
fn icon_ink_height_does_not_track_the_callers_dwrite_size() {
let bar = bar();
let rect = Rect::new(0.0, 0.0, W, H);
let theme = Theme::default();
let ink_height = |dwrite_size_pt: f32| -> u32 {
let surface = HeadlessSurface::new(W as u32, H as u32).expect("create surface");
let (dwrite, _, _) =
DWrite::new("Segoe UI", dwrite_size_pt, None).expect("create DWrite");
surface
.paint(|target| {
draw_activity_bar(target, &dwrite, rect, &bar, None, &theme);
})
.expect("paint activity bar");
let bg = theme.tab_bar_bg;
let y0 = H as u32 - ACTIVITY_ROW_DIP as u32;
let y1 = H as u32;
let mut found: Option<(u32, u32)> = None;
for y in y0..y1 {
for x in 0..(W as u32 - 1) {
let px = surface.pixel_at(x, y);
if (px.r, px.g, px.b) != (bg.r, bg.g, bg.b) {
found = Some(match found {
Some((min_y, max_y)) => (min_y.min(y), max_y.max(y)),
None => (y, y),
});
}
}
}
found.map_or(0, |(min_y, max_y)| max_y - min_y + 1)
};
let small_caller = ink_height(6.0);
let huge_caller = ink_height(40.0);
assert!(small_caller > 0, "sanity: the glyph should paint some ink");
assert_eq!(
small_caller, huge_caller,
"icon ink height must not track the caller's DWrite font size \
(6pt vs 40pt should paint identically)"
);
}
#[test]
fn paints_at_rect_origin_not_at_surface_origin() {
const ORIGIN_X: f32 = 10.0;
const ORIGIN_Y: f32 = 6.0;
let surface = HeadlessSurface::new((W + ORIGIN_X) as u32, (H + ORIGIN_Y) as u32)
.expect("create surface");
let (dwrite, _, _) = DWrite::new("Segoe UI", 10.0, None).expect("create DWrite");
let bar = bar();
let rect = Rect::new(ORIGIN_X, ORIGIN_Y, W, H);
let theme = Theme::default();
surface
.paint(|target| {
draw_activity_bar(target, &dwrite, rect, &bar, None, &theme);
})
.expect("paint activity bar");
let accent = Color::rgb(80, 140, 255);
let mid_y = ORIGIN_Y as u32 + (ACTIVITY_ROW_DIP / 2.0) as u32;
let px = surface.pixel_at(ORIGIN_X as u32, mid_y);
assert_eq!(
(px.r, px.g, px.b),
(accent.r, accent.g, accent.b),
"active row's accent strip should shift with rect's own origin",
);
let corner = surface.pixel_at(0, 0);
assert_ne!(
(corner.r, corner.g, corner.b),
(theme.tab_bar_bg.r, theme.tab_bar_bg.g, theme.tab_bar_bg.b),
"activity bar background must not leak past rect's own origin",
);
}
}