#[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::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("×")
} 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,
}
}
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 {
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,
);
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,
);
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,
) {
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 {
"●"
} else {
"×"
};
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"
);
}
}