chain word and better save/load UI
This commit is contained in:
110
crates/ratatui/src/file_browser.rs
Normal file
110
crates/ratatui/src/file_browser.rs
Normal file
@@ -0,0 +1,110 @@
|
||||
use ratatui::layout::{Constraint, Layout, Rect};
|
||||
use ratatui::style::{Color, Style};
|
||||
use ratatui::text::{Line, Span};
|
||||
use ratatui::widgets::Paragraph;
|
||||
use ratatui::Frame;
|
||||
|
||||
use super::ModalFrame;
|
||||
|
||||
pub struct FileBrowserModal<'a> {
|
||||
title: &'a str,
|
||||
input: &'a str,
|
||||
entries: &'a [(String, bool)],
|
||||
selected: usize,
|
||||
scroll_offset: usize,
|
||||
border_color: Color,
|
||||
width: u16,
|
||||
height: u16,
|
||||
}
|
||||
|
||||
impl<'a> FileBrowserModal<'a> {
|
||||
pub fn new(title: &'a str, input: &'a str, entries: &'a [(String, bool)]) -> Self {
|
||||
Self {
|
||||
title,
|
||||
input,
|
||||
entries,
|
||||
selected: 0,
|
||||
scroll_offset: 0,
|
||||
border_color: Color::White,
|
||||
width: 60,
|
||||
height: 16,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn selected(mut self, idx: usize) -> Self {
|
||||
self.selected = idx;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn scroll_offset(mut self, offset: usize) -> Self {
|
||||
self.scroll_offset = offset;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn border_color(mut self, c: Color) -> Self {
|
||||
self.border_color = c;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn width(mut self, w: u16) -> Self {
|
||||
self.width = w;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn height(mut self, h: u16) -> Self {
|
||||
self.height = h;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn render_centered(self, frame: &mut Frame, term: Rect) {
|
||||
let inner = ModalFrame::new(self.title)
|
||||
.width(self.width)
|
||||
.height(self.height)
|
||||
.border_color(self.border_color)
|
||||
.render_centered(frame, term);
|
||||
|
||||
let rows = Layout::vertical([Constraint::Length(1), Constraint::Min(1)]).split(inner);
|
||||
|
||||
// Input line
|
||||
frame.render_widget(
|
||||
Paragraph::new(Line::from(vec![
|
||||
Span::raw("> "),
|
||||
Span::styled(self.input, Style::new().fg(Color::Cyan)),
|
||||
Span::styled("█", Style::new().fg(Color::White)),
|
||||
])),
|
||||
rows[0],
|
||||
);
|
||||
|
||||
// Entries list
|
||||
let visible_height = rows[1].height as usize;
|
||||
let visible_entries = self
|
||||
.entries
|
||||
.iter()
|
||||
.skip(self.scroll_offset)
|
||||
.take(visible_height);
|
||||
|
||||
let lines: Vec<Line> = visible_entries
|
||||
.enumerate()
|
||||
.map(|(i, (name, is_dir))| {
|
||||
let abs_idx = i + self.scroll_offset;
|
||||
let is_selected = abs_idx == self.selected;
|
||||
let prefix = if is_selected { "> " } else { " " };
|
||||
let display = if *is_dir {
|
||||
format!("{prefix}{name}/")
|
||||
} else {
|
||||
format!("{prefix}{name}")
|
||||
};
|
||||
let color = if is_selected {
|
||||
Color::Yellow
|
||||
} else if *is_dir {
|
||||
Color::Blue
|
||||
} else {
|
||||
Color::White
|
||||
};
|
||||
Line::from(Span::styled(display, Style::new().fg(color)))
|
||||
})
|
||||
.collect();
|
||||
|
||||
frame.render_widget(Paragraph::new(lines), rows[1]);
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,6 @@
|
||||
mod confirm;
|
||||
mod file_browser;
|
||||
mod list_select;
|
||||
mod modal;
|
||||
mod scope;
|
||||
mod spectrum;
|
||||
@@ -6,6 +8,8 @@ mod text_input;
|
||||
mod vu_meter;
|
||||
|
||||
pub use confirm::ConfirmModal;
|
||||
pub use file_browser::FileBrowserModal;
|
||||
pub use list_select::ListSelect;
|
||||
pub use modal::ModalFrame;
|
||||
pub use scope::{Orientation, Scope};
|
||||
pub use spectrum::Spectrum;
|
||||
|
||||
103
crates/ratatui/src/list_select.rs
Normal file
103
crates/ratatui/src/list_select.rs
Normal file
@@ -0,0 +1,103 @@
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
use ratatui::text::{Line, Span};
|
||||
use ratatui::widgets::Paragraph;
|
||||
use ratatui::Frame;
|
||||
|
||||
pub struct ListSelect<'a> {
|
||||
items: &'a [String],
|
||||
selected: usize,
|
||||
cursor: usize,
|
||||
focused: bool,
|
||||
visible_count: usize,
|
||||
scroll_offset: usize,
|
||||
}
|
||||
|
||||
impl<'a> ListSelect<'a> {
|
||||
pub fn new(items: &'a [String], selected: usize, cursor: usize) -> Self {
|
||||
Self {
|
||||
items,
|
||||
selected,
|
||||
cursor,
|
||||
focused: false,
|
||||
visible_count: 5,
|
||||
scroll_offset: 0,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn focused(mut self, focused: bool) -> Self {
|
||||
self.focused = focused;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn scroll_offset(mut self, offset: usize) -> Self {
|
||||
self.scroll_offset = offset;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn visible_count(mut self, n: usize) -> Self {
|
||||
self.visible_count = n;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn height(&self) -> u16 {
|
||||
let item_lines = self.items.len().min(self.visible_count) as u16;
|
||||
if self.items.len() > self.visible_count {
|
||||
item_lines + 1
|
||||
} else {
|
||||
item_lines
|
||||
}
|
||||
}
|
||||
|
||||
pub fn render(self, frame: &mut Frame, area: Rect) {
|
||||
let cursor_style = Style::new().fg(Color::Yellow).add_modifier(Modifier::BOLD);
|
||||
let selected_style = Style::new().fg(Color::Cyan);
|
||||
let normal_style = Style::default();
|
||||
let indicator_style = Style::new().fg(Color::DarkGray);
|
||||
|
||||
let visible_end = (self.scroll_offset + self.visible_count).min(self.items.len());
|
||||
let has_above = self.scroll_offset > 0;
|
||||
let has_below = visible_end < self.items.len();
|
||||
|
||||
let mut lines: Vec<Line> = Vec::new();
|
||||
|
||||
for i in self.scroll_offset..visible_end {
|
||||
let name = &self.items[i];
|
||||
let is_cursor = self.focused && i == self.cursor;
|
||||
let is_selected = i == self.selected;
|
||||
|
||||
let style = if is_cursor {
|
||||
cursor_style
|
||||
} else if is_selected {
|
||||
selected_style
|
||||
} else {
|
||||
normal_style
|
||||
};
|
||||
|
||||
let prefix = if is_selected { "● " } else { " " };
|
||||
let mut spans = vec![
|
||||
Span::styled(prefix.to_string(), style),
|
||||
Span::styled(name.clone(), style),
|
||||
];
|
||||
|
||||
if has_above && i == self.scroll_offset {
|
||||
spans.push(Span::styled(" ▲", indicator_style));
|
||||
} else if has_below && i == visible_end - 1 {
|
||||
spans.push(Span::styled(" ▼", indicator_style));
|
||||
}
|
||||
|
||||
lines.push(Line::from(spans));
|
||||
}
|
||||
|
||||
if self.items.len() > self.visible_count {
|
||||
let position = self.cursor + 1;
|
||||
let total = self.items.len();
|
||||
lines.push(Line::from(Span::styled(
|
||||
format!(" ({position}/{total})"),
|
||||
indicator_style,
|
||||
)));
|
||||
}
|
||||
|
||||
frame.render_widget(Paragraph::new(lines), area);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user