use std::ops::Range;
use rich::cells::{cell_len, split_graphemes};
use rich::{Segment, Style};
pub use crate::components::Theme;
use crate::event::{Event, Key, KeyCode, Mouse, MouseKind};
use crate::fuzzy::rank;
use crate::item::Action;
pub fn text(text: impl Into<String>, style: &Style) -> Segment {
Segment::new(text, Some(style.clone()))
}
pub fn icon(text: &rich::Text) -> Vec<Segment> {
let console = rich::Console::builder().width(4096).build();
let options = console.options().update_width(4096);
let mut text = text.clone();
text.set_no_wrap(Some(true));
console
.render_lines(&text, &options, false)
.into_iter()
.next()
.unwrap_or_default()
.into_iter()
.filter(|segment| !segment.text.is_empty())
.collect()
}
pub fn plain(text: impl Into<String>) -> Segment {
Segment::new(text, None)
}
pub fn width(line: &[Segment]) -> usize {
line.iter().map(Segment::cell_length).sum()
}
pub fn fit(line: Vec<Segment>, columns: usize) -> Vec<Segment> {
if width(&line) > columns {
Segment::adjust_line_length(&line, columns, None)
} else {
line
}
}
pub fn pad(line: Vec<Segment>, columns: usize) -> Vec<Segment> {
Segment::adjust_line_length(&line, columns, None)
}
pub fn highlight(
label: &str,
positions: &[usize],
base: Option<&Style>,
matched: &Style,
) -> Vec<Segment> {
let hit = match base {
Some(base) => base.combine(matched),
None => matched.clone(),
};
let mut segments: Vec<Segment> = Vec::new();
let mut run = String::new();
let mut run_hit = false;
let mut next = positions.iter().peekable();
for (index, c) in label.chars().enumerate() {
let is_hit = next.peek() == Some(&&index);
if is_hit {
next.next();
}
if is_hit != run_hit && !run.is_empty() {
let style = if run_hit {
Some(hit.clone())
} else {
base.cloned()
};
segments.push(Segment::new(std::mem::take(&mut run), style));
}
run_hit = is_hit;
run.push(c);
}
if !run.is_empty() {
let style = if run_hit { Some(hit) } else { base.cloned() };
segments.push(Segment::new(run, style));
}
segments
}
pub fn slice(line: &[Segment], from: usize, to: usize) -> Vec<Segment> {
let mut out: Vec<Segment> = Vec::new();
let mut at = 0;
for segment in line.iter().filter(|segment| !segment.control) {
if at >= to {
break;
}
let length = segment.cell_length();
if at >= from && at + length <= to {
out.push(segment.clone());
at += length;
continue;
}
let (spans, _) = split_graphemes(&segment.text);
let mut piece = String::new();
for (start, end, cells) in spans {
let (left, right) = (at, at + cells);
if left >= from && right <= to {
piece.push_str(&segment.text[start..end]);
} else if right > from && left < to {
let covered = right.min(to) - left.max(from);
piece.push_str(&" ".repeat(covered));
}
at = right;
}
if !piece.is_empty() {
out.push(Segment::new(piece, segment.style.clone()));
}
}
out
}
pub fn overlay(base: &[Segment], x: usize, top: &[Segment]) -> Vec<Segment> {
let end = x + width(top);
let mut line = pad(slice(base, 0, x), x);
line.extend(top.iter().cloned());
let base_width = width(base);
if base_width > end {
line.extend(slice(base, end, base_width));
}
line
}
pub fn restyle(line: &[Segment], style: &Style) -> Vec<Segment> {
line.iter()
.map(|segment| {
if segment.control {
return segment.clone();
}
let styled = match &segment.style {
Some(own) => own.combine(style),
None => style.clone(),
};
Segment::new(segment.text.clone(), Some(styled))
})
.collect()
}
pub fn frame(
lines: Vec<Vec<Segment>>,
inner: usize,
title: Option<&str>,
style: &Style,
) -> Vec<Vec<Segment>> {
let mut top = vec![text("â•", style)];
match title {
Some(title) if inner >= 4 => {
let title = fit(vec![text(format!(" {title} "), style)], inner - 1);
let used = width(&title) + 1;
top.push(text("─", style));
top.extend(title);
top.push(text("─".repeat(inner.saturating_sub(used)), style));
}
_ => top.push(text("─".repeat(inner), style)),
}
top.push(text("â•®", style));
let mut boxed = vec![top];
for line in lines {
let mut row = vec![text("│", style)];
row.extend(pad(line, inner));
row.push(text("│", style));
boxed.push(row);
}
boxed.push(vec![
text("â•°", style),
text("─".repeat(inner), style),
text("╯", style),
]);
boxed
}
pub fn place(
base: &mut Vec<Vec<Segment>>,
x: usize,
y: usize,
top: &[Vec<Segment>],
backdrop: Option<&Style>,
) {
if let Some(style) = backdrop {
for line in base.iter_mut() {
*line = restyle(line, style);
}
}
if base.len() < y + top.len() {
base.resize_with(y + top.len(), Vec::new);
}
for (row, line) in top.iter().enumerate() {
let under = &mut base[y + row];
*under = overlay(under, x, line);
}
}
pub fn pasted(text: &str, newline: &str) -> String {
let mut out = String::with_capacity(text.len());
for c in text.chars() {
match c {
'\n' | '\r' => out.push_str(newline),
'\t' => out.push(' '),
'\0'..='\u{1f}' | '\u{7f}'..='\u{9f}' => {}
_ => out.push(c),
}
}
out
}
pub fn shown(text: &str) -> String {
text.chars()
.map(|c| crate::paint::visible(c).unwrap_or(c))
.collect()
}
pub fn question(theme: &Theme, prompt: &str) -> Vec<Segment> {
vec![
text(theme.question.clone(), &theme.question_style),
plain(" "),
text(prompt, &theme.prompt),
plain(" "),
text("›", &theme.hint),
plain(" "),
]
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct ListState {
len: usize,
cursor: usize,
offset: usize,
selected: Vec<bool>,
}
impl ListState {
pub fn new() -> ListState {
ListState::default()
}
pub fn len(&self) -> usize {
self.len
}
pub fn is_empty(&self) -> bool {
self.len == 0
}
pub fn set_len(&mut self, len: usize) {
self.len = len;
}
pub fn cursor(&self) -> usize {
self.cursor
}
pub fn set_cursor(&mut self, row: usize) {
self.cursor = row;
}
pub fn offset(&self) -> usize {
self.offset
}
pub fn set_offset(&mut self, offset: usize) {
self.offset = offset;
}
pub fn reset(&mut self, row: usize, page: usize) {
self.cursor = row;
self.offset = 0;
self.follow(page);
}
pub fn follow(&mut self, page: usize) {
if self.cursor < self.offset {
self.offset = self.cursor;
} else if self.cursor >= self.offset + page {
self.offset = self.cursor + 1 - page;
}
}
pub fn step(&mut self, delta: isize, page: usize) {
if self.len == 0 {
return;
}
let last = self.len - 1;
self.cursor = self.cursor.saturating_add_signed(delta).min(last);
self.follow(page);
}
pub fn select_row(&mut self, row: usize, page: usize) {
if self.len == 0 {
return;
}
self.cursor = row.min(self.len - 1);
self.follow(page);
}
pub fn visible(&self, page: usize) -> Range<usize> {
let end = (self.offset + page).min(self.len);
self.offset.min(end)..end
}
pub fn clear_selection(&mut self, count: usize) {
self.selected = vec![false; count];
}
pub fn is_selected(&self, index: usize) -> bool {
self.selected.get(index).copied().unwrap_or(false)
}
pub fn set_selected(&mut self, index: usize, on: bool) {
if index >= self.selected.len() {
self.selected.resize(index + 1, false);
}
self.selected[index] = on;
}
pub fn toggle(&mut self, index: usize) {
let on = !self.is_selected(index);
self.set_selected(index, on);
}
pub fn selected(&self) -> Vec<usize> {
(0..self.selected.len())
.filter(|&index| self.selected[index])
.collect()
}
pub fn selected_count(&self) -> usize {
self.selected.iter().filter(|on| **on).count()
}
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ScrollState {
len: usize,
offset: usize,
}
impl ScrollState {
pub fn new(len: usize) -> ScrollState {
ScrollState { len, offset: 0 }
}
pub fn len(&self) -> usize {
self.len
}
pub fn is_empty(&self) -> bool {
self.len == 0
}
pub fn set_len(&mut self, len: usize) {
self.len = len;
self.offset = self.offset.min(len.saturating_sub(1));
}
pub fn offset(&self) -> usize {
self.offset
}
fn last_offset(&self, page: usize) -> usize {
self.len.saturating_sub(page)
}
pub fn scroll(&mut self, delta: isize, page: usize) -> bool {
let target = self
.offset
.saturating_add_signed(delta)
.min(self.last_offset(page));
let changed = target != self.offset;
self.offset = target;
changed
}
pub fn scroll_to(&mut self, line: usize, page: usize) -> bool {
let target = line.min(self.last_offset(page));
let changed = target != self.offset;
self.offset = target;
changed
}
pub fn show(&mut self, line: usize, page: usize) -> bool {
if line < self.offset {
self.scroll_to(line, page)
} else if line >= self.offset + page {
self.scroll_to(line + 1 - page, page)
} else {
false
}
}
pub fn visible(&self, page: usize) -> Range<usize> {
let end = (self.offset + page).min(self.len);
self.offset.min(end)..end
}
pub fn keymap(context: &str) -> crate::keymap::Keymap {
use crate::keymap::keys;
crate::keymap::Keymap::new(context)
.bind("scroll-up", keys("up k"), "scroll up")
.bind("scroll-down", keys("down j"), "scroll down")
.bind("page-up", keys("pageup b"), "page up")
.bind("page-down", keys("pagedown space"), "page down")
.bind("top", keys("home g"), "to the top")
.bind("bottom", keys("end G"), "to the bottom")
}
pub fn act(&mut self, action: &str, page: usize) -> bool {
let page_step = page.max(1) as isize;
match action {
"scroll-up" => self.scroll(-1, page),
"scroll-down" => self.scroll(1, page),
"page-up" => self.scroll(-page_step, page),
"page-down" => self.scroll(page_step, page),
"top" => self.scroll_to(0, page),
"bottom" => self.scroll_to(usize::MAX, page),
_ => return false,
};
true
}
pub fn handle(&mut self, event: &Event, page: usize) -> bool {
let page_step = page.max(1) as isize;
match event {
Event::Key(key) if !key.modifiers.ctrl && !key.modifiers.alt => {
match key.code {
KeyCode::Up | KeyCode::Char('k') => self.scroll(-1, page),
KeyCode::Down | KeyCode::Char('j') => self.scroll(1, page),
KeyCode::PageUp | KeyCode::Char('b') => self.scroll(-page_step, page),
KeyCode::PageDown | KeyCode::Char(' ') => self.scroll(page_step, page),
KeyCode::Home | KeyCode::Char('g') => self.scroll_to(0, page),
KeyCode::End | KeyCode::Char('G') => self.scroll_to(usize::MAX, page),
_ => return false,
};
true
}
Event::Mouse(mouse) => match mouse.kind {
MouseKind::ScrollUp => {
self.scroll(-3, page);
true
}
MouseKind::ScrollDown => {
self.scroll(3, page);
true
}
_ => false,
},
_ => false,
}
}
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct FilterState {
query: String,
candidates: Vec<String>,
hidden: Vec<bool>,
limits: Vec<usize>,
matches: Vec<(usize, Vec<usize>)>,
parents: Option<Vec<Option<usize>>>,
context: Vec<bool>,
best: Option<usize>,
}
impl FilterState {
pub fn new<I, S>(candidates: I) -> FilterState
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
let mut filter = FilterState::default();
filter.set_candidates(candidates);
filter
}
pub fn set_candidates<I, S>(&mut self, candidates: I)
where
I: IntoIterator<Item = S>,
S: Into<String>,
{
self.candidates = candidates.into_iter().map(Into::into).collect();
self.refilter();
}
pub fn candidates(&self) -> &[String] {
&self.candidates
}
pub fn set_hidden(&mut self, hidden: Vec<bool>) {
self.hidden = hidden;
}
pub fn set_tree(&mut self, parents: Option<Vec<Option<usize>>>) {
self.parents = parents;
}
pub fn set_highlight_limits(&mut self, limits: Vec<usize>) {
self.limits = limits;
}
pub fn query(&self) -> &str {
&self.query
}
pub fn query_mut(&mut self) -> &mut String {
&mut self.query
}
pub fn is_filtering(&self) -> bool {
!self.query.is_empty()
}
pub fn set_query(&mut self, query: impl Into<String>) {
self.query = query.into();
self.refilter();
}
pub fn push(&mut self, c: char) {
self.query.push(c);
self.refilter();
}
pub fn push_str(&mut self, text: &str) {
self.query.push_str(text);
self.refilter();
}
pub fn pop(&mut self) -> bool {
let popped = self.query.pop().is_some();
if popped {
self.refilter();
}
popped
}
pub fn clear(&mut self) {
self.query.clear();
self.refilter();
}
pub fn refilter(&mut self) {
let filtering = !self.query.is_empty();
let hidden = &self.hidden;
let limits = &self.limits;
let candidates = &self.candidates;
let ranked: Vec<(usize, Vec<usize>)> =
rank(&self.query, candidates.iter().map(String::as_str))
.into_iter()
.filter(|(index, _)| filtering || !hidden.get(*index).copied().unwrap_or(false))
.map(|(index, found)| {
let mut positions = found.positions;
if let Some(limit) = limits.get(index) {
positions.retain(|position| position < limit);
}
(index, positions)
})
.collect();
match &self.parents {
Some(parents) if filtering => {
let best = ranked.first().map(|(index, _)| *index);
let (matches, context) = keep_ancestors(ranked, parents);
self.best = best.and_then(|best| matches.iter().position(|(i, _)| *i == best));
self.matches = matches;
self.context = context;
}
_ => {
self.best = (!ranked.is_empty()).then_some(0);
self.context = vec![false; ranked.len()];
self.matches = ranked;
}
}
}
pub fn is_context(&self, position: usize) -> bool {
self.context.get(position).copied().unwrap_or(false)
}
pub fn best(&self) -> Option<usize> {
self.best
}
pub fn matches(&self) -> &[(usize, Vec<usize>)] {
&self.matches
}
pub fn len(&self) -> usize {
self.matches.len()
}
pub fn is_empty(&self) -> bool {
self.matches.is_empty()
}
pub fn index(&self, position: usize) -> Option<usize> {
self.matches.get(position).map(|(index, _)| *index)
}
pub fn position_of(&self, index: usize) -> Option<usize> {
self.matches.iter().position(|(i, _)| *i == index)
}
}
pub fn ancestors(parents: &[Option<usize>], index: usize) -> impl Iterator<Item = usize> + '_ {
let mut at = parents.get(index).copied().flatten();
let mut left = parents.len();
std::iter::from_fn(move || {
let current = at.filter(|_| left > 0)?;
left -= 1;
at = parents.get(current).copied().flatten();
Some(current)
})
}
pub fn keep_ancestors(
matches: Vec<(usize, Vec<usize>)>,
parents: &[Option<usize>],
) -> (Vec<(usize, Vec<usize>)>, Vec<bool>) {
let count = parents
.len()
.max(matches.iter().map(|(i, _)| i + 1).max().unwrap_or(0));
let mut found: Vec<Option<Vec<usize>>> = vec![None; count];
let mut kept = vec![false; count];
for (index, positions) in matches {
for ancestor in ancestors(parents, index) {
if kept.get(ancestor).is_none_or(|&kept| kept) {
break;
}
kept[ancestor] = true;
}
kept[index] = true;
found[index] = Some(positions);
}
let mut list = Vec::new();
let mut context = Vec::new();
for (index, positions) in found.into_iter().enumerate() {
if !kept[index] {
continue;
}
context.push(positions.is_none());
list.push((index, positions.unwrap_or_default()));
}
(list, context)
}
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub struct TextBuffer {
value: String,
caret: usize,
undo: Vec<(String, usize)>,
}
impl TextBuffer {
pub fn new() -> TextBuffer {
TextBuffer::default()
}
pub fn with_text(text: impl Into<String>) -> TextBuffer {
let mut buffer = TextBuffer::new();
buffer.set_text(text);
buffer
}
pub fn text(&self) -> &str {
&self.value
}
pub fn is_empty(&self) -> bool {
self.value.is_empty()
}
pub fn len(&self) -> usize {
self.value.chars().count()
}
pub fn set_text(&mut self, text: impl Into<String>) {
self.value = text.into();
self.caret = self.len();
}
pub fn caret(&self) -> usize {
self.caret
}
pub fn set_caret(&mut self, caret: usize) {
self.caret = caret.min(self.len());
self.snap(false);
}
fn snap(&mut self, forward: bool) {
let stops = self.stops();
self.caret = if forward {
stops.iter().copied().find(|&stop| stop >= self.caret)
} else {
stops.iter().rev().copied().find(|&stop| stop <= self.caret)
}
.unwrap_or(self.caret);
}
pub fn byte(&self, caret: usize) -> usize {
self.value
.char_indices()
.nth(caret)
.map_or(self.value.len(), |(index, _)| index)
}
pub fn before_caret(&self) -> &str {
&self.value[..self.byte(self.caret)]
}
pub fn stops(&self) -> Vec<usize> {
let (spans, _) = split_graphemes(&self.value);
let mut stops = Vec::with_capacity(spans.len() + 1);
let (mut chars, mut byte) = (0, 0);
for (start, _, _) in spans {
chars += self.value[byte..start].chars().count();
byte = start;
stops.push(chars);
}
stops.push(self.value.chars().count());
stops
}
pub fn previous_stop(&self) -> usize {
self.stops()
.iter()
.rev()
.copied()
.find(|&stop| stop < self.caret)
.unwrap_or(0)
}
pub fn next_stop(&self) -> usize {
self.stops()
.iter()
.copied()
.find(|&stop| stop > self.caret)
.unwrap_or(self.caret)
}
pub fn insert(&mut self, text: &str) {
let at = self.byte(self.caret);
self.value.insert_str(at, text);
self.caret += text.chars().count();
self.snap(true);
}
pub fn left(&mut self) {
self.caret = self.previous_stop();
}
pub fn right(&mut self) {
self.caret = self.next_stop().min(self.len());
}
pub fn home(&mut self) {
self.caret = 0;
}
pub fn end(&mut self) {
self.caret = self.len();
}
pub fn backspace(&mut self) -> bool {
if self.caret == 0 {
return false;
}
let previous = self.previous_stop();
let (from, to) = (self.byte(previous), self.byte(self.caret));
self.value.replace_range(from..to, "");
self.caret = previous;
self.snap(false);
true
}
pub fn delete(&mut self) -> bool {
if self.caret >= self.len() {
return false;
}
let (from, to) = (self.byte(self.caret), self.byte(self.next_stop()));
self.value.replace_range(from..to, "");
self.snap(false);
true
}
pub fn delete_to_start(&mut self) {
let at = self.byte(self.caret);
self.value.replace_range(..at, "");
self.caret = 0;
}
pub fn delete_word(&mut self) {
let chars: Vec<char> = self.value.chars().collect();
let mut start = self.caret;
while start > 0 && chars[start - 1] == ' ' {
start -= 1;
}
while start > 0 && chars[start - 1] != ' ' {
start -= 1;
}
let (from, to) = (self.byte(start), self.byte(self.caret));
self.value.replace_range(from..to, "");
self.caret = start;
self.snap(false);
}
pub fn checkpoint(&mut self) {
let point = (self.value.clone(), self.caret);
if self.undo.last() != Some(&point) {
self.undo.push(point);
}
}
pub fn undo(&mut self) -> bool {
match self.undo.pop() {
Some((value, caret)) => {
self.value = value;
self.caret = caret;
true
}
None => false,
}
}
pub fn masked(text: &str, mask: Option<char>) -> String {
match mask {
Some(mask) => std::iter::repeat_n(mask, text.chars().count()).collect(),
None => text.to_string(),
}
}
pub fn window(&self, available: usize, mask: Option<char>) -> (String, usize) {
let shown = Self::masked(&self.value, mask);
let caret = shown
.char_indices()
.nth(self.caret)
.map_or(shown.len(), |(index, _)| index);
let (spans, _) = split_graphemes(&shown);
let before: usize = spans
.iter()
.filter(|(start, _, _)| *start < caret)
.map(|(_, _, cells)| cells)
.sum();
let skip = (before + 1).saturating_sub(available.max(1));
if skip == 0 {
return (shown, before);
}
let mut dropped = 0;
let mut from = shown.len();
for (start, _, cells) in spans {
if dropped >= skip || start >= caret {
from = start;
break;
}
dropped += cells;
}
(shown[from..].to_string(), before.saturating_sub(dropped))
}
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct Divider {
position: Option<usize>,
dragging: bool,
pub min_before: usize,
pub min_after: usize,
pub thickness: usize,
}
impl Default for Divider {
fn default() -> Self {
Divider::new(1)
}
}
impl Divider {
pub fn new(thickness: usize) -> Divider {
Divider {
position: None,
dragging: false,
min_before: 0,
min_after: 0,
thickness,
}
}
pub fn min(mut self, cells: usize) -> Divider {
self.min_before = cells;
self.min_after = cells;
self
}
pub fn at(mut self, position: usize) -> Divider {
self.position = Some(position);
self
}
pub fn position(&self) -> Option<usize> {
self.position
}
pub fn set_position(&mut self, position: Option<usize>) {
self.position = position;
}
pub fn dragging(&self) -> bool {
self.dragging
}
pub fn clamp(&self, position: usize, total: usize) -> usize {
let most = total
.saturating_sub(self.min_after + self.thickness)
.max(self.min_before.min(total));
position.clamp(self.min_before.min(most), most)
}
pub fn resolve(&self, total: usize, default: usize) -> usize {
match self.position {
Some(position) => self.clamp(position, total),
None => default,
}
}
pub fn hit(&self, at: usize, first: usize) -> bool {
(first..first + self.thickness.max(1)).contains(&at)
}
pub fn press(&mut self, at: usize, first: usize) -> bool {
self.dragging = self.hit(at, first);
self.dragging
}
pub fn drag(&mut self, at: usize, total: usize) -> bool {
if !self.dragging {
return false;
}
self.position = Some(self.clamp(at, total));
true
}
pub fn release(&mut self) {
self.dragging = false;
}
pub fn handle_mouse(&mut self, mouse: &Mouse, at: usize, first: usize, total: usize) -> bool {
match mouse.kind {
MouseKind::Down(crate::event::Button::Left) => self.press(at, first),
MouseKind::Drag(crate::event::Button::Left) => self.drag(at, total),
MouseKind::Up(_) if self.dragging => {
self.release();
true
}
_ => false,
}
}
pub fn nudge(&mut self, delta: isize, first: usize, total: usize) {
self.position = Some(self.clamp(first.saturating_add_signed(delta), total));
}
}
const MENU_HINTS: &str = " ↑↓ move · enter run · esc close";
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum MenuReply {
Stay,
Close,
Run(Action),
}
#[derive(Clone, Debug)]
pub struct ActionMenu {
pub actions: Vec<Action>,
pub focus: usize,
pub key: Key,
}
impl ActionMenu {
pub fn new(actions: Vec<Action>, key: Key) -> ActionMenu {
ActionMenu {
actions,
focus: 0,
key,
}
}
pub fn handle(&mut self, event: &Event, first: usize) -> MenuReply {
let count = self.actions.len();
if let Some(mouse) = event.mouse() {
if mouse.is_click() {
let row = mouse.row as usize;
if row >= first && row < first + count {
return MenuReply::Run(self.actions[row - first].clone());
}
return MenuReply::Close;
}
return MenuReply::Stay;
}
let Some(key) = event.key() else {
return MenuReply::Stay;
};
match key.code {
KeyCode::Escape => return MenuReply::Close,
_ if key == self.key => return MenuReply::Close,
KeyCode::Up => self.focus = (self.focus + count - 1) % count.max(1),
KeyCode::Down | KeyCode::Tab => self.focus = (self.focus + 1) % count.max(1),
KeyCode::Enter => return MenuReply::Run(self.actions[self.focus].clone()),
_ => {
if let Some(action) = self.actions.iter().find(|a| a.key == Some(key)) {
return MenuReply::Run(action.clone());
}
}
}
MenuReply::Stay
}
pub fn size(&self) -> (usize, usize) {
let label_width = self
.actions
.iter()
.map(|action| cell_len(&action.label))
.max()
.unwrap_or(0);
let key_width = self
.actions
.iter()
.filter_map(|action| action.key.map(|key| cell_len(&key.to_string()) + 2))
.max()
.unwrap_or(0);
let hints = cell_len(MENU_HINTS);
(
(4 + label_width + key_width).max(hints) + 2,
self.actions.len() + 1,
)
}
pub fn render(&self, theme: &Theme, width: usize) -> Vec<Vec<Segment>> {
let label_width = self
.actions
.iter()
.map(|action| cell_len(&action.label))
.max()
.unwrap_or(0);
let mut lines = Vec::new();
for (index, action) in self.actions.iter().enumerate() {
let focused = index == self.focus;
let mut line = if focused {
vec![text(format!(" {} ", theme.pointer), &theme.pointer_style)]
} else {
vec![plain(" ")]
};
let padding = label_width - cell_len(&action.label);
line.push(if focused {
text(action.label.clone(), &theme.focused)
} else {
plain(action.label.clone())
});
if let Some(key) = action.key {
line.push(text(format!("{} {key}", " ".repeat(padding)), &theme.hint));
}
lines.push(fit(line, width));
}
lines.push(fit(vec![text(MENU_HINTS.to_string(), &theme.hint)], width));
lines
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn highlights_matched_characters() {
let matched = Style::parse("bold").unwrap();
let segments = highlight("main.rs", &[0, 1, 5], None, &matched);
let texts: Vec<(&str, bool)> = segments
.iter()
.map(|s| (s.text.as_str(), s.style.is_some()))
.collect();
assert_eq!(
texts,
[("ma", true), ("in.", false), ("r", true), ("s", false)]
);
}
#[test]
fn pastes_drop_terminal_controls() {
assert_eq!(
pasted("X\x1bcY\u{9b}2JZ\x07\x08W\tV\r\nU", " "),
"XcY2JZW V U"
);
assert_eq!(pasted("a\nb", ""), "ab");
}
#[test]
fn fits_and_pads_lines() {
let line = vec![plain("hello "), plain("world")];
assert_eq!(width(&fit(line.clone(), 7)), 7);
assert_eq!(width(&fit(line.clone(), 20)), 11);
assert_eq!(width(&pad(line, 20)), 20);
}
#[test]
fn slices_and_overlays_by_cell() {
let base = vec![plain("ab"), plain("日本"), plain("cd")];
let cut: String = slice(&base, 3, 7).iter().map(|s| s.text.as_str()).collect();
assert_eq!(cut, " 本c", "half a wide character is a space");
let over = overlay(&base, 1, &[plain("XY")]);
let text: String = over.iter().map(|s| s.text.as_str()).collect();
assert_eq!(text, "aXY 本cd");
let past = overlay(&[plain("ab")], 4, &[plain("Z")]);
assert_eq!(width(&past), 5);
}
#[test]
fn a_list_follows_its_cursor() {
let mut list = ListState::new();
list.set_len(20);
list.step(7, 5);
assert_eq!((list.cursor(), list.offset()), (7, 3));
assert_eq!(list.visible(5), 3..8);
list.step(-100, 5);
assert_eq!((list.cursor(), list.offset()), (0, 0));
list.step(isize::MAX / 2, 5);
assert_eq!(list.cursor(), 19);
list.toggle(4);
list.toggle(2);
assert_eq!(list.selected(), [2, 4]);
list.toggle(4);
assert_eq!(list.selected_count(), 1);
}
#[test]
fn a_filter_ranks_and_hides() {
let mut filter = FilterState::new(["alpha", "beta", "gamma"]);
filter.set_hidden(vec![false, true, false]);
filter.refilter();
assert_eq!(filter.len(), 2, "hidden while nothing is typed");
filter.set_query("ta");
assert_eq!(filter.index(0), Some(1), "hidden ones still match a query");
assert!(filter.pop());
filter.clear();
assert!(!filter.is_filtering());
filter.set_highlight_limits(vec![2, 2, 2]);
filter.set_query("ma");
assert!(filter.matches()[0].1.iter().all(|p| *p < 2));
}
#[test]
fn a_text_buffer_edits_by_grapheme() {
let mut buffer = TextBuffer::with_text("cafe\u{301} au lait");
buffer.checkpoint();
buffer.delete_word();
assert_eq!(buffer.text(), "cafe\u{301} au ");
buffer.home();
buffer.right();
buffer.right();
buffer.right();
buffer.right();
assert_eq!(buffer.caret(), 5, "the accent moves with its letter");
assert!(buffer.backspace());
assert_eq!(buffer.text(), "caf au ");
buffer.delete_to_start();
assert_eq!(buffer.text(), " au ");
assert!(buffer.undo());
assert_eq!(buffer.text(), "cafe\u{301} au lait");
let (shown, column) = TextBuffer::with_text("abcdefgh").window(4, Some('*'));
assert_eq!((shown.as_str(), column), ("***", 3));
}
#[test]
fn a_divider_drags_within_its_minimums() {
let mut divider = Divider::new(3).min(12);
assert_eq!(divider.clamp(5, 80), 12);
assert_eq!(divider.clamp(79, 80), 80 - 12 - 3);
assert_eq!(divider.resolve(80, 36), 36);
assert!(!divider.press(10, 36));
assert!(divider.press(37, 36));
assert!(divider.drag(50, 80));
divider.release();
assert!(!divider.drag(60, 80));
assert_eq!(divider.resolve(80, 36), 50);
divider.nudge(-100, 50, 80);
assert_eq!(divider.resolve(80, 36), 12);
}
}