use super::*;
const MAX_STREAM_INFO_WIDTH: u16 = 36;
pub(in crate::tui::ui) fn render_stream_info(
frame: &mut Frame,
area: Rect,
state: &DashboardState,
) {
let lines = stream_info_lines_for_area(state, area);
let popup = stream_info_area(area, &lines);
if popup.is_empty() {
return;
}
clear_area(frame, popup);
frame.render_widget(
Paragraph::new(lines)
.style(theme::current().style(theme::HighlightGroup::Normal))
.block(panel_block(" Streams ", false)),
popup,
);
}
pub(in crate::tui::ui) fn stream_info_area(area: Rect, lines: &[Line<'_>]) -> Rect {
if lines.is_empty() || area.width < 3 || area.height < 3 {
return Rect::default();
}
let content_width = lines.iter().map(Line::width).max().unwrap_or(1) as u16;
let width = content_width
.saturating_add(2)
.min(MAX_STREAM_INFO_WIDTH)
.min(area.width);
let height = (lines.len() as u16).saturating_add(2).min(area.height);
Rect {
x: area.x + area.width.saturating_sub(width),
y: area.y + area.height.saturating_sub(height),
width,
height,
}
}
#[cfg(test)]
pub(in crate::tui::ui) fn stream_info_lines(state: &DashboardState) -> Vec<Line<'static>> {
stream_info_lines_for_width(state, usize::from(MAX_STREAM_INFO_WIDTH.saturating_sub(2)))
}
pub(in crate::tui::ui) fn stream_info_lines_for_area(
state: &DashboardState,
area: Rect,
) -> Vec<Line<'static>> {
let content_width = area.width.min(MAX_STREAM_INFO_WIDTH).saturating_sub(2);
stream_info_lines_for_width(state, usize::from(content_width))
}
pub(in crate::tui::ui) fn stream_info_lines_for_width(
state: &DashboardState,
content_width: usize,
) -> Vec<Line<'static>> {
let sections = state.stream_info_sections();
let mut lines = Vec::new();
let content_width = content_width.max(1);
for (section_index, section) in sections.iter().enumerate() {
if section_index > 0 {
lines.push(Line::styled(
"=".repeat(content_width),
theme::current().style(theme::HighlightGroup::Muted),
));
}
let status = if section.paused { "⏸" } else { "🔴" };
lines.push(Line::from(vec![
Span::raw(section.broadcaster.clone()),
Span::styled(
format!(" {status}"),
theme::current().style(theme::HighlightGroup::Success),
),
]));
lines.push(Line::styled(
"-".repeat(content_width),
theme::current().style(theme::HighlightGroup::Muted),
));
for viewer in §ion.viewers {
lines.extend(
wrap_stream_info_text(viewer, content_width)
.into_iter()
.map(Line::from),
);
}
}
lines
}
fn wrap_stream_info_text(value: &str, width: usize) -> Vec<String> {
let lines = wrap_plain_text_at_words(value, width.max(1));
if lines.is_empty() {
vec![String::new()]
} else {
lines
}
}
#[cfg(test)]
mod tests {
use super::wrap_stream_info_text;
#[test]
fn stream_info_wrap_preserves_empty_spaces_and_long_unicode_words() {
for (value, width, expected) in [
("", 4, vec![""]),
(" ", 4, vec![""]),
("alpha beta", 5, vec!["alpha", "beta"]),
("가나다라마바사", 4, vec!["가나", "다라", "마바", "사"]),
] {
assert_eq!(wrap_stream_info_text(value, width), expected, "{value:?}");
}
}
}