use std::collections::BTreeMap;
use std::ops::Range;
use crate::editor::buffer::{BufferSnapshot, Version};
use crate::editor::position::Position;
#[derive(Debug, Clone)]
pub struct LineLayout {
pub logical_line: usize,
pub visual_rows: Vec<VisualRow>,
pub version: Version,
}
#[derive(Debug, Clone, PartialEq)]
pub struct VisualRow {
pub byte_range: Range<usize>,
pub char_range: Range<usize>,
pub width: usize,
}
impl VisualRow {
pub fn new(byte_start: usize, byte_end: usize, char_start: usize, char_end: usize) -> Self {
Self {
byte_range: byte_start..byte_end,
char_range: char_start..char_end,
width: char_end - char_start,
}
}
}
#[derive(Debug, Clone)]
pub struct ViewportLayout {
pub lines: Vec<ViewportLine>,
pub cursor_screen_pos: ScreenPosition,
pub total_visual_rows: usize,
pub version: Version,
}
#[derive(Debug, Clone)]
pub struct ViewportLine {
pub logical_line: usize,
pub visual_rows: Vec<VisualRow>,
pub is_current_line: bool,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct ScreenPosition {
pub row: usize,
pub col: usize,
}
impl ScreenPosition {
pub fn new(row: usize, col: usize) -> Self {
Self { row, col }
}
}
#[derive(Debug, Default)]
pub struct LayoutCache {
entries: BTreeMap<(Version, usize), LineLayout>,
max_entries: usize,
}
impl LayoutCache {
pub fn new() -> Self {
Self {
entries: BTreeMap::new(),
max_entries: 1000,
}
}
pub fn get(&self, version: Version, line: usize) -> Option<&LineLayout> {
self.entries.get(&(version, line))
}
pub fn insert(&mut self, version: Version, line: usize, layout: LineLayout) {
if self.entries.len() >= self.max_entries
&& let Some(first_key) = self.entries.keys().next().copied() {
self.entries.remove(&first_key);
}
self.entries.insert((version, line), layout);
}
pub fn invalidate_for_version(&mut self, version: Version) {
self.entries.retain(|(v, _), _| *v != version);
}
pub fn clear(&mut self) {
self.entries.clear();
}
}
pub struct LayoutEngine {
cache: LayoutCache,
wrap_width: usize,
wrap_enabled: bool,
}
impl Default for LayoutEngine {
fn default() -> Self {
Self::new()
}
}
impl LayoutEngine {
pub fn new() -> Self {
Self {
cache: LayoutCache::new(),
wrap_width: 80,
wrap_enabled: true,
}
}
pub fn with_wrap_width(mut self, width: usize) -> Self {
self.wrap_width = width;
self
}
pub fn with_wrap_enabled(mut self, enabled: bool) -> Self {
self.wrap_enabled = enabled;
self
}
pub fn set_wrap_width(&mut self, width: usize) {
if self.wrap_width != width {
self.wrap_width = width;
self.cache.clear();
}
}
pub fn set_wrap_enabled(&mut self, enabled: bool) {
if self.wrap_enabled != enabled {
self.wrap_enabled = enabled;
self.cache.clear();
}
}
pub fn wrap_width(&self) -> usize {
self.wrap_width
}
pub fn wrap_enabled(&self) -> bool {
self.wrap_enabled
}
pub fn invalidate_cache(&mut self, version: Version) {
self.cache.invalidate_for_version(version);
}
pub fn compute_line_layout(
&mut self,
line: &str,
logical_line: usize,
version: Version,
) -> LineLayout {
if let Some(cached) = self.cache.get(version, logical_line) {
return cached.clone();
}
let visual_rows = if self.wrap_enabled && self.wrap_width > 0 {
self.compute_wrap_segments(line)
} else {
vec![VisualRow::new(0, line.len(), 0, line.chars().count())]
};
let layout = LineLayout {
logical_line,
visual_rows,
version,
};
self.cache.insert(version, logical_line, layout.clone());
layout
}
fn compute_wrap_segments(&self, line: &str) -> Vec<VisualRow> {
let mut rows = Vec::new();
let chars: Vec<char> = line.chars().collect();
let total_chars = chars.len();
if total_chars == 0 {
rows.push(VisualRow::new(0, 0, 0, 0));
return rows;
}
let mut char_pos = 0;
let mut byte_offset = 0;
while char_pos < total_chars {
let remaining = total_chars - char_pos;
let segment_width = remaining.min(self.wrap_width);
let segment_end = char_pos + segment_width;
let mut segment_byte_end = byte_offset;
for (i, ch) in chars[char_pos..].iter().enumerate() {
if i >= segment_width {
break;
}
segment_byte_end += ch.len_utf8();
}
rows.push(VisualRow::new(byte_offset, segment_byte_end, char_pos, segment_end));
char_pos = segment_end;
byte_offset = segment_byte_end;
}
if rows.is_empty() {
rows.push(VisualRow::new(0, 0, 0, 0));
}
rows
}
pub fn compute_viewport(
&mut self,
snapshot: &BufferSnapshot,
cursor: Position,
scroll: usize,
height: usize,
width: usize,
) -> ViewportLayout {
let version = snapshot.version;
if self.wrap_width != width && self.wrap_enabled {
self.wrap_width = width;
self.cache.clear();
}
let mut visible_lines = Vec::new();
let mut visual_row = 0;
let mut cursor_screen_pos = ScreenPosition::new(0, 0);
let lines = snapshot.lines();
let total_lines = lines.len();
let mut buffer_line = scroll;
while visual_row < height && buffer_line < total_lines {
let line_text = &lines[buffer_line];
let layout = self.compute_line_layout(line_text, buffer_line, version);
let is_current_line = buffer_line == cursor.line;
if is_current_line && cursor_screen_pos.row == 0 {
cursor_screen_pos = self.map_cursor_to_screen(
cursor,
&layout,
visual_row,
);
}
for row in &layout.visual_rows {
if visual_row >= height {
break;
}
visible_lines.push(ViewportLine {
logical_line: buffer_line,
visual_rows: vec![row.clone()],
is_current_line,
});
visual_row += 1;
}
buffer_line += 1;
}
ViewportLayout {
lines: visible_lines,
cursor_screen_pos,
total_visual_rows: visual_row,
version,
}
}
fn map_cursor_to_screen(
&self,
cursor: Position,
layout: &LineLayout,
start_visual_row: usize,
) -> ScreenPosition {
if layout.visual_rows.is_empty() {
return ScreenPosition::new(start_visual_row, 0);
}
for (row_idx, visual_row) in layout.visual_rows.iter().enumerate() {
if cursor.column >= visual_row.char_range.start
&& cursor.column < visual_row.char_range.end
{
return ScreenPosition::new(
start_visual_row + row_idx,
cursor.column - visual_row.char_range.start,
);
}
if cursor.column >= visual_row.char_range.end && row_idx == layout.visual_rows.len() - 1 {
return ScreenPosition::new(
start_visual_row + row_idx,
visual_row.width,
);
}
}
let last_row = layout.visual_rows.last();
if let Some(row) = last_row {
if cursor.column >= row.char_range.end {
ScreenPosition::new(start_visual_row + layout.visual_rows.len() - 1, row.width)
} else {
ScreenPosition::new(start_visual_row, 0)
}
} else {
ScreenPosition::new(start_visual_row, 0)
}
}
pub fn compute_cursor_screen_position(
&mut self,
lines: &[String],
cursor: Position,
scroll: usize,
height: usize,
version: Version,
) -> ScreenPosition {
let mut visual_row = 0;
let mut buffer_line = scroll;
while visual_row < height && buffer_line <= cursor.line {
let line_text = lines.get(buffer_line).map(|s| s.as_str()).unwrap_or("");
let layout = self.compute_line_layout(line_text, buffer_line, version);
for (row_idx, row) in layout.visual_rows.iter().enumerate() {
if visual_row + row_idx >= height {
return ScreenPosition::new(visual_row + row_idx, 0);
}
if buffer_line == cursor.line {
if cursor.column >= row.char_range.start
&& cursor.column < row.char_range.end
{
return ScreenPosition::new(visual_row + row_idx, cursor.column - row.char_range.start);
}
if row_idx == layout.visual_rows.len() - 1
&& cursor.column >= row.char_range.end
{
return ScreenPosition::new(visual_row + row_idx, row.width);
}
}
if buffer_line < cursor.line {
visual_row += 1;
}
}
if buffer_line < cursor.line {
visual_row += layout.visual_rows.len();
}
buffer_line += 1;
}
ScreenPosition::new(visual_row, cursor.column)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn wrap_segments_basic() {
let engine = LayoutEngine::new().with_wrap_width(10);
let layout = engine.compute_wrap_segments("hello world foo bar");
assert!(layout.len() > 1);
let total_chars: usize = layout.iter().map(|r| r.width).sum();
assert_eq!(total_chars, 19);
}
#[test]
fn wrap_segments_short_line() {
let engine = LayoutEngine::new().with_wrap_width(80);
let layout = engine.compute_wrap_segments("hello");
assert_eq!(layout.len(), 1);
assert_eq!(layout[0].width, 5);
}
#[test]
fn wrap_segments_empty_line() {
let engine = LayoutEngine::new().with_wrap_width(80);
let layout = engine.compute_wrap_segments("");
assert_eq!(layout.len(), 1);
assert_eq!(layout[0].width, 0);
}
#[test]
fn cache_operations() {
let mut cache = LayoutCache::new();
let version = Version::new();
let layout = LineLayout {
logical_line: 0,
visual_rows: vec![VisualRow::new(0, 5, 0, 5)],
version,
};
cache.insert(version, 0, layout.clone());
assert!(cache.get(version, 0).is_some());
cache.invalidate_for_version(version);
assert!(cache.get(version, 0).is_none());
}
}