use crate::tui::text_cursor::previous_char_boundary;
use super::EmojiPickerEntry;
use super::composer::{is_emoji_query_char, should_start_completion_query};
use super::scroll::clamp_list_scroll;
const EMOJI_PICKER_VISIBLE: usize = 8;
#[derive(Debug, Default)]
pub(in crate::tui::state) struct EmojiCompletionState {
active: Option<ActiveEmojiCompletion>,
}
#[derive(Debug)]
struct ActiveEmojiCompletion {
start: usize,
query: String,
candidates: Vec<EmojiPickerEntry>,
selected: usize,
scroll: usize,
}
impl EmojiCompletionState {
pub(in crate::tui::state) fn close(&mut self) {
self.active = None;
}
pub(in crate::tui::state) fn is_active(&self) -> bool {
self.active.is_some()
}
pub(in crate::tui::state) fn query(&self) -> Option<&str> {
self.active.as_ref().map(|active| active.query.as_str())
}
pub(in crate::tui::state) fn selected(&self) -> usize {
self.active
.as_ref()
.map(|active| active.selected)
.unwrap_or(0)
}
pub(in crate::tui::state) fn candidates(&self) -> &[EmojiPickerEntry] {
self.active
.as_ref()
.map(|active| active.candidates.as_slice())
.unwrap_or(&[])
}
pub(in crate::tui::state) fn start(&self) -> Option<usize> {
self.active.as_ref().map(|active| active.start)
}
pub(in crate::tui::state) fn selected_entry(&self) -> Option<&EmojiPickerEntry> {
self.active
.as_ref()
.and_then(|active| active.candidates.get(active.selected))
}
pub(in crate::tui::state) fn window_start(&self, visible_count: usize) -> usize {
match &self.active {
Some(active) if !active.candidates.is_empty() => clamp_list_scroll(
active.selected.min(active.candidates.len() - 1),
active.scroll,
visible_count.max(1),
active.candidates.len(),
),
_ => 0,
}
}
pub(in crate::tui::state) fn move_selection(&mut self, delta: isize) {
let Some(active) = self.active.as_mut() else {
return;
};
let len = active.candidates.len();
if len == 0 {
return;
}
let current = active.selected.min(len - 1) as isize;
active.selected = (current + delta).clamp(0, len as isize - 1) as usize;
active.scroll =
clamp_list_scroll(active.selected, active.scroll, EMOJI_PICKER_VISIBLE, len);
}
pub(in crate::tui::state) fn detect(value: &str, cursor: usize) -> Option<(usize, String)> {
let cursor = cursor.min(value.len());
let mut query_start = cursor;
while query_start > 0 {
let previous = previous_char_boundary(value, query_start);
let character = value[previous..query_start].chars().next()?;
if !is_emoji_query_char(character) {
break;
}
query_start = previous;
}
if query_start == 0 {
return None;
}
let colon_start = previous_char_boundary(value, query_start);
if &value[colon_start..query_start] != ":" {
return None;
}
let query = &value[query_start..cursor];
if query.chars().count() < 2 || !should_start_completion_query(&value[..colon_start]) {
return None;
}
Some((colon_start, query.to_owned()))
}
pub(in crate::tui::state) fn set(
&mut self,
detected: Option<(usize, String)>,
candidates: Vec<EmojiPickerEntry>,
) {
let Some((start, query)) = detected else {
self.active = None;
return;
};
if candidates.is_empty() {
self.active = None;
return;
}
let (selected, scroll) = match &self.active {
Some(active) => {
let selected = active.selected.min(candidates.len() - 1);
let scroll = clamp_list_scroll(
selected,
active.scroll,
EMOJI_PICKER_VISIBLE,
candidates.len(),
);
(selected, scroll)
}
None => (0, 0),
};
self.active = Some(ActiveEmojiCompletion {
start,
query,
candidates,
selected,
scroll,
});
}
}