#[cfg(target_os = "windows")]
use windows::Win32::Graphics::Direct2D::ID2D1RenderTarget;
#[cfg(target_os = "windows")]
use super::text::fill_rect;
#[cfg(target_os = "windows")]
use super::text::DWrite;
use crate::backend::{shift_tab_bar_hits, tab_bar_hits_from_layout};
use crate::event::Rect;
use crate::primitives::layout_metrics::TextMeasure;
#[cfg(target_os = "windows")]
use crate::primitives::tab_bar::TabBarGlyphs;
#[cfg(target_os = "windows")]
use crate::theme::Theme;
#[allow(deprecated)]
use crate::{tab_icon_at, TabBar, TabBarHits, TabBarLayout, TabChrome, TabIcon};
const TAB_PAD_DIP: f32 = 14.0;
const TAB_INNER_GAP_DIP: f32 = 10.0;
const TAB_OUTER_GAP_DIP: f32 = 1.0;
const TAB_ICON_GAP_DIP: f32 = 6.0;
#[cfg(target_os = "windows")]
const TAB_ACTIVE_ACCENT_DIP: f32 = 2.0;
#[cfg(target_os = "windows")]
fn close_glyph_width(measure: &dyn TextMeasure, bar: &TabBar) -> f32 {
if bar.show_tab_close {
measure.width_of(&crate::codicon::CLOSE.to_string())
} else {
0.0
}
}
fn icon_extra_width(measure: &dyn TextMeasure, icons: &[Option<TabIcon>], i: usize) -> f32 {
match tab_icon_at(icons, i) {
Some(icon) => measure.width_of(&icon.glyph) + TAB_ICON_GAP_DIP,
None => 0.0,
}
}
#[cfg(target_os = "windows")]
fn tab_glyph_text(
icon: Option<&crate::types::Icon>,
nerd_fonts_enabled: bool,
default: char,
) -> String {
icon.map(|icon| icon.resolve(nerd_fonts_enabled).to_string())
.unwrap_or_else(|| default.to_string())
}
fn pixel_layout(
measure: &dyn TextMeasure,
rect: Rect,
bar: &TabBar,
icons: &[Option<TabIcon>],
) -> (TabBarLayout, usize, usize) {
let tab_name_widths: Vec<f32> = bar
.tabs
.iter()
.map(|t| measure.width_of(&t.label))
.collect();
let icon_extras: Vec<f32> = (0..bar.tabs.len())
.map(|i| icon_extra_width(measure, icons, i))
.collect();
crate::primitives::layout_metrics::pixel_tab_bar_layout(
bar,
rect.width,
rect.height,
TAB_PAD_DIP,
TAB_INNER_GAP_DIP,
TAB_OUTER_GAP_DIP,
&tab_name_widths,
&icon_extras,
&TabChrome::default(),
measure,
)
}
fn compute_layout(
measure: &dyn TextMeasure,
rect: Rect,
bar: &TabBar,
icons: &[Option<TabIcon>],
) -> TabBarLayout {
pixel_layout(measure, rect, bar, icons).0
}
#[allow(deprecated)] fn hits_from_layout(
rect: Rect,
bar: &TabBar,
layout: &TabBarLayout,
corrected_scroll_offset: usize,
available_cols: usize,
) -> TabBarHits {
let mut hits = tab_bar_hits_from_layout(layout, bar);
shift_tab_bar_hits(&mut hits, rect.x as f64);
hits.correct_scroll_offset = corrected_scroll_offset;
hits.available_cols = available_cols;
hits
}
#[allow(deprecated)] pub fn win_tab_bar_layout_icons(
measure: &dyn TextMeasure,
rect: Rect,
bar: &TabBar,
icons: &[Option<TabIcon>],
) -> TabBarHits {
let (layout, corrected_scroll_offset, available_cols) = pixel_layout(measure, rect, bar, icons);
hits_from_layout(rect, bar, &layout, corrected_scroll_offset, available_cols)
}
#[allow(deprecated)] pub fn win_tab_bar_layout(measure: &dyn TextMeasure, rect: Rect, bar: &TabBar) -> TabBarHits {
win_tab_bar_layout_icons(measure, rect, bar, &[])
}
pub fn win_tab_bar_native_layout_icons(
measure: &dyn TextMeasure,
rect: Rect,
bar: &TabBar,
icons: &[Option<TabIcon>],
) -> TabBarLayout {
compute_layout(measure, rect, bar, icons)
}
pub fn win_tab_bar_native_layout(
measure: &dyn TextMeasure,
rect: Rect,
bar: &TabBar,
) -> TabBarLayout {
win_tab_bar_native_layout_icons(measure, rect, bar, &[])
}
#[cfg(target_os = "windows")]
#[allow(deprecated)] pub fn draw_tab_bar_icons(
target: &ID2D1RenderTarget,
dwrite: &DWrite,
rect: Rect,
bar: &TabBar,
icons: &[Option<TabIcon>],
hovered_close_tab: Option<usize>,
theme: &Theme,
) -> TabBarHits {
draw_tab_bar_icons_with_glyphs(
target,
dwrite,
rect,
bar,
icons,
hovered_close_tab,
theme,
None,
false,
)
}
#[cfg(target_os = "windows")]
#[allow(deprecated, clippy::too_many_arguments)] pub fn draw_tab_bar_icons_with_glyphs(
target: &ID2D1RenderTarget,
dwrite: &DWrite,
rect: Rect,
bar: &TabBar,
icons: &[Option<TabIcon>],
hovered_close_tab: Option<usize>,
theme: &Theme,
glyphs: Option<&TabBarGlyphs>,
nerd_fonts_enabled: bool,
) -> TabBarHits {
let (layout, corrected_scroll_offset, available_cols) = pixel_layout(dwrite, rect, bar, icons);
paint_tab_bar_icons_from_layout(
target,
dwrite,
rect,
bar,
icons,
hovered_close_tab,
&layout,
theme,
glyphs,
nerd_fonts_enabled,
);
hits_from_layout(rect, bar, &layout, corrected_scroll_offset, available_cols)
}
#[cfg(target_os = "windows")]
pub fn draw_tab_bar_icons_layout(
target: &ID2D1RenderTarget,
dwrite: &DWrite,
rect: Rect,
bar: &TabBar,
icons: &[Option<TabIcon>],
hovered_close_tab: Option<usize>,
theme: &Theme,
) -> TabBarLayout {
let layout = compute_layout(dwrite, rect, bar, icons);
paint_tab_bar_icons_from_layout(
target,
dwrite,
rect,
bar,
icons,
hovered_close_tab,
&layout,
theme,
None,
false,
);
layout
}
#[cfg(target_os = "windows")]
#[allow(clippy::too_many_arguments)]
fn paint_tab_bar_icons_from_layout(
target: &ID2D1RenderTarget,
dwrite: &DWrite,
rect: Rect,
bar: &TabBar,
icons: &[Option<TabIcon>],
hovered_close_tab: Option<usize>,
layout: &TabBarLayout,
theme: &Theme,
glyphs: Option<&TabBarGlyphs>,
nerd_fonts_enabled: bool,
) {
let _ = fill_rect(target, rect, theme.tab_bar_bg);
let close_w = close_glyph_width(dwrite, bar);
for vt in &layout.visible_tabs {
let tab = &bar.tabs[vt.tab_idx];
let visual_w = (vt.bounds.width - TAB_OUTER_GAP_DIP).max(0.0);
let tab_rect = Rect::new(
rect.x + vt.bounds.x,
rect.y + vt.bounds.y,
visual_w,
vt.bounds.height,
);
let bg = if tab.is_active {
theme.tab_active_bg
} else {
theme.tab_bar_bg
};
let _ = fill_rect(target, tab_rect, bg);
if tab.is_active {
if let Some(accent) = bar.active_accent {
let accent_rect = Rect::new(
tab_rect.x,
tab_rect.y,
tab_rect.width,
TAB_ACTIVE_ACCENT_DIP,
);
let _ = fill_rect(target, accent_rect, accent);
}
}
let fg = if tab.is_active {
theme.tab_active_fg
} else {
theme.tab_inactive_fg
};
let mut cursor_x = tab_rect.x + TAB_PAD_DIP;
if let Some(icon) = tab_icon_at(icons, vt.tab_idx) {
let (iw, ih) = dwrite.measure_text(&icon.glyph).unwrap_or((0.0, 0.0));
let icon_rect = Rect::new(cursor_x, tab_rect.y + (tab_rect.height - ih) / 2.0, iw, ih);
let _ = dwrite.draw_text(target, &icon.glyph, icon_rect, icon.color);
cursor_x += iw + TAB_ICON_GAP_DIP;
}
let (name_w, name_h) = dwrite.measure_text(&tab.label).unwrap_or((0.0, 0.0));
let label_rect = Rect::new(
cursor_x,
tab_rect.y + (tab_rect.height - name_h) / 2.0,
name_w,
name_h,
);
let _ = dwrite.draw_text(target, &tab.label, label_rect, fg);
if bar.show_tab_close && tab.is_closable {
if let Some(cb) = vt.close_bounds {
let close_x = rect.x + cb.x + TAB_INNER_GAP_DIP;
let is_close_hovered = hovered_close_tab == Some(vt.tab_idx);
if is_close_hovered {
let hover_bg = theme.tab_bar_bg.lighten(0.15);
let hover_rect = Rect::new(
close_x - 2.0,
tab_rect.y + 2.0,
close_w + 4.0,
(tab_rect.height - 4.0).max(0.0),
);
let _ = fill_rect(target, hover_rect, hover_bg);
}
let close_glyph = if tab.is_dirty && !is_close_hovered {
tab_glyph_text(
glyphs.and_then(TabBarGlyphs::dirty_glyph),
nerd_fonts_enabled,
crate::codicon::DIRTY,
)
} else {
tab_glyph_text(
glyphs.and_then(TabBarGlyphs::close_glyph),
nerd_fonts_enabled,
crate::codicon::CLOSE,
)
};
let close_fg = if tab.is_dirty || is_close_hovered {
theme.foreground
} else if tab.is_active {
theme.tab_inactive_fg
} else {
theme.separator
};
let (cgw, cgh) = dwrite.measure_text(&close_glyph).unwrap_or((0.0, 0.0));
let close_rect = Rect::new(
close_x,
tab_rect.y + (tab_rect.height - cgh) / 2.0,
cgw,
cgh,
);
let _ = dwrite.draw_text(target, &close_glyph, close_rect, close_fg);
}
}
}
for vs in &layout.visible_segments {
let seg = &bar.right_segments[vs.segment_idx];
let fg = if seg.is_active {
theme.tab_active_fg
} else {
theme.tab_inactive_fg
};
let (seg_w, seg_h) = dwrite.measure_text(&seg.text).unwrap_or((0.0, 0.0));
let seg_rect = Rect::new(
rect.x + vs.bounds.x,
rect.y + vs.bounds.y + (vs.bounds.height - seg_h) / 2.0,
seg_w,
seg_h,
);
let _ = dwrite.draw_text(target, &seg.text, seg_rect, fg);
}
}
#[cfg(target_os = "windows")]
#[allow(deprecated)] pub fn draw_tab_bar(
target: &ID2D1RenderTarget,
dwrite: &DWrite,
rect: Rect,
bar: &TabBar,
hovered_close_tab: Option<usize>,
theme: &Theme,
) -> TabBarHits {
draw_tab_bar_icons(target, dwrite, rect, bar, &[], hovered_close_tab, theme)
}
#[cfg(target_os = "windows")]
pub fn draw_tab_bar_layout(
target: &ID2D1RenderTarget,
dwrite: &DWrite,
rect: Rect,
bar: &TabBar,
hovered_close_tab: Option<usize>,
theme: &Theme,
) -> TabBarLayout {
draw_tab_bar_icons_layout(target, dwrite, rect, bar, &[], hovered_close_tab, theme)
}
#[cfg(all(test, target_os = "windows"))]
mod tests {
use super::*;
use crate::primitives::tab_bar::{TabBarHit, TabBarSegment, TabItem};
use crate::types::{Color, WidgetId};
use crate::win::testing::HeadlessSurface;
const W: f32 = 300.0;
const H: f32 = 30.0;
fn bar() -> TabBar {
TabBar {
id: WidgetId::new("tabs"),
tabs: vec![
TabItem {
label: "main.rs".into(),
is_active: true,
is_dirty: false,
is_preview: false,
is_closable: true,
},
TabItem {
label: "lib.rs".into(),
is_active: false,
is_dirty: false,
is_preview: false,
is_closable: true,
},
],
scroll_offset: 0,
right_segments: vec![TabBarSegment {
text: " ⇅ ".into(),
width_cells: 3,
id: Some(WidgetId::new("tab:split")),
is_active: false,
}],
active_accent: Some(Color::rgb(80, 140, 255)),
show_tab_close: true,
compact: false,
}
}
#[test]
#[allow(deprecated)] 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_tab_bar(target, &dwrite, rect, &bar, None, &Theme::default());
})
.expect("paint tab bar");
let layout = compute_layout(&dwrite, rect, &bar, &[]);
assert_eq!(layout.visible_tabs.len(), 2, "both tabs should fit");
for vt in &layout.visible_tabs {
let cx = vt.bounds.x + vt.bounds.width / 2.0;
let cy = vt.bounds.y + vt.bounds.height / 2.0;
assert_eq!(
layout.hit_test(cx, cy),
TabBarHit::Tab(vt.tab_idx),
"tab {} centre should hit-test back to itself",
vt.tab_idx,
);
}
let theme = Theme::default();
let active_bounds = layout.visible_tabs[0].bounds;
let sample_x = (active_bounds.x + 2.0) as u32;
let sample_y = (active_bounds.y + active_bounds.height - 4.0) as u32;
let px = surface.pixel_at(sample_x, sample_y);
assert_eq!(
(px.r, px.g, px.b),
(
theme.tab_active_bg.r,
theme.tab_active_bg.g,
theme.tab_active_bg.b
),
"active tab should paint its background at its own bounds"
);
let hits = win_tab_bar_layout(&dwrite, rect, &bar);
let vs = &layout.visible_segments[0];
assert_eq!(
hits.right_segment_bounds[0],
(vs.bounds.x as f64, (vs.bounds.x + vs.bounds.width) as f64),
"TabBarHits right-segment bounds must be absolute (rect.x == 0 here) \
and agree with the layout's own visible_segments"
);
}
#[test]
fn no_paint_layout_matches_paint_layout() {
let (dwrite, _, _) = DWrite::new("Segoe UI", 10.0, None).expect("create DWrite");
let bar = bar();
let rect = Rect::new(5.0, 0.0, W, H);
let surface = HeadlessSurface::new((W + 5.0) as u32, H as u32).expect("create surface");
let mut painted = None;
surface
.paint(|target| {
painted = Some(draw_tab_bar(
target,
&dwrite,
rect,
&bar,
None,
&Theme::default(),
));
})
.expect("paint");
let painted = painted.expect("draw_tab_bar ran");
let no_paint = win_tab_bar_layout(&dwrite, rect, &bar);
assert_eq!(painted, no_paint);
}
#[test]
fn native_layout_no_paint_matches_native_layout_paint() {
let (dwrite, _, _) = DWrite::new("Segoe UI", 10.0, None).expect("create DWrite");
let bar = bar();
let rect = Rect::new(5.0, 0.0, W, H);
let surface = HeadlessSurface::new((W + 5.0) as u32, H as u32).expect("create surface");
let mut painted = None;
surface
.paint(|target| {
painted = Some(draw_tab_bar_layout(
target,
&dwrite,
rect,
&bar,
None,
&Theme::default(),
));
})
.expect("paint");
let painted = painted.expect("draw_tab_bar_layout ran");
let no_paint = win_tab_bar_native_layout(&dwrite, rect, &bar);
assert_eq!(painted, no_paint);
}
#[test]
#[allow(deprecated)] fn native_layout_agrees_with_hits_layout() {
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 hits = win_tab_bar_layout(&dwrite, rect, &bar);
let native = win_tab_bar_native_layout(&dwrite, rect, &bar);
let mut native_as_hits = tab_bar_hits_from_layout(&native, &bar);
shift_tab_bar_hits(&mut native_as_hits, rect.x as f64);
assert_eq!(
hits.slot_positions, native_as_hits.slot_positions,
"tab slots must agree between the two accessors"
);
assert_eq!(
hits.close_bounds, native_as_hits.close_bounds,
"close-button spans must agree too"
);
assert_eq!(
hits.right_segment_bounds, native_as_hits.right_segment_bounds,
"right-segment spans must agree too"
);
}
#[test]
fn close_dirty_glyph_override_paints_app_glyph_default_paints_codicon() {
let (dwrite, _, _) = DWrite::new("Segoe UI", 10.0, None).expect("create DWrite");
let mut dirty_bar = bar();
dirty_bar.tabs[0].is_dirty = true;
let rect = Rect::new(0.0, 0.0, W, H);
let glyphs = TabBarGlyphs::new()
.with_close_glyph("x")
.with_dirty_glyph("*");
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_tab_bar_icons_with_glyphs(
target,
&dwrite,
rect,
&dirty_bar,
&[],
None,
&Theme::default(),
Some(&glyphs),
true,
);
})
.expect("paint with glyph override");
let runs = crate::testing::take_text_run_sink(previous);
assert!(
runs.iter().any(|r| r.text == "*"),
"expected the overridden dirty 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::DIRTY)));
assert!(
runs.iter().any(|r| r.text == "x"),
"expected the overridden close 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::CLOSE)));
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_tab_bar_icons_with_glyphs(
target,
&dwrite,
rect,
&dirty_bar,
&[],
None,
&Theme::default(),
None,
true,
);
})
.expect("paint with no override");
let runs = crate::testing::take_text_run_sink(previous);
assert!(
runs.iter().any(|r| r.text.contains(crate::codicon::DIRTY)),
"expected the default codicon dirty 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::CLOSE)),
"expected the default codicon close glyph among painted runs, got {:?}",
runs.iter().map(|r| r.text.as_str()).collect::<Vec<_>>()
);
}
#[test]
fn backend_trait_close_dirty_glyph_override_paints_app_glyph_default_paints_codicon() {
use crate::backend::Backend;
use crate::win::backend::WinBackend;
let mut dirty_bar = bar();
dirty_bar.tabs[0].is_dirty = true;
let rect = Rect::new(0.0, 0.0, W, H);
let chrome = TabChrome::default();
let paint = |glyphs: Option<&TabBarGlyphs>| {
let surface = HeadlessSurface::new(W as u32, H as u32).expect("create surface");
let mut backend = WinBackend::new();
backend
.attach_headless(surface.target().clone(), W as u32, H as u32)
.expect("attach headless surface");
backend.set_painted_text_recording(true);
backend.begin_frame(crate::event::Viewport::new(W, H, 1.0));
#[allow(deprecated)] let _ = backend
.draw_tab_bar_with_chrome_and_glyphs(rect, &dirty_bar, None, &chrome, glyphs);
backend.end_frame();
backend.text_runs().to_vec()
};
let glyphs = TabBarGlyphs::new()
.with_close_glyph("x")
.with_dirty_glyph("*");
let overridden = paint(Some(&glyphs));
assert!(
overridden.iter().any(|r| r.text == "*"),
"expected the overridden dirty glyph among painted runs, got {:?}",
overridden
.iter()
.map(|r| r.text.as_str())
.collect::<Vec<_>>()
);
assert!(
overridden.iter().any(|r| r.text == "x"),
"expected the overridden close glyph among painted runs, got {:?}",
overridden
.iter()
.map(|r| r.text.as_str())
.collect::<Vec<_>>()
);
assert!(!overridden
.iter()
.any(|r| r.text.contains(crate::codicon::DIRTY)));
assert!(!overridden
.iter()
.any(|r| r.text.contains(crate::codicon::CLOSE)));
let default = paint(None);
assert!(
default
.iter()
.any(|r| r.text.contains(crate::codicon::DIRTY)),
"expected the default codicon dirty glyph among painted runs, got {:?}",
default.iter().map(|r| r.text.as_str()).collect::<Vec<_>>()
);
assert!(
default
.iter()
.any(|r| r.text.contains(crate::codicon::CLOSE)),
"expected the default codicon close glyph among painted runs, got {:?}",
default.iter().map(|r| r.text.as_str()).collect::<Vec<_>>()
);
}
}