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.
//! Vista del picker: campo de búsqueda, lista virtualizada y atajos del contexto `Picker`.

use crate::core::px;
use crate::view::keymap::Keymap;
use crate::view::prelude::*;
use crate::widgets::picker::{Picker, PickerDelegate, PickerEvent};
use crate::widgets::separator::horizontal_separator;

pub const SELECT_NEXT: Action = Action("picker::SelectNext");
pub const SELECT_PREVIOUS: Action = Action("picker::SelectPrevious");
pub const CONFIRM: Action = Action("picker::Confirm");
pub const DISMISS: Action = Action("picker::Dismiss");
pub const NEXT_FILTER: Action = Action("picker::NextFilter");
pub const PREVIOUS_FILTER: Action = Action("picker::PreviousFilter");

pub fn bind_picker_keys(keymap: &mut Keymap) {
    let keys = [
        ("down", SELECT_NEXT),
        ("up", SELECT_PREVIOUS),
        ("enter", CONFIRM),
        ("escape", DISMISS),
        ("tab", NEXT_FILTER),
        ("shift-tab", PREVIOUS_FILTER),
    ];
    for (key, action) in keys {
        if let Err(error) = keymap.bind(key, Some(action), Some("Picker")) {
            eprintln!("atajo inválido: {error}");
        }
    }
}

impl<D: PickerDelegate> Picker<D> {
    fn confirm_selected(&mut self, cx: &mut ListenerContext<Self>) {
        if self.delegate.match_count() == 0 {
            return;
        }
        let index = self.selected;
        self.delegate.confirm(index, cx);
        cx.emit(PickerEvent::Confirmed);
        cx.notify();
    }
}

impl<D: PickerDelegate> Picker<D> {
    /// Una fila de fichas por grupo de filtros; la activa va resaltada.
    fn filter_rows(&self, cx: &mut ViewContext<Self>) -> Vec<AnyElement> {
        let rows = self.delegate.filters();
        rows.into_iter()
            .enumerate()
            .map(|(row, chips)| {
                let chips = chips.into_iter().enumerate().map(|(index, (label, on))| {
                    div()
                        .id(format!("picker-filter-{row}-{index}"))
                        .px_2()
                        .py(px(2.0))
                        .rounded_full()
                        .border_1()
                        .text_xs()
                        .whitespace_nowrap()
                        .cursor_pointer()
                        .border_color(if on { Token::Accent } else { Token::BorderSubtle })
                        .text_color(if on { Token::Accent } else { Token::TextMuted })
                        .when(on, |chip| chip.bg(Token::Active))
                        .hover(|style| style.bg(Token::Hover))
                        .on_click(cx.listener(move |this: &mut Picker<D>, _: &ClickEvent, cx| {
                            let handle = cx.handle();
                            this.apply_filter(row, index, handle, cx);
                            cx.notify();
                        }))
                        .child(label)
                        .into_element()
                });
                div().flex_row().flex_wrap().gap_1().px_1().children(chips).into_element()
            })
            .collect()
    }
}

impl<D: PickerDelegate> Render for Picker<D> {
    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement {
        let row_height = self.delegate.row_height();
        let count = self.delegate.match_count();
        let selected = self.selected;
        let delegate = &self.delegate;
        let list = uniform_list(&self.list, count, px(row_height), |range| {
            range
                .map(|index| {
                    div()
                        .w_full()
                        .h(px(row_height))
                        .rounded_md()
                        .when(index == selected, |row| row.bg(Token::Selection))
                        .hover(|style| style.bg(Token::Hover))
                        .on_click(cx.listener(move |this: &mut Picker<D>, _: &ClickEvent, cx| {
                            this.select(index);
                            this.confirm_selected(cx);
                        }))
                        .child(delegate.render_match(index, index == selected))
                        .into_element()
                })
                .collect()
        })
        .w_full()
        .h(px((count as f32 * row_height).min(self.delegate.max_list_height())));
        let input = cx.render_child(self.input);
        let filters = self.filter_rows(cx);

        div()
            .key_context("Picker")
            .occlude()
            .flex_col()
            .w(px(self.delegate.width()))
            .p_2()
            .gap_1()
            .rounded_lg()
            .border_1()
            .border_color(Token::Border)
            .bg(Token::ElevatedSurface)
            .shadow_lg()
            .on_action(
                SELECT_NEXT,
                cx.listener(|this: &mut Picker<D>, _: &ActionEvent, cx| {
                    this.select_relative(1);
                    cx.notify();
                }),
            )
            .on_action(
                SELECT_PREVIOUS,
                cx.listener(|this: &mut Picker<D>, _: &ActionEvent, cx| {
                    this.select_relative(-1);
                    cx.notify();
                }),
            )
            .on_action(CONFIRM, cx.listener(|this: &mut Picker<D>, _: &ActionEvent, cx| this.confirm_selected(cx)))
            .on_action(
                DISMISS,
                cx.listener(|_: &mut Picker<D>, _: &ActionEvent, cx| {
                    cx.emit(PickerEvent::Dismissed);
                    cx.notify();
                }),
            )
            .on_action(
                NEXT_FILTER,
                cx.listener(|this: &mut Picker<D>, _: &ActionEvent, cx| {
                    let handle = cx.handle();
                    this.cycle_filter(1, handle, cx);
                    cx.notify();
                }),
            )
            .on_action(
                PREVIOUS_FILTER,
                cx.listener(|this: &mut Picker<D>, _: &ActionEvent, cx| {
                    let handle = cx.handle();
                    this.cycle_filter(-1, handle, cx);
                    cx.notify();
                }),
            )
            .child(input)
            .children(filters)
            .child(horizontal_separator())
            .when(count == 0, |panel| {
                panel.child(div().p_3().text_sm().text_color(Token::TextMuted).child("Sin resultados"))
            })
            .child(list)
            .into_element()
    }
}