use compact_str::CompactString;
use serde::{Deserialize, Serialize};
use smallvec::SmallVec;
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Candidate {
pub text: CompactString,
pub annotation: Option<CompactString>,
pub kind: CandidateKind,
}
impl Candidate {
pub fn new(text: impl Into<CompactString>) -> Self {
Self {
text: text.into(),
annotation: None,
kind: CandidateKind::Normal,
}
}
pub fn with_annotation(mut self, annotation: impl Into<CompactString>) -> Self {
self.annotation = Some(annotation.into());
self
}
pub fn with_kind(mut self, kind: CandidateKind) -> Self {
self.kind = kind;
self
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Default, Serialize, Deserialize)]
pub enum CandidateKind {
#[default]
Normal,
UserDictionary,
Learned,
Prediction,
Symbol,
Emoji,
}
#[derive(Debug, Clone, Default)]
pub struct CandidateList {
candidates: SmallVec<[Candidate; 16]>,
selected: usize,
page_size: usize,
page_start: usize,
}
impl CandidateList {
pub fn new() -> Self {
Self {
candidates: SmallVec::new(),
selected: 0,
page_size: 9,
page_start: 0,
}
}
pub fn with_candidates(candidates: impl IntoIterator<Item = Candidate>) -> Self {
Self {
candidates: candidates.into_iter().collect(),
selected: 0,
page_size: 9,
page_start: 0,
}
}
pub fn with_page_size(mut self, size: usize) -> Self {
self.page_size = size.max(1);
self
}
pub fn is_empty(&self) -> bool {
self.candidates.is_empty()
}
pub fn len(&self) -> usize {
self.candidates.len()
}
pub fn selected(&self) -> usize {
self.selected
}
pub fn selected_candidate(&self) -> Option<&Candidate> {
self.candidates.get(self.selected)
}
pub fn candidates(&self) -> &[Candidate] {
&self.candidates
}
pub fn current_page(&self) -> &[Candidate] {
let end = (self.page_start + self.page_size).min(self.candidates.len());
&self.candidates[self.page_start..end]
}
pub fn page_info(&self) -> (usize, usize) {
let total = self.candidates.len().div_ceil(self.page_size);
let current = self.page_start / self.page_size;
(current, total)
}
pub fn select_next(&mut self) {
if self.candidates.is_empty() {
return;
}
self.selected = (self.selected + 1) % self.candidates.len();
self.update_page();
}
pub fn select_prev(&mut self) {
if self.candidates.is_empty() {
return;
}
if self.selected == 0 {
self.selected = self.candidates.len() - 1;
} else {
self.selected -= 1;
}
self.update_page();
}
pub fn select_by_number(&mut self, num: usize) -> bool {
if num == 0 || num > self.page_size {
return false;
}
let index = self.page_start + num - 1;
if index < self.candidates.len() {
self.selected = index;
true
} else {
false
}
}
pub fn next_page(&mut self) {
let next = self.page_start + self.page_size;
if next < self.candidates.len() {
self.page_start = next;
self.selected = self.page_start;
}
}
pub fn prev_page(&mut self) {
if self.page_start >= self.page_size {
self.page_start -= self.page_size;
self.selected = self.page_start;
} else if self.page_start > 0 {
self.page_start = 0;
self.selected = 0;
}
}
fn update_page(&mut self) {
if self.selected < self.page_start || self.selected >= self.page_start + self.page_size {
self.page_start = (self.selected / self.page_size) * self.page_size;
}
}
pub fn set_candidates(&mut self, candidates: impl IntoIterator<Item = Candidate>) {
self.candidates = candidates.into_iter().collect();
self.selected = 0;
self.page_start = 0;
}
pub fn clear(&mut self) {
self.candidates.clear();
self.selected = 0;
self.page_start = 0;
}
pub fn add(&mut self, candidate: Candidate) {
self.candidates.push(candidate);
}
}
#[cfg(test)]
mod tests {
use super::{Candidate, CandidateList};
use vize_carton::cstr;
#[test]
fn test_candidate_new() {
let candidate = Candidate::new("日本");
assert_eq!(candidate.text.as_str(), "日本");
assert!(candidate.annotation.is_none());
}
#[test]
fn test_candidate_with_annotation() {
let candidate = Candidate::new("日本").with_annotation("にほん");
assert_eq!(candidate.annotation.as_deref(), Some("にほん"));
}
#[test]
fn test_candidate_list_new() {
let list = CandidateList::new();
assert!(list.is_empty());
assert_eq!(list.selected(), 0);
}
#[test]
fn test_candidate_list_navigation() {
let mut list = CandidateList::with_candidates(vec![
Candidate::new("日本"),
Candidate::new("二本"),
Candidate::new("にほん"),
]);
assert_eq!(list.selected(), 0);
list.select_next();
assert_eq!(list.selected(), 1);
list.select_next();
assert_eq!(list.selected(), 2);
list.select_next();
assert_eq!(list.selected(), 0);
list.select_prev();
assert_eq!(list.selected(), 2); }
#[test]
fn test_candidate_list_select_by_number() {
let mut list = CandidateList::with_candidates(vec![
Candidate::new("a"),
Candidate::new("b"),
Candidate::new("c"),
]);
assert!(list.select_by_number(2));
assert_eq!(list.selected(), 1);
assert!(!list.select_by_number(10)); }
#[test]
fn test_candidate_list_pagination() {
let candidates: Vec<_> = (0..25).map(|i| Candidate::new(cstr!("item{i}"))).collect();
let mut list = CandidateList::with_candidates(candidates).with_page_size(10);
assert_eq!(list.page_info(), (0, 3));
assert_eq!(list.current_page().len(), 10);
list.next_page();
assert_eq!(list.page_info(), (1, 3));
assert_eq!(list.selected(), 10);
list.next_page();
assert_eq!(list.page_info(), (2, 3));
assert_eq!(list.current_page().len(), 5);
}
}