use ratatui::{
Frame,
layout::{Alignment, Rect},
style::Style,
text::{Line, Span, Text},
widgets::{Block, BorderType, Borders, Padding, Paragraph},
};
use crate::app::App;
use crate::theme;
use crate::tooltip::{get_operator_content, get_tooltip_content};
use crate::widgets::popup;
const TOOLTIP_MIN_WIDTH: u16 = 40;
const TOOLTIP_MAX_WIDTH: u16 = 90;
const TOOLTIP_BORDER_HEIGHT: u16 = 4; const TOOLTIP_BORDER_WIDTH: u16 = 6; const TOOLTIP_MIN_HEIGHT: u16 = 8;
const TOOLTIP_MAX_HEIGHT: u16 = 18;
fn wrap_text(text: &str, max_width: usize) -> Vec<String> {
if text.len() <= max_width {
return vec![text.to_string()];
}
let mut lines = Vec::new();
let mut current_line = String::new();
for word in text.split_whitespace() {
if current_line.is_empty() {
current_line = word.to_string();
} else if current_line.len() + 1 + word.len() <= max_width {
current_line.push(' ');
current_line.push_str(word);
} else {
lines.push(current_line);
current_line = word.to_string();
}
}
if !current_line.is_empty() {
lines.push(current_line);
}
if lines.len() > 2 {
lines.truncate(2);
}
lines
}
pub fn render_popup(app: &App, frame: &mut Frame, input_area: Rect) -> Option<Rect> {
let (title_prefix, name, content) = if let Some(func) = &app.tooltip.current_function {
if let Some(c) = get_tooltip_content(func) {
("fn", func.as_str(), c)
} else {
return None;
}
} else if let Some(op) = &app.tooltip.current_operator {
if let Some(c) = get_operator_content(op) {
("operator", op.as_str(), c)
} else {
return None;
}
} else {
return None;
};
let parsed_examples: Vec<(&str, &str)> = content
.examples
.iter()
.map(|e| {
if let Some(idx) = e.find('#') {
(e[..idx].trim_end(), e[idx + 1..].trim_start())
} else {
(*e, "")
}
})
.collect();
let max_code_width = parsed_examples
.iter()
.map(|(code, _)| code.len())
.max()
.unwrap_or(0);
let description_width = content.description.len();
let max_example_width = parsed_examples
.iter()
.map(|(code, desc)| {
if desc.is_empty() {
code.len() + 2 } else {
max_code_width + 3 + desc.len() + 2 }
})
.max()
.unwrap_or(0);
let title_width = title_prefix.len() + 2 + name.len() + 2;
let content_width = description_width.max(max_example_width).max(title_width);
let popup_width =
((content_width as u16) + TOOLTIP_BORDER_WIDTH).clamp(TOOLTIP_MIN_WIDTH, TOOLTIP_MAX_WIDTH);
let tip_available_width = (popup_width as usize).saturating_sub(6); let wrapped_tip_lines: Vec<String> = if let Some(tip) = content.tip {
wrap_text(tip, tip_available_width)
} else {
Vec::new()
};
let tip_line_count = wrapped_tip_lines.len() as u16;
let example_count = parsed_examples.len() as u16;
let tip_height = if content.tip.is_some() {
1 + tip_line_count } else {
0
};
let content_height = 1 + 1 + example_count + tip_height; let popup_height =
(content_height + TOOLTIP_BORDER_HEIGHT).clamp(TOOLTIP_MIN_HEIGHT, TOOLTIP_MAX_HEIGHT);
let frame_area = frame.area();
let max_allowed_width = (frame_area.width * 3) / 4;
let final_width = popup_width.min(max_allowed_width);
let popup_x = frame_area.width.saturating_sub(final_width + 2);
let popup_y = input_area.y.saturating_sub(popup_height);
let popup_area = Rect {
x: popup_x,
y: popup_y,
width: final_width,
height: popup_height.min(input_area.y),
};
popup::clear_area(frame, popup_area);
let mut lines: Vec<Line> = Vec::new();
lines.push(Line::from(vec![Span::styled(
content.description,
Style::default().fg(theme::tooltip::DESCRIPTION),
)]));
lines.push(Line::from(""));
for (code, desc) in &parsed_examples {
if desc.is_empty() {
lines.push(Line::from(vec![Span::styled(
format!(" {}", code),
Style::default().fg(theme::tooltip::EXAMPLE),
)]));
} else {
let padded_code = format!("{:width$}", code, width = max_code_width);
lines.push(Line::from(vec![
Span::styled(
format!(" {}", padded_code),
Style::default().fg(theme::tooltip::EXAMPLE),
),
Span::styled(" │ ", Style::default().fg(theme::tooltip::SEPARATOR)),
Span::styled(*desc, Style::default().fg(theme::tooltip::EXAMPLE_DESC)),
]));
}
}
if content.tip.is_some() && !wrapped_tip_lines.is_empty() {
lines.push(Line::from(""));
lines.push(Line::from(vec![
Span::styled("💡 ", Style::default()),
Span::styled(
wrapped_tip_lines[0].clone(),
Style::default().fg(theme::tooltip::TIP),
),
]));
for line in wrapped_tip_lines.iter().skip(1) {
lines.push(Line::from(vec![
Span::raw(" "), Span::styled(line.clone(), Style::default().fg(theme::tooltip::TIP)),
]));
}
}
let text = Text::from(lines);
let title = Line::from(vec![
Span::raw(" "),
Span::styled(format!("{}: {}", title_prefix, name), theme::tooltip::TITLE),
Span::raw(" "),
]);
let dismiss_hint =
theme::border_hints::build_hints(&[("Ctrl+T", "Dismiss")], theme::tooltip::BORDER);
let popup_widget = Paragraph::new(text).block(
Block::default()
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.title(title)
.title_bottom(dismiss_hint.alignment(Alignment::Center))
.border_style(Style::default().fg(theme::tooltip::BORDER))
.style(Style::default().bg(theme::tooltip::BACKGROUND))
.padding(Padding::uniform(1)),
);
frame.render_widget(popup_widget, popup_area);
Some(popup_area)
}
#[cfg(test)]
pub fn format_tooltip_title(is_function: bool, name: &str) -> String {
if is_function {
format!("fn: {}", name)
} else {
format!("operator: {}", name)
}
}
#[cfg(test)]
#[path = "tooltip_render_tests.rs"]
mod tooltip_render_tests;