use super::super::folding::fold_adjusted_visible_count;
use super::super::gutter::render_compose_margins;
use super::super::layout::{
calculate_compose_layout, calculate_view_anchor, calculate_viewport_end, ComposeLayout,
};
use super::super::post_pass::{
apply_background_to_lines, render_column_guides, render_cursor_column_bg, render_ruler_bg,
};
use super::super::view_data::build_view_data;
use super::contexts::SelectionContext;
use super::overlays::{decoration_context, selection_context};
use super::render_line::{render_view_lines, LastLineEnd, LineRenderInput, LineRenderOutput};
use crate::app::types::{CellThemeInfo, ViewLineMapping};
use crate::model::cursor::Cursors;
use crate::model::event::{BufferId, EventLog};
use crate::primitives::ansi_background::AnsiBackground;
use crate::state::{EditorState, ViewMode};
use crate::view::folding::FoldManager;
use crate::view::theme::Theme;
use crate::view::viewport::Viewport;
use fresh_core::api::ViewTransformPayload;
use ratatui::layout::Rect;
use ratatui::style::{Color, Modifier, Style};
use ratatui::widgets::{Block, Borders, Clear, Paragraph};
use ratatui::Frame;
pub(crate) struct BufferLayoutOutput {
pub view_line_mappings: Vec<ViewLineMapping>,
pub render_output: LineRenderOutput,
pub render_area: Rect,
pub compose_layout: ComposeLayout,
pub effective_editor_bg: Color,
pub view_mode: ViewMode,
pub left_column: usize,
pub gutter_width: usize,
pub buffer_ends_with_newline: bool,
pub selection: SelectionContext,
}
pub(crate) fn resolve_cursor_fallback(
current_cursor: Option<(u16, u16)>,
primary_cursor_position: usize,
buffer_len: usize,
buffer_ends_with_newline: bool,
last_line_end: Option<LastLineEnd>,
lines_rendered: usize,
gutter_width: usize,
) -> Option<(u16, u16)> {
if current_cursor.is_some() || primary_cursor_position != buffer_len {
return current_cursor;
}
if buffer_ends_with_newline {
if let Some(end) = last_line_end {
let y = if end.terminated_with_newline {
end.pos.1.saturating_add(1)
} else {
end.pos.1
};
return Some((gutter_width as u16, y));
}
return Some((gutter_width as u16, lines_rendered as u16));
}
last_line_end.map(|end| end.pos)
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn compute_buffer_layout(
state: &mut EditorState,
cursors: &Cursors,
viewport: &mut Viewport,
folds: &mut FoldManager,
area: Rect,
is_active: bool,
theme: &Theme,
lsp_waiting: bool,
view_mode: ViewMode,
compose_width: Option<u16>,
view_transform: Option<ViewTransformPayload>,
estimated_line_length: usize,
highlight_context_bytes: usize,
relative_line_numbers: bool,
use_terminal_bg: bool,
session_mode: bool,
software_cursor_only: bool,
show_line_numbers: bool,
highlight_current_line: bool,
diagnostics_inline_text: bool,
show_tilde: bool,
cell_theme_map: Option<(&mut Vec<CellThemeInfo>, u16)>,
) -> BufferLayoutOutput {
let _span = tracing::trace_span!("compute_buffer_layout").entered();
state.margins.configure_for_line_numbers(show_line_numbers);
let effective_editor_bg = if use_terminal_bg {
Color::Reset
} else {
theme.editor_bg
};
let line_wrap = viewport.line_wrap_enabled;
let overlay_count = state.overlays.all().len();
if overlay_count > 0 {
tracing::trace!("render_content: {} overlays present", overlay_count);
}
let visible_count = viewport.visible_line_count();
let buffer_len = state.buffer.len();
let byte_offset_mode = state.buffer.line_count().is_none();
let estimated_lines = if byte_offset_mode {
buffer_len.max(1)
} else {
state.buffer.line_count().unwrap_or(1)
};
state
.margins
.update_width_for_buffer(estimated_lines, show_line_numbers);
let gutter_width = state.margins.left_total_width();
let compose_layout = calculate_compose_layout(area, &view_mode, compose_width);
let render_area = compose_layout.render_area;
let view_transform_for_rebuild = view_transform.clone();
let view_data = {
let _span = tracing::trace_span!("build_view_data").entered();
build_view_data(
state,
viewport,
view_transform,
estimated_line_length,
visible_count,
line_wrap,
render_area.width as usize,
gutter_width,
&view_mode,
folds,
theme,
)
};
let sync_scrolled = if viewport.sync_scroll_to_end {
viewport.sync_scroll_to_end = false;
viewport.scroll_to_end_of_view(&view_data.lines)
} else {
false
};
let (view_data, view_transform_for_rebuild) = if sync_scrolled {
viewport.top_view_line_offset = 0;
let rebuilt = build_view_data(
state,
viewport,
view_transform_for_rebuild,
estimated_line_length,
visible_count,
line_wrap,
render_area.width as usize,
gutter_width,
&view_mode,
folds,
theme,
);
viewport.scroll_to_end_of_view(&rebuilt.lines);
(rebuilt, None)
} else {
(view_data, Some(view_transform_for_rebuild))
};
let primary = *cursors.primary();
let top_byte_before_scroll = viewport.top_byte;
let scrolled = viewport.ensure_visible_in_layout(&view_data.lines, &primary, gutter_width);
let view_data = if scrolled && viewport.top_byte != top_byte_before_scroll {
if let Some(vt) = view_transform_for_rebuild {
viewport.top_view_line_offset = 0;
let rebuilt = build_view_data(
state,
viewport,
vt,
estimated_line_length,
visible_count,
line_wrap,
render_area.width as usize,
gutter_width,
&view_mode,
folds,
theme,
);
let _ = viewport.ensure_visible_in_layout(&rebuilt.lines, &primary, gutter_width);
rebuilt
} else {
view_data
}
} else {
view_data
};
let view_anchor = calculate_view_anchor(&view_data.lines, viewport.top_byte);
let selection = selection_context(state, cursors);
tracing::trace!(
"Rendering buffer with {} cursors at positions: {:?}, primary at {}, is_active: {}, buffer_len: {}",
selection.cursor_positions.len(),
selection.cursor_positions,
selection.primary_cursor_position,
is_active,
state.buffer.len()
);
if !selection.cursor_positions.is_empty()
&& !selection
.cursor_positions
.contains(&selection.primary_cursor_position)
{
tracing::warn!(
"Primary cursor position {} not found in cursor_positions list: {:?}",
selection.primary_cursor_position,
selection.cursor_positions
);
}
let adjusted_visible_count = fold_adjusted_visible_count(
&state.buffer,
&state.marker_list,
folds,
viewport.top_byte,
visible_count,
);
let _ = state
.buffer
.populate_line_cache(viewport.top_byte, adjusted_visible_count);
let viewport_start = viewport.top_byte;
let viewport_end = calculate_viewport_end(
state,
viewport_start,
estimated_line_length,
adjusted_visible_count,
);
let decorations = decoration_context(
state,
viewport_start,
viewport_end,
selection.primary_cursor_position,
folds,
theme,
highlight_context_bytes,
&view_mode,
diagnostics_inline_text,
&view_data.lines,
);
let calculated_offset = viewport.top_view_line_offset;
tracing::trace!(
top_byte = viewport.top_byte,
top_view_line_offset = viewport.top_view_line_offset,
calculated_offset,
view_data_lines = view_data.lines.len(),
"view line offset calculation"
);
let (view_lines_to_render, adjusted_view_anchor) =
if calculated_offset > 0 && calculated_offset < view_data.lines.len() {
let sliced = &view_data.lines[calculated_offset..];
let adjusted_anchor = calculate_view_anchor(sliced, viewport.top_byte);
(sliced, adjusted_anchor)
} else {
(&view_data.lines[..], view_anchor)
};
let mut dummy_map = Vec::new();
let (map_ref, sw) = match cell_theme_map {
Some((map, w)) => (map, w),
None => (&mut dummy_map, 0u16),
};
let render_output = render_view_lines(LineRenderInput {
state,
theme,
view_lines: view_lines_to_render,
view_anchor: adjusted_view_anchor,
render_area,
gutter_width,
selection: &selection,
decorations: &decorations,
visible_line_count: visible_count,
lsp_waiting,
is_active,
line_wrap,
estimated_lines,
left_column: viewport.left_column,
relative_line_numbers,
session_mode,
software_cursor_only,
show_line_numbers,
byte_offset_mode,
show_tilde,
highlight_current_line,
cell_theme_map: map_ref,
screen_width: sw,
});
let view_line_mappings = render_output.view_line_mappings.clone();
let buffer_ends_with_newline = if !state.buffer.is_empty() {
let last_char = state.get_text_range(state.buffer.len() - 1, state.buffer.len());
last_char == "\n"
} else {
false
};
BufferLayoutOutput {
view_line_mappings,
render_output,
render_area,
compose_layout,
effective_editor_bg,
view_mode,
left_column: viewport.left_column,
gutter_width,
buffer_ends_with_newline,
selection,
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn draw_buffer_in_split(
frame: &mut Frame,
state: &EditorState,
cursors: &Cursors,
layout_output: BufferLayoutOutput,
event_log: Option<&mut EventLog>,
area: Rect,
is_active: bool,
theme: &Theme,
ansi_background: Option<&AnsiBackground>,
background_fade: f32,
hide_cursor: bool,
software_cursor_only: bool,
rulers: &[usize],
compose_column_guides: Option<Vec<u16>>,
highlight_current_column: bool,
pending_hardware_cursor: &mut Option<(u16, u16)>,
) {
let render_area = layout_output.render_area;
let effective_editor_bg = layout_output.effective_editor_bg;
let gutter_width = layout_output.gutter_width;
let starting_line_num = 0;
render_compose_margins(
frame,
area,
&layout_output.compose_layout,
&layout_output.view_mode,
theme,
effective_editor_bg,
);
let mut lines = layout_output.render_output.lines;
let background_x_offset = layout_output.left_column;
if let Some(bg) = ansi_background {
apply_background_to_lines(
&mut lines,
render_area.width,
bg,
effective_editor_bg,
theme.editor_fg,
background_fade,
background_x_offset,
starting_line_num,
);
}
frame.render_widget(Clear, render_area);
let editor_block = Block::default()
.borders(Borders::NONE)
.style(Style::default().bg(effective_editor_bg));
frame.render_widget(Paragraph::new(lines).block(editor_block), render_area);
let cursor = resolve_cursor_fallback(
layout_output.render_output.cursor,
layout_output.selection.primary_cursor_position,
state.buffer.len(),
layout_output.buffer_ends_with_newline,
layout_output.render_output.last_line_end,
layout_output.render_output.content_lines_rendered,
gutter_width,
);
let cursor_screen_pos = if is_active && state.show_cursors && !hide_cursor {
cursor.map(|(cx, cy)| {
let screen_x = render_area.x.saturating_add(cx);
let max_y = render_area.height.saturating_sub(1);
let screen_y = render_area.y.saturating_add(cy.min(max_y));
(screen_x, screen_y)
})
} else {
None
};
if !rulers.is_empty() {
let ruler_cols: Vec<u16> = rulers.iter().map(|&r| r as u16).collect();
render_ruler_bg(
frame,
&ruler_cols,
theme.ruler_bg,
render_area,
gutter_width,
layout_output.render_output.content_lines_rendered,
layout_output.left_column,
);
}
if highlight_current_column && is_active && !hide_cursor {
if let Some((cx, _)) = cursor {
if (cx as usize) >= gutter_width {
render_cursor_column_bg(
frame,
render_area,
cx,
theme.current_line_bg,
layout_output.render_output.content_lines_rendered,
);
}
}
}
if let Some(guides) = compose_column_guides {
let guide_style = Style::default()
.fg(theme.line_number_fg)
.add_modifier(Modifier::DIM);
render_column_guides(
frame,
&guides,
guide_style,
render_area,
gutter_width,
layout_output.render_output.content_lines_rendered,
0,
);
}
if let Some((screen_x, screen_y)) = cursor_screen_pos {
*pending_hardware_cursor = Some((screen_x, screen_y));
if software_cursor_only {
let buf = frame.buffer_mut();
let area = buf.area;
if screen_x < area.x + area.width && screen_y < area.y + area.height {
let cell = &mut buf[(screen_x, screen_y)];
if !cell.modifier.contains(Modifier::REVERSED) {
cell.set_char(' ');
cell.fg = theme.editor_fg;
cell.bg = theme.editor_bg;
cell.modifier.insert(Modifier::REVERSED);
}
}
}
if let Some(event_log) = event_log {
let cursor_pos = cursors.primary().position;
let buffer_len = state.buffer.len();
event_log.log_render_state(cursor_pos, screen_x, screen_y, buffer_len);
}
}
}
#[allow(clippy::too_many_arguments)]
pub(crate) fn render_buffer_in_split(
frame: &mut Frame,
state: &mut EditorState,
cursors: &Cursors,
viewport: &mut Viewport,
folds: &mut FoldManager,
event_log: Option<&mut EventLog>,
area: Rect,
is_active: bool,
theme: &Theme,
ansi_background: Option<&AnsiBackground>,
background_fade: f32,
lsp_waiting: bool,
view_mode: ViewMode,
compose_width: Option<u16>,
compose_column_guides: Option<Vec<u16>>,
view_transform: Option<ViewTransformPayload>,
estimated_line_length: usize,
highlight_context_bytes: usize,
_buffer_id: BufferId,
hide_cursor: bool,
relative_line_numbers: bool,
use_terminal_bg: bool,
session_mode: bool,
software_cursor_only: bool,
rulers: &[usize],
show_line_numbers: bool,
highlight_current_line: bool,
diagnostics_inline_text: bool,
show_tilde: bool,
highlight_current_column: bool,
cell_theme_map: &mut Vec<CellThemeInfo>,
screen_width: u16,
pending_hardware_cursor: &mut Option<(u16, u16)>,
) -> Vec<ViewLineMapping> {
let layout_output = compute_buffer_layout(
state,
cursors,
viewport,
folds,
area,
is_active,
theme,
lsp_waiting,
view_mode.clone(),
compose_width,
view_transform,
estimated_line_length,
highlight_context_bytes,
relative_line_numbers,
use_terminal_bg,
session_mode,
software_cursor_only,
show_line_numbers,
highlight_current_line,
diagnostics_inline_text,
show_tilde,
Some((cell_theme_map, screen_width)),
);
let view_line_mappings = layout_output.view_line_mappings.clone();
draw_buffer_in_split(
frame,
state,
cursors,
layout_output,
event_log,
area,
is_active,
theme,
ansi_background,
background_fade,
hide_cursor,
software_cursor_only,
rulers,
compose_column_guides,
highlight_current_column,
pending_hardware_cursor,
);
view_line_mappings
}