Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Scrolling Bufferline #8362

Open
wants to merge 10 commits into
base: master
Choose a base branch
from
3 changes: 2 additions & 1 deletion book/src/themes.md
Original file line number Diff line number Diff line change
Expand Up @@ -293,8 +293,9 @@ These scopes are used for theming the editor interface:
| `ui.statusline.select` | Statusline mode during select mode ([only if `editor.color-modes` is enabled][editor-section]) |
| `ui.statusline.separator` | Separator character in statusline |
| `ui.bufferline` | Style for the buffer line |
| `ui.bufferline.active` | Style for the active buffer in buffer line |
| `ui.bufferline.active` | Style for the active buffer in buffer line |
| `ui.bufferline.background` | Style for bufferline background |
| `ui.bufferline.marker` | Style for bufferline underflow and overflow markers |
| `ui.popup` | Documentation popups (e.g. Space + k) |
| `ui.popup.info` | Prompt for multiple key options |
| `ui.window` | Borderlines separating splits |
Expand Down
143 changes: 123 additions & 20 deletions helix-term/src/ui/editor.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,12 +16,12 @@ use helix_core::{
movement::Direction,
syntax::{self, HighlightEvent},
text_annotations::TextAnnotations,
unicode::width::UnicodeWidthStr,
unicode::{segmentation::UnicodeSegmentation, width::UnicodeWidthStr},
visual_offset_from_block, Change, Position, Range, Selection, Transaction,
};
use helix_view::{
document::{Mode, SavePoint, SCRATCH_BUFFER_NAME},
editor::{CompleteAction, CursorShapeConfig},
editor::{BufferLine, CompleteAction, CursorShapeConfig},
graphics::{Color, CursorKind, Modifier, Rect, Style},
input::{KeyEvent, MouseButton, MouseEvent, MouseEventKind},
keyboard::{KeyCode, KeyModifiers},
Expand Down Expand Up @@ -56,6 +56,15 @@ pub enum InsertEvent {
RequestCompletion,
}

#[derive(Debug, Clone)]
pub struct BufferTab {
active: bool,
text: String,
width: u16,
x: i32,
style: Style,
}

impl Default for EditorView {
fn default() -> Self {
Self::new(Keymaps::default())
Expand Down Expand Up @@ -579,15 +588,7 @@ impl EditorView {

/// Render bufferline at the top
pub fn render_bufferline(editor: &Editor, viewport: Rect, surface: &mut Surface) {
let scratch = PathBuf::from(SCRATCH_BUFFER_NAME); // default filename to use for scratch buffer
surface.clear_with(
viewport,
editor
.theme
.try_get("ui.bufferline.background")
.unwrap_or_else(|| editor.theme.get("ui.statusline")),
);

// Define styles
let bufferline_active = editor
.theme
.try_get("ui.bufferline.active")
Expand All @@ -598,9 +599,13 @@ impl EditorView {
.try_get("ui.bufferline")
.unwrap_or_else(|| editor.theme.get("ui.statusline.inactive"));

let mut x = viewport.x;
let mut x = viewport.x as i32;
let current_doc = view!(editor).doc;

// Gather info on buffertabs
let mut buffertabs = Vec::new();

let scratch = PathBuf::from(SCRATCH_BUFFER_NAME); // default filename to use for scratch buffer
for doc in editor.documents() {
let fname = doc
.path()
Expand All @@ -610,23 +615,122 @@ impl EditorView {
.to_str()
.unwrap_or_default();

let style = if current_doc == doc.id() {
let active = current_doc == doc.id();

let style = if active {
bufferline_active
} else {
bufferline_inactive
};

let text = format!(" {}{} ", fname, if doc.is_modified() { "[+]" } else { "" });
let used_width = viewport.x.saturating_sub(x);
let rem_width = surface.area.width.saturating_sub(used_width);
let text_width = text.grapheme_indices(true).count();

x = surface
.set_stringn(x, viewport.y, text, rem_width as usize, style)
.0;
buffertabs.push(BufferTab {
active,
text,
width: text_width as _,
x,
style,
});
x = x.saturating_add(text_width as _);
}

if x >= surface.area.right() {
surface.clear_with(
viewport,
editor
.theme
.try_get("ui.bufferline.background")
.unwrap_or_else(|| editor.theme.get("ui.statusline")),
);

// Scroll the tabs correctly
// The idea is to align the center of the buffer tab
// as close to the center of the viewport as possible
let viewport_center = (viewport.width as f64 / 2.).floor() as i32 + viewport.x as i32;

let active_buffertab = buffertabs.iter().find(|tab| tab.active).unwrap();

let active_buffertab_center =
(active_buffertab.width as f64 / 2.).floor() as i32 + active_buffertab.x;

let right_of_center = active_buffertab_center as i32 - viewport_center as i32;

// If the active tab falls on the right, we have to move it left by some amount.
// For easthetics, I've chosen to have the rightmost tab not to scroll further left
// than needed, clamping it to the right of the viewport.

// Get the full width of the bufferline
let rightmost = buffertabs.last().unwrap();
let full_width = rightmost.x + rightmost.width as i32;

// The maximum possible displacement is amount of overflow on the right
// of the viewport. If no overflow, maximum displacement is 0.
let max_displacement = (full_width - viewport.width as i32).max(0);

// This part clamps the scrolling of the bufferline to the right of the viewport.
let displacement = right_of_center.clamp(0, max_displacement);

// If there's any displacement, there's underflow of the bufferline.
let mark_underflow = displacement > 0;

// If the displacement is not at max, there's overflow of the bufferline.
let mark_overflow = displacement < max_displacement;

for tab in buffertabs.iter_mut() {
tab.x = tab.x.saturating_sub(displacement);
}

// Itterate over buffertabs, skip or slice them if left off screen, stop if right of screen.
for tab in buffertabs.iter_mut() {
if tab.x < viewport.x as i32 {
if tab.x + tab.width as i32 > viewport.x as i32 {
// Draw on screen portion
let new_width = tab.width as i32 + tab.x;

tab.text = tab
.text
.graphemes(true)
.into_iter()
.skip((tab.width as i32 - new_width) as usize)
.collect();

tab.width -= new_width as u16;
tab.x = viewport.x as _;
} else {
// Skip tabs completely of screen
continue;
}
}
if tab.x > viewport.right() as i32 {
// Stop when off screen to the right
break;
}

// Actually put the string on the screen
let _ = surface
.set_stringn(
tab.x as _,
viewport.y,
tab.text.clone(),
(viewport.right() as usize).saturating_sub(tab.x as _),
tab.style,
)
.0;
}

// Add under and overflow markers.
let markers = editor
.theme
.try_get("ui.bufferline.marker")
.unwrap_or_else(|| editor.theme.get("ui.bufferline"));

if mark_underflow {
let _ = surface.set_string(viewport.left(), viewport.top(), " < ", markers);
}

if mark_overflow {
let _ = surface.set_string(viewport.right() - 3, viewport.top(), " > ", markers);
}
}

Expand Down Expand Up @@ -1472,7 +1576,6 @@ impl Component for EditorView {
let config = cx.editor.config();

// check if bufferline should be rendered
use helix_view::editor::BufferLine;
let use_bufferline = match config.bufferline {
BufferLine::Always => true,
BufferLine::Multiple if cx.editor.documents.len() > 1 => true,
Expand Down