use super::option::{SettingAvailability, SettingOption};
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum PickerNavResult {
Preview,
Commit,
Cancel,
ItemAction,
None,
}
#[derive(Debug, Clone)]
pub struct SettingsPickerController {
options: Vec<SettingOption>,
tabs: Vec<String>,
active_tab: usize,
query: String,
filtered: Vec<usize>,
selected_visible: usize,
original_id: String,
}
impl SettingsPickerController {
#[must_use]
pub fn new(options: Vec<SettingOption>, original_id: impl Into<String>) -> Self {
let mut tabs = vec!["all".to_string()];
for option in &options {
let tab = option.tab.as_ref();
if tab != "all" && !tabs.iter().any(|existing| existing == tab) {
tabs.push(tab.to_string());
}
}
let mut controller = Self {
options,
tabs,
active_tab: 0,
query: String::new(),
filtered: Vec::new(),
selected_visible: 0,
original_id: original_id.into(),
};
controller.recompute_filter(None);
if !controller.original_id.is_empty() {
let original = controller.original_id.clone();
controller.recompute_filter(Some(&original));
}
controller
}
#[must_use]
#[allow(dead_code)] pub fn options(&self) -> &[SettingOption] {
&self.options
}
#[must_use]
#[allow(dead_code)] pub fn tabs(&self) -> &[String] {
&self.tabs
}
#[must_use]
#[allow(dead_code)] pub fn active_tab(&self) -> usize {
self.active_tab
}
#[must_use]
pub fn active_tab_name(&self) -> &str {
self.tabs
.get(self.active_tab)
.map(String::as_str)
.unwrap_or("all")
}
#[must_use]
#[allow(dead_code)] pub fn query(&self) -> &str {
&self.query
}
#[must_use]
pub fn original_id(&self) -> &str {
&self.original_id
}
#[must_use]
pub fn visible(&self) -> &[usize] {
&self.filtered
}
#[must_use]
pub fn selected_visible(&self) -> usize {
self.selected_visible
}
#[must_use]
pub fn selected_source_index(&self) -> Option<usize> {
self.filtered.get(self.selected_visible).copied()
}
#[must_use]
pub fn selected_option(&self) -> Option<&SettingOption> {
self.selected_source_index()
.and_then(|idx| self.options.get(idx))
}
#[must_use]
pub fn selected_id(&self) -> Option<&str> {
self.selected_option().map(|option| option.id.as_ref())
}
pub fn recompute_filter(&mut self, prefer_source_id: Option<&str>) {
let tab = self.active_tab_name().to_string();
let query = self.query.to_ascii_lowercase();
let search_all_tabs = !query.is_empty();
self.filtered = self
.options
.iter()
.enumerate()
.filter(|(_, option)| {
(search_all_tabs || tab == "all" || option.tab.as_ref() == tab)
&& (query.is_empty()
|| option.id.to_ascii_lowercase().contains(&query)
|| option.label.to_ascii_lowercase().contains(&query)
|| option.summary.to_ascii_lowercase().contains(&query)
|| option.detail.to_ascii_lowercase().contains(&query))
})
.map(|(idx, _)| idx)
.collect();
if let Some(id) = prefer_source_id
&& let Some(visible) = self.filtered.iter().position(|&source| {
self.options
.get(source)
.is_some_and(|o| o.id.as_ref() == id)
})
{
self.selected_visible = visible;
return;
}
if self.filtered.is_empty() {
self.selected_visible = 0;
} else {
self.selected_visible = self
.selected_visible
.min(self.filtered.len().saturating_sub(1));
}
}
#[allow(dead_code)] pub fn set_query(&mut self, query: impl Into<String>) {
let keep = self.selected_id().map(str::to_string);
self.query = query.into();
self.recompute_filter(keep.as_deref());
}
pub fn push_query_char(&mut self, ch: char) {
let keep = self.selected_id().map(str::to_string);
self.query.push(ch);
self.recompute_filter(keep.as_deref());
}
pub fn pop_query_char(&mut self) {
let keep = self.selected_id().map(str::to_string);
self.query.pop();
self.recompute_filter(keep.as_deref());
}
pub fn clear_query(&mut self) {
let keep = self.selected_id().map(str::to_string);
self.query.clear();
self.recompute_filter(keep.as_deref());
}
pub fn set_active_tab(&mut self, tab_idx: usize) {
if tab_idx >= self.tabs.len() {
return;
}
let keep = self.selected_id().map(str::to_string);
self.active_tab = tab_idx;
self.recompute_filter(keep.as_deref());
}
pub fn next_tab(&mut self) {
if self.tabs.is_empty() {
return;
}
let next = (self.active_tab + 1) % self.tabs.len();
self.set_active_tab(next);
}
pub fn prev_tab(&mut self) {
if self.tabs.is_empty() {
return;
}
let prev = (self.active_tab + self.tabs.len() - 1) % self.tabs.len();
self.set_active_tab(prev);
}
pub fn select_source_index(&mut self, source: usize) -> PickerNavResult {
let Some(visible) = self.filtered.iter().position(|&idx| idx == source) else {
return PickerNavResult::None;
};
self.selected_visible = visible;
if self
.selected_option()
.is_some_and(|o| o.availability.is_available())
{
PickerNavResult::Preview
} else {
PickerNavResult::None
}
}
pub fn move_up(&mut self) -> PickerNavResult {
if self.filtered.is_empty() {
return PickerNavResult::None;
}
self.selected_visible =
(self.selected_visible + self.filtered.len() - 1) % self.filtered.len();
self.preview_if_available()
}
pub fn move_down(&mut self) -> PickerNavResult {
if self.filtered.is_empty() {
return PickerNavResult::None;
}
self.selected_visible = (self.selected_visible + 1) % self.filtered.len();
self.preview_if_available()
}
pub fn jump_home(&mut self) -> PickerNavResult {
if self.filtered.is_empty() {
return PickerNavResult::None;
}
self.selected_visible = 0;
self.preview_if_available()
}
pub fn jump_end(&mut self) -> PickerNavResult {
if self.filtered.is_empty() {
return PickerNavResult::None;
}
self.selected_visible = self.filtered.len().saturating_sub(1);
self.preview_if_available()
}
pub fn jump_digit(&mut self, digit: u8) -> PickerNavResult {
if !(1..=9).contains(&digit) || self.filtered.is_empty() {
return PickerNavResult::None;
}
let idx = usize::from(digit - 1);
if idx >= self.filtered.len() {
return PickerNavResult::None;
}
self.selected_visible = idx;
self.preview_if_available()
}
pub fn request_commit(&self) -> PickerNavResult {
match self.selected_option() {
Some(option) if option.availability.is_available() => PickerNavResult::Commit,
_ => PickerNavResult::None,
}
}
pub fn request_cancel(&self) -> PickerNavResult {
PickerNavResult::Cancel
}
pub fn request_item_action(&self) -> PickerNavResult {
match self.selected_option() {
Some(option) if option.action.is_some() && option.availability.is_available() => {
PickerNavResult::ItemAction
}
_ => PickerNavResult::None,
}
}
fn preview_if_available(&self) -> PickerNavResult {
match self.selected_option() {
Some(option) if option.availability.is_available() => PickerNavResult::Preview,
Some(SettingOption {
availability: SettingAvailability::Disabled { .. },
..
}) => PickerNavResult::None,
_ => PickerNavResult::None,
}
}
}