style: add density fades to tabs and status

This commit is contained in:
Valdemar Lorenzen
2026-10-12 10:39:59 +02:00
parent f85f84a5d1
commit 9dd6a666d0
2 changed files with 51 additions and 30 deletions
+1 -1
View File
@@ -218,7 +218,7 @@ Use `:tab.new` to open the current buffer in a new, focused tab with one pane. E
| `:tab.close` | `config.core.tab.close` | Close the current tab and all its panes, protecting unsaved changes | | `:tab.close` | `config.core.tab.close` | Close the current tab and all its panes, protecting unsaved changes |
| `:tab.force_close` | `config.core.tab.force_close` | Explicitly discard exclusively displayed edits and close the tab | | `:tab.force_close` | `config.core.tab.force_close` | Explicitly discard exclusively displayed edits and close the tab |
Multiple tabs show a single top row with padded numbered filenames and dirty markers. The active tab uses bold accent text; the row keeps the terminal's background instead of painting a full-width band. Narrow terminals keep the active tab visible. New/close actions have no default keybinding; for example, `config.bind "leader t", config.core.tab.new`. Multiple tabs show a single top row with padded numbered filenames and dirty markers. The active tab has an accent-colored background fading into the terminal background through `░▒▓` / `▓▒░`; inactive tabs remain unfilled. Narrow terminals omit the fade before clipping the active title. The status bar uses the same fade around its mode and right-aligned caret position, leaving file metadata unfilled. New/close actions have no default keybinding; for example, `config.bind "leader t", config.core.tab.new`.
Closing a tab refuses dirty buffers whose final displayed owner would disappear, including a comparison's working and return buffers. Buffers displayed in another tab remain open with their edits, even on force-close. Other hidden buffers remain loaded. Closing the last tab quits only if every open buffer is clean; force-close quits without saving. Closing a tab refuses dirty buffers whose final displayed owner would disappear, including a comparison's working and return buffers. Buffers displayed in another tab remain open with their edits, even on force-close. Other hidden buffers remain loaded. Closing the last tab quits only if every open buffer is clean; force-close quits without saving.
+50 -29
View File
@@ -1041,7 +1041,8 @@ fn draw_tab_bar(frame: &mut Frame, editor: &Editor, area: Rect) -> Rect {
let active = editor.tab_index(); let active = editor.tab_index();
let mut first = active; let mut first = active;
let padding = u16::from(bar.width > 1); let padding = u16::from(bar.width > 1);
let mut width = labels[active].width() + usize::from(padding); let fade = usize::from(bar.width) >= labels[active].width() + usize::from(padding) + 6;
let mut width = labels[active].width() + usize::from(padding) + if fade { 6 } else { 0 };
while first > 0 && width + labels[first - 1].width() <= usize::from(bar.width) { while first > 0 && width + labels[first - 1].width() <= usize::from(bar.width) {
first -= 1; first -= 1;
width += labels[first].width(); width += labels[first].width();
@@ -1049,18 +1050,27 @@ fn draw_tab_bar(frame: &mut Frame, editor: &Editor, area: Rect) -> Rect {
let inactive = Style::default().fg(Color::Gray); let inactive = Style::default().fg(Color::Gray);
let mut x = bar.x + padding; let mut x = bar.x + padding;
for (index, label) in labels.iter().enumerate().skip(first) { for (index, label) in labels.iter().enumerate().skip(first) {
let width = label.width().min(usize::from(bar.right() - x)) as u16; let decorated = index == active && fade;
let style = if index == active { let width = (label.width() + if decorated { 6 } else { 0 })
Style::default() .min(usize::from(bar.right() - x)) as u16;
.fg(Color::Cyan) let highlight = Style::default()
.add_modifier(Modifier::BOLD) .fg(Color::Black)
.bg(Color::Cyan)
.add_modifier(Modifier::BOLD);
let edge = Style::default().fg(Color::Cyan);
let line = if decorated {
Line::from(vec![
Span::styled("░▒▓", edge),
Span::styled(label.as_str(), highlight),
Span::styled("▓▒░", edge),
])
} else { } else {
inactive Line::from(Span::styled(
label.as_str(),
if index == active { highlight } else { inactive },
))
}; };
frame.render_widget( frame.render_widget(Paragraph::new(line), Rect::new(x, bar.y, width, 1));
Paragraph::new(label.as_str()).style(style),
Rect::new(x, bar.y, width, 1),
);
x += width; x += width;
if x == bar.right() { if x == bar.right() {
break; break;
@@ -1604,6 +1614,7 @@ fn draw_status(frame: &mut Frame, editor: &Editor, syntax: &SyntaxCache, status_
} else { } else {
format!(" {}", editor.mode()) format!(" {}", editor.mode())
}; };
let mode_end = status.len();
if let Some(sequence) = editor.sequence_pending() { if let Some(sequence) = editor.sequence_pending() {
write!(status, " {sequence}").unwrap(); write!(status, " {sequence}").unwrap();
} }
@@ -1636,11 +1647,9 @@ fn draw_status(frame: &mut Frame, editor: &Editor, syntax: &SyntaxCache, status_
} }
write!( write!(
status, status,
" {}{} {}:{}", " {}{}",
path, path,
if editor.dirty() { " [+]" } else { "" }, if editor.dirty() { " [+]" } else { "" },
cursor.row + 1,
cursor.column + 1,
) )
.unwrap(); .unwrap();
if editor.selection_count() > 1 { if editor.selection_count() > 1 {
@@ -1654,10 +1663,34 @@ fn draw_status(frame: &mut Frame, editor: &Editor, syntax: &SyntaxCache, status_
} else if let Some(language) = syntax.language() { } else if let Some(language) = syntax.language() {
write!(status, " {language}").unwrap(); write!(status, " {language}").unwrap();
} }
frame.render_widget( let highlight = Style::default().fg(Color::Black).bg(Color::Cyan);
Paragraph::new(status).style(Style::default().fg(Color::Black).bg(Color::Cyan)), let edge = Style::default().fg(Color::Cyan);
status_area, let position = format!(" {}:{} ", cursor.row + 1, cursor.column + 1);
let right_width = (position.width() + 3) as u16;
let show_right = status_area.width >= right_width + 12;
let left = Rect::new(
status_area.x,
status_area.y,
status_area.width - if show_right { right_width } else { 0 },
status_area.height,
); );
frame.render_widget(
Paragraph::new(Line::from(vec![
Span::styled(&status[..mode_end], highlight),
Span::styled("▓▒░", edge),
Span::raw(&status[mode_end..]),
])),
left,
);
if show_right {
frame.render_widget(
Paragraph::new(Line::from(vec![
Span::styled("░▒▓", edge),
Span::styled(position, highlight),
])),
Rect::new(left.right(), status_area.y, right_width, status_area.height),
);
}
} }
struct DiffGeometry { struct DiffGeometry {
@@ -9201,9 +9234,6 @@ end
assert_eq!(buffer[(x, y)].bg, Color::Reset); assert_eq!(buffer[(x, y)].bg, Color::Reset);
} }
} }
for x in area.x..area.right() {
assert_eq!(buffer[(x, area.bottom() - 2)].bg, Color::Cyan);
}
assert_eq!(buffer[(0, 0)].symbol(), " "); assert_eq!(buffer[(0, 0)].symbol(), " ");
editor.handle(Input::Char(' ')); editor.handle(Input::Char(' '));
editor.handle(Input::Char('e')); editor.handle(Input::Char('e'));
@@ -10419,25 +10449,16 @@ end
.unwrap(); .unwrap();
assert!(!screen_text(&terminal).contains("[buffer")); assert!(!screen_text(&terminal).contains("[buffer"));
editor.execute(&format!("edit {}", b.display())); editor.execute(&format!("edit {}", b.display()));
let b_id = editor.buffer_id();
terminal terminal
.draw(|frame| viewports.draw(frame, &mut editor)) .draw(|frame| viewports.draw(frame, &mut editor))
.unwrap(); .unwrap();
let text = screen_text(&terminal); let text = screen_text(&terminal);
assert!(
text.contains(&format!("NORMAL [buffer {b_id}/2]")),
"{text}"
);
assert!(!text.contains("3 selections"), "{text}"); assert!(!text.contains("3 selections"), "{text}");
editor.execute(&format!("buffer {a_id}")); editor.execute(&format!("buffer {a_id}"));
terminal terminal
.draw(|frame| viewports.draw(frame, &mut editor)) .draw(|frame| viewports.draw(frame, &mut editor))
.unwrap(); .unwrap();
let text = screen_text(&terminal); let text = screen_text(&terminal);
assert!(
text.contains(&format!("VISUAL [buffer {a_id}/2]")),
"{text}"
);
assert!(text.contains("3 selections"), "{text}"); assert!(text.contains("3 selections"), "{text}");
let (top, left) = viewports.offsets[&a_id]; let (top, left) = viewports.offsets[&a_id];
for selection in editor.selections() { for selection in editor.selections() {