Skip to main content

rich_interact/
kit.rs

1//! The building-block kit: the pieces the built-in components are made of,
2//! public so your own components can use them too (0.0.14 workstream 1).
3//!
4//! Two kinds of piece live here.
5//!
6//! **Line helpers** build and measure the lines of a [`View`](crate::View):
7//! [`text`] and [`plain`] make segments, [`width`] measures a line,
8//! [`fit`] crops one and [`pad`] crops or pads it to an exact width,
9//! [`highlight`] styles the characters a filter matched, and [`question`]
10//! is the `? prompt › ` line every built-in starts with. [`slice`](fn@slice) cuts
11//! cells out of a line, [`overlay`] draws one line over another and
12//! [`restyle`] puts a style over a line (a backdrop); [`frame`] boxes
13//! lines and [`place`] draws a box over a view, as a modal is drawn.
14//! [`pasted`] and
15//! [`shown`] make untrusted text safe for a one-line field or a table cell.
16//!
17//! **State types** hold what a component remembers between events, with the
18//! editing and movement rules the built-ins share, and no rendering of
19//! their own:
20//!
21//! - [`ListState`]: a cursor over rows, the first row shown, and a
22//!   selection (what [`Select`](crate::Select) moves and marks with);
23//! - [`ScrollState`]: an offset into lines (what a
24//!   [`Viewport`](crate::Viewport) scrolls with);
25//! - [`FilterState`]: a query, and the candidates that match it, ranked by
26//!   the [fuzzy](crate::fuzzy) matcher, with the characters to highlight;
27//! - [`TextBuffer`]: one line of text and a caret that moves by grapheme
28//!   cluster, with the shell editing keys' operations and an undo point
29//!   (what [`Input`](crate::Input) edits);
30//! - [`Divider`]: a border between two panes that the mouse drags or the
31//!   keyboard nudges (what [`Split`](crate::compose::Split) and
32//!   `Select`'s preview resize with);
33//! - [`ActionMenu`]: the Ctrl+K menu of [`Action`]s for one target.
34//!
35//! ```
36//! use rich_interact::kit::{FilterState, ListState};
37//!
38//! let mut filter = FilterState::new(["main.rs", "lib.rs", "README.md"]);
39//! filter.set_query("rs");
40//! assert_eq!(filter.len(), 2);
41//! let mut list = ListState::new();
42//! list.set_len(filter.len());
43//! list.step(1, 10);
44//! let second = filter.index(list.cursor()).unwrap();
45//! assert!(filter.candidates()[second].ends_with(".rs"));
46//! ```
47
48use std::ops::Range;
49
50use rich::cells::{cell_len, split_graphemes};
51use rich::{Segment, Style};
52
53pub use crate::components::Theme;
54use crate::event::{Event, Key, KeyCode, Mouse, MouseKind};
55use crate::fuzzy::rank;
56use crate::item::Action;
57
58// ---------------------------------------------------------------------------
59// Line helpers.
60
61/// A segment of `text` in `style`.
62pub fn text(text: impl Into<String>, style: &Style) -> Segment {
63    Segment::new(text.into(), Some(style.clone()))
64}
65
66/// The first line of `text` as segments, unwrapped, its styles and style
67/// metadata kept: an icon for a list row, a crumb or a status item. A micro
68/// asset's placeholder (`rich_micro::placeholder`) stays tagged, so the
69/// painter's graphics can draw the asset over its cells.
70pub fn icon(text: &rich::Text) -> Vec<Segment> {
71    let console = rich::Console::builder().width(4096).build();
72    let options = console.options().update_width(4096);
73    let mut text = text.clone();
74    text.set_no_wrap(Some(true));
75    console
76        .render_lines(&text, &options, false)
77        .into_iter()
78        .next()
79        .unwrap_or_default()
80        .into_iter()
81        .filter(|segment| !segment.text.is_empty())
82        .collect()
83}
84
85/// A segment of unstyled `text`.
86pub fn plain(text: impl Into<String>) -> Segment {
87    Segment::new(text.into(), None)
88}
89
90/// The width of a line in cells.
91pub fn width(line: &[Segment]) -> usize {
92    line.iter().map(Segment::cell_length).sum()
93}
94
95/// Crop a line to `columns` cells; a shorter line is left as it is.
96pub fn fit(line: Vec<Segment>, columns: usize) -> Vec<Segment> {
97    if width(&line) > columns {
98        Segment::adjust_line_length(&line, columns, None)
99    } else {
100        line
101    }
102}
103
104/// Crop or pad a line to exactly `columns` cells.
105pub fn pad(line: Vec<Segment>, columns: usize) -> Vec<Segment> {
106    Segment::adjust_line_length(&line, columns, None)
107}
108
109/// `label` with the characters at `positions` (character indices, in
110/// order) in `matched`, the rest in `base`: how a filter's matches show.
111pub fn highlight(
112    label: &str,
113    positions: &[usize],
114    base: Option<&Style>,
115    matched: &Style,
116) -> Vec<Segment> {
117    let hit = match base {
118        Some(base) => base.combine(matched),
119        None => matched.clone(),
120    };
121    let mut segments: Vec<Segment> = Vec::new();
122    let mut run = String::new();
123    let mut run_hit = false;
124    let mut next = positions.iter().peekable();
125    for (index, c) in label.chars().enumerate() {
126        let is_hit = next.peek() == Some(&&index);
127        if is_hit {
128            next.next();
129        }
130        if is_hit != run_hit && !run.is_empty() {
131            let style = if run_hit {
132                Some(hit.clone())
133            } else {
134                base.cloned()
135            };
136            segments.push(Segment::new(std::mem::take(&mut run), style));
137        }
138        run_hit = is_hit;
139        run.push(c);
140    }
141    if !run.is_empty() {
142        let style = if run_hit { Some(hit) } else { base.cloned() };
143        segments.push(Segment::new(run, style));
144    }
145    segments
146}
147
148/// The cells `from..to` of a line. A wide character cut by either end
149/// becomes spaces in its style, so the result is exactly the cells asked
150/// for (fewer when the line is shorter). Control segments are dropped.
151pub fn slice(line: &[Segment], from: usize, to: usize) -> Vec<Segment> {
152    let mut out: Vec<Segment> = Vec::new();
153    let mut at = 0;
154    for segment in line.iter().filter(|segment| !segment.control) {
155        if at >= to {
156            break;
157        }
158        let length = segment.cell_length();
159        if at >= from && at + length <= to {
160            out.push(segment.clone());
161            at += length;
162            continue;
163        }
164        let (spans, _) = split_graphemes(&segment.text);
165        let mut piece = String::new();
166        for (start, end, cells) in spans {
167            let (left, right) = (at, at + cells);
168            if left >= from && right <= to {
169                piece.push_str(&segment.text[start..end]);
170            } else if right > from && left < to {
171                let covered = right.min(to) - left.max(from);
172                piece.push_str(&" ".repeat(covered));
173            }
174            at = right;
175        }
176        if !piece.is_empty() {
177            out.push(Segment::new(piece, segment.style.clone()));
178        }
179    }
180    out
181}
182
183/// `top` drawn over `base` from cell `x`: `base`'s cells before `x` (padded
184/// out to `x`), then `top`, then what of `base` is right of it.
185pub fn overlay(base: &[Segment], x: usize, top: &[Segment]) -> Vec<Segment> {
186    let end = x + width(top);
187    let mut line = pad(slice(base, 0, x), x);
188    line.extend(top.iter().cloned());
189    let base_width = width(base);
190    if base_width > end {
191        line.extend(slice(base, end, base_width));
192    }
193    line
194}
195
196/// `line` with `style` over each segment's own: a backdrop's dimming.
197pub fn restyle(line: &[Segment], style: &Style) -> Vec<Segment> {
198    line.iter()
199        .map(|segment| {
200            if segment.control {
201                return segment.clone();
202            }
203            let styled = match &segment.style {
204                Some(own) => own.combine(style),
205                None => style.clone(),
206            };
207            Segment::new(segment.text.clone(), Some(styled))
208        })
209        .collect()
210}
211
212/// `lines` in a rounded box `inner` cells wide inside, each padded to that
213/// width, with `title` in the top border: how a modal or a popover is
214/// drawn, by [`Layers`](crate::compose::Layers) and the action menu alike.
215pub fn frame(
216    lines: Vec<Vec<Segment>>,
217    inner: usize,
218    title: Option<&str>,
219    style: &Style,
220) -> Vec<Vec<Segment>> {
221    let mut top = vec![text("╭", style)];
222    match title {
223        Some(title) if inner >= 4 => {
224            let title = fit(vec![text(format!(" {title} "), style)], inner - 1);
225            let used = width(&title) + 1;
226            top.push(text("─", style));
227            top.extend(title);
228            top.push(text("─".repeat(inner.saturating_sub(used)), style));
229        }
230        _ => top.push(text("─".repeat(inner), style)),
231    }
232    top.push(text("╮", style));
233    let mut boxed = vec![top];
234    for line in lines {
235        let mut row = vec![text("│", style)];
236        row.extend(pad(line, inner));
237        row.push(text("│", style));
238        boxed.push(row);
239    }
240    boxed.push(vec![
241        text("╰", style),
242        text("─".repeat(inner), style),
243        text("╯", style),
244    ]);
245    boxed
246}
247
248/// Draw `top`'s lines over `base` from cell `x` of row `y`, adding rows
249/// to `base` where `top` reaches below it; with a `backdrop`, first put
250/// that style over every line of `base` (a modal's dimming).
251pub fn place(
252    base: &mut Vec<Vec<Segment>>,
253    x: usize,
254    y: usize,
255    top: &[Vec<Segment>],
256    backdrop: Option<&Style>,
257) {
258    if let Some(style) = backdrop {
259        for line in base.iter_mut() {
260            *line = restyle(line, style);
261        }
262    }
263    if base.len() < y + top.len() {
264        base.resize_with(y + top.len(), Vec::new);
265    }
266    for (row, line) in top.iter().enumerate() {
267        let under = &mut base[y + row];
268        *under = overlay(under, x, line);
269    }
270}
271
272/// Pasted text for a one-line field: line breaks become `newline`, a tab a
273/// space, and other terminal controls (C0, DEL, C1) are dropped, so a paste
274/// cannot carry an escape sequence into the answer or onto the screen.
275pub fn pasted(text: &str, newline: &str) -> String {
276    let mut out = String::with_capacity(text.len());
277    for c in text.chars() {
278        match c {
279            '\n' | '\r' => out.push_str(newline),
280            '\t' => out.push(' '),
281            '\0'..='\u{1f}' | '\u{7f}'..='\u{9f}' => {}
282            _ => out.push(c),
283        }
284    }
285    out
286}
287
288/// `text` with its terminal controls as the characters a view paints them
289/// as, one for one, so cells line up (a table's columns) before painting.
290pub fn shown(text: &str) -> String {
291    text.chars()
292        .map(|c| crate::paint::visible(c).unwrap_or(c))
293        .collect()
294}
295
296/// The question line every built-in component starts with: `? prompt › `.
297pub fn question(theme: &Theme, prompt: &str) -> Vec<Segment> {
298    vec![
299        text(theme.question.clone(), &theme.question_style),
300        plain(" "),
301        text(prompt, &theme.prompt),
302        plain(" "),
303        text("›", &theme.hint),
304        plain(" "),
305    ]
306}
307
308// ---------------------------------------------------------------------------
309// ListState.
310
311/// A cursor over a list of rows, the first row shown, and a selection.
312///
313/// Rows are positions in what is listed (after filtering, say); the
314/// selection is kept by *index*, whatever the caller's indices are (an
315/// item's index before filtering), so marks survive a change of filter.
316/// Movement takes the page, the rows on screen, since that can change with
317/// every render.
318#[derive(Clone, Debug, Default, PartialEq, Eq)]
319pub struct ListState {
320    len: usize,
321    cursor: usize,
322    offset: usize,
323    selected: Vec<bool>,
324}
325
326impl ListState {
327    pub fn new() -> ListState {
328        ListState::default()
329    }
330
331    /// How many rows there are.
332    pub fn len(&self) -> usize {
333        self.len
334    }
335
336    pub fn is_empty(&self) -> bool {
337        self.len == 0
338    }
339
340    /// Set the number of rows. The cursor is left where it is: call
341    /// [`reset`](Self::reset) or [`set_cursor`](Self::set_cursor) to put it
342    /// on a row that exists.
343    pub fn set_len(&mut self, len: usize) {
344        self.len = len;
345    }
346
347    /// The row the cursor is on.
348    pub fn cursor(&self) -> usize {
349        self.cursor
350    }
351
352    /// Put the cursor on `row`, without scrolling (see
353    /// [`follow`](Self::follow)).
354    pub fn set_cursor(&mut self, row: usize) {
355        self.cursor = row;
356    }
357
358    /// The first row shown.
359    pub fn offset(&self) -> usize {
360        self.offset
361    }
362
363    pub fn set_offset(&mut self, offset: usize) {
364        self.offset = offset;
365    }
366
367    /// Put the cursor on `row` and the first row shown back at the top,
368    /// then scroll just enough to show the cursor: after the rows changed.
369    pub fn reset(&mut self, row: usize, page: usize) {
370        self.cursor = row;
371        self.offset = 0;
372        self.follow(page);
373    }
374
375    /// Scroll just enough that the cursor's row is among the `page` shown.
376    pub fn follow(&mut self, page: usize) {
377        if self.cursor < self.offset {
378            self.offset = self.cursor;
379        } else if self.cursor >= self.offset + page {
380            self.offset = self.cursor + 1 - page;
381        }
382    }
383
384    /// Move the cursor by `delta` rows (negative: up), stopping at the
385    /// ends, and scroll to follow it. Nothing happens with no rows.
386    pub fn step(&mut self, delta: isize, page: usize) {
387        if self.len == 0 {
388            return;
389        }
390        let last = self.len - 1;
391        self.cursor = self.cursor.saturating_add_signed(delta).min(last);
392        self.follow(page);
393    }
394
395    /// Move to `row` (clamped to the last) and scroll to show it.
396    pub fn select_row(&mut self, row: usize, page: usize) {
397        if self.len == 0 {
398            return;
399        }
400        self.cursor = row.min(self.len - 1);
401        self.follow(page);
402    }
403
404    /// The rows shown on a page of `page` rows.
405    pub fn visible(&self, page: usize) -> Range<usize> {
406        let end = (self.offset + page).min(self.len);
407        self.offset.min(end)..end
408    }
409
410    /// Make room to select `count` indices, all unselected.
411    pub fn clear_selection(&mut self, count: usize) {
412        self.selected = vec![false; count];
413    }
414
415    pub fn is_selected(&self, index: usize) -> bool {
416        self.selected.get(index).copied().unwrap_or(false)
417    }
418
419    /// Select or unselect `index`; indices past the room made are grown to.
420    pub fn set_selected(&mut self, index: usize, on: bool) {
421        if index >= self.selected.len() {
422            self.selected.resize(index + 1, false);
423        }
424        self.selected[index] = on;
425    }
426
427    /// Flip whether `index` is selected.
428    pub fn toggle(&mut self, index: usize) {
429        let on = !self.is_selected(index);
430        self.set_selected(index, on);
431    }
432
433    /// The selected indices, in order.
434    pub fn selected(&self) -> Vec<usize> {
435        (0..self.selected.len())
436            .filter(|&index| self.selected[index])
437            .collect()
438    }
439
440    /// How many indices are selected.
441    pub fn selected_count(&self) -> usize {
442        self.selected.iter().filter(|on| **on).count()
443    }
444}
445
446// ---------------------------------------------------------------------------
447// ScrollState.
448
449/// An offset into `len` lines, a page at a time: the scroll position of a
450/// pager or a preview.
451#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
452pub struct ScrollState {
453    len: usize,
454    offset: usize,
455}
456
457impl ScrollState {
458    pub fn new(len: usize) -> ScrollState {
459        ScrollState { len, offset: 0 }
460    }
461
462    pub fn len(&self) -> usize {
463        self.len
464    }
465
466    pub fn is_empty(&self) -> bool {
467        self.len == 0
468    }
469
470    /// Set the number of lines, keeping the offset where it still fits.
471    pub fn set_len(&mut self, len: usize) {
472        self.len = len;
473        self.offset = self.offset.min(len.saturating_sub(1));
474    }
475
476    /// The first line shown.
477    pub fn offset(&self) -> usize {
478        self.offset
479    }
480
481    fn last_offset(&self, page: usize) -> usize {
482        self.len.saturating_sub(page)
483    }
484
485    /// Scroll by `delta` lines (negative: up), within the lines. Returns
486    /// whether the offset changed.
487    pub fn scroll(&mut self, delta: isize, page: usize) -> bool {
488        let target = self
489            .offset
490            .saturating_add_signed(delta)
491            .min(self.last_offset(page));
492        let changed = target != self.offset;
493        self.offset = target;
494        changed
495    }
496
497    /// Scroll to put `line` first, or as near as the last page allows.
498    pub fn scroll_to(&mut self, line: usize, page: usize) -> bool {
499        let target = line.min(self.last_offset(page));
500        let changed = target != self.offset;
501        self.offset = target;
502        changed
503    }
504
505    /// Scroll just enough that `line` is shown.
506    pub fn show(&mut self, line: usize, page: usize) -> bool {
507        if line < self.offset {
508            self.scroll_to(line, page)
509        } else if line >= self.offset + page {
510            self.scroll_to(line + 1 - page, page)
511        } else {
512            false
513        }
514    }
515
516    /// The lines shown on a page of `page` lines.
517    pub fn visible(&self, page: usize) -> Range<usize> {
518        let end = (self.offset + page).min(self.len);
519        self.offset.min(end)..end
520    }
521
522    /// The keys [`handle`](Self::handle) scrolls with, in `context`, for a
523    /// component's [`keymap`](crate::Component::keymap).
524    pub fn keymap(context: &str) -> crate::keymap::Keymap {
525        use crate::keymap::keys;
526        crate::keymap::Keymap::new(context)
527            .bind("scroll-up", keys("up k"), "scroll up")
528            .bind("scroll-down", keys("down j"), "scroll down")
529            .bind("page-up", keys("pageup b"), "page up")
530            .bind("page-down", keys("pagedown space"), "page down")
531            .bind("top", keys("home g"), "to the top")
532            .bind("bottom", keys("end G"), "to the bottom")
533    }
534
535    /// Do one of [`keymap`](Self::keymap)'s actions (`scroll-up`,
536    /// `page-down`, `top`, ...): how a component that looks its keys up in
537    /// a keymap, and so honours rebinding, scrolls. Returns whether
538    /// `action` is a scroll action (moved or not).
539    pub fn act(&mut self, action: &str, page: usize) -> bool {
540        let page_step = page.max(1) as isize;
541        match action {
542            "scroll-up" => self.scroll(-1, page),
543            "scroll-down" => self.scroll(1, page),
544            "page-up" => self.scroll(-page_step, page),
545            "page-down" => self.scroll(page_step, page),
546            "top" => self.scroll_to(0, page),
547            "bottom" => self.scroll_to(usize::MAX, page),
548            _ => return false,
549        };
550        true
551    }
552
553    /// Move with the usual keys and the mouse wheel: arrows and `j`/`k` by a
554    /// line, PageUp/PageDown, `b` and Space by a page, Home/End and `g`/`G`
555    /// to the ends, and the wheel by three lines. Returns whether the event
556    /// was a scroll key (moved or not). The keys are fixed; see
557    /// [`act`](Self::act) for keys from a keymap.
558    pub fn handle(&mut self, event: &Event, page: usize) -> bool {
559        let page_step = page.max(1) as isize;
560        match event {
561            Event::Key(key) if !key.modifiers.ctrl && !key.modifiers.alt => {
562                match key.code {
563                    KeyCode::Up | KeyCode::Char('k') => self.scroll(-1, page),
564                    KeyCode::Down | KeyCode::Char('j') => self.scroll(1, page),
565                    KeyCode::PageUp | KeyCode::Char('b') => self.scroll(-page_step, page),
566                    KeyCode::PageDown | KeyCode::Char(' ') => self.scroll(page_step, page),
567                    KeyCode::Home | KeyCode::Char('g') => self.scroll_to(0, page),
568                    KeyCode::End | KeyCode::Char('G') => self.scroll_to(usize::MAX, page),
569                    _ => return false,
570                };
571                true
572            }
573            Event::Mouse(mouse) => match mouse.kind {
574                MouseKind::ScrollUp => {
575                    self.scroll(-3, page);
576                    true
577                }
578                MouseKind::ScrollDown => {
579                    self.scroll(3, page);
580                    true
581                }
582                _ => false,
583            },
584            _ => false,
585        }
586    }
587}
588
589// ---------------------------------------------------------------------------
590// FilterState.
591
592/// A query and the candidates that match it, best first.
593///
594/// Candidates are strings to search; a match is a candidate's index and the
595/// character positions that matched, for [`highlight`]. With an empty query
596/// every candidate matches, in order, except those hidden
597/// ([`set_hidden`](Self::set_hidden)): a collapsed tree node's children
598/// stay out of the list until something is typed.
599#[derive(Clone, Debug, Default, PartialEq, Eq)]
600pub struct FilterState {
601    query: String,
602    candidates: Vec<String>,
603    hidden: Vec<bool>,
604    limits: Vec<usize>,
605    matches: Vec<(usize, Vec<usize>)>,
606    /// Each candidate's parent, for a tree: matches then keep their
607    /// ancestors, in tree order ([`set_tree`](Self::set_tree)).
608    parents: Option<Vec<Option<usize>>>,
609    /// Which matches are only there as a match's ancestor.
610    context: Vec<bool>,
611    /// The position of the best match.
612    best: Option<usize>,
613}
614
615impl FilterState {
616    /// Filter `candidates`, with nothing typed yet.
617    pub fn new<I, S>(candidates: I) -> FilterState
618    where
619        I: IntoIterator<Item = S>,
620        S: Into<String>,
621    {
622        let mut filter = FilterState::default();
623        filter.set_candidates(candidates);
624        filter
625    }
626
627    /// Replace the candidates and filter them again.
628    pub fn set_candidates<I, S>(&mut self, candidates: I)
629    where
630        I: IntoIterator<Item = S>,
631        S: Into<String>,
632    {
633        self.candidates = candidates.into_iter().map(Into::into).collect();
634        self.refilter();
635    }
636
637    pub fn candidates(&self) -> &[String] {
638        &self.candidates
639    }
640
641    /// Leave these candidates out while the query is empty (`true` hides).
642    /// Takes effect on the next [`refilter`](Self::refilter).
643    pub fn set_hidden(&mut self, hidden: Vec<bool>) {
644        self.hidden = hidden;
645    }
646
647    /// Filter as a tree (#428): `parents[i]` is candidate `i`'s parent,
648    /// and candidates come in tree order, each parent before its children.
649    /// While a query is typed, what matches keeps its ancestors, so a match
650    /// shows where it is: the list is the matches and their ancestors in
651    /// tree order, the ancestors marked as [context](Self::is_context) with
652    /// nothing highlighted, and [`best`](Self::best) is where the best match
653    /// is. `None` goes back to a flat ranking. Takes effect on the next
654    /// [`refilter`](Self::refilter).
655    pub fn set_tree(&mut self, parents: Option<Vec<Option<usize>>>) {
656        self.parents = parents;
657    }
658
659    /// Highlight at most the first `limits[i]` characters of candidate `i`:
660    /// for a candidate searched with more than its label (a description),
661    /// so only the label highlights. Takes effect on the next
662    /// [`refilter`](Self::refilter).
663    pub fn set_highlight_limits(&mut self, limits: Vec<usize>) {
664        self.limits = limits;
665    }
666
667    /// What is typed.
668    pub fn query(&self) -> &str {
669        &self.query
670    }
671
672    /// The query, to edit in place; call [`refilter`](Self::refilter)
673    /// after.
674    pub fn query_mut(&mut self) -> &mut String {
675        &mut self.query
676    }
677
678    pub fn is_filtering(&self) -> bool {
679        !self.query.is_empty()
680    }
681
682    /// Replace the query and filter again.
683    pub fn set_query(&mut self, query: impl Into<String>) {
684        self.query = query.into();
685        self.refilter();
686    }
687
688    /// Type `c`.
689    pub fn push(&mut self, c: char) {
690        self.query.push(c);
691        self.refilter();
692    }
693
694    /// Type `text` (a paste: see [`pasted`]).
695    pub fn push_str(&mut self, text: &str) {
696        self.query.push_str(text);
697        self.refilter();
698    }
699
700    /// Delete the last character; `false` when there was none.
701    pub fn pop(&mut self) -> bool {
702        let popped = self.query.pop().is_some();
703        if popped {
704            self.refilter();
705        }
706        popped
707    }
708
709    /// Clear the query.
710    pub fn clear(&mut self) {
711        self.query.clear();
712        self.refilter();
713    }
714
715    /// Match the candidates against the query again.
716    pub fn refilter(&mut self) {
717        let filtering = !self.query.is_empty();
718        let hidden = &self.hidden;
719        let limits = &self.limits;
720        let candidates = &self.candidates;
721        let ranked: Vec<(usize, Vec<usize>)> =
722            rank(&self.query, candidates.iter().map(String::as_str))
723                .into_iter()
724                .filter(|(index, _)| filtering || !hidden.get(*index).copied().unwrap_or(false))
725                .map(|(index, found)| {
726                    let mut positions = found.positions;
727                    if let Some(limit) = limits.get(index) {
728                        positions.retain(|position| position < limit);
729                    }
730                    (index, positions)
731                })
732                .collect();
733        match &self.parents {
734            Some(parents) if filtering => {
735                let best = ranked.first().map(|(index, _)| *index);
736                let (matches, context) = keep_ancestors(ranked, parents);
737                self.best = best.and_then(|best| matches.iter().position(|(i, _)| *i == best));
738                self.matches = matches;
739                self.context = context;
740            }
741            _ => {
742                self.best = (!ranked.is_empty()).then_some(0);
743                self.context = vec![false; ranked.len()];
744                self.matches = ranked;
745            }
746        }
747    }
748
749    /// Whether the match at `position` is only there as an ancestor of a
750    /// match ([`set_tree`](Self::set_tree)): drawn dim, not picked first.
751    pub fn is_context(&self, position: usize) -> bool {
752        self.context.get(position).copied().unwrap_or(false)
753    }
754
755    /// The position of the best match: the first, unless a tree keeps
756    /// ancestors above it.
757    pub fn best(&self) -> Option<usize> {
758        self.best
759    }
760
761    /// The matches, best first: candidate indices and the positions to
762    /// highlight.
763    pub fn matches(&self) -> &[(usize, Vec<usize>)] {
764        &self.matches
765    }
766
767    /// How many candidates match.
768    pub fn len(&self) -> usize {
769        self.matches.len()
770    }
771
772    pub fn is_empty(&self) -> bool {
773        self.matches.is_empty()
774    }
775
776    /// The candidate at `position` in the matches.
777    pub fn index(&self, position: usize) -> Option<usize> {
778        self.matches.get(position).map(|(index, _)| *index)
779    }
780
781    /// Where candidate `index` is in the matches, if it matches.
782    pub fn position_of(&self, index: usize) -> Option<usize> {
783        self.matches.iter().position(|(i, _)| *i == index)
784    }
785}
786
787/// `index`'s ancestors in a tree given by `parents`, nearest first.
788///
789/// ```
790/// use rich_interact::kit::ancestors;
791///
792/// // 0 ─ 1 ─ 2, and 3 under 0.
793/// let parents = [None, Some(0), Some(1), Some(0)];
794/// assert_eq!(ancestors(&parents, 2).collect::<Vec<_>>(), [1, 0]);
795/// ```
796pub fn ancestors(parents: &[Option<usize>], index: usize) -> impl Iterator<Item = usize> + '_ {
797    let mut at = parents.get(index).copied().flatten();
798    // A malformed parent list (a cycle) stops after every node once.
799    let mut left = parents.len();
800    std::iter::from_fn(move || {
801        let current = at.filter(|_| left > 0)?;
802        left -= 1;
803        at = parents.get(current).copied().flatten();
804        Some(current)
805    })
806}
807
808/// Tree filtering that keeps ancestors (#428): `matches` (candidate
809/// indices with the positions to highlight, in any order) plus every
810/// ancestor of each, in tree order (index order, parents before
811/// children). Returns the list and, for each entry, whether it is only
812/// there as an ancestor (context), with nothing highlighted. A parent
813/// index past the list is not a node: the walk up stops there.
814///
815/// ```
816/// use rich_interact::kit::keep_ancestors;
817///
818/// //  0 config
819/// //  1 ├── server
820/// //  2 │   └── port
821/// //  3 └── debug
822/// let parents = [None, Some(0), Some(1), Some(0)];
823/// let (list, context) = keep_ancestors(vec![(2, vec![0, 1])], &parents);
824/// assert_eq!(list, [(0, vec![]), (1, vec![]), (2, vec![0, 1])]);
825/// assert_eq!(context, [true, true, false]);
826/// ```
827pub fn keep_ancestors(
828    matches: Vec<(usize, Vec<usize>)>,
829    parents: &[Option<usize>],
830) -> (Vec<(usize, Vec<usize>)>, Vec<bool>) {
831    let count = parents
832        .len()
833        .max(matches.iter().map(|(i, _)| i + 1).max().unwrap_or(0));
834    let mut found: Vec<Option<Vec<usize>>> = vec![None; count];
835    let mut kept = vec![false; count];
836    for (index, positions) in matches {
837        // A parent past the list (a malformed parent list) ends the walk.
838        for ancestor in ancestors(parents, index) {
839            if kept.get(ancestor).is_none_or(|&kept| kept) {
840                break;
841            }
842            kept[ancestor] = true;
843        }
844        kept[index] = true;
845        found[index] = Some(positions);
846    }
847    let mut list = Vec::new();
848    let mut context = Vec::new();
849    for (index, positions) in found.into_iter().enumerate() {
850        if !kept[index] {
851            continue;
852        }
853        context.push(positions.is_none());
854        list.push((index, positions.unwrap_or_default()));
855    }
856    (list, context)
857}
858
859// ---------------------------------------------------------------------------
860// TextBuffer.
861
862/// One line of text and a caret, for a text field.
863///
864/// The caret is a character index and moves by grapheme cluster, so an
865/// accent is never split from its letter. The operations are the ones a
866/// shell line has (and [`Input`](crate::Input) binds): insert, Backspace,
867/// Delete, Ctrl+U (delete to the start), Ctrl+W (delete a word), and moving
868/// by grapheme or to the ends. [`checkpoint`](Self::checkpoint) keeps an
869/// undo point and [`undo`](Self::undo) goes back to the last one.
870#[derive(Clone, Debug, Default, PartialEq, Eq)]
871pub struct TextBuffer {
872    value: String,
873    caret: usize,
874    undo: Vec<(String, usize)>,
875}
876
877impl TextBuffer {
878    pub fn new() -> TextBuffer {
879        TextBuffer::default()
880    }
881
882    /// A buffer holding `text`, the caret at its end.
883    pub fn with_text(text: impl Into<String>) -> TextBuffer {
884        let mut buffer = TextBuffer::new();
885        buffer.set_text(text);
886        buffer
887    }
888
889    pub fn text(&self) -> &str {
890        &self.value
891    }
892
893    pub fn is_empty(&self) -> bool {
894        self.value.is_empty()
895    }
896
897    /// The length in characters.
898    pub fn len(&self) -> usize {
899        self.value.chars().count()
900    }
901
902    /// Replace the text, the caret at its end.
903    pub fn set_text(&mut self, text: impl Into<String>) {
904        self.value = text.into();
905        self.caret = self.len();
906    }
907
908    /// The caret, in characters.
909    pub fn caret(&self) -> usize {
910        self.caret
911    }
912
913    /// Put the caret at character `caret` (clamped to the end), or at the
914    /// start of the grapheme `caret` falls inside.
915    pub fn set_caret(&mut self, caret: usize) {
916        self.caret = caret.min(self.len());
917        self.snap(false);
918    }
919
920    /// Move the caret onto a grapheme boundary: on to the end of the
921    /// grapheme it is inside (`forward`) or back to its start. Typing a
922    /// base character in front of a combining mark or U+FE0F, or deleting
923    /// what kept two clusters apart, can leave it inside one.
924    fn snap(&mut self, forward: bool) {
925        let stops = self.stops();
926        self.caret = if forward {
927            stops.iter().copied().find(|&stop| stop >= self.caret)
928        } else {
929            stops.iter().rev().copied().find(|&stop| stop <= self.caret)
930        }
931        .unwrap_or(self.caret);
932    }
933
934    /// The byte offset of character `caret`.
935    pub fn byte(&self, caret: usize) -> usize {
936        self.value
937            .char_indices()
938            .nth(caret)
939            .map_or(self.value.len(), |(index, _)| index)
940    }
941
942    /// The text before the caret.
943    pub fn before_caret(&self) -> &str {
944        &self.value[..self.byte(self.caret)]
945    }
946
947    /// The character indices where graphemes start, and the end: where
948    /// the caret may stop.
949    pub fn stops(&self) -> Vec<usize> {
950        let (spans, _) = split_graphemes(&self.value);
951        let mut stops = Vec::with_capacity(spans.len() + 1);
952        let (mut chars, mut byte) = (0, 0);
953        for (start, _, _) in spans {
954            chars += self.value[byte..start].chars().count();
955            byte = start;
956            stops.push(chars);
957        }
958        stops.push(self.value.chars().count());
959        stops
960    }
961
962    /// The caret one grapheme back.
963    pub fn previous_stop(&self) -> usize {
964        self.stops()
965            .iter()
966            .rev()
967            .copied()
968            .find(|&stop| stop < self.caret)
969            .unwrap_or(0)
970    }
971
972    /// The caret one grapheme on.
973    pub fn next_stop(&self) -> usize {
974        self.stops()
975            .iter()
976            .copied()
977            .find(|&stop| stop > self.caret)
978            .unwrap_or(self.caret)
979    }
980
981    /// Insert `text` at the caret, and move past it.
982    pub fn insert(&mut self, text: &str) {
983        let at = self.byte(self.caret);
984        self.value.insert_str(at, text);
985        self.caret += text.chars().count();
986        self.snap(true);
987    }
988
989    /// Move the caret one grapheme left.
990    pub fn left(&mut self) {
991        self.caret = self.previous_stop();
992    }
993
994    /// Move the caret one grapheme right.
995    pub fn right(&mut self) {
996        self.caret = self.next_stop().min(self.len());
997    }
998
999    pub fn home(&mut self) {
1000        self.caret = 0;
1001    }
1002
1003    pub fn end(&mut self) {
1004        self.caret = self.len();
1005    }
1006
1007    /// Delete the grapheme before the caret; `false` at the start.
1008    pub fn backspace(&mut self) -> bool {
1009        if self.caret == 0 {
1010            return false;
1011        }
1012        let previous = self.previous_stop();
1013        let (from, to) = (self.byte(previous), self.byte(self.caret));
1014        self.value.replace_range(from..to, "");
1015        self.caret = previous;
1016        self.snap(false);
1017        true
1018    }
1019
1020    /// Delete the grapheme after the caret; `false` at the end.
1021    pub fn delete(&mut self) -> bool {
1022        if self.caret >= self.len() {
1023            return false;
1024        }
1025        let (from, to) = (self.byte(self.caret), self.byte(self.next_stop()));
1026        self.value.replace_range(from..to, "");
1027        self.snap(false);
1028        true
1029    }
1030
1031    /// Delete everything before the caret (Ctrl+U).
1032    pub fn delete_to_start(&mut self) {
1033        let at = self.byte(self.caret);
1034        self.value.replace_range(..at, "");
1035        self.caret = 0;
1036    }
1037
1038    /// Delete the word before the caret, and the spaces after it (Ctrl+W).
1039    pub fn delete_word(&mut self) {
1040        let chars: Vec<char> = self.value.chars().collect();
1041        let mut start = self.caret;
1042        while start > 0 && chars[start - 1] == ' ' {
1043            start -= 1;
1044        }
1045        while start > 0 && chars[start - 1] != ' ' {
1046            start -= 1;
1047        }
1048        let (from, to) = (self.byte(start), self.byte(self.caret));
1049        self.value.replace_range(from..to, "");
1050        self.caret = start;
1051        self.snap(false);
1052    }
1053
1054    /// Keep the text and caret as an undo point.
1055    pub fn checkpoint(&mut self) {
1056        let point = (self.value.clone(), self.caret);
1057        if self.undo.last() != Some(&point) {
1058            self.undo.push(point);
1059        }
1060    }
1061
1062    /// Go back to the last undo point; `false` when there is none.
1063    pub fn undo(&mut self) -> bool {
1064        match self.undo.pop() {
1065            Some((value, caret)) => {
1066                self.value = value;
1067                self.caret = caret;
1068                true
1069            }
1070            None => false,
1071        }
1072    }
1073
1074    /// `text` as shown: every character as `mask` when there is one.
1075    pub fn masked(text: &str, mask: Option<char>) -> String {
1076        match mask {
1077            Some(mask) => std::iter::repeat_n(mask, text.chars().count()).collect(),
1078            None => text.to_string(),
1079        }
1080    }
1081
1082    /// The text as shown (masked or not), cut to `available` cells so the
1083    /// caret stays in view: a line longer than the space scrolls. Returns
1084    /// what to show and the caret's column in it.
1085    pub fn window(&self, available: usize, mask: Option<char>) -> (String, usize) {
1086        let shown = Self::masked(&self.value, mask);
1087        // Masking keeps one character for each, so the caret is the same
1088        // character index in what is shown.
1089        let caret = shown
1090            .char_indices()
1091            .nth(self.caret)
1092            .map_or(shown.len(), |(index, _)| index);
1093        // The cells before the caret and the cells dropped are both counted
1094        // grapheme by grapheme: `cell_len` of the whole prefix can differ
1095        // from that (a leading U+200D joins what follows it).
1096        let (spans, _) = split_graphemes(&shown);
1097        let before: usize = spans
1098            .iter()
1099            .filter(|(start, _, _)| *start < caret)
1100            .map(|(_, _, cells)| cells)
1101            .sum();
1102        // One cell for the caret after the last character.
1103        let skip = (before + 1).saturating_sub(available.max(1));
1104        if skip == 0 {
1105            return (shown, before);
1106        }
1107        let mut dropped = 0;
1108        let mut from = shown.len();
1109        for (start, _, cells) in spans {
1110            if dropped >= skip || start >= caret {
1111                from = start;
1112                break;
1113            }
1114            dropped += cells;
1115        }
1116        (shown[from..].to_string(), before.saturating_sub(dropped))
1117    }
1118}
1119
1120// ---------------------------------------------------------------------------
1121// Divider.
1122
1123/// A border between two panes along one axis, where the mouse can grab and
1124/// drag it and the keyboard can nudge it.
1125///
1126/// Positions are cells along the axis: the first pane's size, then
1127/// `thickness` cells of border, then the second pane. Both panes keep at
1128/// least their minimum while the total has room for that.
1129#[derive(Clone, Copy, Debug, PartialEq, Eq)]
1130#[must_use = "builder methods return a new value; it does nothing until it is rendered or used"]
1131pub struct Divider {
1132    position: Option<usize>,
1133    dragging: bool,
1134    /// The fewest cells the first pane takes.
1135    pub min_before: usize,
1136    /// The fewest cells the second pane takes.
1137    pub min_after: usize,
1138    /// The cells the border takes.
1139    pub thickness: usize,
1140}
1141
1142impl Default for Divider {
1143    fn default() -> Self {
1144        Divider::new(1)
1145    }
1146}
1147
1148impl Divider {
1149    /// A border `thickness` cells thick, with no minimum pane size.
1150    pub fn new(thickness: usize) -> Divider {
1151        Divider {
1152            position: None,
1153            dragging: false,
1154            min_before: 0,
1155            min_after: 0,
1156            thickness,
1157        }
1158    }
1159
1160    /// Both panes at least `cells`.
1161    pub fn min(mut self, cells: usize) -> Divider {
1162        self.min_before = cells;
1163        self.min_after = cells;
1164        self
1165    }
1166
1167    /// Put the border at `position` (the first pane's size), until dragged.
1168    pub fn at(mut self, position: usize) -> Divider {
1169        self.position = Some(position);
1170        self
1171    }
1172
1173    /// Where the border was put or dragged to, before clamping.
1174    pub fn position(&self) -> Option<usize> {
1175        self.position
1176    }
1177
1178    pub fn set_position(&mut self, position: Option<usize>) {
1179        self.position = position;
1180    }
1181
1182    /// Whether a drag is under way.
1183    pub fn dragging(&self) -> bool {
1184        self.dragging
1185    }
1186
1187    /// The first pane's size for a border at `position` in `total` cells:
1188    /// both panes at least their minimum when there is room for that.
1189    pub fn clamp(&self, position: usize, total: usize) -> usize {
1190        let most = total
1191            .saturating_sub(self.min_after + self.thickness)
1192            .max(self.min_before.min(total));
1193        position.clamp(self.min_before.min(most), most)
1194    }
1195
1196    /// The first pane's size in `total` cells: where the border was put or
1197    /// dragged, clamped, or `default` when it has not been.
1198    pub fn resolve(&self, total: usize, default: usize) -> usize {
1199        match self.position {
1200            Some(position) => self.clamp(position, total),
1201            None => default,
1202        }
1203    }
1204
1205    /// Whether `at` (a cell along the axis) is on the border when the first
1206    /// pane is `first` cells.
1207    pub fn hit(&self, at: usize, first: usize) -> bool {
1208        (first..first + self.thickness.max(1)).contains(&at)
1209    }
1210
1211    /// A press at `at`: grab the border if it is there. Returns whether it
1212    /// was grabbed.
1213    pub fn press(&mut self, at: usize, first: usize) -> bool {
1214        self.dragging = self.hit(at, first);
1215        self.dragging
1216    }
1217
1218    /// A drag to `at` in `total` cells: move the border there if grabbed.
1219    /// Returns whether it moved.
1220    pub fn drag(&mut self, at: usize, total: usize) -> bool {
1221        if !self.dragging {
1222            return false;
1223        }
1224        self.position = Some(self.clamp(at, total));
1225        true
1226    }
1227
1228    /// Let go of the border.
1229    pub fn release(&mut self) {
1230        self.dragging = false;
1231    }
1232
1233    /// Press, drag and release from one mouse event along the axis, `at`
1234    /// being the event's cell along it. Returns whether the divider used
1235    /// the event.
1236    pub fn handle_mouse(&mut self, mouse: &Mouse, at: usize, first: usize, total: usize) -> bool {
1237        match mouse.kind {
1238            MouseKind::Down(crate::event::Button::Left) => self.press(at, first),
1239            MouseKind::Drag(crate::event::Button::Left) => self.drag(at, total),
1240            MouseKind::Up(_) if self.dragging => {
1241                self.release();
1242                true
1243            }
1244            _ => false,
1245        }
1246    }
1247
1248    /// Move the border by `delta` cells from `first`, within `total`.
1249    pub fn nudge(&mut self, delta: isize, first: usize, total: usize) {
1250        self.position = Some(self.clamp(first.saturating_add_signed(delta), total));
1251    }
1252}
1253
1254// ---------------------------------------------------------------------------
1255// ActionMenu.
1256
1257/// The hints under an [`ActionMenu`]'s rows.
1258const MENU_HINTS: &str = "  ↑↓ move · enter run · esc close";
1259
1260/// What an [`ActionMenu`] did with an event.
1261#[derive(Clone, Debug, PartialEq, Eq)]
1262pub enum MenuReply {
1263    /// Nothing to do (moved, or not its event).
1264    Stay,
1265    /// Closed without running anything.
1266    Close,
1267    /// Run this action.
1268    Run(Action),
1269}
1270
1271/// A menu of [`Action`]s for one target, as Ctrl+K opens in
1272/// [`Select`](crate::Select): the arrows move, Enter runs, an action's own
1273/// key runs it, Escape or the key that opened it closes it, and a click
1274/// runs the action under it or closes the menu.
1275#[derive(Clone, Debug)]
1276pub struct ActionMenu {
1277    pub actions: Vec<Action>,
1278    pub focus: usize,
1279    /// The key that opened it, which closes it again.
1280    pub key: Key,
1281}
1282
1283impl ActionMenu {
1284    pub fn new(actions: Vec<Action>, key: Key) -> ActionMenu {
1285        ActionMenu {
1286            actions,
1287            focus: 0,
1288            key,
1289        }
1290    }
1291
1292    /// Handle `event`, the menu's first row being `first` in the view the
1293    /// mouse's rows count from.
1294    pub fn handle(&mut self, event: &Event, first: usize) -> MenuReply {
1295        let count = self.actions.len();
1296        if let Some(mouse) = event.mouse() {
1297            if mouse.is_click() {
1298                let row = mouse.row as usize;
1299                if row >= first && row < first + count {
1300                    return MenuReply::Run(self.actions[row - first].clone());
1301                }
1302                return MenuReply::Close;
1303            }
1304            return MenuReply::Stay;
1305        }
1306        let Some(key) = event.key() else {
1307            return MenuReply::Stay;
1308        };
1309        match key.code {
1310            KeyCode::Escape => return MenuReply::Close,
1311            _ if key.matches(&self.key) => return MenuReply::Close,
1312            KeyCode::Up => self.focus = (self.focus + count - 1) % count.max(1),
1313            KeyCode::Down | KeyCode::Tab => self.focus = (self.focus + 1) % count.max(1),
1314            KeyCode::Enter => return MenuReply::Run(self.actions[self.focus].clone()),
1315            _ => {
1316                if let Some(at) = key.pick(self.actions.iter().map(|a| a.key.as_slice())) {
1317                    return MenuReply::Run(self.actions[at].clone());
1318                }
1319            }
1320        }
1321        MenuReply::Stay
1322    }
1323
1324    /// The width and rows [`render`](Self::render) needs: the widest row,
1325    /// and a row per action and one for the hints.
1326    pub fn size(&self) -> (usize, usize) {
1327        let label_width = self
1328            .actions
1329            .iter()
1330            .map(|action| cell_len(&action.label))
1331            .max()
1332            .unwrap_or(0);
1333        let key_width = self
1334            .actions
1335            .iter()
1336            .filter_map(|action| action.key.map(|key| cell_len(&key.to_string()) + 2))
1337            .max()
1338            .unwrap_or(0);
1339        let hints = cell_len(MENU_HINTS);
1340        (
1341            (4 + label_width + key_width).max(hints) + 2,
1342            self.actions.len() + 1,
1343        )
1344    }
1345
1346    /// The menu's rows at `width`, and a line of hints under them.
1347    pub fn render(&self, theme: &Theme, width: usize) -> Vec<Vec<Segment>> {
1348        let label_width = self
1349            .actions
1350            .iter()
1351            .map(|action| cell_len(&action.label))
1352            .max()
1353            .unwrap_or(0);
1354        let mut lines = Vec::new();
1355        for (index, action) in self.actions.iter().enumerate() {
1356            let focused = index == self.focus;
1357            let mut line = if focused {
1358                vec![text(format!("  {} ", theme.pointer), &theme.pointer_style)]
1359            } else {
1360                vec![plain("    ")]
1361            };
1362            let padding = label_width - cell_len(&action.label);
1363            line.push(if focused {
1364                text(action.label.clone(), &theme.focused)
1365            } else {
1366                plain(action.label.clone())
1367            });
1368            if let Some(key) = action.key {
1369                line.push(text(format!("{}  {key}", " ".repeat(padding)), &theme.hint));
1370            }
1371            lines.push(fit(line, width));
1372        }
1373        lines.push(fit(vec![text(MENU_HINTS.to_string(), &theme.hint)], width));
1374        lines
1375    }
1376}
1377
1378#[cfg(test)]
1379mod tests {
1380    use super::*;
1381
1382    #[test]
1383    fn highlights_matched_characters() {
1384        let matched = Style::parse("bold").unwrap();
1385        let segments = highlight("main.rs", &[0, 1, 5], None, &matched);
1386        let texts: Vec<(&str, bool)> = segments
1387            .iter()
1388            .map(|s| (s.text.as_str(), s.style.is_some()))
1389            .collect();
1390        assert_eq!(
1391            texts,
1392            [("ma", true), ("in.", false), ("r", true), ("s", false)]
1393        );
1394    }
1395
1396    #[test]
1397    fn pastes_drop_terminal_controls() {
1398        assert_eq!(
1399            pasted("X\x1bcY\u{9b}2JZ\x07\x08W\tV\r\nU", " "),
1400            "XcY2JZW V  U"
1401        );
1402        assert_eq!(pasted("a\nb", ""), "ab");
1403    }
1404
1405    #[test]
1406    fn fits_and_pads_lines() {
1407        let line = vec![plain("hello "), plain("world")];
1408        assert_eq!(width(&fit(line.clone(), 7)), 7);
1409        assert_eq!(width(&fit(line.clone(), 20)), 11);
1410        assert_eq!(width(&pad(line, 20)), 20);
1411    }
1412
1413    #[test]
1414    fn slices_and_overlays_by_cell() {
1415        let base = vec![plain("ab"), plain("日本"), plain("cd")];
1416        let cut: String = slice(&base, 3, 7).iter().map(|s| s.text.as_str()).collect();
1417        assert_eq!(cut, " 本c", "half a wide character is a space");
1418        let over = overlay(&base, 1, &[plain("XY")]);
1419        let text: String = over.iter().map(|s| s.text.as_str()).collect();
1420        assert_eq!(text, "aXY 本cd");
1421        let past = overlay(&[plain("ab")], 4, &[plain("Z")]);
1422        assert_eq!(width(&past), 5);
1423    }
1424
1425    #[test]
1426    fn a_list_follows_its_cursor() {
1427        let mut list = ListState::new();
1428        list.set_len(20);
1429        list.step(7, 5);
1430        assert_eq!((list.cursor(), list.offset()), (7, 3));
1431        assert_eq!(list.visible(5), 3..8);
1432        list.step(-100, 5);
1433        assert_eq!((list.cursor(), list.offset()), (0, 0));
1434        list.step(isize::MAX / 2, 5);
1435        assert_eq!(list.cursor(), 19);
1436        list.toggle(4);
1437        list.toggle(2);
1438        assert_eq!(list.selected(), [2, 4]);
1439        list.toggle(4);
1440        assert_eq!(list.selected_count(), 1);
1441    }
1442
1443    #[test]
1444    fn a_filter_ranks_and_hides() {
1445        let mut filter = FilterState::new(["alpha", "beta", "gamma"]);
1446        filter.set_hidden(vec![false, true, false]);
1447        filter.refilter();
1448        assert_eq!(filter.len(), 2, "hidden while nothing is typed");
1449        filter.set_query("ta");
1450        assert_eq!(filter.index(0), Some(1), "hidden ones still match a query");
1451        assert!(filter.pop());
1452        filter.clear();
1453        assert!(!filter.is_filtering());
1454        filter.set_highlight_limits(vec![2, 2, 2]);
1455        filter.set_query("ma");
1456        assert!(filter.matches()[0].1.iter().all(|p| *p < 2));
1457    }
1458
1459    #[test]
1460    fn a_text_buffer_edits_by_grapheme() {
1461        let mut buffer = TextBuffer::with_text("cafe\u{301} au lait");
1462        buffer.checkpoint();
1463        buffer.delete_word();
1464        assert_eq!(buffer.text(), "cafe\u{301} au ");
1465        buffer.home();
1466        buffer.right();
1467        buffer.right();
1468        buffer.right();
1469        buffer.right();
1470        assert_eq!(buffer.caret(), 5, "the accent moves with its letter");
1471        assert!(buffer.backspace());
1472        assert_eq!(buffer.text(), "caf au ");
1473        buffer.delete_to_start();
1474        assert_eq!(buffer.text(), " au ");
1475        assert!(buffer.undo());
1476        assert_eq!(buffer.text(), "cafe\u{301} au lait");
1477        let (shown, column) = TextBuffer::with_text("abcdefgh").window(4, Some('*'));
1478        assert_eq!((shown.as_str(), column), ("***", 3));
1479    }
1480
1481    #[test]
1482    fn a_divider_drags_within_its_minimums() {
1483        let mut divider = Divider::new(3).min(12);
1484        assert_eq!(divider.clamp(5, 80), 12);
1485        assert_eq!(divider.clamp(79, 80), 80 - 12 - 3);
1486        assert_eq!(divider.resolve(80, 36), 36);
1487        assert!(!divider.press(10, 36));
1488        assert!(divider.press(37, 36));
1489        assert!(divider.drag(50, 80));
1490        divider.release();
1491        assert!(!divider.drag(60, 80));
1492        assert_eq!(divider.resolve(80, 36), 50);
1493        divider.nudge(-100, 50, 80);
1494        assert_eq!(divider.resolve(80, 36), 12);
1495    }
1496}