diff --git a/book/src/themes.md b/book/src/themes.md index e3b95c0a7af0..c205fce5dc83 100644 --- a/book/src/themes.md +++ b/book/src/themes.md @@ -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 | diff --git a/helix-term/src/ui/editor.rs b/helix-term/src/ui/editor.rs index a071bfaa8138..8f3272d6211e 100644 --- a/helix-term/src/ui/editor.rs +++ b/helix-term/src/ui/editor.rs @@ -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}, @@ -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()) @@ -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") @@ -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() @@ -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); } } @@ -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,