use windows::Win32::Graphics::Direct2D::ID2D1RenderTarget;
use super::text::DWrite;
use crate::event::Rect;
use crate::primitives::layout_metrics::TextMeasure;
use crate::primitives::tree::{TreeView, TreeViewLayout};
use crate::theme::Theme;
pub fn win_tree_layout(tree: &TreeView, rect: Rect, line_height: f32) -> TreeViewLayout {
crate::primitives::layout_metrics::tree_layout(tree, rect, line_height as f64)
}
pub fn win_tree_layout_measured(
measure: &dyn TextMeasure,
rect: Rect,
tree: &TreeView,
line_height: f32,
) -> TreeViewLayout {
crate::primitives::layout_metrics::tree_layout_measured(tree, rect, line_height as f64, measure)
}
pub fn draw_tree(
target: &ID2D1RenderTarget,
dwrite: &DWrite,
rect: Rect,
tree: &TreeView,
line_height: f32,
nerd_fonts_enabled: bool,
theme: &Theme,
) -> TreeViewLayout {
let layout = win_tree_layout_measured(dwrite, rect, tree, line_height);
let mut surface = super::surface::D2dSurface {
target,
dwrite: Some(dwrite),
};
crate::primitives::tree::native_surface_paint::paint(
tree,
rect,
&layout,
line_height,
nerd_fonts_enabled,
&mut surface,
theme,
);
layout
}
#[cfg(test)]
mod tests {
use super::*;
use crate::primitives::tree::{TreeRow, TreeViewHit};
use crate::types::{Badge, Decoration, Icon, SelectionMode, StyledText, TreeStyle, WidgetId};
use crate::win::testing::HeadlessSurface;
const W: f32 = 200.0;
const H: f32 = 200.0;
const LINE_HEIGHT: f32 = 14.0;
fn leaf(idx: u16, label: &str) -> TreeRow {
TreeRow {
path: vec![idx],
indent: 0,
icon: None,
text: StyledText::plain(label.to_string()),
badge: None,
is_expanded: None,
decoration: Decoration::Normal,
edit: None,
}
}
fn branch(idx: u16, label: &str, expanded: bool) -> TreeRow {
TreeRow {
path: vec![idx],
indent: 0,
icon: Some(Icon::new("", "D")),
text: StyledText::plain(label.to_string()),
badge: Some(Badge::plain("3")),
is_expanded: Some(expanded),
decoration: Decoration::Normal,
edit: None,
}
}
fn make_tree(rows: Vec<TreeRow>) -> TreeView {
TreeView {
id: WidgetId::new("tree"),
rows,
selection_mode: SelectionMode::Single,
selected_path: None,
scroll_offset: 0,
style: TreeStyle::default(),
has_focus: true,
}
}
#[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 mut tree = make_tree(vec![
branch(0, "src", true),
leaf(1, "main.rs"),
leaf(2, "lib.rs"),
leaf(3, "util.rs"),
]);
tree.selected_path = Some(vec![2]);
let rect = Rect::new(0.0, 0.0, W, H);
let layout = surface
.paint(|target| {
draw_tree(
target,
&dwrite,
rect,
&tree,
LINE_HEIGHT,
false,
&Theme::default(),
);
})
.map(|_| win_tree_layout(&tree, rect, LINE_HEIGHT))
.expect("paint tree");
assert_eq!(layout.visible_rows.len(), 4, "all four rows should fit");
for vis in &layout.visible_rows {
let cx = vis.bounds.x + vis.bounds.width / 2.0;
let cy = vis.bounds.y + vis.bounds.height / 2.0;
let hit = layout.hit_test(cx, cy);
match hit {
TreeViewHit::Row(idx) | TreeViewHit::Chevron(idx) => {
assert_eq!(
idx, vis.row_idx,
"row centre should hit-test back to itself"
);
}
other => panic!("expected a row hit at row {}, got {:?}", vis.row_idx, other),
}
}
let theme = Theme::default();
let sel_bounds = layout.visible_rows[2].bounds;
let px = surface.pixel_at((sel_bounds.x + 1.0) as u32, (sel_bounds.y + 1.0) as u32);
assert_eq!(
(px.r, px.g, px.b),
(
theme.selected_bg.r,
theme.selected_bg.g,
theme.selected_bg.b
),
"selected row should paint selected_bg at its own bounds"
);
}
#[test]
fn chevron_and_body_hit_split() {
let tree = make_tree(vec![branch(0, "src", true)]);
let rect = Rect::new(0.0, 0.0, W, H);
let layout = win_tree_layout(&tree, rect, LINE_HEIGHT);
let hit = layout.hit_test(1.0, layout.visible_rows[0].bounds.y + 1.0);
assert!(matches!(hit, TreeViewHit::Chevron(0)), "got {:?}", hit);
let hit = layout.hit_test(100.0, layout.visible_rows[0].bounds.y + 1.0);
assert!(matches!(hit, TreeViewHit::Row(0)), "got {:?}", hit);
}
#[test]
fn scroll_offset_paint_and_click_agree() {
let mut tree = make_tree((0..8).map(|i| leaf(i, &format!("file-{i}.rs"))).collect());
tree.scroll_offset = 2;
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 layout = surface
.paint(|target| {
draw_tree(
target,
&dwrite,
rect,
&tree,
LINE_HEIGHT,
false,
&Theme::default(),
);
})
.map(|_| win_tree_layout(&tree, rect, LINE_HEIGHT))
.expect("paint");
let first = layout.visible_rows.first().expect("has visible rows");
assert_eq!(
first.row_idx, 2,
"scroll_offset=2 should put rows[2] at top"
);
let hit = layout.hit_test(
first.bounds.x + 5.0,
first.bounds.y + first.bounds.height / 2.0,
);
assert!(matches!(hit, TreeViewHit::Row(2)), "got {:?}", hit);
}
#[test]
fn click_below_last_row_returns_empty() {
let tree = make_tree(vec![leaf(0, "a"), leaf(1, "b")]);
let rect = Rect::new(0.0, 0.0, W, H);
let layout = win_tree_layout(&tree, rect, LINE_HEIGHT);
let last = layout.visible_rows.last().expect("has rows");
let hit = layout.hit_test(10.0, last.bounds.y + last.bounds.height + 5.0);
assert!(matches!(hit, TreeViewHit::Empty), "got {:?}", hit);
}
#[test]
fn no_paint_layout_matches_paint_layout() {
let tree = make_tree(vec![branch(0, "src", true), leaf(1, "main.rs")]);
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_tree(
target,
&dwrite,
rect,
&tree,
LINE_HEIGHT,
false,
&Theme::default(),
);
})
.map(|_| win_tree_layout(&tree, rect, LINE_HEIGHT))
.expect("paint");
let no_paint = win_tree_layout(&tree, rect, LINE_HEIGHT);
assert_eq!(painted, no_paint);
}
#[test]
fn nerd_fonts_flag_selects_glyph_or_fallback() {
let row = TreeRow {
path: vec![0],
indent: 0,
icon: Some(Icon::new("WWWW", "E")),
text: StyledText::plain(String::new()),
badge: None,
is_expanded: None,
decoration: Decoration::Normal,
edit: None,
};
let tree = make_tree(vec![row]);
let rect = Rect::new(0.0, 0.0, W, H);
let theme = Theme::default();
let bg = (theme.tab_bar_bg.r, theme.tab_bar_bg.g, theme.tab_bar_bg.b);
let painted_width = |nerd_fonts_enabled: bool| -> u32 {
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_tree(
target,
&dwrite,
rect,
&tree,
LINE_HEIGHT,
nerd_fonts_enabled,
&Theme::default(),
);
})
.expect("paint");
let mid_y = (LINE_HEIGHT * 1.4 / 2.0) as u32;
let mut left = None;
let mut right = None;
for x in 0..(W as u32) {
let px = surface.pixel_at(x, mid_y);
if (px.r, px.g, px.b) != bg {
left.get_or_insert(x);
right = Some(x);
}
}
match (left, right) {
(Some(l), Some(r)) => r - l + 1,
_ => 0,
}
};
let glyph_width = painted_width(true);
let fallback_width = painted_width(false);
assert!(
glyph_width > fallback_width,
"nerd_fonts_enabled: true should paint the wider glyph icon \
(\"WWWW\", measured {glyph_width}px) vs the narrower fallback \
(\"E\", measured {fallback_width}px) painted when false"
);
}
#[test]
fn icon_color_paints_icon_glyph_in_that_color_else_default_fg() {
let icon_color = crate::types::Color::rgb(220, 80, 20);
let theme = Theme::default();
let default_fg = theme.foreground;
let bg = theme.tab_bar_bg;
let dist = |a: (u8, u8, u8), c: crate::types::Color| {
let d = |x: u8, y: u8| (x as i32 - y as i32).pow(2);
d(a.0, c.r) + d(a.1, c.g) + d(a.2, c.b)
};
let most_inked_on_row = |icon: Icon| -> (u8, u8, u8) {
let row = TreeRow {
path: vec![0],
indent: 0,
icon: Some(icon),
text: StyledText::plain(String::new()),
badge: None,
is_expanded: None,
decoration: Decoration::Normal,
edit: None,
};
let tree = make_tree(vec![row]);
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_tree(
target,
&dwrite,
rect,
&tree,
LINE_HEIGHT,
false,
&Theme::default(),
);
})
.expect("paint");
let mid_y = (LINE_HEIGHT * 1.4 / 2.0) as u32;
let mut best = (bg.r, bg.g, bg.b);
let mut best_d = -1i32;
for x in 0..(W as u32) {
let px = surface.pixel_at(x, mid_y);
let c = (px.r, px.g, px.b);
let d = dist(c, bg);
if d > best_d {
best_d = d;
best = c;
}
}
best
};
let colored = most_inked_on_row(Icon::new("R", "R").with_color(icon_color));
assert!(
dist(colored, icon_color) < dist(colored, default_fg),
"row with Icon::color set: most-inked pixel {colored:?} should be \
closer to Icon::color {icon_color:?} than default fg {default_fg:?}"
);
let uncolored = most_inked_on_row(Icon::new("R", "R"));
assert!(
dist(uncolored, default_fg) < dist(uncolored, icon_color),
"row without Icon::color: most-inked pixel {uncolored:?} should be \
closer to default fg {default_fg:?} than the unrelated colour \
{icon_color:?}"
);
}
#[test]
fn paints_row_being_edited() {
let rightmost_painted_x = |edit: Option<crate::primitives::tree::TreeRowEditState>| -> u32 {
let row = TreeRow {
path: vec![0],
indent: 0,
icon: None,
text: StyledText::plain("x".to_string()),
badge: None,
is_expanded: None,
decoration: Decoration::Normal,
edit,
};
let tree = make_tree(vec![row]);
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 layout = surface
.paint(|target| {
draw_tree(
target,
&dwrite,
rect,
&tree,
LINE_HEIGHT,
false,
&Theme::default(),
);
})
.map(|_| win_tree_layout(&tree, rect, LINE_HEIGHT))
.expect("paint tree");
let theme = Theme::default();
let bg = (theme.tab_bar_bg.r, theme.tab_bar_bg.g, theme.tab_bar_bg.b);
let row0 = &layout.visible_rows[0];
let y = (row0.bounds.y + row0.bounds.height / 2.0) as u32;
let mut rightmost = 0u32;
for x in (row0.bounds.x as u32)..(row0.bounds.x + row0.bounds.width) as u32 {
let px = surface.pixel_at(x.min(W as u32 - 1), y.min(H as u32 - 1));
if (px.r, px.g, px.b) != bg {
rightmost = x;
}
}
rightmost
};
let plain_label_x = rightmost_painted_x(None);
let editing_x = rightmost_painted_x(Some(crate::primitives::tree::TreeRowEditState {
text: "a much wider inline-rename string".into(),
cursor: 3,
selection_anchor: None,
placeholder: None,
}));
assert!(
editing_x > plain_label_x + 20,
"a row with `edit: Some(_)` carrying much wider text must paint \
noticeably further right ({editing_x}) than the same row's plain \
label ({plain_label_x}) — this is the #1075 fix for Win never \
painting TreeRow::edit at all (pre-fix, both values were equal)"
);
}
#[test]
fn paints_vertical_scrollbar_track_when_overflowing() {
let tree = make_tree((0..40).map(|i| leaf(i, &format!("row-{i}"))).collect());
let rect = Rect::new(0.0, 0.0, W, H);
let item_height =
crate::primitives::layout_metrics::tree_row_pitch(&tree, LINE_HEIGHT as f64) as f32;
let expected = tree
.vscrollbar_with_track_width(
rect,
item_height,
crate::primitives::layout_metrics::pixel::TREE_VSCROLLBAR_WIDTH,
)
.expect("40 rows in this viewport must need a v-scrollbar");
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_tree(
target,
&dwrite,
rect,
&tree,
LINE_HEIGHT,
false,
&Theme::default(),
);
})
.expect("paint tree");
let theme = Theme::default();
let probe_x = (expected.track.x + expected.track.width / 2.0) as u32;
let probe_y = (expected.track.y + expected.track.height / 2.0) as u32;
let px = surface.pixel_at(probe_x.min(W as u32 - 1), probe_y.min(H as u32 - 1));
assert_ne!(
(px.r, px.g, px.b),
(theme.tab_bar_bg.r, theme.tab_bar_bg.g, theme.tab_bar_bg.b),
"expected the v-scrollbar track at ({probe_x}, {probe_y}) to be painted \
(non-background) — the #1075 regression this test guards"
);
}
#[test]
fn win_tree_layout_measured_uses_real_chevron_glyph_width_not_the_estimate() {
let tree = make_tree(vec![branch(0, "alpha", true)]);
let rect = Rect::new(0.0, 0.0, W, H);
let (dwrite, _, _) = DWrite::new("Segoe UI", 10.0, None).expect("create DWrite");
let estimated = win_tree_layout(&tree, rect, LINE_HEIGHT);
let measured = win_tree_layout_measured(&dwrite, rect, &tree, LINE_HEIGHT);
let est_x = estimated.hit_regions[0].0.width;
let measured_x = measured.hit_regions[0].0.width;
assert_ne!(
est_x, measured_x,
"measured chevron end-x must use the real DirectWrite glyph width \
({measured_x}), not the line_height * 0.65 estimate ({est_x})",
);
let glyph_w = dwrite.width_of(&crate::primitives::tree::tree_chevron_glyph(
&tree.style,
true,
));
let indent_px = (LINE_HEIGHT * 0.9).round();
let expected = 2.0 + tree.rows[0].indent as f32 * indent_px + glyph_w + 4.0;
assert!(
(measured_x - expected).abs() < 0.01,
"measured chevron end-x {measured_x} must equal the DirectWrite-measured \
geometry {expected} (glyph advance {glyph_w})",
);
assert_eq!(
measured.hit_test(measured_x - 1.0, 4.0),
TreeViewHit::Chevron(0),
"the measured boundary itself must still resolve to the chevron hit region",
);
}
#[test]
fn chevron_override_paints_app_glyph_default_paints_codicon() {
let (dwrite, _, _) = DWrite::new("Segoe UI", 10.0, None).expect("create DWrite");
let rect = Rect::new(0.0, 0.0, W, H);
let mut overridden = make_tree(vec![branch(0, "src", true)]);
overridden.style.chevron_expanded = "V".to_string();
let surface = HeadlessSurface::new(W as u32, H as u32).expect("create surface");
let previous = crate::testing::install_text_run_sink();
surface
.paint(|target| {
draw_tree(
target,
&dwrite,
rect,
&overridden,
LINE_HEIGHT,
false,
&Theme::default(),
);
})
.expect("paint with chevron override");
let runs = crate::testing::take_text_run_sink(previous);
assert!(
runs.iter().any(|r| r.text == "V"),
"expected the overridden chevron glyph among painted runs, got {:?}",
runs.iter().map(|r| r.text.as_str()).collect::<Vec<_>>()
);
assert!(!runs
.iter()
.any(|r| r.text.contains(crate::codicon::CHEVRON_DOWN)));
let default_tree = make_tree(vec![branch(0, "src", true)]);
let surface = HeadlessSurface::new(W as u32, H as u32).expect("create surface");
let previous = crate::testing::install_text_run_sink();
surface
.paint(|target| {
draw_tree(
target,
&dwrite,
rect,
&default_tree,
LINE_HEIGHT,
false,
&Theme::default(),
);
})
.expect("paint with default style");
let runs = crate::testing::take_text_run_sink(previous);
assert!(
runs.iter()
.any(|r| r.text.contains(crate::codicon::CHEVRON_DOWN)),
"expected the default codicon chevron glyph among painted runs, got {:?}",
runs.iter().map(|r| r.text.as_str()).collect::<Vec<_>>()
);
}
}
#[cfg(test)]
mod multi_select_driver_tests {
use super::super::testing::WinDriver;
use crate::primitives::tree::{selected_paths_for_indices, TreeEvent, TreeRow, TreeRowMeasure};
use crate::runner::{AppLogic, Reaction};
use crate::{
apply_selection_gesture, Backend, Decoration, Modifiers, MouseButton, Point, Rect,
SelectionGesture, SelectionMode, StyledText, TreeView, TreeViewHit, UiEvent, WidgetId,
};
const VIEWPORT_W: f32 = 40.0;
const VIEWPORT_H: f32 = 10.0;
const ROW_H: f32 = 1.0;
fn leaf(idx: u16, label: &str) -> TreeRow {
TreeRow {
path: vec![idx],
indent: 0,
icon: None,
text: StyledText::plain(label.to_string()),
badge: None,
is_expanded: None,
decoration: Decoration::Normal,
edit: None,
}
}
fn sample_tree() -> TreeView {
TreeView {
id: WidgetId::new("t"),
rows: (0..5).map(|i| leaf(i, &format!("row {i}"))).collect(),
selection_mode: SelectionMode::Multi,
selected_path: None,
scroll_offset: 0,
style: Default::default(),
has_focus: true,
}
}
struct TreeApp {
tree: TreeView,
selected: Vec<usize>,
anchor: usize,
events: Vec<TreeEvent>,
}
impl TreeApp {
fn new() -> Self {
Self {
tree: sample_tree(),
selected: Vec::new(),
anchor: 0,
events: Vec::new(),
}
}
}
impl AppLogic for TreeApp {
type AreaId = ();
fn render(&self, backend: &mut dyn Backend, _area: ()) {
backend.draw_tree(Rect::new(0.0, 0.0, VIEWPORT_W, VIEWPORT_H), &self.tree);
}
fn handle(&mut self, event: UiEvent, _backend: &mut dyn Backend) -> Reaction {
let UiEvent::MouseDown {
position,
modifiers,
..
} = event
else {
return Reaction::Continue;
};
let layout = self
.tree
.layout(VIEWPORT_W, VIEWPORT_H, |_| TreeRowMeasure::new(ROW_H));
let hit = layout.hit_test(position.x, position.y);
let idx = match hit {
TreeViewHit::Row(idx) | TreeViewHit::Chevron(idx) => idx,
TreeViewHit::Empty => return Reaction::Continue,
};
let gesture = SelectionGesture::from_modifiers(modifiers);
let indices = apply_selection_gesture(&self.selected, self.anchor, idx, gesture);
self.selected = indices.clone();
if !matches!(gesture, SelectionGesture::Range) {
self.anchor = idx;
}
let paths = selected_paths_for_indices(&self.tree.rows, &indices);
self.events.push(TreeEvent::SelectionSet { paths });
Reaction::Redraw
}
}
fn mouse_down(x: f32, y: f32, modifiers: Modifiers) -> UiEvent {
UiEvent::MouseDown {
widget: None,
button: MouseButton::Left,
position: Point::new(x, y),
modifiers,
}
}
#[test]
fn plain_click_replaces_selection() {
let mut driver = WinDriver::new(TreeApp::new(), 80, 40);
driver.dispatch(mouse_down(2.0, 2.0, Modifiers::default()));
assert_eq!(driver.app().selected, vec![2]);
}
#[test]
fn ctrl_click_toggles_rows_into_a_multi_selection() {
let mut driver = WinDriver::new(TreeApp::new(), 80, 40);
driver.dispatch(mouse_down(2.0, 0.0, Modifiers::default()));
let ctrl = Modifiers {
ctrl: true,
..Default::default()
};
driver.dispatch(mouse_down(10.0, 2.0, ctrl));
assert_eq!(driver.app().selected, vec![0, 2]);
driver.dispatch(mouse_down(18.0, 2.0, ctrl));
assert_eq!(driver.app().selected, vec![0]);
assert_eq!(
driver.app().events.last(),
Some(&TreeEvent::SelectionSet {
paths: vec![vec![0]]
})
);
}
#[test]
fn shift_click_selects_the_range_from_the_anchor() {
let mut driver = WinDriver::new(TreeApp::new(), 80, 40);
driver.dispatch(mouse_down(2.0, 0.0, Modifiers::default()));
let shift = Modifiers {
shift: true,
..Default::default()
};
driver.dispatch(mouse_down(10.0, 3.0, shift));
assert_eq!(driver.app().selected, vec![0, 1, 2, 3]);
assert_eq!(
driver.app().events.last(),
Some(&TreeEvent::SelectionSet {
paths: vec![vec![0], vec![1], vec![2], vec![3]]
})
);
}
#[test]
fn cmd_click_toggles_the_same_as_ctrl_click() {
let mut driver = WinDriver::new(TreeApp::new(), 80, 40);
driver.dispatch(mouse_down(2.0, 1.0, Modifiers::default()));
driver.dispatch(mouse_down(
10.0,
4.0,
Modifiers {
cmd: true,
..Default::default()
},
));
assert_eq!(driver.app().selected, vec![1, 4]);
}
}