Feat: entretien de la codebase
This commit is contained in:
145
crates/ratatui/src/category_list.rs
Normal file
145
crates/ratatui/src/category_list.rs
Normal file
@@ -0,0 +1,145 @@
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::{Color, Modifier, Style};
|
||||
use ratatui::widgets::{Block, Borders, List, ListItem};
|
||||
use ratatui::Frame;
|
||||
|
||||
use crate::theme;
|
||||
|
||||
pub struct CategoryItem<'a> {
|
||||
pub label: &'a str,
|
||||
pub is_section: bool,
|
||||
}
|
||||
|
||||
pub struct CategoryList<'a> {
|
||||
items: &'a [CategoryItem<'a>],
|
||||
selected: usize,
|
||||
focused: bool,
|
||||
title: &'a str,
|
||||
section_color: Color,
|
||||
focused_color: Color,
|
||||
selected_color: Color,
|
||||
normal_color: Color,
|
||||
dimmed_color: Option<Color>,
|
||||
}
|
||||
|
||||
impl<'a> CategoryList<'a> {
|
||||
pub fn new(items: &'a [CategoryItem<'a>], selected: usize) -> Self {
|
||||
let theme = theme::get();
|
||||
Self {
|
||||
items,
|
||||
selected,
|
||||
focused: false,
|
||||
title: "",
|
||||
section_color: theme.ui.text_dim,
|
||||
focused_color: theme.dict.category_focused,
|
||||
selected_color: theme.dict.category_selected,
|
||||
normal_color: theme.dict.category_normal,
|
||||
dimmed_color: None,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn focused(mut self, focused: bool) -> Self {
|
||||
self.focused = focused;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn title(mut self, title: &'a str) -> Self {
|
||||
self.title = title;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn selected_color(mut self, color: Color) -> Self {
|
||||
self.selected_color = color;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn normal_color(mut self, color: Color) -> Self {
|
||||
self.normal_color = color;
|
||||
self
|
||||
}
|
||||
|
||||
pub fn dimmed(mut self, color: Color) -> Self {
|
||||
self.dimmed_color = Some(color);
|
||||
self
|
||||
}
|
||||
|
||||
pub fn render(self, frame: &mut Frame, area: Rect) {
|
||||
let theme = theme::get();
|
||||
|
||||
let visible_height = area.height.saturating_sub(2) as usize;
|
||||
let total_items = self.items.len();
|
||||
|
||||
let selected_visual_idx = {
|
||||
let mut visual = 0;
|
||||
let mut selectable_count = 0;
|
||||
for item in self.items.iter() {
|
||||
if !item.is_section {
|
||||
if selectable_count == self.selected {
|
||||
break;
|
||||
}
|
||||
selectable_count += 1;
|
||||
}
|
||||
visual += 1;
|
||||
}
|
||||
visual
|
||||
};
|
||||
|
||||
let scroll = if selected_visual_idx < visible_height / 2 {
|
||||
0
|
||||
} else if selected_visual_idx > total_items.saturating_sub(visible_height / 2) {
|
||||
total_items.saturating_sub(visible_height)
|
||||
} else {
|
||||
selected_visual_idx.saturating_sub(visible_height / 2)
|
||||
};
|
||||
|
||||
let mut selectable_idx = self.items
|
||||
.iter()
|
||||
.take(scroll)
|
||||
.filter(|e| !e.is_section)
|
||||
.count();
|
||||
|
||||
let is_dimmed = self.dimmed_color.is_some();
|
||||
|
||||
let items: Vec<ListItem> = self.items
|
||||
.iter()
|
||||
.skip(scroll)
|
||||
.take(visible_height)
|
||||
.map(|item| {
|
||||
if item.is_section {
|
||||
let style = Style::new().fg(self.section_color);
|
||||
ListItem::new(format!("─ {} ─", item.label)).style(style)
|
||||
} else {
|
||||
let is_selected = selectable_idx == self.selected;
|
||||
let style = if let Some(dim_color) = self.dimmed_color {
|
||||
Style::new().fg(dim_color)
|
||||
} else if is_selected && self.focused {
|
||||
Style::new()
|
||||
.fg(self.focused_color)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
} else if is_selected {
|
||||
Style::new()
|
||||
.fg(self.selected_color)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::new().fg(self.normal_color)
|
||||
};
|
||||
let prefix = if is_selected && !is_dimmed { "> " } else { " " };
|
||||
selectable_idx += 1;
|
||||
ListItem::new(format!("{prefix}{}", item.label)).style(style)
|
||||
}
|
||||
})
|
||||
.collect();
|
||||
|
||||
let border_color = if self.focused {
|
||||
theme.dict.border_focused
|
||||
} else {
|
||||
theme.dict.border_normal
|
||||
};
|
||||
let block = Block::default()
|
||||
.borders(Borders::ALL)
|
||||
.border_style(Style::new().fg(border_color))
|
||||
.title(self.title);
|
||||
let list = List::new(items).block(block);
|
||||
frame.render_widget(list, area);
|
||||
}
|
||||
}
|
||||
27
crates/ratatui/src/hint_bar.rs
Normal file
27
crates/ratatui/src/hint_bar.rs
Normal file
@@ -0,0 +1,27 @@
|
||||
use ratatui::text::{Line, Span};
|
||||
use ratatui::style::Style;
|
||||
|
||||
use crate::theme;
|
||||
|
||||
pub fn hint_line(pairs: &[(&str, &str)]) -> Line<'static> {
|
||||
let theme = theme::get();
|
||||
let key_style = Style::default().fg(theme.hint.key);
|
||||
let text_style = Style::default().fg(theme.hint.text);
|
||||
|
||||
let spans: Vec<Span> = pairs
|
||||
.iter()
|
||||
.enumerate()
|
||||
.flat_map(|(i, (key, action))| {
|
||||
let mut s = vec![
|
||||
Span::styled(key.to_string(), key_style),
|
||||
Span::styled(format!(" {action}"), text_style),
|
||||
];
|
||||
if i + 1 < pairs.len() {
|
||||
s.push(Span::styled(" ", text_style));
|
||||
}
|
||||
s
|
||||
})
|
||||
.collect();
|
||||
|
||||
Line::from(spans)
|
||||
}
|
||||
@@ -1,12 +1,18 @@
|
||||
mod active_patterns;
|
||||
mod category_list;
|
||||
mod confirm;
|
||||
mod editor;
|
||||
mod file_browser;
|
||||
mod hint_bar;
|
||||
mod list_select;
|
||||
mod modal;
|
||||
mod nav_minimap;
|
||||
mod props_form;
|
||||
mod sample_browser;
|
||||
mod scope;
|
||||
mod scroll_indicators;
|
||||
mod search_bar;
|
||||
mod section_header;
|
||||
mod sparkles;
|
||||
mod spectrum;
|
||||
mod text_input;
|
||||
@@ -15,14 +21,20 @@ mod vu_meter;
|
||||
mod waveform;
|
||||
|
||||
pub use active_patterns::{ActivePatterns, MuteStatus};
|
||||
pub use category_list::{CategoryItem, CategoryList};
|
||||
pub use confirm::ConfirmModal;
|
||||
pub use editor::{fuzzy_match, CompletionCandidate, Editor};
|
||||
pub use file_browser::FileBrowserModal;
|
||||
pub use hint_bar::hint_line;
|
||||
pub use list_select::ListSelect;
|
||||
pub use modal::ModalFrame;
|
||||
pub use nav_minimap::{NavMinimap, NavTile};
|
||||
pub use props_form::render_props_form;
|
||||
pub use sample_browser::{SampleBrowser, TreeLine, TreeLineKind};
|
||||
pub use scope::{Orientation, Scope};
|
||||
pub use scroll_indicators::{render_scroll_indicators, IndicatorAlign};
|
||||
pub use search_bar::render_search_bar;
|
||||
pub use section_header::render_section_header;
|
||||
pub use sparkles::Sparkles;
|
||||
pub use spectrum::Spectrum;
|
||||
pub use text_input::TextInputModal;
|
||||
|
||||
42
crates/ratatui/src/props_form.rs
Normal file
42
crates/ratatui/src/props_form.rs
Normal file
@@ -0,0 +1,42 @@
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::{Modifier, Style};
|
||||
use ratatui::widgets::Paragraph;
|
||||
use ratatui::Frame;
|
||||
|
||||
use crate::theme;
|
||||
|
||||
pub fn render_props_form(frame: &mut Frame, area: Rect, fields: &[(&str, &str, bool)]) {
|
||||
let theme = theme::get();
|
||||
|
||||
for (i, (label, value, selected)) in fields.iter().enumerate() {
|
||||
let y = area.y + i as u16;
|
||||
if y >= area.y + area.height {
|
||||
break;
|
||||
}
|
||||
|
||||
let (label_style, value_style) = if *selected {
|
||||
(
|
||||
Style::default()
|
||||
.fg(theme.hint.key)
|
||||
.add_modifier(Modifier::BOLD),
|
||||
Style::default()
|
||||
.fg(theme.ui.text_primary)
|
||||
.bg(theme.ui.surface),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
Style::default().fg(theme.ui.text_muted),
|
||||
Style::default().fg(theme.ui.text_primary),
|
||||
)
|
||||
};
|
||||
|
||||
let label_area = Rect::new(area.x + 1, y, 14, 1);
|
||||
let value_area = Rect::new(area.x + 16, y, area.width.saturating_sub(18), 1);
|
||||
|
||||
frame.render_widget(
|
||||
Paragraph::new(format!("{label}:")).style(label_style),
|
||||
label_area,
|
||||
);
|
||||
frame.render_widget(Paragraph::new(*value).style(value_style), value_area);
|
||||
}
|
||||
}
|
||||
53
crates/ratatui/src/scroll_indicators.rs
Normal file
53
crates/ratatui/src/scroll_indicators.rs
Normal file
@@ -0,0 +1,53 @@
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::{Color, Style};
|
||||
use ratatui::widgets::Paragraph;
|
||||
use ratatui::Frame;
|
||||
|
||||
pub enum IndicatorAlign {
|
||||
Center,
|
||||
Right,
|
||||
}
|
||||
|
||||
pub fn render_scroll_indicators(
|
||||
frame: &mut Frame,
|
||||
area: Rect,
|
||||
offset: usize,
|
||||
visible: usize,
|
||||
total: usize,
|
||||
color: Color,
|
||||
align: IndicatorAlign,
|
||||
) {
|
||||
let style = Style::new().fg(color);
|
||||
|
||||
match align {
|
||||
IndicatorAlign::Center => {
|
||||
if offset > 0 {
|
||||
let indicator = Paragraph::new("▲")
|
||||
.style(style)
|
||||
.alignment(ratatui::layout::Alignment::Center);
|
||||
frame.render_widget(indicator, Rect { height: 1, ..area });
|
||||
}
|
||||
if offset + visible < total {
|
||||
let y = area.y + area.height.saturating_sub(1);
|
||||
let indicator = Paragraph::new("▼")
|
||||
.style(style)
|
||||
.alignment(ratatui::layout::Alignment::Center);
|
||||
frame.render_widget(indicator, Rect { y, height: 1, ..area });
|
||||
}
|
||||
}
|
||||
IndicatorAlign::Right => {
|
||||
let x = area.x + area.width.saturating_sub(1);
|
||||
if offset > 0 {
|
||||
let indicator = Paragraph::new("▲").style(style);
|
||||
frame.render_widget(indicator, Rect::new(x, area.y, 1, 1));
|
||||
}
|
||||
if offset + visible < total {
|
||||
let indicator = Paragraph::new("▼").style(style);
|
||||
frame.render_widget(
|
||||
indicator,
|
||||
Rect::new(x, area.y + area.height.saturating_sub(1), 1, 1),
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
20
crates/ratatui/src/search_bar.rs
Normal file
20
crates/ratatui/src/search_bar.rs
Normal file
@@ -0,0 +1,20 @@
|
||||
use ratatui::layout::Rect;
|
||||
use ratatui::style::Style;
|
||||
use ratatui::text::{Line, Span};
|
||||
use ratatui::widgets::Paragraph;
|
||||
use ratatui::Frame;
|
||||
|
||||
use crate::theme;
|
||||
|
||||
pub fn render_search_bar(frame: &mut Frame, area: Rect, query: &str, active: bool) {
|
||||
let theme = theme::get();
|
||||
let style = if active {
|
||||
Style::new().fg(theme.search.active)
|
||||
} else {
|
||||
Style::new().fg(theme.search.inactive)
|
||||
};
|
||||
let cursor = if active { "_" } else { "" };
|
||||
let text = format!(" /{query}{cursor}");
|
||||
let line = Line::from(Span::styled(text, style));
|
||||
frame.render_widget(Paragraph::new(vec![line]), area);
|
||||
}
|
||||
30
crates/ratatui/src/section_header.rs
Normal file
30
crates/ratatui/src/section_header.rs
Normal file
@@ -0,0 +1,30 @@
|
||||
use ratatui::layout::{Constraint, Layout, Rect};
|
||||
use ratatui::style::{Modifier, Style};
|
||||
use ratatui::widgets::Paragraph;
|
||||
use ratatui::Frame;
|
||||
|
||||
use crate::theme;
|
||||
|
||||
pub fn render_section_header(frame: &mut Frame, title: &str, focused: bool, area: Rect) {
|
||||
let theme = theme::get();
|
||||
let [header_area, divider_area] =
|
||||
Layout::vertical([Constraint::Length(1), Constraint::Length(1)]).areas(area);
|
||||
|
||||
let header_style = if focused {
|
||||
Style::new()
|
||||
.fg(theme.engine.header_focused)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
} else {
|
||||
Style::new()
|
||||
.fg(theme.engine.header)
|
||||
.add_modifier(Modifier::BOLD)
|
||||
};
|
||||
|
||||
frame.render_widget(Paragraph::new(title).style(header_style), header_area);
|
||||
|
||||
let divider = "─".repeat(area.width as usize);
|
||||
frame.render_widget(
|
||||
Paragraph::new(divider).style(Style::new().fg(theme.engine.divider)),
|
||||
divider_area,
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user