use std::io::Write;
use anyhow::Result;
use crossterm::{
cursor::MoveTo,
queue,
style::{
Attribute, Color, Print, ResetColor, SetAttribute, SetBackgroundColor, SetForegroundColor,
},
terminal::{Clear, ClearType},
};
use yuru_core::{Candidate, ScoredCandidate};
use crate::api::{TuiLayout, TuiStyle};
use crate::preview::PreviewRender;
use crate::state::TuiState;
use super::highlight::highlight_segments_for_result;
use super::layout::{
content_start_row, footer_start_row, preview_width, scroll_offset,
truncate_to_width_with_ellipsis, visible_line_count, Viewport,
};
use super::preview_pane::render_preview;
pub(crate) fn render(
output: &mut impl Write,
state: &TuiState,
results: &[ScoredCandidate],
mut context: RenderContext<'_>,
) -> Result<()> {
queue!(output, MoveTo(0, 0), Clear(ClearType::All))?;
let prompt_row = if context.no_input {
None
} else if context.layout.prompt_at_bottom() {
Some(context.viewport.rows)
} else {
Some(0)
};
let content_top = content_start_row(context.layout, prompt_row.is_some());
if let Some(prompt_row) = prompt_row {
let input = if state.query().is_empty() {
format!("{}{}", context.prompt, "")
} else {
format!("{}{}", context.prompt, state.query())
};
queue!(
output,
MoveTo(0, prompt_row as u16),
Print(truncate_to_width_with_ellipsis(
&input,
context.viewport.width,
context.ellipsis
))
)?;
}
let header_rows = visible_line_count(context.header);
if let Some(header) = context.header {
for (row, line) in header.lines().enumerate() {
queue!(
output,
MoveTo(0, (content_top + row) as u16),
Print(truncate_to_width_with_ellipsis(
line,
context.viewport.width,
context.ellipsis
))
)?;
}
}
let footer_rows = visible_line_count(context.footer);
if let Some(footer) = context.footer {
let footer_start = footer_start_row(content_top, context.viewport.rows, footer_rows);
for (row, line) in footer.lines().enumerate() {
queue!(
output,
MoveTo(0, (footer_start + row) as u16),
Print(truncate_to_width_with_ellipsis(
line,
context.viewport.width,
context.ellipsis
))
)?;
}
}
let preview_width = preview_width(context.viewport.width, context.preview.as_ref());
let list_width = if preview_width == 0 {
context.viewport.width
} else {
context
.viewport
.width
.saturating_sub(preview_width)
.saturating_sub(1)
.max(1)
};
let result_rows = context
.viewport
.rows
.saturating_sub(header_rows + footer_rows)
.max(1);
let result_bottom = content_top
.saturating_add(context.viewport.rows)
.saturating_sub(footer_rows + 1);
let offset = scroll_offset(state.selected(), results.len(), result_rows);
for (row, result) in results.iter().skip(offset).take(result_rows).enumerate() {
let result_row = if context.layout.list_bottom_up() {
result_bottom.saturating_sub(row)
} else {
content_top + row + header_rows
};
queue!(output, MoveTo(0, result_row as u16))?;
let mark = if context.multi && state.marked().contains(&result.id) {
context.marker
} else {
" "
};
let selected = offset + row == state.selected();
let pointer_width = context.pointer.chars().count() + context.marker.chars().count();
let result_width = list_width.saturating_sub(pointer_width);
let selected_row_background = selected && context.highlight_line;
if selected_row_background {
queue!(
output,
SetBackgroundColor(context.style.selected_bg_color())
)?;
if let Some(color) = context.style.selected_fg_color() {
queue!(output, SetForegroundColor(color))?;
}
}
if selected {
if let Some(color) = context.style.pointer_color() {
queue!(output, SetForegroundColor(color))?;
}
queue!(output, Print(context.pointer), Print(mark))?;
if selected_row_background {
if let Some(color) = context.style.selected_fg_color() {
queue!(output, SetForegroundColor(color))?;
} else {
queue!(output, SetForegroundColor(Color::Reset))?;
}
} else {
queue!(output, SetForegroundColor(Color::Reset))?;
}
} else {
queue!(output, Print(" "), Print(mark))?;
}
let printed = render_highlighted_result(
output,
state.query(),
result,
&context,
result_width,
selected,
)?;
if selected_row_background {
let padding = result_width.saturating_sub(printed);
if padding > 0 {
queue!(output, Print(" ".repeat(padding)))?;
}
queue!(output, ResetColor)?;
}
queue!(output, SetAttribute(Attribute::Reset))?;
}
render_preview(output, &mut context, preview_width, content_top)?;
if let Some(prompt_row) = prompt_row {
let cursor_column =
context.prompt.chars().count() + state.query()[..state.cursor()].chars().count();
queue!(
output,
MoveTo(
cursor_column.min(context.viewport.width - 1) as u16,
prompt_row as u16
)
)?;
}
output.flush()?;
Ok(())
}
pub(crate) struct RenderContext<'a> {
pub(crate) candidates: &'a [Candidate],
pub(crate) prompt: &'a str,
pub(crate) header: Option<&'a str>,
pub(crate) footer: Option<&'a str>,
pub(crate) viewport: Viewport,
pub(crate) layout: TuiLayout,
pub(crate) preview: Option<PreviewRender<'a>>,
pub(crate) style: &'a TuiStyle,
pub(crate) highlight_line: bool,
pub(crate) case_sensitive: bool,
pub(crate) multi: bool,
pub(crate) no_input: bool,
pub(crate) pointer: &'a str,
pub(crate) marker: &'a str,
pub(crate) ellipsis: &'a str,
}
fn render_highlighted_result(
output: &mut impl Write,
query: &str,
result: &ScoredCandidate,
context: &RenderContext<'_>,
width: usize,
selected: bool,
) -> Result<usize> {
let mut printed = 0;
for segment in highlight_segments_for_result(
query,
result,
context.candidates,
context.case_sensitive,
width,
) {
if segment.highlighted {
queue!(
output,
SetForegroundColor(context.style.highlight_color(selected)),
SetAttribute(Attribute::Bold)
)?;
}
printed += segment.text.chars().count();
queue!(output, Print(segment.text))?;
if segment.highlighted {
if selected && context.highlight_line {
if let Some(color) = context.style.selected_fg_color() {
queue!(output, SetForegroundColor(color))?;
} else {
queue!(output, SetForegroundColor(Color::Reset))?;
}
} else {
queue!(output, SetForegroundColor(Color::Reset))?;
}
queue!(output, SetAttribute(Attribute::NormalIntensity))?;
}
}
Ok(printed)
}