use ratatui::{
Frame,
prelude::Rect,
style::{Modifier, Style},
text::Line,
widgets::{Block, BorderType, Borders, Paragraph, Wrap},
};
use unicode_width::UnicodeWidthStr;
use crate::i18n;
use crate::state::AppState;
use crate::theme::theme;
const fn calculate_content_layout(details_area: Rect) -> (u16, u16, u16) {
let border_inset = 1u16;
let content_x = details_area.x.saturating_add(border_inset);
let content_y = details_area.y.saturating_add(border_inset);
let inner_w = details_area.width.saturating_sub(2);
(content_x, content_y, inner_w)
}
fn style_url_in_lines(
details_lines: &mut [Line],
url_label: &str,
url_text: &str,
th: &crate::theme::Theme,
) {
for line in details_lines.iter_mut() {
if line.spans.len() >= 2 {
let key_txt = line.spans[0].content.to_string();
if key_txt.starts_with(url_label) {
let style = if url_text.is_empty() {
Style::default().fg(th.text)
} else {
Style::default()
.fg(th.mauve)
.add_modifier(Modifier::UNDERLINED | Modifier::BOLD)
};
line.spans[1] = ratatui::text::Span::styled(url_text.to_string(), style);
}
}
}
}
fn calculate_wrapped_line_rows(line_len: usize, inner_w: u16) -> u16 {
if inner_w == 0 {
1
} else {
u16::try_from(line_len)
.unwrap_or(u16::MAX)
.div_ceil(inner_w)
.max(1)
}
}
fn calculate_url_button_rect(
key_txt: &str,
url_txt: &str,
content_x: u16,
cur_y: u16,
inner_w: u16,
) -> Option<(u16, u16, u16, u16)> {
if url_txt.is_empty() {
return None;
}
let key_len = u16::try_from(key_txt.width()).unwrap_or(u16::MAX);
let x_start = content_x.saturating_add(key_len);
let max_w = inner_w.saturating_sub(key_len);
let w = u16::try_from(url_txt.width().min(max_w as usize)).unwrap_or(u16::MAX);
if w > 0 {
Some((x_start, cur_y, w, 1))
} else {
None
}
}
fn calculate_pkgbuild_button_rect(
txt: &str,
content_x: u16,
cur_y: u16,
inner_w: u16,
) -> Option<(u16, u16, u16, u16)> {
let w = u16::try_from(txt.width().min(inner_w as usize)).unwrap_or(u16::MAX);
if w > 0 {
Some((content_x, cur_y, w, 1))
} else {
None
}
}
struct ButtonContext<'a> {
content_x: u16,
content_y: u16,
inner_w: u16,
url_label: &'a str,
url_text: &'a str,
}
fn calculate_button_rects(
details_lines: &[Line],
visible_lines: &[Line],
scroll_offset: usize,
ctx: &ButtonContext<'_>,
app: &mut AppState,
) {
app.url_button_rect = None;
app.pkgb_button_rect = None;
app.comments_button_rect = None;
let show_pkgb = crate::i18n::t(app, "app.details.show_pkgbuild").to_lowercase();
let hide_pkgb = crate::i18n::t(app, "app.details.hide_pkgbuild").to_lowercase();
let show_comments = crate::i18n::t(app, "app.details.show_comments").to_lowercase();
let hide_comments = crate::i18n::t(app, "app.details.hide_comments").to_lowercase();
let mut cur_y = ctx.content_y;
for (vis_idx, vis_line) in visible_lines.iter().enumerate() {
let line_idx = vis_idx + scroll_offset;
let original_line = &details_lines[line_idx];
if original_line.spans.len() >= 2 {
let key_txt = original_line.spans[0].content.to_string();
if key_txt.starts_with(ctx.url_label)
&& let Some(rect) = calculate_url_button_rect(
&key_txt,
ctx.url_text,
ctx.content_x,
cur_y,
ctx.inner_w,
)
{
app.url_button_rect = Some(rect);
}
}
if original_line.spans.len() == 1 {
let txt = original_line.spans[0].content.to_string();
let lowered = txt.to_lowercase();
if (lowered.contains(&show_pkgb) || lowered.contains(&hide_pkgb))
&& let Some(rect) =
calculate_pkgbuild_button_rect(&txt, ctx.content_x, cur_y, ctx.inner_w)
{
app.pkgb_button_rect = Some(rect);
} else if (lowered.contains(&show_comments) || lowered.contains(&hide_comments))
&& let Some(rect) =
calculate_pkgbuild_button_rect(&txt, ctx.content_x, cur_y, ctx.inner_w)
{
app.comments_button_rect = Some(rect);
}
}
let line_len: usize = vis_line.spans.iter().map(|s| s.content.len()).sum();
let rows = calculate_wrapped_line_rows(line_len, ctx.inner_w);
cur_y = cur_y.saturating_add(rows);
}
}
pub fn render_package_info(f: &mut Frame, app: &mut AppState, details_area: Rect) {
let th = theme();
let url_label = crate::i18n::t(app, "app.details.url_label");
let url_text = app.details.url.clone();
let mut details_lines = crate::ui::helpers::format_details_lines(app, details_area.width, &th);
app.details_rect = Some((
details_area.x + 1,
details_area.y + 1,
details_area.width.saturating_sub(2),
details_area.height.saturating_sub(2),
));
style_url_in_lines(&mut details_lines, &url_label, &url_text, &th);
let scroll_offset = app.details_scroll as usize;
let visible_lines: Vec<_> = details_lines.iter().skip(scroll_offset).cloned().collect();
let (content_x, content_y, inner_w) = calculate_content_layout(details_area);
let button_ctx = ButtonContext {
content_x,
content_y,
inner_w,
url_label: &url_label,
url_text: &url_text,
};
calculate_button_rects(
&details_lines,
&visible_lines,
scroll_offset,
&button_ctx,
app,
);
let package_info_title = i18n::t(app, "app.headings.package_info");
let details_block = Block::default()
.title(ratatui::text::Span::styled(
&package_info_title,
Style::default().fg(th.overlay1),
))
.borders(Borders::ALL)
.border_type(BorderType::Rounded)
.border_style(Style::default().fg(th.surface2));
let details = Paragraph::new(visible_lines)
.style(Style::default().fg(th.text).bg(th.base))
.wrap(Wrap { trim: true })
.block(details_block);
f.render_widget(details, details_area);
app.mouse_disabled_in_details = true;
}