use ratatui::prelude::*;
use ratatui::widgets::Paragraph;
use super::HeatmapState;
use super::color::value_to_color;
use crate::theme::Theme;
pub(super) fn render_heatmap(
state: &HeatmapState,
frame: &mut Frame,
area: Rect,
theme: &Theme,
focused: bool,
disabled: bool,
) {
let num_rows = state.rows();
let num_cols = state.cols();
if num_rows == 0 || num_cols == 0 {
return;
}
let row_label_width: u16 = if state.row_labels().is_empty() {
0
} else {
state
.row_labels()
.iter()
.map(|l| l.len() as u16)
.max()
.unwrap_or(0)
+ 1 };
let col_label_height: u16 = if state.col_labels().is_empty() { 0 } else { 1 };
let grid_x = area.x + row_label_width;
let grid_y = area.y + col_label_height;
let grid_width = area.width.saturating_sub(row_label_width);
let grid_height = area.height.saturating_sub(col_label_height);
if grid_width == 0 || grid_height == 0 {
return;
}
let cell_width = (grid_width / num_cols as u16).max(1);
let cell_height: u16 = 1;
let min_val = state.effective_min();
let max_val = state.effective_max();
if col_label_height > 0 {
render_col_labels(state, frame, area, grid_x, cell_width, theme, disabled);
}
for ri in 0..num_rows {
let y = grid_y + (ri as u16) * cell_height;
if y >= area.bottom() {
break;
}
if !state.row_labels().is_empty() {
render_row_label(
state,
frame,
LabelPosition {
x: area.x,
y,
width: row_label_width,
},
ri,
theme,
disabled,
);
}
render_row_cells(
state,
frame,
ri,
CellRenderParams {
grid_x,
y,
cell_width,
cell_height,
area,
min_val,
max_val,
},
theme,
focused,
disabled,
);
}
}
fn render_col_labels(
state: &HeatmapState,
frame: &mut Frame,
area: Rect,
grid_x: u16,
cell_width: u16,
theme: &Theme,
disabled: bool,
) {
for (ci, label) in state.col_labels().iter().enumerate() {
let x = grid_x + (ci as u16) * cell_width;
if x >= area.right() {
break;
}
let available = cell_width.min(area.right().saturating_sub(x));
if available == 0 {
continue;
}
let label_area = Rect::new(x, area.y, available, 1);
let truncated = truncate_str(label, available as usize);
let style = if disabled {
theme.disabled_style()
} else {
theme.normal_style().add_modifier(Modifier::BOLD)
};
let p = Paragraph::new(truncated)
.style(style)
.alignment(Alignment::Center);
frame.render_widget(p, label_area);
}
}
struct LabelPosition {
x: u16,
y: u16,
width: u16,
}
fn render_row_label(
state: &HeatmapState,
frame: &mut Frame,
pos: LabelPosition,
row_index: usize,
theme: &Theme,
disabled: bool,
) {
if let Some(label) = state.row_labels().get(row_index) {
let label_area = Rect::new(pos.x, pos.y, pos.width, 1);
let truncated = truncate_str(label, pos.width as usize);
let style = if disabled {
theme.disabled_style()
} else {
theme.normal_style()
};
let p = Paragraph::new(truncated).style(style);
frame.render_widget(p, label_area);
}
}
struct CellRenderParams {
grid_x: u16,
y: u16,
cell_width: u16,
cell_height: u16,
area: Rect,
min_val: f64,
max_val: f64,
}
fn render_row_cells(
state: &HeatmapState,
frame: &mut Frame,
ri: usize,
params: CellRenderParams,
theme: &Theme,
focused: bool,
disabled: bool,
) {
let _ = theme; let row_data = &state.data()[ri];
for (ci, &value) in row_data.iter().enumerate() {
let x = params.grid_x + (ci as u16) * params.cell_width;
if x >= params.area.right() {
break;
}
let available_w = params.cell_width.min(params.area.right().saturating_sub(x));
if available_w == 0 {
continue;
}
let cell_area = Rect::new(x, params.y, available_w, params.cell_height);
let bg_color = if disabled {
Color::DarkGray
} else {
value_to_color(value, params.min_val, params.max_val, state.color_scale())
};
let is_selected = state.selected() == Some((ri, ci));
let cell_style = if is_selected && focused && !disabled {
Style::default()
.fg(bg_color)
.bg(Color::White)
.add_modifier(Modifier::BOLD)
} else {
let fg = contrasting_fg(bg_color);
Style::default().bg(bg_color).fg(fg)
};
let text = if state.show_values() {
format_value(value, available_w as usize)
} else {
" ".repeat(available_w as usize)
};
let p = Paragraph::new(text)
.style(cell_style)
.alignment(Alignment::Center);
frame.render_widget(p, cell_area);
}
}
pub(super) fn truncate_str(s: &str, max_width: usize) -> String {
if s.len() <= max_width {
s.to_string()
} else if max_width > 0 {
s[..max_width].to_string()
} else {
String::new()
}
}
pub(super) fn format_value(value: f64, width: usize) -> String {
if width == 0 {
return String::new();
}
let formatted = if width >= 6 {
format!("{value:.1}")
} else if width >= 3 {
format!("{value:.0}")
} else {
let s = format!("{value:.0}");
s[..s.len().min(width)].to_string()
};
if formatted.len() <= width {
formatted
} else {
formatted[..width].to_string()
}
}
pub(super) fn contrasting_fg(bg: Color) -> Color {
match bg {
Color::Rgb(r, g, b) => {
let luminance = 0.299 * (r as f64) + 0.587 * (g as f64) + 0.114 * (b as f64);
if luminance > 128.0 {
Color::Black
} else {
Color::White
}
}
Color::DarkGray | Color::Black => Color::White,
_ => Color::Black,
}
}