aura-gui 0.3.0

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.
//! Lista de alto variable: construye solo lo visible, mide y conserva el ancla.

use aura_ui::core::InputEvent;
use aura_ui::view::prelude::*;
use aura_ui::view::test_context::TestWindow;

const COUNT: usize = 1_000;

fn height(index: usize) -> f32 {
    20.0 + (index % 3) as f32 * 15.0
}

struct Feed {
    state: ListState,
    built: Vec<usize>,
}

impl Component for Feed {
    fn view(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
        self.built.clear();
        let built = &mut self.built;
        let feed = list(&self.state, |index| {
            built.push(index);
            div()
                .h(px(height(index)))
                .id(format!("row-{index}"))
                .on_click(|_, _| {})
                .child(format!("mensaje {index}"))
                .into_element()
        })
        .h(px(200.0))
        .w_full();
        div().size_full().child(feed).into_element()
    }
}

fn feed() -> (TestWindow, Handle<Feed>) {
    let (mut test, feed) = TestWindow::new(|_| Feed { state: ListState::new(COUNT, px(30.0)), built: Vec::new() });
    test.run_until_parked();
    (test, feed)
}

#[test]
fn builds_only_what_fits_and_learns_real_heights() {
    let (test, feed) = feed();
    test.read(feed, |feed| {
        assert!(feed.built.len() < 20, "construyó {} de {COUNT}", feed.built.len());
        assert_eq!(feed.state.measured_height(1), Some(35.0));
        assert_eq!(feed.state.measured_height(500), None, "lo nunca visto sigue estimado");
    });
}

#[test]
fn wheel_scroll_moves_the_anchor() {
    let (mut test, feed) = feed();
    test.window.handle_event(&InputEvent::Scroll {
        position: Point::new(px(10.0), px(10.0)),
        delta: Point::new(px(0.0), px(-1_000.0)),
    });
    test.window.draw(None);
    test.run_until_parked();
    test.read(feed, |feed| {
        let first = feed.built[0];
        assert!((25..40).contains(&first), "tras 1000 px el primero es {first}");
    });
}

#[test]
fn inserting_above_keeps_the_visible_item_in_place() {
    let (mut test, feed) = feed();
    let state = test.read(feed, |feed| feed.state.clone());
    state.scroll_to_item(100);
    test.window.draw(None);
    test.run_until_parked();
    state.splice(0..0, 5);
    test.window.draw(None);
    test.run_until_parked();
    test.read(feed, |feed| assert_eq!(feed.built[0], 105, "el mismo mensaje sigue arriba"));
}

#[test]
fn dragging_the_list_scrollbar_continues_when_visible_rows_change() {
    let (mut test, feed) = feed();
    // La barra vive en el borde derecho de la lista (800 px de ancho).
    test.simulate_drag((794.0, 10.0), (794.0, 190.0));
    test.run_until_parked();
    test.read(feed, |feed| {
        assert!(feed.built[0] > 800, "el arrastre siguió hasta abajo; primero visible: {}", feed.built[0]);
    });
}

#[test]
fn following_the_end_shows_new_items_until_the_wheel_goes_up() {
    let (mut test, feed) = feed();
    let state = test.read(feed, |feed| feed.state.clone());
    state.follow_end();
    test.window.draw(None);
    test.run_until_parked();
    test.read(feed, |feed| assert_eq!(feed.built.last(), Some(&(COUNT - 1)), "el final está a la vista"));
    state.splice(COUNT..COUNT, 3);
    test.window.draw(None);
    test.run_until_parked();
    test.read(feed, |feed| assert_eq!(feed.built.last(), Some(&(COUNT + 2)), "sigue lo que llega"));
    test.window.handle_event(&InputEvent::Scroll {
        position: Point::new(px(10.0), px(10.0)),
        delta: Point::new(px(0.0), px(400.0)),
    });
    test.window.draw(None);
    test.run_until_parked();
    assert!(!state.is_following_end(), "subir suelta el final");
    state.splice(COUNT + 3..COUNT + 3, 1);
    test.window.draw(None);
    test.run_until_parked();
    test.read(feed, |feed| assert!(feed.built.last() < Some(&(COUNT + 3)), "no salta al nuevo"));
}

struct Clicks {
    state: ListState,
    clicked: std::rc::Rc<std::cell::RefCell<Vec<usize>>>,
}

impl Component for Clicks {
    fn view(&mut self, _cx: &mut ViewContext<Self>) -> impl IntoElement {
        let clicked = self.clicked.clone();
        let rows = list(&self.state, |index| {
            let clicked = clicked.clone();
            div().h(px(height(index))).w_full().on_click(move |_, _| clicked.borrow_mut().push(index)).into_element()
        });
        div().size_full().child(rows.h(px(200.0)).w_full()).into_element()
    }
}

#[test]
fn following_the_end_keeps_short_content_at_the_bottom() {
    let clicked = std::rc::Rc::default();
    let shared = std::rc::Rc::clone(&clicked);
    let (mut test, _) = TestWindow::new(move |_| {
        let state = ListState::new(2, px(30.0));
        state.follow_end();
        Clicks { state, clicked: shared }
    });
    test.run_until_parked();
    // Alturas 20 y 35 en 200 px: arriba queda hueco y el último termina en el borde inferior.
    for y in [10.0, 150.0, 190.0] {
        test.simulate_click(10.0, y);
        test.run_until_parked();
    }
    assert_eq!(*clicked.borrow(), vec![0, 1]);
}