feat(tui): scroll long names in every list and page through popups and chat
The sideways keys the results had (h/l, arrows, 0 and $) now scroll a transfer's file name and a query too, each list on its own. The browse tree and the room list take Home/End, PgUp/PgDn and the vim jumps like the main lists, and a chat log, room or private, pages back through its history and holds its place while new messages arrive; what arrives meanwhile counts as unread until the reader returns to the end.
This commit is contained in:
@@ -1,4 +1,4 @@
|
||||
use crate::models::{AppState, MessageDirection};
|
||||
use crate::models::{AppState, ChatMessage, LogView, MessageDirection};
|
||||
use crate::ui::{
|
||||
accent_style, border_style, dimmed_style, highlight_style, info_style,
|
||||
primary_style, wrap_chat_line,
|
||||
@@ -17,10 +17,11 @@ use ratatui::{
|
||||
pub fn render_chat_pane(
|
||||
frame: &mut Frame,
|
||||
area: Rect,
|
||||
state: &AppState,
|
||||
state: &mut AppState,
|
||||
own_username: &str,
|
||||
) {
|
||||
let peer = state.active_chat_peer();
|
||||
let peer = state.active_chat_peer().map(str::to_string);
|
||||
let peer = peer.as_deref();
|
||||
let title = peer.map_or_else(
|
||||
|| " Messages (m: compose, i/Esc: close) ".to_string(),
|
||||
|peer| format!(" {peer} (↑↓/Tab: switch, m: to…, i/Esc: close) "),
|
||||
@@ -51,7 +52,14 @@ pub fn render_chat_pane(
|
||||
return;
|
||||
};
|
||||
|
||||
render_messages(frame, body[0], state, peer, own_username);
|
||||
render_messages(
|
||||
frame,
|
||||
body[0],
|
||||
&state.messages,
|
||||
&mut state.chat_view,
|
||||
peer,
|
||||
own_username,
|
||||
);
|
||||
render_compose(frame, chunks[1], state);
|
||||
}
|
||||
|
||||
@@ -94,13 +102,13 @@ fn render_peers(
|
||||
fn render_messages(
|
||||
frame: &mut Frame,
|
||||
area: Rect,
|
||||
state: &AppState,
|
||||
messages: &[ChatMessage],
|
||||
view: &mut LogView,
|
||||
peer: &str,
|
||||
own_username: &str,
|
||||
) {
|
||||
let width = area.width as usize;
|
||||
let lines: Vec<Line> = state
|
||||
.messages
|
||||
let lines: Vec<Line> = messages
|
||||
.iter()
|
||||
.filter(|m| m.peer == peer)
|
||||
.flat_map(|m| {
|
||||
@@ -123,9 +131,9 @@ fn render_messages(
|
||||
})
|
||||
.collect();
|
||||
|
||||
// Auto-scroll: tail by rendered rows so the newest message stays visible.
|
||||
let start = lines.len().saturating_sub((area.height as usize).max(1));
|
||||
frame.render_widget(Paragraph::new(lines[start..].to_vec()), area);
|
||||
// The newest rows, unless the reader is holding a place in the history.
|
||||
let window = view.window(lines.len(), usize::from(area.height));
|
||||
frame.render_widget(Paragraph::new(lines[window].to_vec()), area);
|
||||
}
|
||||
|
||||
fn render_compose(frame: &mut Frame, area: Rect, state: &AppState) {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use super::name_scroll::{column_width, end_offset, scroll_text};
|
||||
use crate::models::DownloadEntry;
|
||||
use crate::ui::{
|
||||
GLYPH_ACTIVE, GLYPH_DONE, GLYPH_FAILED, GLYPH_QUEUED, HIGHLIGHT_SYMBOL,
|
||||
@@ -8,12 +9,43 @@ use crate::ui::{
|
||||
};
|
||||
use ratatui::{
|
||||
Frame,
|
||||
layout::Rect,
|
||||
layout::{Constraint, Rect},
|
||||
widgets::{Cell, HighlightSpacing, Paragraph, Row, Table, TableState},
|
||||
};
|
||||
use soulseek_rs::DownloadStatus;
|
||||
use soulseek_rs::types::{UploadInfo, UploadStatus};
|
||||
|
||||
// Sized to the widest value each column shows in ordinary use — a
|
||||
// three-digit megabyte count on both sides of the progress — so the file
|
||||
// name keeps whatever width the pane has beyond that.
|
||||
const WIDTHS: [Constraint; 5] = [
|
||||
Constraint::Length(6),
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(12),
|
||||
Constraint::Length(25),
|
||||
Constraint::Length(10),
|
||||
];
|
||||
|
||||
/// Filename is the second column.
|
||||
const NAME_COLUMN: usize = 1;
|
||||
|
||||
/// The offset that brings the end of a transfer's name into view.
|
||||
#[must_use]
|
||||
pub fn name_end_offset(name: &str, area: Rect) -> usize {
|
||||
end_offset(name, area, &WIDTHS, NAME_COLUMN)
|
||||
}
|
||||
|
||||
/// What the list shows for an upload: the peer-facing path is ours, so only
|
||||
/// the file's own name is worth a column.
|
||||
#[must_use]
|
||||
pub fn upload_display_name(upload: &UploadInfo) -> &str {
|
||||
upload
|
||||
.filename
|
||||
.rsplit(['\\', '/'])
|
||||
.next()
|
||||
.unwrap_or(&upload.filename)
|
||||
}
|
||||
|
||||
pub fn render_downloads_pane(
|
||||
frame: &mut Frame,
|
||||
area: Rect,
|
||||
@@ -21,6 +53,7 @@ pub fn render_downloads_pane(
|
||||
uploads: &[UploadInfo],
|
||||
table_state: &mut TableState,
|
||||
focused: bool,
|
||||
name_offset: usize,
|
||||
) {
|
||||
if downloads.is_empty() && uploads.is_empty() {
|
||||
let empty_block = pane_block(focused).title(pane_title(
|
||||
@@ -47,6 +80,7 @@ pub fn render_downloads_pane(
|
||||
])
|
||||
.height(1);
|
||||
|
||||
let name_width = column_width(area, &WIDTHS, NAME_COLUMN);
|
||||
let mut rows: Vec<Row> = downloads
|
||||
.iter()
|
||||
.map(|download_entry| {
|
||||
@@ -106,7 +140,12 @@ pub fn render_downloads_pane(
|
||||
|
||||
Row::new(vec![
|
||||
Cell::from(status_icon).style(status_style),
|
||||
Cell::from(download.filename.clone()).style(body_style()),
|
||||
Cell::from(scroll_text(
|
||||
&download.filename,
|
||||
name_offset,
|
||||
name_width,
|
||||
))
|
||||
.style(body_style()),
|
||||
Cell::from(download.username.clone()).style(info_style()),
|
||||
Cell::from(progress_text).style(progress_style),
|
||||
Cell::from(speed_text).style(warning_style()),
|
||||
@@ -149,32 +188,21 @@ pub fn render_downloads_pane(
|
||||
}
|
||||
_ => "-".to_string(),
|
||||
};
|
||||
let basename = upload
|
||||
.filename
|
||||
.rsplit(['\\', '/'])
|
||||
.next()
|
||||
.unwrap_or(&upload.filename);
|
||||
Row::new(vec![
|
||||
Cell::from(format!("↑ {status_icon}")).style(status_style),
|
||||
Cell::from(basename.to_string()).style(body_style()),
|
||||
Cell::from(scroll_text(
|
||||
upload_display_name(upload),
|
||||
name_offset,
|
||||
name_width,
|
||||
))
|
||||
.style(body_style()),
|
||||
Cell::from(upload.username.clone()).style(info_style()),
|
||||
Cell::from(progress_text).style(primary_style()),
|
||||
Cell::from(speed_text).style(warning_style()),
|
||||
])
|
||||
}));
|
||||
|
||||
// Sized to the widest value each column shows in ordinary use — a
|
||||
// three-digit megabyte count on both sides of the progress — so the file
|
||||
// name keeps whatever width the pane has beyond that.
|
||||
let widths = [
|
||||
ratatui::layout::Constraint::Length(6),
|
||||
ratatui::layout::Constraint::Fill(1),
|
||||
ratatui::layout::Constraint::Length(12),
|
||||
ratatui::layout::Constraint::Length(25),
|
||||
ratatui::layout::Constraint::Length(10),
|
||||
];
|
||||
|
||||
let table = Table::new(rows, widths)
|
||||
let table = Table::new(rows, WIDTHS)
|
||||
.header(header)
|
||||
.row_highlight_style(row_highlight_style())
|
||||
.highlight_symbol(HIGHLIGHT_SYMBOL)
|
||||
|
||||
@@ -2,6 +2,7 @@ mod browse_pane;
|
||||
mod chat_pane;
|
||||
mod download_info_pane;
|
||||
mod downloads_pane;
|
||||
mod name_scroll;
|
||||
mod results_pane;
|
||||
mod rooms_pane;
|
||||
mod searches_pane;
|
||||
@@ -11,9 +12,12 @@ pub use chat_pane::render_chat_pane;
|
||||
pub use download_info_pane::{
|
||||
InfoSubject, render_download_info_pane, selected_transfer,
|
||||
};
|
||||
pub use downloads_pane::render_downloads_pane;
|
||||
pub use downloads_pane::{
|
||||
name_end_offset as transfer_name_end_offset, render_downloads_pane,
|
||||
upload_display_name,
|
||||
};
|
||||
pub use results_pane::{
|
||||
ResultsPaneParams, name_end_offset, render_results_pane,
|
||||
};
|
||||
pub use rooms_pane::render_rooms_pane;
|
||||
pub use searches_pane::render_searches_pane;
|
||||
pub use searches_pane::{query_end_offset, render_searches_pane};
|
||||
|
||||
@@ -0,0 +1,126 @@
|
||||
//! Sideways scrolling for a table column that holds text wider than it is:
|
||||
//! a file name, a query. Every list scrolls the same way, so the arithmetic
|
||||
//! lives here and each pane only says which of its columns it applies to.
|
||||
|
||||
use crate::ui::{HIGHLIGHT_SYMBOL, pane_block};
|
||||
use ratatui::{
|
||||
layout::{Constraint, Layout, Rect},
|
||||
text::{Line, Span},
|
||||
};
|
||||
use unicode_width::UnicodeWidthChar;
|
||||
|
||||
/// Cells column `column` of a table laid out with `widths` gets inside a pane
|
||||
/// drawn over `area`: what the pane keeps for its border, padding and the
|
||||
/// highlight symbol comes off first.
|
||||
#[must_use]
|
||||
pub fn column_width(area: Rect, widths: &[Constraint], column: usize) -> usize {
|
||||
let inner = pane_block(false).inner(area);
|
||||
let symbol = Line::from(HIGHLIGHT_SYMBOL).width() as u16;
|
||||
let [_, columns] =
|
||||
Layout::horizontal([Constraint::Length(symbol), Constraint::Fill(0)])
|
||||
.areas(inner);
|
||||
usize::from(
|
||||
Layout::horizontal(widths).spacing(1).split(columns)[column].width,
|
||||
)
|
||||
}
|
||||
|
||||
/// How far a text of `cells` can scroll before its tail is in view, keeping
|
||||
/// one cell for the ellipsis that marks the cut.
|
||||
const fn overflow(cells: usize, width: usize) -> usize {
|
||||
if cells <= width { 0 } else { cells + 1 - width }
|
||||
}
|
||||
|
||||
/// The offset that brings the end of `text` into view in the column.
|
||||
#[must_use]
|
||||
pub fn end_offset(
|
||||
text: &str,
|
||||
area: Rect,
|
||||
widths: &[Constraint],
|
||||
column: usize,
|
||||
) -> usize {
|
||||
overflow(Span::raw(text).width(), column_width(area, widths, column))
|
||||
}
|
||||
|
||||
/// `text` scrolled `offset` cells to the left, with an ellipsis where it was
|
||||
/// cut. Scrolls by cells, so a wide glyph counts double, and never starts on
|
||||
/// a combining mark. Beyond the end it shows the tail.
|
||||
#[must_use]
|
||||
pub fn scroll_text(text: &str, offset: usize, width: usize) -> Line<'_> {
|
||||
if offset == 0 {
|
||||
return Line::from(text);
|
||||
}
|
||||
let mut tail_cells = 0;
|
||||
let mut furthest = text.len();
|
||||
for (index, ch) in text.char_indices().rev() {
|
||||
let cells = ch.width().unwrap_or(0);
|
||||
if tail_cells + cells >= width {
|
||||
if index == 0 && tail_cells + cells == width {
|
||||
return Line::from(text);
|
||||
}
|
||||
break;
|
||||
}
|
||||
tail_cells += cells;
|
||||
furthest = index;
|
||||
}
|
||||
if furthest == 0 {
|
||||
return Line::from(text);
|
||||
}
|
||||
let mut skipped = 0;
|
||||
let mut start = furthest;
|
||||
for (index, ch) in text.char_indices() {
|
||||
if index >= furthest || skipped >= offset {
|
||||
start = index;
|
||||
break;
|
||||
}
|
||||
skipped += ch.width().unwrap_or(0);
|
||||
}
|
||||
while let Some(ch) = text[start..].chars().next()
|
||||
&& ch.width() == Some(0)
|
||||
{
|
||||
start += ch.len_utf8();
|
||||
}
|
||||
Line::from(vec![Span::raw("…"), Span::raw(&text[start..])])
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
fn text_of(line: &Line<'_>) -> String {
|
||||
line.spans.iter().map(|s| s.content.as_ref()).collect()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_text_that_fits_never_scrolls() {
|
||||
assert_eq!(text_of(&scroll_text("short", 8, 10)), "short");
|
||||
assert_eq!(
|
||||
end_offset(
|
||||
"short",
|
||||
Rect::new(0, 0, 40, 5),
|
||||
&[Constraint::Fill(1)],
|
||||
0
|
||||
),
|
||||
0
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn the_end_offset_leaves_room_for_the_ellipsis() {
|
||||
// Column of 40 - 2 border - 2 padding - 1 symbol = 35 cells.
|
||||
let area = Rect::new(0, 0, 40, 5);
|
||||
let widths = [Constraint::Fill(1)];
|
||||
let fits = "x".repeat(35);
|
||||
let over = "x".repeat(36);
|
||||
assert_eq!(end_offset(&fits, area, &widths, 0), 0);
|
||||
assert_eq!(end_offset(&over, area, &widths, 0), 2);
|
||||
let line = scroll_text(&over, 2, 35);
|
||||
assert_eq!(text_of(&line).chars().count(), 35);
|
||||
assert!(text_of(&line).starts_with('…'));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn scrolling_past_the_end_shows_the_tail() {
|
||||
let line = scroll_text("abcdefghij", usize::MAX, 5);
|
||||
assert_eq!(text_of(&line), "…ghij");
|
||||
}
|
||||
}
|
||||
@@ -1,3 +1,4 @@
|
||||
use super::name_scroll::{column_width, end_offset, scroll_text};
|
||||
use crate::models::FileDisplayData;
|
||||
use crate::ui::{
|
||||
BYTES_PER_MB, HIGHLIGHT_SYMBOL, body_style, dimmed_style, format_bytes,
|
||||
@@ -6,12 +7,11 @@ use crate::ui::{
|
||||
};
|
||||
use ratatui::{
|
||||
Frame,
|
||||
layout::{Constraint, Layout, Rect},
|
||||
layout::{Constraint, Rect},
|
||||
text::{Line, Span},
|
||||
widgets::{Cell, HighlightSpacing, Paragraph, Row, Table, TableState},
|
||||
};
|
||||
use std::collections::HashSet;
|
||||
use unicode_width::UnicodeWidthChar;
|
||||
|
||||
const VERSION: &str = env!("CARGO_PKG_VERSION");
|
||||
|
||||
@@ -39,59 +39,12 @@ pub struct ResultsPaneParams<'a> {
|
||||
pub name_offset: usize,
|
||||
}
|
||||
|
||||
fn name_column_width(area: Rect) -> usize {
|
||||
let inner = pane_block(false).inner(area);
|
||||
let symbol = Line::from(HIGHLIGHT_SYMBOL).width() as u16;
|
||||
let [_, columns] =
|
||||
Layout::horizontal([Constraint::Length(symbol), Constraint::Fill(0)])
|
||||
.areas(inner);
|
||||
usize::from(Layout::horizontal(WIDTHS).spacing(1).split(columns)[1].width)
|
||||
}
|
||||
|
||||
const fn overflow(cells: usize, width: usize) -> usize {
|
||||
if cells <= width { 0 } else { cells + 1 - width }
|
||||
}
|
||||
/// Filename is the second column.
|
||||
const NAME_COLUMN: usize = 1;
|
||||
|
||||
#[must_use]
|
||||
pub fn name_end_offset(name: &str, area: Rect) -> usize {
|
||||
overflow(Span::raw(name).width(), name_column_width(area))
|
||||
}
|
||||
|
||||
fn scroll_name(name: &str, offset: usize, width: usize) -> Line<'_> {
|
||||
if offset == 0 {
|
||||
return Line::from(name);
|
||||
}
|
||||
let mut tail_cells = 0;
|
||||
let mut furthest = name.len();
|
||||
for (index, ch) in name.char_indices().rev() {
|
||||
let cells = ch.width().unwrap_or(0);
|
||||
if tail_cells + cells >= width {
|
||||
if index == 0 && tail_cells + cells == width {
|
||||
return Line::from(name);
|
||||
}
|
||||
break;
|
||||
}
|
||||
tail_cells += cells;
|
||||
furthest = index;
|
||||
}
|
||||
if furthest == 0 {
|
||||
return Line::from(name);
|
||||
}
|
||||
let mut skipped = 0;
|
||||
let mut start = furthest;
|
||||
for (index, ch) in name.char_indices() {
|
||||
if index >= furthest || skipped >= offset {
|
||||
start = index;
|
||||
break;
|
||||
}
|
||||
skipped += ch.width().unwrap_or(0);
|
||||
}
|
||||
while let Some(ch) = name[start..].chars().next()
|
||||
&& ch.width() == Some(0)
|
||||
{
|
||||
start += ch.len_utf8();
|
||||
}
|
||||
Line::from(vec![Span::raw("…"), Span::raw(&name[start..])])
|
||||
end_offset(name, area, &WIDTHS, NAME_COLUMN)
|
||||
}
|
||||
|
||||
/// Whether the rendered row `display_idx` is selected. `selected_indices` holds
|
||||
@@ -173,7 +126,7 @@ pub fn render_results_pane(
|
||||
])
|
||||
.height(1);
|
||||
|
||||
let name_width = name_column_width(area);
|
||||
let name_width = column_width(area, &WIDTHS, NAME_COLUMN);
|
||||
let rows: Vec<Row> = items
|
||||
.iter()
|
||||
.enumerate()
|
||||
@@ -206,7 +159,7 @@ pub fn render_results_pane(
|
||||
|
||||
Row::new(vec![
|
||||
Cell::from(checkbox).style(checkbox_style),
|
||||
Cell::from(scroll_name(
|
||||
Cell::from(scroll_text(
|
||||
&file.filename,
|
||||
name_offset,
|
||||
name_width,
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
use crate::models::{RoomsState, RoomsView};
|
||||
use crate::models::{LogView, RoomsState, RoomsView};
|
||||
use crate::ui::{
|
||||
HIGHLIGHT_SYMBOL, PANE_PADDING, accent_style, dimmed_style,
|
||||
highlight_style, info_style, pane_block, plain_title, primary_style,
|
||||
@@ -20,7 +20,7 @@ use ratatui::{
|
||||
pub fn render_rooms_pane(
|
||||
frame: &mut Frame,
|
||||
area: Rect,
|
||||
rooms: &RoomsState,
|
||||
rooms: &mut RoomsState,
|
||||
list_table_state: &mut TableState,
|
||||
) {
|
||||
match rooms.view {
|
||||
@@ -94,7 +94,7 @@ fn render_list(
|
||||
frame.render_stateful_widget(table, area, table_state);
|
||||
}
|
||||
|
||||
fn render_chat(frame: &mut Frame, area: Rect, rooms: &RoomsState) {
|
||||
fn render_chat(frame: &mut Frame, area: Rect, rooms: &mut RoomsState) {
|
||||
let block = pane_block(true).title(
|
||||
" Chat rooms (Tab: switch, l: room list, x: leave, Esc: back) ",
|
||||
);
|
||||
@@ -110,7 +110,10 @@ fn render_chat(frame: &mut Frame, area: Rect, rooms: &RoomsState) {
|
||||
|
||||
render_tab_bar(frame, chunks[0], rooms);
|
||||
|
||||
let Some(active) = rooms.active_room() else {
|
||||
let user_selected = rooms.user_selected;
|
||||
let composing = rooms.composing;
|
||||
let active_index = rooms.active;
|
||||
let Some(active) = rooms.open.get_mut(active_index) else {
|
||||
frame.render_widget(
|
||||
Paragraph::new("No open rooms. Press l for the room list.")
|
||||
.style(dimmed_style()),
|
||||
@@ -124,11 +127,11 @@ fn render_chat(frame: &mut Frame, area: Rect, rooms: &RoomsState) {
|
||||
Layout::horizontal([Constraint::Fill(1), Constraint::Length(22)])
|
||||
.split(chunks[1]);
|
||||
|
||||
render_messages(frame, body[0], active.lines.as_slice());
|
||||
render_users(frame, body[1], &active.users, rooms.user_selected);
|
||||
render_messages(frame, body[0], active.lines.as_slice(), &mut active.view);
|
||||
render_users(frame, body[1], &active.users, user_selected);
|
||||
|
||||
// Compose line or hint.
|
||||
if rooms.composing {
|
||||
if composing {
|
||||
let line = Line::from(vec![
|
||||
Span::styled("› ", accent_style()),
|
||||
Span::styled(active.input.clone(), primary_style()),
|
||||
@@ -173,9 +176,10 @@ fn render_messages(
|
||||
frame: &mut Frame,
|
||||
area: Rect,
|
||||
lines: &[crate::models::RoomLine],
|
||||
view: &mut LogView,
|
||||
) {
|
||||
// Wrap each message to the pane width, then auto-scroll: tail by rendered
|
||||
// rows so the newest message is always fully visible.
|
||||
// Wrap each message to the pane width, then show the rows the reader is
|
||||
// at: the newest ones, unless they are holding a place in the history.
|
||||
let width = area.width as usize;
|
||||
let rendered: Vec<Line> = lines
|
||||
.iter()
|
||||
@@ -198,8 +202,8 @@ fn render_messages(
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
let start = rendered.len().saturating_sub((area.height as usize).max(1));
|
||||
frame.render_widget(Paragraph::new(rendered[start..].to_vec()), area);
|
||||
let window = view.window(rendered.len(), usize::from(area.height));
|
||||
frame.render_widget(Paragraph::new(rendered[window].to_vec()), area);
|
||||
}
|
||||
|
||||
fn render_users(
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
use super::name_scroll::{column_width, end_offset, scroll_text};
|
||||
use crate::models::{SearchEntry, SearchStatus};
|
||||
use crate::ui::{
|
||||
HIGHLIGHT_SYMBOL, body_style, dimmed_style, header_style, pane_block,
|
||||
@@ -5,16 +6,34 @@ use crate::ui::{
|
||||
};
|
||||
use ratatui::{
|
||||
Frame,
|
||||
layout::Rect,
|
||||
layout::{Constraint, Rect},
|
||||
widgets::{Cell, HighlightSpacing, Row, Table, TableState},
|
||||
};
|
||||
|
||||
// The pane shares a row with two others, so the fixed columns are as
|
||||
// narrow as their widest word: the query gets the rest.
|
||||
const WIDTHS: [Constraint; 3] = [
|
||||
Constraint::Length(6),
|
||||
Constraint::Fill(1),
|
||||
Constraint::Length(7),
|
||||
];
|
||||
|
||||
/// Query is the second column.
|
||||
const QUERY_COLUMN: usize = 1;
|
||||
|
||||
/// The offset that brings the end of a query into view.
|
||||
#[must_use]
|
||||
pub fn query_end_offset(query: &str, area: Rect) -> usize {
|
||||
end_offset(query, area, &WIDTHS, QUERY_COLUMN)
|
||||
}
|
||||
|
||||
pub fn render_searches_pane(
|
||||
frame: &mut Frame,
|
||||
area: Rect,
|
||||
searches: &[SearchEntry],
|
||||
table_state: &mut TableState,
|
||||
focused: bool,
|
||||
query_offset: usize,
|
||||
) {
|
||||
let header = Row::new(vec![
|
||||
Cell::from("Status").style(header_style()),
|
||||
@@ -23,6 +42,7 @@ pub fn render_searches_pane(
|
||||
])
|
||||
.height(1);
|
||||
|
||||
let query_width = column_width(area, &WIDTHS, QUERY_COLUMN);
|
||||
let rows: Vec<Row> = searches
|
||||
.iter()
|
||||
.map(|search| {
|
||||
@@ -55,21 +75,18 @@ pub fn render_searches_pane(
|
||||
|
||||
Row::new(vec![
|
||||
status_cell,
|
||||
Cell::from(search.query.clone()).style(body_style()),
|
||||
Cell::from(scroll_text(
|
||||
&search.query,
|
||||
query_offset,
|
||||
query_width,
|
||||
))
|
||||
.style(body_style()),
|
||||
Cell::from(results_text).style(dimmed_style()),
|
||||
])
|
||||
})
|
||||
.collect();
|
||||
|
||||
// The pane shares a row with two others, so the fixed columns are as
|
||||
// narrow as their widest word: the query gets the rest.
|
||||
let widths = [
|
||||
ratatui::layout::Constraint::Length(6),
|
||||
ratatui::layout::Constraint::Fill(1),
|
||||
ratatui::layout::Constraint::Length(7),
|
||||
];
|
||||
|
||||
let table = Table::new(rows, widths)
|
||||
let table = Table::new(rows, WIDTHS)
|
||||
.header(header)
|
||||
.row_highlight_style(row_highlight_style())
|
||||
.highlight_symbol(HIGHLIGHT_SYMBOL)
|
||||
|
||||
Reference in New Issue
Block a user