use super::highlight::apply_block_highlight;
use super::state::VisualRange;
use crate::app::App;
use crate::theme::Palette;
use ratatui::{
Frame,
layout::{Alignment, Rect},
style::Style,
text::{Line, Span, Text},
widgets::{Block, Borders, Paragraph, Wrap},
};
pub struct MermaidDrawParams<'a> {
pub fully_visible: bool,
pub id: crate::markdown::MermaidBlockId,
pub source: &'a str,
pub focused: bool,
pub cursor_line: u32,
pub block_start: u32,
pub block_end: u32,
pub visual_mode: Option<VisualRange>,
}
pub fn draw_mermaid_block(
f: &mut Frame,
app: &mut App,
rect: Rect,
p: &Palette,
params: &MermaidDrawParams,
) {
use crate::mermaid::MermaidEntry;
let entry = app.mermaid_cache.get_mut(params.id);
let cursor_in_block = params.focused
&& params.cursor_line >= params.block_start
&& params.cursor_line < params.block_end;
match entry {
None => {
render_mermaid_placeholder(f, rect, "mermaid diagram", p);
}
Some(MermaidEntry::Pending) => {
render_mermaid_placeholder(f, rect, "rendering\u{2026}", p);
}
Some(MermaidEntry::Ready { protocol, .. }) => {
if params.fully_visible {
use ratatui_image::{Resize, StatefulImage};
f.render_widget(
Block::default().style(Style::default().bg(p.background)),
rect,
);
let highlighted_rows: Vec<u32> = match params.visual_mode {
Some(range) => (0..params.block_end.saturating_sub(params.block_start))
.filter(|&offset| range.contains(params.block_start + offset))
.collect(),
None if cursor_in_block => {
vec![params.cursor_line - params.block_start]
}
None => vec![],
};
for row_offset in highlighted_rows {
let row_offset = crate::cast::u16_from_u32(row_offset);
if row_offset < rect.height {
let bar_rect = Rect {
x: rect.x,
y: rect.y + row_offset,
width: rect.width,
height: 1,
};
f.render_widget(
Block::default().style(Style::default().bg(p.selection_bg)),
bar_rect,
);
}
}
let padded = padded_rect(rect, 4, 1);
let image = StatefulImage::new().resize(Resize::Fit(None));
f.render_stateful_widget(image, padded, protocol.as_mut());
} else {
render_mermaid_placeholder(f, rect, "scroll to view diagram", p);
}
}
Some(MermaidEntry::Failed(msg)) => {
let footer = format!("[mermaid \u{2014} {}]", truncate(msg.as_str(), 60));
let mut text = render_mermaid_source_text(params.source, &footer, p);
if params.focused {
apply_block_highlight(
&mut text.lines,
params.visual_mode,
params.cursor_line,
params.block_start,
params.block_end,
0,
p.selection_bg,
);
}
render_mermaid_source_styled(f, rect, text, p);
}
Some(MermaidEntry::SourceOnly(reason)) => {
let footer = format!("[mermaid \u{2014} {reason}]");
let mut text = render_mermaid_source_text(params.source, &footer, p);
if params.focused {
apply_block_highlight(
&mut text.lines,
params.visual_mode,
params.cursor_line,
params.block_start,
params.block_end,
0,
p.selection_bg,
);
}
render_mermaid_source_styled(f, rect, text, p);
}
}
}
pub fn padded_rect(rect: Rect, h: u16, v: u16) -> Rect {
if rect.width <= h * 2 || rect.height <= v * 2 {
return rect;
}
Rect {
x: rect.x + h,
y: rect.y + v,
width: rect.width - h * 2,
height: rect.height - v * 2,
}
}
pub fn render_mermaid_placeholder(f: &mut Frame, rect: Rect, msg: &str, p: &Palette) {
let block = Block::default()
.borders(Borders::ALL)
.border_style(p.border_style())
.style(Style::default().bg(p.background));
let inner = block.inner(rect);
f.render_widget(block, rect);
if inner.height > 0 {
let line = Line::from(Span::styled(msg.to_string(), p.dim_style()));
let para = Paragraph::new(Text::from(vec![line])).alignment(Alignment::Center);
let y_offset = inner.height / 2;
let target = Rect {
y: inner.y + y_offset,
height: 1,
..inner
};
f.render_widget(para, target);
}
}
pub fn render_mermaid_source_text(source: &str, footer: &str, p: &Palette) -> Text<'static> {
let code_style = Style::default().fg(p.code_fg).bg(p.code_bg);
let dim_style = p.dim_style();
let mut lines: Vec<Line<'static>> = source
.lines()
.map(|l| Line::from(Span::styled(l.to_string(), code_style)))
.collect();
lines.push(Line::from(Span::styled(footer.to_string(), dim_style)));
Text::from(lines)
}
pub fn render_mermaid_source_styled(f: &mut Frame, rect: Rect, text: Text<'static>, p: &Palette) {
let block = Block::default()
.borders(Borders::ALL)
.border_style(p.border_style())
.style(Style::default().bg(p.background));
let para = Paragraph::new(text).block(block).wrap(Wrap { trim: false });
f.render_widget(para, rect);
}
pub fn truncate(s: &str, max: usize) -> &str {
if s.len() <= max { s } else { &s[..max] }
}