aura-gui 0.2.2

UI nativa acelerada por GPU para escritorio, Android e iOS, en un solo crate: estilo tipo Tailwind, componentes, sombras, animaciones y agentes de IA. Reposo a cero. Licencia AGPL-3.0 o comercial.
//! Selector genérico con búsqueda (patrón Strategy: el `PickerDelegate` decide qué
//! se lista). Lo reutilizan la paleta, el buscador de archivos y los demás selectores.

use crate::core::{EntityMap, Handle};
use crate::view::uniform_list::UniformListState;
use crate::widgets::text_input::{InputChanged, TextInput};

pub trait PickerDelegate: Sized + 'static {
    fn placeholder(&self) -> String;
    fn match_count(&self) -> usize;
    /// Recalcula las coincidencias; puede terminar más tarde desde otro hilo con
    /// `entities.foreground().update(picker, …)`.
    fn update_matches(&mut self, query: &str, picker: Handle<Picker<Self>>, entities: &mut EntityMap);
    fn render_match(&self, index: usize, selected: bool) -> crate::view::any_element::AnyElement;
    fn confirm(&mut self, index: usize, entities: &mut EntityMap);
    /// Altura de cada fila de resultados.
    fn row_height(&self) -> f32 {
        32.0
    }
    /// Ancho del selector y alto máximo de su lista; las vistas con más texto por fila los amplían.
    fn width(&self) -> f32 {
        600.0
    }
    fn max_list_height(&self) -> f32 {
        360.0
    }
    /// Fichas de filtro encima de la lista: filas de `(etiqueta, activa)`. Sin filas no se dibuja nada.
    fn filters(&self) -> Vec<Vec<(String, bool)>> {
        Vec::new()
    }
    /// Se pulsó la ficha `index` de la fila `row`; después el picker recalcula con la consulta actual.
    fn select_filter(&mut self, _row: usize, _index: usize) {}
}

/// Lo emite el picker para que su dueño lo cierre.
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum PickerEvent {
    Confirmed,
    Dismissed,
}

pub struct Picker<D: PickerDelegate> {
    pub delegate: D,
    pub(crate) input: Handle<TextInput>,
    pub(crate) selected: usize,
    pub(crate) list: UniformListState,
}

impl<D: PickerDelegate> Picker<D> {
    /// Crea el picker con su campo de búsqueda enfocado y calcula las coincidencias iniciales.
    pub fn open(delegate: D, entities: &mut EntityMap) -> Handle<Picker<D>> {
        let focus = entities.focus.new_handle();
        let input = entities.insert(TextInput::new(focus).placeholder(delegate.placeholder()));
        let picker = entities.insert(Picker { delegate, input, selected: 0, list: UniformListState::default() });
        entities.subscribe(input, picker, |picker: &mut Picker<D>, event: &InputChanged, cx| {
            let handle = cx.handle();
            picker.selected = 0;
            picker.list.reset();
            picker.delegate.update_matches(&event.0, handle, cx);
        });
        entities.focus.focus(&focus);
        entities.update(picker, |picker, cx| {
            let handle = cx.handle();
            picker.delegate.update_matches("", handle, cx);
        });
        picker
    }

    pub fn query(&self, entities: &EntityMap) -> String {
        entities.read(self.input).map(|input| input.text().to_owned()).unwrap_or_default()
    }

    pub fn selected(&self) -> usize {
        self.selected
    }

    pub fn select(&mut self, index: usize) {
        let count = self.delegate.match_count();
        if count == 0 {
            self.selected = 0;
            return;
        }
        self.selected = index.min(count - 1);
        self.list.scroll_to_item(self.selected, self.delegate.row_height());
    }

    pub fn select_relative(&mut self, delta: isize) {
        let count = self.delegate.match_count() as isize;
        if count > 0 {
            self.select((self.selected as isize + delta).rem_euclid(count) as usize);
        }
    }

    /// Activa una ficha de filtro y vuelve a filtrar con lo escrito.
    pub fn apply_filter(&mut self, row: usize, index: usize, handle: Handle<Picker<D>>, entities: &mut EntityMap) {
        self.delegate.select_filter(row, index);
        let query = self.query(entities);
        self.selected = 0;
        self.list.reset();
        self.delegate.update_matches(&query, handle, entities);
    }

    /// `Tab`: pasa a la ficha siguiente (o anterior) de la última fila de filtros.
    pub fn cycle_filter(&mut self, delta: isize, handle: Handle<Picker<D>>, entities: &mut EntityMap) {
        let filters = self.delegate.filters();
        let Some(row) = filters.len().checked_sub(1) else { return };
        let count = filters[row].len() as isize;
        let active = filters[row].iter().position(|(_, on)| *on).unwrap_or(0) as isize;
        if count > 0 {
            self.apply_filter(row, (active + delta).rem_euclid(count) as usize, handle, entities);
        }
    }

    /// Avisa de que las coincidencias cambiaron (por ejemplo, al terminar una búsqueda).
    pub fn matches_updated(&mut self) {
        let index = self.selected;
        self.select(index);
    }
}