diff options
| author | Can Celik <ogulcancelik@gmail.com> | 2026-09-01 15:48:22 +0300 |
|---|---|---|
| committer | GitHub <noreply@github.com> | 2026-09-01 15:48:22 +0300 |
| commit | 207be3c771d281baae6e5fa0fb74be9a056e97a2 (patch) | |
| tree | d7b037ecdf629224a048c841d11659efdc23507d /src/ui/tabs.rs | |
| parent | 8a6d697308162874a236c84aac0b0f8c7ac01f83 (diff) | |
refactor: render the shell in the client (#3487)
* feat: add opt-in client-rendered shell
* feat: add client-owned shell mouse controls
* feat: complete client-owned shell chrome
* feat: add client-owned shell settings and notifications
* feat: run client shell custom commands
* feat: render popup terminals in client shell
* feat: add client-owned scrollback and copy mode
* fix: preserve client worktree trust defaults
* feat: render startup diagnostics in client shell
* feat: render onboarding in client shell
* feat: render product announcements in client shell
* feat: render release notes in client shell
* feat: render mobile switcher in client shell
* feat: render kitty graphics in client shell
* fix: preserve client shell lifecycle coherence
* refactor: route client shell commands through endpoint connections
* refactor: make the client-rendered shell the default
* refactor: remove monolithic launch mode
* refactor: complete client-rendered shell cutover
* perf: retain client pane surface updates
* perf: scale retained pane surface updates
* perf: remove remote pane input latency
* test: make client shell checks platform-independent
* fix: prevent client timer starvation
* test: fix client shell platform gates
* test: accept retained surfaces in cross-area checks
* test: make client mode assertions platform-neutral
* fix: badge only outdated integrations
Diffstat (limited to 'src/ui/tabs.rs')
| -rw-r--r-- | src/ui/tabs.rs | 788 |
1 files changed, 0 insertions, 788 deletions
diff --git a/src/ui/tabs.rs b/src/ui/tabs.rs deleted file mode 100644 index 603422fb..00000000 --- a/src/ui/tabs.rs +++ /dev/null @@ -1,788 +0,0 @@ -use ratatui::{ - layout::Rect, - style::{Modifier, Style}, - widgets::Paragraph, - Frame, -}; - -use super::text::display_width_u16; -use super::widgets::panel_contrast_fg; -use crate::app::AppState; - -const MIN_TAB_WIDTH: u16 = 8; -const NEW_TAB_WIDTH: u16 = 3; -const TAB_SCROLL_BUTTON_WIDTH: u16 = 3; -const ZOOM_INDICATOR: &str = "ZOOM"; -// The narrowest overflowing tab strip worth keeping interactive: one -// minimum-width tab, both scroll controls, and the new-tab control. -const MIN_TAB_STRIP_WIDTH: u16 = - MIN_TAB_WIDTH + NEW_TAB_WIDTH + TAB_SCROLL_BUTTON_WIDTH.saturating_mul(2); - -#[derive(Debug, Clone, Default)] -pub(crate) struct TabBarView { - pub scroll: usize, - pub tab_hit_areas: Vec<Rect>, - pub scroll_left_hit_area: Rect, - pub scroll_right_hit_area: Rect, - pub new_tab_hit_area: Rect, -} - -fn tab_width(ws: &crate::workspace::Workspace, tab_idx: usize) -> u16 { - display_width_u16(&tab_chrome_label(ws, tab_idx)) - .saturating_add(4) - .max(MIN_TAB_WIDTH) -} - -fn tab_chrome_label(ws: &crate::workspace::Workspace, tab_idx: usize) -> String { - let name = ws - .tab_display_name(tab_idx) - .unwrap_or_else(|| (tab_idx + 1).to_string()); - if ws.tabs.get(tab_idx).is_some_and(|tab| tab.zoomed) { - format!("{name} Z") - } else { - name - } -} - -#[derive(Clone, Copy)] -struct VisibleStatusSegment<'a> { - text: &'a str, - accent: bool, -} - -fn visible_status_segments(app: &AppState) -> Vec<VisibleStatusSegment<'_>> { - let zoomed = app - .active - .and_then(|index| app.workspaces.get(index)) - .is_some_and(|workspace| workspace.zoomed); - app.tab_bar_right - .iter() - .filter_map(|segment| match segment { - crate::app::state::TabBarStatusSegment::Zoom if zoomed => Some(VisibleStatusSegment { - text: ZOOM_INDICATOR, - accent: true, - }), - crate::app::state::TabBarStatusSegment::Text(Some(text)) - if display_width_u16(text) > 0 => - { - Some(VisibleStatusSegment { - text, - accent: false, - }) - } - crate::app::state::TabBarStatusSegment::Zoom - | crate::app::state::TabBarStatusSegment::Text(_) => None, - }) - .collect() -} - -fn tab_bar_status_width(app: &AppState) -> u16 { - let segments = visible_status_segments(app); - let content_width = segments.iter().fold(0_u16, |width, segment| { - width.saturating_add(display_width_u16(segment.text)) - }); - let separators = u16::try_from(segments.len().saturating_sub(1)).unwrap_or(u16::MAX); - content_width - .saturating_add(display_width_u16(&app.tab_bar_right_separator).saturating_mul(separators)) -} - -fn tab_bar_status_area(app: &AppState, area: Rect) -> Option<Rect> { - let width = tab_bar_status_width(app); - if width == 0 { - return None; - } - let reserved = width.saturating_add(1); - (area.width.saturating_sub(reserved) >= MIN_TAB_STRIP_WIDTH) - .then(|| Rect::new(area.x + area.width.saturating_sub(width), area.y, width, 1)) -} - -// Tabs win over status decoration on narrow rows. The extra reserved cell is -// the gap between the interactive strip and the right-aligned status entries. -pub(crate) fn tab_bar_content_area(app: &AppState, area: Rect) -> Rect { - let reserved = tab_bar_status_area(app, area) - .map(|status| status.width.saturating_add(1)) - .unwrap_or(0); - Rect { - width: area.width.saturating_sub(reserved), - ..area - } -} - -fn layout_tab_hit_areas(ws: &crate::workspace::Workspace, area: Rect, scroll: usize) -> Vec<Rect> { - let mut rects = vec![Rect::default(); ws.tabs.len()]; - if area.width == 0 || area.height == 0 { - return rects; - } - - let mut x = area.x; - let right = area.x + area.width; - for (idx, rect) in rects.iter_mut().enumerate().skip(scroll) { - if x >= right { - break; - } - let desired = tab_width(ws, idx); - let remaining = right.saturating_sub(x); - let width = desired.min(remaining).max(1); - *rect = Rect::new(x, area.y, width, 1); - x = x.saturating_add(width + 1); - } - rects -} - -fn centered_tab_scroll(ws: &crate::workspace::Workspace, area: Rect) -> usize { - let mut best_scroll = ws.active_tab; - let mut best_distance = u16::MAX; - let viewport_center = area.x.saturating_mul(2).saturating_add(area.width); - - for scroll in 0..=ws.active_tab { - let rects = layout_tab_hit_areas(ws, area, scroll); - let Some(active_rect) = rects.get(ws.active_tab).copied() else { - continue; - }; - if active_rect.width == 0 { - continue; - } - - let active_center = active_rect - .x - .saturating_mul(2) - .saturating_add(active_rect.width); - let distance = active_center.abs_diff(viewport_center); - if distance <= best_distance { - best_distance = distance; - best_scroll = scroll; - } - } - - best_scroll -} - -fn trailing_tab_controls_x(tab_hit_areas: &[Rect], fallback_x: u16) -> u16 { - tab_hit_areas - .iter() - .rev() - .find(|rect| rect.width > 0) - .map(|rect| rect.x + rect.width) - .unwrap_or(fallback_x) -} - -fn max_tab_scroll(ws: &crate::workspace::Workspace, area: Rect) -> usize { - (0..ws.tabs.len()) - .find(|&scroll| { - layout_tab_hit_areas(ws, area, scroll) - .last() - .is_some_and(|rect| rect.width > 0) - }) - .unwrap_or(0) -} - -pub(crate) fn compute_tab_bar_view( - ws: &crate::workspace::Workspace, - area: Rect, - current_scroll: usize, - follow_active: bool, - mouse_chrome: bool, -) -> TabBarView { - if area.width == 0 || area.height == 0 { - return TabBarView::default(); - } - - if !mouse_chrome { - let max_scroll = max_tab_scroll(ws, area); - let scroll = if follow_active { - centered_tab_scroll(ws, area).min(max_scroll) - } else { - current_scroll.min(max_scroll) - }; - return TabBarView { - scroll, - tab_hit_areas: layout_tab_hit_areas(ws, area, scroll), - scroll_left_hit_area: Rect::default(), - scroll_right_hit_area: Rect::default(), - new_tab_hit_area: Rect::default(), - }; - } - - let area_right = area.x + area.width; - let all_tabs_area = Rect::new( - area.x, - area.y, - area.width.saturating_sub(NEW_TAB_WIDTH), - area.height, - ); - let all_tabs = layout_tab_hit_areas(ws, all_tabs_area, 0); - let overflow = all_tabs.iter().any(|rect| rect.width == 0); - if !overflow { - let new_tab_x = trailing_tab_controls_x(&all_tabs, area.x); - let new_tab_hit_area = Rect::new( - new_tab_x, - area.y, - area_right.saturating_sub(new_tab_x).min(NEW_TAB_WIDTH), - 1, - ); - return TabBarView { - scroll: 0, - tab_hit_areas: all_tabs, - scroll_left_hit_area: Rect::default(), - scroll_right_hit_area: Rect::default(), - new_tab_hit_area, - }; - } - - let left_hit_area = Rect::new(area.x, area.y, TAB_SCROLL_BUTTON_WIDTH.min(area.width), 1); - let tab_area_x = left_hit_area.x + left_hit_area.width; - let reserved_trailing_width = NEW_TAB_WIDTH.saturating_add(TAB_SCROLL_BUTTON_WIDTH); - let tab_area_right = area_right.saturating_sub(reserved_trailing_width); - let tab_area = Rect::new( - tab_area_x, - area.y, - tab_area_right.saturating_sub(tab_area_x), - area.height, - ); - - let max_scroll = max_tab_scroll(ws, tab_area); - let scroll = if follow_active { - centered_tab_scroll(ws, tab_area).min(max_scroll) - } else { - current_scroll.min(max_scroll) - }; - let tab_hit_areas = layout_tab_hit_areas(ws, tab_area, scroll); - let trailing_x = trailing_tab_controls_x(&tab_hit_areas, tab_area_x).min(tab_area_right); - let right_hit_area = Rect::new( - trailing_x, - area.y, - area_right - .saturating_sub(trailing_x) - .min(TAB_SCROLL_BUTTON_WIDTH), - 1, - ); - let new_tab_x = right_hit_area.x + right_hit_area.width; - let new_tab_hit_area = Rect::new( - new_tab_x, - area.y, - area_right.saturating_sub(new_tab_x).min(NEW_TAB_WIDTH), - 1, - ); - - TabBarView { - scroll, - tab_hit_areas, - scroll_left_hit_area: left_hit_area, - scroll_right_hit_area: right_hit_area, - new_tab_hit_area, - } -} - -fn tab_drop_indicator_x( - app: &AppState, - ws: &crate::workspace::Workspace, - insert_idx: usize, -) -> Option<u16> { - let mut visible_tabs = app - .view - .tab_hit_areas - .iter() - .enumerate() - .filter(|(_, rect)| rect.width > 0); - let first_visible = visible_tabs.clone().next()?; - let last_visible = visible_tabs.next_back().unwrap_or(first_visible); - - if insert_idx == 0 { - return Some(if first_visible.0 == 0 { - first_visible.1.x - } else { - app.view.tab_scroll_left_hit_area.x + app.view.tab_scroll_left_hit_area.width - }); - } - - if let Some((_, rect)) = app - .view - .tab_hit_areas - .iter() - .enumerate() - .find(|(idx, rect)| *idx == insert_idx && rect.width > 0) - { - return Some(rect.x.saturating_sub(1)); - } - - if insert_idx >= ws.tabs.len() { - return Some(if last_visible.0 + 1 >= ws.tabs.len() { - last_visible.1.x + last_visible.1.width - } else { - app.view.tab_scroll_right_hit_area.x.saturating_sub(1) - }); - } - - None -} - -pub(super) fn render_tab_bar(app: &AppState, frame: &mut Frame, area: Rect) { - if area.width == 0 || area.height == 0 { - return; - } - let Some(active_ws_idx) = app.active else { - return; - }; - let Some(ws) = app.workspaces.get(active_ws_idx) else { - return; - }; - let p = &app.palette; - - frame.render_widget( - Paragraph::new(" ".repeat(area.width as usize)).style(Style::default().bg(p.panel_bg)), - area, - ); - - let first_visible_idx = app - .view - .tab_hit_areas - .iter() - .enumerate() - .find(|(_, rect)| rect.width > 0) - .map(|(idx, _)| idx); - let last_visible_idx = app - .view - .tab_hit_areas - .iter() - .enumerate() - .rev() - .find(|(_, rect)| rect.width > 0) - .map(|(idx, _)| idx); - let can_scroll_left = app.view.tab_scroll_left_hit_area.width > 0 && app.tab_scroll > 0; - let can_scroll_right = app.view.tab_scroll_right_hit_area.width > 0 - && last_visible_idx.is_some_and(|idx| idx + 1 < ws.tabs.len()); - - if app.mouse_capture && app.view.tab_scroll_left_hit_area.width > 0 { - let style = if can_scroll_left { - Style::default().fg(p.overlay1).bg(p.surface0) - } else { - Style::default() - .fg(p.overlay0) - .bg(p.surface0) - .add_modifier(Modifier::DIM) - }; - frame.render_widget( - Paragraph::new(" < ").style(style), - app.view.tab_scroll_left_hit_area, - ); - } - - if app.mouse_capture && app.view.tab_scroll_right_hit_area.width > 0 { - let style = if can_scroll_right { - Style::default().fg(p.overlay1).bg(p.surface0) - } else { - Style::default() - .fg(p.overlay0) - .bg(p.surface0) - .add_modifier(Modifier::DIM) - }; - frame.render_widget( - Paragraph::new(" > ").style(style), - app.view.tab_scroll_right_hit_area, - ); - } - - for (idx, tab) in ws.tabs.iter().enumerate() { - let Some(rect) = app.view.tab_hit_areas.get(idx).copied() else { - break; - }; - if rect.width == 0 { - continue; - } - let active = idx == ws.active_tab; - let style = if active { - let base = Style::default().fg(panel_contrast_fg(p)).bg(p.accent); - if tab.is_auto_named() { - base - } else { - base.add_modifier(Modifier::BOLD) - } - } else if tab.is_auto_named() { - Style::default() - .fg(p.overlay0) - .bg(p.surface0) - .add_modifier(Modifier::DIM) - } else { - Style::default().fg(p.overlay1).bg(p.surface0) - }; - let width = rect.width as usize; - let name = tab_chrome_label(ws, idx); - // Pad by terminal columns, not chars, so wide glyphs stay centered. - let padding = width.saturating_sub(display_width_u16(&name) as usize); - let left = padding / 2; - let text = format!( - "{empty:left$}{name}{empty:right$}", - empty = "", - right = padding - left - ); - frame.render_widget(Paragraph::new(text).style(style), rect); - } - - if let Some(crate::app::state::DragState { - target: - crate::app::state::DragTarget::TabReorder { - ws_idx, - insert_idx: Some(insert_idx), - .. - }, - }) = &app.drag - { - if *ws_idx == active_ws_idx { - if let Some(x) = tab_drop_indicator_x(app, ws, *insert_idx) { - frame.buffer_mut()[(x.min(area.x + area.width.saturating_sub(1)), area.y)] - .set_symbol("│") - .set_style(Style::default().fg(p.accent)); - } - } - } - - if app.mouse_capture && app.view.new_tab_hit_area.width > 0 { - frame.render_widget( - Paragraph::new(" + ").style(Style::default().fg(p.overlay1)), - app.view.new_tab_hit_area, - ); - } - - if first_visible_idx.is_some_and(|idx| idx > 0) { - let x = if app.mouse_capture && app.view.tab_scroll_left_hit_area.width > 0 { - app.view.tab_scroll_left_hit_area.x + app.view.tab_scroll_left_hit_area.width - } else { - area.x - }; - if x < area.x + area.width { - frame.buffer_mut()[(x, area.y)] - .set_symbol("…") - .set_style(Style::default().fg(p.overlay0)); - } - } - if last_visible_idx.is_some_and(|idx| idx + 1 < ws.tabs.len()) { - let content = tab_bar_content_area(app, area); - let content_right = content.x + content.width; - let x = if app.mouse_capture && app.view.tab_scroll_right_hit_area.width > 0 { - app.view.tab_scroll_right_hit_area.x.saturating_sub(1) - } else { - content_right.saturating_sub(1) - }; - if x >= area.x && x < area.x + area.width { - frame.buffer_mut()[(x, area.y)] - .set_symbol("…") - .set_style(Style::default().fg(p.overlay0)); - } - } - - if let Some(status_area) = tab_bar_status_area(app, area) { - let segments = visible_status_segments(app); - let separator_width = display_width_u16(&app.tab_bar_right_separator); - let mut x = status_area.x; - for (index, segment) in segments.iter().enumerate() { - if index > 0 && separator_width > 0 { - let rect = Rect::new(x, area.y, separator_width, 1); - frame.render_widget( - Paragraph::new(app.tab_bar_right_separator.as_str()) - .style(Style::default().fg(p.overlay0).bg(p.panel_bg)), - rect, - ); - x = x.saturating_add(separator_width); - } - - let width = display_width_u16(segment.text); - let rect = Rect::new(x, area.y, width, 1); - let style = if segment.accent { - Style::default() - .fg(panel_contrast_fg(p)) - .bg(p.accent) - .add_modifier(Modifier::BOLD) - } else { - Style::default().fg(p.overlay1).bg(p.panel_bg) - }; - frame.render_widget(Paragraph::new(segment.text).style(style), rect); - x = x.saturating_add(width); - } - } -} - -#[cfg(test)] -mod tests { - use super::*; - use crate::app::state::AppState; - use crate::workspace::Workspace; - use ratatui::{backend::TestBackend, Terminal}; - - fn buffer_row_text(buffer: &ratatui::buffer::Buffer, area: Rect, row: u16) -> String { - (area.x..area.x + area.width) - .map(|x| buffer[(x, row)].symbol()) - .collect::<String>() - .trim_end() - .to_string() - } - - #[test] - fn tab_bar_marks_zoomed_tabs_without_renaming_them() { - let mut app = AppState::test_new(); - let mut ws = Workspace::test_new("test"); - ws.tabs[0].zoomed = true; - let custom_tab = ws.test_add_tab(Some("test")); - ws.tabs[custom_tab].zoomed = true; - - app.workspaces = vec![ws]; - app.active = Some(0); - app.view.tab_bar_rect = Rect::new(0, 0, 30, 1); - let view = compute_tab_bar_view(&app.workspaces[0], app.view.tab_bar_rect, 0, true, false); - app.view.tab_hit_areas = view.tab_hit_areas; - - let backend = TestBackend::new(30, 1); - let mut terminal = Terminal::new(backend).unwrap(); - terminal - .draw(|frame| render_tab_bar(&app, frame, app.view.tab_bar_rect)) - .unwrap(); - - let row = buffer_row_text(terminal.backend().buffer(), app.view.tab_bar_rect, 0); - assert!(row.contains(" 1 Z"), "tab row: {row:?}"); - assert!(row.contains(" test Z"), "tab row: {row:?}"); - assert_eq!(app.workspaces[0].tab_display_name(0).as_deref(), Some("1")); - assert_eq!( - app.workspaces[0].tab_display_name(custom_tab).as_deref(), - Some("test") - ); - } - - #[test] - fn tab_bar_renders_ordered_status_entries_with_separator() { - let mut app = AppState::test_new(); - let mut ws = Workspace::test_new("test"); - ws.tabs[0].zoomed = true; - app.tab_bar_right = vec![ - crate::app::state::TabBarStatusSegment::Zoom, - crate::app::state::TabBarStatusSegment::Text(Some("wintermute".into())), - crate::app::state::TabBarStatusSegment::Text(Some("14:30".into())), - ]; - app.tab_bar_right_separator = " · ".into(); - - app.workspaces = vec![ws]; - app.active = Some(0); - app.view.tab_bar_rect = Rect::new(0, 0, 60, 1); - let content = tab_bar_content_area(&app, app.view.tab_bar_rect); - let view = compute_tab_bar_view(&app.workspaces[0], content, 0, true, false); - app.view.tab_hit_areas = view.tab_hit_areas.clone(); - - let backend = TestBackend::new(60, 1); - let mut terminal = Terminal::new(backend).unwrap(); - terminal - .draw(|frame| render_tab_bar(&app, frame, app.view.tab_bar_rect)) - .unwrap(); - - let buffer = terminal.backend().buffer(); - let row = buffer_row_text(buffer, app.view.tab_bar_rect, 0); - assert!( - row.ends_with("ZOOM · wintermute · 14:30"), - "tab row: {row:?}" - ); - let status_x = 60 - display_width_u16("ZOOM · wintermute · 14:30"); - assert_eq!(buffer[(status_x, 0)].style().bg, Some(app.palette.accent)); - for rect in &view.tab_hit_areas { - assert!(rect.x + rect.width <= content.x + content.width); - } - } - - #[test] - fn hidden_status_entries_do_not_leave_dangling_separators() { - let mut app = AppState::test_new(); - app.tab_bar_right = vec![ - crate::app::state::TabBarStatusSegment::Zoom, - crate::app::state::TabBarStatusSegment::Text(None), - crate::app::state::TabBarStatusSegment::Text(Some("wintermute".into())), - ]; - app.tab_bar_right_separator = " | ".into(); - app.workspaces = vec![Workspace::test_new("test")]; - app.active = Some(0); - app.view.tab_bar_rect = Rect::new(0, 0, 40, 1); - let content = tab_bar_content_area(&app, app.view.tab_bar_rect); - let view = compute_tab_bar_view(&app.workspaces[0], content, 0, true, false); - app.view.tab_hit_areas = view.tab_hit_areas; - - let backend = TestBackend::new(40, 1); - let mut terminal = Terminal::new(backend).unwrap(); - terminal - .draw(|frame| render_tab_bar(&app, frame, app.view.tab_bar_rect)) - .unwrap(); - - let row = buffer_row_text(terminal.backend().buffer(), app.view.tab_bar_rect, 0); - assert!(row.ends_with("wintermute"), "tab row: {row:?}"); - assert!(!row.contains(" | "), "tab row: {row:?}"); - } - - #[test] - fn status_reservation_keeps_a_minimum_width_tab_between_scroll_controls() { - let mut app = AppState::test_new(); - app.tab_bar_right = vec![crate::app::state::TabBarStatusSegment::Text(Some( - "x".into(), - ))]; - let mut workspace = Workspace::test_new("test"); - workspace.test_add_tab(None); - workspace.test_add_tab(None); - app.workspaces = vec![workspace]; - app.active = Some(0); - - let too_narrow = Rect::new(0, 0, MIN_TAB_STRIP_WIDTH + 1, 1); - assert_eq!(tab_bar_content_area(&app, too_narrow), too_narrow); - - let wide_enough = Rect::new(0, 0, MIN_TAB_STRIP_WIDTH + 2, 1); - let content = tab_bar_content_area(&app, wide_enough); - assert_eq!(content.width, MIN_TAB_STRIP_WIDTH); - let view = compute_tab_bar_view(&app.workspaces[0], content, 0, true, true); - assert!(view.tab_hit_areas[0].width >= MIN_TAB_WIDTH); - } - - #[test] - fn combined_status_entries_yield_to_tab_controls_on_narrow_rows() { - let mut app = AppState::test_new(); - app.tab_bar_right = vec![ - crate::app::state::TabBarStatusSegment::Text(Some( - "a-hostname-wider-than-the-whole-bar".into(), - )), - crate::app::state::TabBarStatusSegment::Text(Some("14:30".into())), - ]; - app.workspaces = vec![Workspace::test_new("test")]; - app.active = Some(0); - app.view.tab_bar_rect = Rect::new(0, 0, 30, 1); - - assert_eq!( - tab_bar_content_area(&app, app.view.tab_bar_rect), - app.view.tab_bar_rect - ); - assert_eq!(tab_bar_status_area(&app, app.view.tab_bar_rect), None); - - let view = compute_tab_bar_view( - &app.workspaces[0], - tab_bar_content_area(&app, app.view.tab_bar_rect), - 0, - true, - true, - ); - assert!(view.tab_hit_areas[0].width > 0); - assert!(view.new_tab_hit_area.width > 0); - } - - #[test] - fn cjk_tab_labels_are_centered_by_display_width() { - let mut app = AppState::test_new(); - let mut ws = Workspace::test_new("test"); - ws.tabs[0].set_custom_name("提交 herdr 的反馈".into()); - - app.workspaces = vec![ws]; - app.active = Some(0); - app.view.tab_bar_rect = Rect::new(0, 0, 30, 1); - let view = compute_tab_bar_view(&app.workspaces[0], app.view.tab_bar_rect, 0, true, false); - app.view.tab_hit_areas = view.tab_hit_areas; - - let backend = TestBackend::new(30, 1); - let mut terminal = Terminal::new(backend).unwrap(); - terminal - .draw(|frame| render_tab_bar(&app, frame, app.view.tab_bar_rect)) - .unwrap(); - - // 17 display columns + 4 padding: two columns each side, wide glyphs - // starting right after the left padding. - let rect = app.view.tab_hit_areas[0]; - assert_eq!(rect.width, 21); - let buffer = terminal.backend().buffer(); - assert_eq!(buffer[(rect.x, rect.y)].symbol(), " "); - assert_eq!(buffer[(rect.x + 1, rect.y)].symbol(), " "); - assert_eq!(buffer[(rect.x + 2, rect.y)].symbol(), "提"); - assert_eq!(buffer[(rect.x + rect.width - 2, rect.y)].symbol(), " "); - assert_eq!(buffer[(rect.x + rect.width - 1, rect.y)].symbol(), " "); - } - - #[test] - fn tab_labels_are_centered_in_their_cells() { - let mut app = AppState::test_new(); - let mut ws = Workspace::test_new("test"); - ws.tabs[0].set_custom_name("omarchy".into()); - - app.workspaces = vec![ws]; - app.active = Some(0); - app.view.tab_bar_rect = Rect::new(0, 0, 30, 1); - let view = compute_tab_bar_view(&app.workspaces[0], app.view.tab_bar_rect, 0, true, false); - app.view.tab_hit_areas = view.tab_hit_areas; - - let backend = TestBackend::new(30, 1); - let mut terminal = Terminal::new(backend).unwrap(); - terminal - .draw(|frame| render_tab_bar(&app, frame, app.view.tab_bar_rect)) - .unwrap(); - - let rect = app.view.tab_hit_areas[0]; - let buffer = terminal.backend().buffer(); - let cell: String = (rect.x..rect.x + rect.width) - .map(|x| buffer[(x, rect.y)].symbol()) - .collect(); - assert_eq!(cell, " omarchy "); - } - - #[test] - fn active_auto_named_tab_keeps_readable_weight() { - let mut app = AppState::test_new(); - let ws = Workspace::test_new("test"); - - app.workspaces = vec![ws]; - app.active = Some(0); - app.view.tab_bar_rect = Rect::new(0, 0, 30, 1); - let view = compute_tab_bar_view(&app.workspaces[0], app.view.tab_bar_rect, 0, true, false); - app.view.tab_hit_areas = view.tab_hit_areas; - - let backend = TestBackend::new(30, 1); - let mut terminal = Terminal::new(backend).unwrap(); - terminal - .draw(|frame| render_tab_bar(&app, frame, app.view.tab_bar_rect)) - .unwrap(); - - let tab_rect = app.view.tab_hit_areas[0]; - let style = terminal.backend().buffer()[(tab_rect.x + 1, tab_rect.y)].style(); - - assert_eq!(style.bg, Some(app.palette.accent)); - assert!(!style.add_modifier.contains(Modifier::DIM)); - assert!(!style.add_modifier.contains(Modifier::BOLD)); - } - - #[test] - fn zoom_marker_counts_toward_tab_width() { - let mut ws = Workspace::test_new("test"); - ws.tabs[0].set_custom_name("abcdefgh".into()); - ws.tabs[0].zoomed = true; - - assert_eq!(tab_width(&ws, 0), 14); - } - - #[test] - fn tab_width_uses_display_width_for_cjk_labels() { - let mut ws = Workspace::test_new("test"); - ws.tabs[0].set_custom_name("提交 herdr 的反馈".into()); - - assert_eq!( - tab_width(&ws, 0), - display_width_u16("提交 herdr 的反馈") + 4 - ); - } - - #[test] - fn tab_bar_renders_trailing_cjk_character() { - let mut app = AppState::test_new(); - let mut ws = Workspace::test_new("test"); - ws.tabs[0].set_custom_name("提交 herdr 的反馈".into()); - - app.active = Some(0); - app.workspaces = vec![ws]; - app.view.tab_bar_rect = Rect::new(0, 0, 30, 1); - let view = compute_tab_bar_view(&app.workspaces[0], app.view.tab_bar_rect, 0, true, false); - app.view.tab_hit_areas = view.tab_hit_areas; - - let backend = TestBackend::new(30, 1); - let mut terminal = Terminal::new(backend).unwrap(); - terminal - .draw(|frame| render_tab_bar(&app, frame, app.view.tab_bar_rect)) - .unwrap(); - - let row = buffer_row_text(terminal.backend().buffer(), app.view.tab_bar_rect, 0); - assert!(row.contains('馈'), "tab row: {row:?}"); - } -} |
