use std::collections::HashMap;
use gen_core::{is_end_node, is_start_node};
use gen_graph::{GenGraph, GraphNode};
use gen_models::{db::GraphConnection, node::Node};
use gen_tui::{
geometry::WorldRect,
graph_controller::{GraphController, WorldBuffer},
graph_widget::{GraphWidget, NODE_GLYPH},
layout::VisualDetail,
plotter::{NodeRenderer, NodeSizer},
theme::Theme,
};
use ratatui::style::{Color, Style};
use crate::config::get_theme_color;
pub mod label {
pub const START: &str = "Start >";
pub const END: &str = "> End";
}
pub struct GenGraphNodeSizer;
impl NodeSizer<&GenGraph> for GenGraphNodeSizer {
fn get_node_size(&self, node: &GraphNode, detail_level: VisualDetail) -> (u64, u64) {
if is_start_node(node.node_id) {
return (label::START.len() as u64, 1u64);
}
if is_end_node(node.node_id) {
return (label::END.len() as u64, 1u64);
}
let sequence_length = (node.sequence_end - node.sequence_start) as u64;
match detail_level {
VisualDetail::Minimal => (1u64, 1u64), VisualDetail::Truncated => (sequence_length.min(12), 1u64), VisualDetail::Full => (sequence_length, 1u64), }
}
}
pub struct GenGraphNodeRenderer<'a> {
conn: &'a GraphConnection,
cache: HashMap<GraphNode, String>,
}
impl<'a> GenGraphNodeRenderer<'a> {
pub fn new(conn: &'a GraphConnection) -> Self {
Self {
conn,
cache: HashMap::new(),
}
}
pub fn connection(&self) -> &'a GraphConnection {
self.conn
}
pub fn get_sequence(&mut self, node_key: &GraphNode) -> String {
if let Some(cached) = self.cache.get(node_key) {
return cached.clone();
}
let (db_node_id, start, end) = (
node_key.node_id,
node_key.sequence_start,
node_key.sequence_end,
);
let sequences = Node::get_sequences_by_node_ids(self.conn, &[db_node_id]);
let sequence = match sequences.get(&db_node_id) {
Some(seq) => seq.get_sequence(start, end),
None => "?".repeat((end - start).max(0) as usize),
};
self.cache.insert(*node_key, sequence.clone());
sequence
}
}
impl NodeRenderer<&GenGraph> for GenGraphNodeRenderer<'_> {
fn render_node(
&mut self,
buffer: &mut WorldBuffer,
area: WorldRect,
node_id: &GraphNode,
detail_level: VisualDetail,
) {
let background_style = Style::default().bg(get_theme_color("node").unwrap_or_default());
let text_style = Style::default()
.bg(get_theme_color("node").unwrap_or(ratatui::style::Color::Blue))
.fg(get_theme_color("text").unwrap_or(ratatui::style::Color::White));
buffer.fill_rect(area, ' ');
buffer.set_char_styled(area.left_center(), ' ', background_style);
if is_start_node(node_id.node_id) {
let edge_style = Style::default()
.bg(get_theme_color("canvas").unwrap_or(ratatui::style::Color::Blue))
.fg(get_theme_color("edge").unwrap_or(ratatui::style::Color::White));
buffer.set_string_styled(area.left_center(), label::START, edge_style);
return;
}
if is_end_node(node_id.node_id) {
let edge_style = Style::default()
.bg(get_theme_color("canvas").unwrap_or(ratatui::style::Color::Blue))
.fg(get_theme_color("edge").unwrap_or(ratatui::style::Color::White));
buffer.set_string_styled(area.left_center(), label::END, edge_style);
return;
}
match detail_level {
VisualDetail::Minimal => {
let text_style = Style::default()
.fg(get_theme_color("text").unwrap_or(ratatui::style::Color::White))
.bg(get_theme_color("canvas").unwrap_or(ratatui::style::Color::Blue));
buffer.set_string_styled(area.left_center(), &NODE_GLYPH.to_string(), text_style);
}
VisualDetail::Truncated => {
let sequence = self.get_sequence(node_id);
let max_width = 12u32;
let truncated = inner_truncation(&sequence, max_width);
buffer.set_string_styled(area.left_center(), &truncated, text_style);
}
VisualDetail::Full => {
let sequence = self.get_sequence(node_id);
buffer.set_string_styled(area.left_center(), &sequence, text_style);
}
}
}
}
pub fn inner_truncation(s: &str, target_length: u32) -> String {
if s.len() <= target_length as usize {
return s.to_string();
} else if target_length < 5 {
return NODE_GLYPH.to_string(); }
let left_len = (target_length - 3) / 2 + ((target_length - 3) % 2);
let right_len = (target_length - 3) / 2;
let left = &s[..left_len as usize];
let right = &s[(s.len() - right_len as usize)..];
format!("{}...{}", left, right)
}
pub fn create_gen_graph_widget(
conn: &GraphConnection,
) -> GraphWidget<'_, &GenGraph, GenGraphNodeSizer, GenGraphNodeRenderer<'_>> {
let renderer = GenGraphNodeRenderer::new(conn);
GraphWidget::with_renderer(renderer)
}
pub fn create_gen_graph_controller(
graph: &GenGraph,
) -> GraphController<&GenGraph, GenGraphNodeSizer> {
let node_sizer = GenGraphNodeSizer;
let mut controller = GraphController::new(graph, node_sizer).with_theme(Theme {
canvas: get_theme_color("canvas").unwrap(),
node_fg: get_theme_color("text").unwrap(),
node_bg: get_theme_color("node").unwrap(),
edge_fg: get_theme_color("edge").unwrap(),
edge_bg: get_theme_color("canvas").unwrap(),
cursor_fg: get_theme_color("cursor_fg").unwrap(),
cursor_bg: get_theme_color("cursor_bg").unwrap(),
highlight: Color::Cyan,
});
controller.set_detail_level(VisualDetail::Truncated);
controller.show_cursor();
controller
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_inner_truncation_no_truncation_needed() {
let s = "hello";
let truncated = inner_truncation(s, 10);
assert_eq!(truncated, "hello");
}
#[test]
fn test_inner_truncation_truncate_to_odd_length() {
let s = "hello world";
let truncated = inner_truncation(s, 7);
assert_eq!(truncated, "he...ld");
}
#[test]
fn test_inner_truncation_truncate_to_even_length() {
let s = "hello world";
let truncated = inner_truncation(s, 8);
assert_eq!(truncated, "hel...ld");
}
#[test]
fn test_inner_truncation_empty_string() {
let s = "";
let truncated = inner_truncation(s, 5);
assert_eq!(truncated, "");
}
#[test]
fn test_inner_truncation_short_target() {
let s = "hello world";
let truncated = inner_truncation(s, 3);
assert_eq!(truncated, NODE_GLYPH.to_string());
}
}