1use crate::app::BufferMetadata;
4use crate::model::event::{BufferId, LeafId};
5use crate::primitives::display_width::str_width;
6use crate::state::EditorState;
7use crate::view::split::TabTarget;
8use crate::view::ui::layout::point_in_rect;
9use ratatui::layout::Rect;
10use ratatui::style::{Modifier, Style};
11use ratatui::text::{Line, Span};
12use ratatui::widgets::{Block, Paragraph};
13use ratatui::Frame;
14use std::collections::HashMap;
15
16#[derive(Debug, Clone)]
18pub struct TabHitArea {
19 pub target: TabTarget,
21 pub tab_area: Rect,
23 pub close_area: Rect,
25}
26
27impl TabHitArea {
28 pub fn buffer_id(&self) -> Option<BufferId> {
30 self.target.as_buffer()
31 }
32}
33
34#[derive(Debug, Clone, Default)]
39pub struct TabLayout {
40 pub tabs: Vec<TabHitArea>,
42 pub bar_area: Rect,
44 pub left_scroll_area: Option<Rect>,
46 pub right_scroll_area: Option<Rect>,
48}
49
50#[derive(Debug, Clone, Copy, PartialEq, Eq)]
52pub enum TabHit {
53 TabName(TabTarget),
55 CloseButton(TabTarget),
57 BarBackground,
59 ScrollLeft,
61 ScrollRight,
63}
64
65impl TabLayout {
66 pub fn new(bar_area: Rect) -> Self {
68 Self {
69 tabs: Vec::new(),
70 bar_area,
71 left_scroll_area: None,
72 right_scroll_area: None,
73 }
74 }
75
76 pub fn hit_test(&self, x: u16, y: u16) -> Option<TabHit> {
78 if let Some(left_area) = self.left_scroll_area {
80 tracing::debug!(
81 "Tab hit_test: checking left_scroll_area {:?} against ({}, {})",
82 left_area,
83 x,
84 y
85 );
86 if point_in_rect(left_area, x, y) {
87 tracing::debug!("Tab hit_test: HIT ScrollLeft");
88 return Some(TabHit::ScrollLeft);
89 }
90 }
91 if let Some(right_area) = self.right_scroll_area {
92 tracing::debug!(
93 "Tab hit_test: checking right_scroll_area {:?} against ({}, {})",
94 right_area,
95 x,
96 y
97 );
98 if point_in_rect(right_area, x, y) {
99 tracing::debug!("Tab hit_test: HIT ScrollRight");
100 return Some(TabHit::ScrollRight);
101 }
102 }
103
104 for tab in &self.tabs {
105 if point_in_rect(tab.close_area, x, y) {
107 return Some(TabHit::CloseButton(tab.target));
108 }
109 if point_in_rect(tab.tab_area, x, y) {
111 return Some(TabHit::TabName(tab.target));
112 }
113 }
114
115 if point_in_rect(self.bar_area, x, y) {
117 return Some(TabHit::BarBackground);
118 }
119
120 None
121 }
122}
123
124pub struct TabsRenderer;
126
127pub fn scroll_to_show_tab(
131 tab_widths: &[usize],
132 active_idx: usize,
133 _current_offset: usize,
134 max_width: usize,
135) -> usize {
136 if tab_widths.is_empty() || max_width == 0 || active_idx >= tab_widths.len() {
137 return 0;
138 }
139
140 let total_width: usize = tab_widths.iter().sum();
141 let tab_start: usize = tab_widths[..active_idx].iter().sum();
142 let tab_width = tab_widths[active_idx];
143 let tab_end = tab_start + tab_width;
144
145 let preferred_position = max_width / 4;
147 let target_offset = tab_start.saturating_sub(preferred_position);
148
149 let max_offset_with_indicator = total_width.saturating_sub(max_width.saturating_sub(1));
158 let max_offset_no_indicator = total_width.saturating_sub(max_width);
159 let max_offset = if total_width > max_width {
160 max_offset_with_indicator
161 } else {
162 0
163 };
164 let mut result = target_offset.min(max_offset);
165
166 let available_worst = max_width.saturating_sub(2);
169
170 if tab_end > result + available_worst {
171 result = tab_end.saturating_sub(available_worst);
174 }
175 if tab_start < result {
176 result = tab_start;
179 }
180 let effective_max = if result > 0 {
183 max_offset
184 } else {
185 max_offset_no_indicator
186 };
187 result = result.min(effective_max);
188
189 tracing::debug!(
190 "scroll_to_show_tab: idx={}, tab={}..{}, target={}, result={}, total={}, max_width={}, max_offset={}",
191 active_idx, tab_start, tab_end, target_offset, result, total_width, max_width, max_offset
192 );
193 result
194}
195
196fn resolve_tab_names(
202 tab_targets: &[TabTarget],
203 buffers: &HashMap<BufferId, EditorState>,
204 buffer_metadata: &HashMap<BufferId, BufferMetadata>,
205 composite_buffers: &HashMap<BufferId, crate::model::composite_buffer::CompositeBuffer>,
206 group_names: &HashMap<LeafId, String>,
207) -> HashMap<TabTarget, String> {
208 let mut names: Vec<(TabTarget, String)> = Vec::new();
209
210 for t in tab_targets.iter() {
211 match t {
212 TabTarget::Buffer(id) => {
213 let is_regular_buffer = buffers.contains_key(id);
214 let is_composite_buffer = composite_buffers.contains_key(id);
215 if !is_regular_buffer && !is_composite_buffer {
216 continue;
217 }
218 if let Some(meta) = buffer_metadata.get(id) {
219 if meta.hidden_from_tabs {
220 continue;
221 }
222 }
223
224 let meta = buffer_metadata.get(id);
225 let is_terminal = meta
226 .and_then(|m| m.virtual_mode())
227 .map(|mode| mode == "terminal")
228 .unwrap_or(false);
229
230 let name = if is_composite_buffer {
231 meta.map(|m| m.display_name.as_str())
232 } else if is_terminal {
233 meta.map(|m| m.display_name.as_str())
234 } else {
235 buffers
236 .get(id)
237 .and_then(|state| state.buffer.file_path())
238 .and_then(|p| p.file_name())
239 .and_then(|n| n.to_str())
240 .or_else(|| meta.map(|m| m.display_name.as_str()))
241 }
242 .unwrap_or("[No Name]");
243
244 names.push((*t, name.to_string()));
245 }
246 TabTarget::Group(leaf_id) => {
247 if let Some(name) = group_names.get(leaf_id) {
248 names.push((*t, name.clone()));
249 }
250 }
251 }
252 }
253
254 let mut name_counts: HashMap<&str, usize> = HashMap::new();
256 for (_, name) in &names {
257 *name_counts.entry(name.as_str()).or_insert(0) += 1;
258 }
259
260 let mut result = HashMap::new();
262 let mut name_indices: HashMap<String, usize> = HashMap::new();
263 for (t, name) in &names {
264 if name_counts.get(name.as_str()).copied().unwrap_or(0) > 1 {
265 let idx = name_indices.entry(name.clone()).or_insert(0);
266 *idx += 1;
267 result.insert(*t, format!("{} {}", name, idx));
268 } else {
269 result.insert(*t, name.clone());
270 }
271 }
272
273 result
274}
275
276pub fn calculate_tab_widths(
280 tab_targets: &[TabTarget],
281 buffers: &HashMap<BufferId, EditorState>,
282 buffer_metadata: &HashMap<BufferId, BufferMetadata>,
283 composite_buffers: &HashMap<BufferId, crate::model::composite_buffer::CompositeBuffer>,
284 group_names: &HashMap<LeafId, String>,
285) -> (Vec<usize>, Vec<TabTarget>) {
286 let mut tab_widths: Vec<usize> = Vec::new();
287 let mut rendered_targets: Vec<TabTarget> = Vec::new();
288 let resolved_names = resolve_tab_names(
289 tab_targets,
290 buffers,
291 buffer_metadata,
292 composite_buffers,
293 group_names,
294 );
295
296 for t in tab_targets.iter() {
297 let Some(name) = resolved_names.get(t) else {
299 continue;
300 };
301
302 let modified = match t {
304 TabTarget::Buffer(id) => {
305 if composite_buffers.contains_key(id) {
306 ""
307 } else if let Some(state) = buffers.get(id) {
308 if state.buffer.is_modified() {
309 "*"
310 } else {
311 ""
312 }
313 } else {
314 ""
315 }
316 }
317 TabTarget::Group(_) => "",
318 };
319
320 let binary_indicator = match t {
321 TabTarget::Buffer(id) => {
322 if buffer_metadata.get(id).map(|m| m.binary).unwrap_or(false) {
323 " [BIN]"
324 } else {
325 ""
326 }
327 }
328 TabTarget::Group(_) => "",
329 };
330
331 let tab_name_text = format!(" {name}{modified}{binary_indicator} ");
333 let close_text = "× ";
334 let tab_width = str_width(&tab_name_text) + str_width(close_text);
335
336 if !rendered_targets.is_empty() {
338 tab_widths.push(1); }
340
341 tab_widths.push(tab_width);
342 rendered_targets.push(*t);
343 }
344
345 (tab_widths, rendered_targets)
346}
347
348impl TabsRenderer {
349 #[allow(clippy::too_many_arguments)]
365 pub fn render_for_split(
366 frame: &mut Frame,
367 area: Rect,
368 tab_targets: &[TabTarget],
369 buffers: &HashMap<BufferId, EditorState>,
370 buffer_metadata: &HashMap<BufferId, BufferMetadata>,
371 composite_buffers: &HashMap<BufferId, crate::model::composite_buffer::CompositeBuffer>,
372 active_target: TabTarget,
373 theme: &crate::view::theme::Theme,
374 is_active_split: bool,
375 tab_scroll_offset: usize,
376 hovered_tab: Option<(TabTarget, bool)>, group_names: &HashMap<LeafId, String>,
378 ) -> TabLayout {
379 let mut layout = TabLayout::new(area);
380 const SCROLL_INDICATOR_LEFT: &str = "<";
381 const SCROLL_INDICATOR_RIGHT: &str = ">";
382 const SCROLL_INDICATOR_WIDTH: usize = 1; let mut all_tab_spans: Vec<(Span, usize)> = Vec::new(); let mut tab_ranges: Vec<(usize, usize, usize)> = Vec::new(); let mut rendered_targets: Vec<TabTarget> = Vec::new(); let resolved_names = resolve_tab_names(
388 tab_targets,
389 buffers,
390 buffer_metadata,
391 composite_buffers,
392 group_names,
393 );
394
395 for t in tab_targets.iter() {
397 let Some(name_owned) = resolved_names.get(t).cloned() else {
399 continue;
400 };
401 let name = name_owned.as_str();
402 rendered_targets.push(*t);
403
404 let modified = match t {
406 TabTarget::Buffer(id) => {
407 if composite_buffers.contains_key(id) {
408 ""
409 } else if let Some(state) = buffers.get(id) {
410 if state.buffer.is_modified() {
411 "*"
412 } else {
413 ""
414 }
415 } else {
416 ""
417 }
418 }
419 TabTarget::Group(_) => "",
420 };
421 let binary_indicator = match t {
422 TabTarget::Buffer(id) => {
423 if buffer_metadata.get(id).map(|m| m.binary).unwrap_or(false) {
424 " [BIN]"
425 } else {
426 ""
427 }
428 }
429 TabTarget::Group(_) => "",
430 };
431
432 let is_active = *t == active_target;
433
434 let (is_hovered_name, is_hovered_close) = match hovered_tab {
436 Some((hover_target, is_close)) if hover_target == *t => (!is_close, is_close),
437 _ => (false, false),
438 };
439
440 let base_style = if is_active {
442 if is_active_split {
443 Style::default()
444 .fg(theme.tab_active_fg)
445 .bg(theme.tab_active_bg)
446 .add_modifier(Modifier::BOLD)
447 } else {
448 Style::default()
449 .fg(theme.tab_active_fg)
450 .bg(theme.tab_inactive_bg)
451 .add_modifier(Modifier::BOLD)
452 }
453 } else if is_hovered_name {
454 Style::default()
456 .fg(theme.tab_inactive_fg)
457 .bg(theme.tab_hover_bg)
458 } else {
459 Style::default()
460 .fg(theme.tab_inactive_fg)
461 .bg(theme.tab_inactive_bg)
462 };
463
464 let close_style = if is_hovered_close {
466 base_style.fg(theme.tab_close_hover_fg)
468 } else {
469 base_style
470 };
471
472 let tab_name_text = format!(" {name}{modified}{binary_indicator} ");
474 let tab_name_width = str_width(&tab_name_text);
475
476 let close_text = "× ";
478 let close_width = str_width(close_text);
479
480 let total_width = tab_name_width + close_width;
481
482 let start_pos: usize = all_tab_spans.iter().map(|(_, w)| w).sum();
483 let close_start_pos = start_pos + tab_name_width;
484 let end_pos = start_pos + total_width;
485 tab_ranges.push((start_pos, end_pos, close_start_pos));
486
487 all_tab_spans.push((Span::styled(tab_name_text, base_style), tab_name_width));
489 all_tab_spans.push((
491 Span::styled(close_text.to_string(), close_style),
492 close_width,
493 ));
494 }
495
496 let mut final_spans: Vec<(Span<'static>, usize)> = Vec::new();
500 let mut separator_offset = 0usize;
501 let spans_per_tab = 2; for (tab_idx, chunk) in all_tab_spans.chunks(spans_per_tab).enumerate() {
503 if separator_offset > 0 {
505 let (start, end, close_start) = tab_ranges[tab_idx];
506 tab_ranges[tab_idx] = (
507 start + separator_offset,
508 end + separator_offset,
509 close_start + separator_offset,
510 );
511 }
512
513 for span in chunk {
514 final_spans.push(span.clone());
515 }
516 if tab_idx < rendered_targets.len().saturating_sub(1) {
518 final_spans.push((
519 Span::styled(" ", Style::default().bg(theme.tab_separator_bg)),
520 1,
521 ));
522 separator_offset += 1;
523 }
524 }
525 #[allow(clippy::let_and_return)]
526 let all_tab_spans = final_spans;
527
528 let mut current_spans: Vec<Span> = Vec::new();
529 let max_width = area.width as usize;
530
531 let total_width: usize = all_tab_spans.iter().map(|(_, w)| w).sum();
532 let _active_tab_idx = rendered_targets.iter().position(|t| *t == active_target);
535
536 let mut tab_widths: Vec<usize> = Vec::new();
537 for (start, end, _close_start) in &tab_ranges {
538 tab_widths.push(end.saturating_sub(*start));
539 }
540
541 let max_offset = total_width.saturating_sub(max_width);
544 let offset = tab_scroll_offset.min(total_width);
545 tracing::trace!(
546 "render_for_split: tab_scroll_offset={}, max_offset={}, offset={}, total={}, max_width={}",
547 tab_scroll_offset, max_offset, offset, total_width, max_width
548 );
549 let show_left = offset > 0;
551 let show_right = total_width.saturating_sub(offset) > max_width;
552 let available = max_width
553 .saturating_sub((show_left as usize + show_right as usize) * SCROLL_INDICATOR_WIDTH);
554
555 let mut rendered_width = 0;
556 let mut skip_chars_count = offset;
557
558 if show_left {
559 current_spans.push(Span::styled(
560 SCROLL_INDICATOR_LEFT,
561 Style::default().bg(theme.tab_separator_bg),
562 ));
563 rendered_width += SCROLL_INDICATOR_WIDTH;
564 }
565
566 for (mut span, width) in all_tab_spans.into_iter() {
567 if skip_chars_count >= width {
568 skip_chars_count -= width;
569 continue;
570 }
571
572 let visible_chars_in_span = width - skip_chars_count;
573 if rendered_width + visible_chars_in_span
574 > max_width.saturating_sub(if show_right {
575 SCROLL_INDICATOR_WIDTH
576 } else {
577 0
578 })
579 {
580 let remaining_width =
581 max_width
582 .saturating_sub(rendered_width)
583 .saturating_sub(if show_right {
584 SCROLL_INDICATOR_WIDTH
585 } else {
586 0
587 });
588 let truncated_content = span
589 .content
590 .chars()
591 .skip(skip_chars_count)
592 .take(remaining_width)
593 .collect::<String>();
594 span.content = std::borrow::Cow::Owned(truncated_content);
595 current_spans.push(span);
596 rendered_width += remaining_width;
597 break;
598 } else {
599 let visible_content = span
600 .content
601 .chars()
602 .skip(skip_chars_count)
603 .collect::<String>();
604 span.content = std::borrow::Cow::Owned(visible_content);
605 current_spans.push(span);
606 rendered_width += visible_chars_in_span;
607 skip_chars_count = 0;
608 }
609 }
610
611 let right_indicator_x = if show_right && rendered_width < max_width {
613 Some(area.x + rendered_width as u16)
614 } else {
615 None
616 };
617
618 if show_right && rendered_width < max_width {
619 current_spans.push(Span::styled(
620 SCROLL_INDICATOR_RIGHT,
621 Style::default().bg(theme.tab_separator_bg),
622 ));
623 rendered_width += SCROLL_INDICATOR_WIDTH;
624 }
625
626 if rendered_width < max_width {
627 current_spans.push(Span::styled(
628 " ".repeat(max_width.saturating_sub(rendered_width)),
629 Style::default().bg(theme.tab_separator_bg),
630 ));
631 }
632
633 let line = Line::from(current_spans);
634 let block = Block::default().style(Style::default().bg(theme.tab_separator_bg));
635 let paragraph = Paragraph::new(line).block(block);
636 frame.render_widget(paragraph, area);
637
638 let left_indicator_offset = if show_left { SCROLL_INDICATOR_WIDTH } else { 0 };
644
645 if show_left {
647 layout.left_scroll_area =
648 Some(Rect::new(area.x, area.y, SCROLL_INDICATOR_WIDTH as u16, 1));
649 }
650 if let Some(right_x) = right_indicator_x {
651 layout.right_scroll_area =
653 Some(Rect::new(right_x, area.y, SCROLL_INDICATOR_WIDTH as u16, 1));
654 }
655
656 for (idx, target) in rendered_targets.iter().enumerate() {
657 let (logical_start, logical_end, logical_close_start) = tab_ranges[idx];
658
659 let visible_start = offset;
663 let visible_end = offset + available;
664
665 if logical_end <= visible_start || logical_start >= visible_end {
667 continue;
668 }
669
670 let screen_start = if logical_start >= visible_start {
672 area.x + left_indicator_offset as u16 + (logical_start - visible_start) as u16
673 } else {
674 area.x + left_indicator_offset as u16
675 };
676
677 let screen_end = if logical_end <= visible_end {
678 area.x + left_indicator_offset as u16 + (logical_end - visible_start) as u16
679 } else {
680 area.x + left_indicator_offset as u16 + available as u16
681 };
682
683 let screen_close_start = if logical_close_start >= visible_start
685 && logical_close_start < visible_end
686 {
687 area.x + left_indicator_offset as u16 + (logical_close_start - visible_start) as u16
688 } else if logical_close_start < visible_start {
689 screen_start
691 } else {
692 screen_end
694 };
695
696 let tab_width = screen_end.saturating_sub(screen_start);
698 let close_width = screen_end.saturating_sub(screen_close_start);
699
700 layout.tabs.push(TabHitArea {
701 target: *target,
702 tab_area: Rect::new(screen_start, area.y, tab_width, 1),
703 close_area: Rect::new(screen_close_start, area.y, close_width, 1),
704 });
705 }
706
707 layout
708 }
709
710 #[allow(dead_code)]
713 pub fn render(
714 frame: &mut Frame,
715 area: Rect,
716 buffers: &HashMap<BufferId, EditorState>,
717 buffer_metadata: &HashMap<BufferId, BufferMetadata>,
718 composite_buffers: &HashMap<BufferId, crate::model::composite_buffer::CompositeBuffer>,
719 active_buffer: BufferId,
720 theme: &crate::view::theme::Theme,
721 ) {
722 let mut buffer_ids: Vec<_> = buffers.keys().copied().collect();
724 buffer_ids.sort_by_key(|id| id.0);
725 let tab_targets: Vec<TabTarget> = buffer_ids.into_iter().map(TabTarget::Buffer).collect();
726 let group_names = HashMap::new();
727
728 Self::render_for_split(
729 frame,
730 area,
731 &tab_targets,
732 buffers,
733 buffer_metadata,
734 composite_buffers,
735 TabTarget::Buffer(active_buffer),
736 theme,
737 true, 0, None, &group_names,
741 );
742 }
743}
744
745#[cfg(test)]
746mod tests {
747 use super::*;
748 use crate::model::event::BufferId;
749
750 #[test]
751 fn scroll_to_show_active_first_tab() {
752 let widths = vec![5, 5, 5];
754 let offset = scroll_to_show_tab(&widths, 0, 10, 20);
755 assert_eq!(offset, 0);
757 }
758
759 #[test]
760 fn scroll_to_show_tab_already_visible() {
761 let widths = vec![5, 5, 5];
763 let offset = scroll_to_show_tab(&widths, 1, 0, 20);
764 assert_eq!(offset, 0);
766 }
767
768 #[test]
769 fn scroll_to_show_tab_on_right() {
770 let widths = vec![10, 10, 10];
772 let offset = scroll_to_show_tab(&widths, 2, 0, 15);
773 assert!(offset > 0);
775 }
776
777 fn visible_range(offset: usize, total_width: usize, max_width: usize) -> (usize, usize) {
780 let show_left = offset > 0;
781 let show_right = total_width.saturating_sub(offset) > max_width;
782 let available = max_width
783 .saturating_sub(if show_left { 1 } else { 0 })
784 .saturating_sub(if show_right { 1 } else { 0 });
785 (offset, offset + available)
786 }
787
788 #[test]
792 fn scroll_to_show_tab_active_always_visible() {
793 let tab_content_width = 33; let num_tabs = 15;
798 let max_width = 40;
799
800 let mut tab_widths = Vec::new();
801 for i in 0..num_tabs {
802 if i > 0 {
803 tab_widths.push(1); }
805 tab_widths.push(tab_content_width);
806 }
807 let total_width: usize = tab_widths.iter().sum();
808
809 for tab_idx in 0..num_tabs {
810 let active_width_idx = if tab_idx == 0 { 0 } else { tab_idx * 2 };
811 let tab_start: usize = tab_widths[..active_width_idx].iter().sum();
812 let tab_end = tab_start + tab_widths[active_width_idx];
813
814 let offset = scroll_to_show_tab(&tab_widths, active_width_idx, 0, max_width);
815 let (vis_start, vis_end) = visible_range(offset, total_width, max_width);
816
817 assert!(
818 tab_start >= vis_start && tab_end <= vis_end,
819 "Tab {} (width_idx={}, {}..{}) not fully visible in range {}..{} (offset={})",
820 tab_idx,
821 active_width_idx,
822 tab_start,
823 tab_end,
824 vis_start,
825 vis_end,
826 offset
827 );
828 }
829 }
830
831 #[test]
833 fn scroll_to_show_tab_property_varied_sizes() {
834 let test_cases: Vec<(Vec<usize>, usize)> = vec![
835 (vec![10, 15, 20, 10, 25], 30),
836 (vec![5; 20], 20),
837 (vec![40], 40), (vec![50], 40), (vec![3, 3, 3], 100), ];
841
842 for (tab_widths, max_width) in test_cases {
843 let total_width: usize = tab_widths.iter().sum();
844 for active_idx in 0..tab_widths.len() {
845 let tab_start: usize = tab_widths[..active_idx].iter().sum();
846 let tab_end = tab_start + tab_widths[active_idx];
847 let tab_w = tab_widths[active_idx];
848
849 let offset = scroll_to_show_tab(&tab_widths, active_idx, 0, max_width);
850 let (vis_start, vis_end) = visible_range(offset, total_width, max_width);
851
852 if tab_w <= max_width.saturating_sub(2) || (active_idx == 0 && tab_w <= max_width) {
854 assert!(
855 tab_start >= vis_start && tab_end <= vis_end,
856 "Tab {} ({}..{}, w={}) not visible in {}..{} (offset={}, max_width={}, widths={:?})",
857 active_idx, tab_start, tab_end, tab_w, vis_start, vis_end, offset, max_width, tab_widths
858 );
859 }
860 }
861 }
862 }
863
864 #[test]
865 fn test_tab_layout_hit_test() {
866 let bar_area = Rect::new(0, 0, 80, 1);
867 let mut layout = TabLayout::new(bar_area);
868
869 let buf1 = BufferId(1);
870 let target1 = TabTarget::Buffer(buf1);
871
872 layout.tabs.push(TabHitArea {
873 target: target1,
874 tab_area: Rect::new(0, 0, 16, 1),
875 close_area: Rect::new(12, 0, 4, 1),
876 });
877
878 assert_eq!(layout.hit_test(5, 0), Some(TabHit::TabName(target1)));
880
881 assert_eq!(layout.hit_test(13, 0), Some(TabHit::CloseButton(target1)));
883
884 assert_eq!(layout.hit_test(50, 0), Some(TabHit::BarBackground));
886
887 assert_eq!(layout.hit_test(50, 5), None);
889 }
890}