tawasuyu/llimphi
Sovereign retained-mode GPU UI framework in Rust (wgpu+vello+taffy+parley), Elm loop, 40+ widgets.
chore: refresco desde el monorepo — vello 0.9 / wgpu 29, 120 miembros
1//! El **indicador deslizante** de `tabs`, medido sobre el árbol montado.
2//!
3//! Los tests de `llimphi-widget-tabs` certifican la aritmética
4//! (`indicator_bounds_px`); esto certifica lo otro: que el nodo que se pinta
5//! aterrice **donde dice la aritmética** una vez que taffy repartió la tira. Es
6//! la junta donde un indicador se rompe de verdad —queda bien calculado y mal
7//! colocado— y no se ve en un test puro.
8//!
9//! Sin GPU: se monta y se mide (CLAUDE.md §8).
10
11use llimphi_test::query::Ui;
12use llimphi_ui::{llimphi_layout::Style, View};
13use llimphi_widget_tabs::{
14 indicator_bounds_px, tab_widths, tabs_view, tabs_view_sliding, TabsPalette, TabsSpec,
15};
16
17#[derive(Clone, Debug, PartialEq)]
18enum Msg {
19 Select(usize),
20}
21
22/// Labels de **anchos bien distintos**: es el caso que el `segmented` no tiene
23/// y por el que este indicador estaba pendiente.
24fn labels() -> Vec<String> {
25 vec![
26 "Red".to_string(),
27 "Configuración avanzada del sistema".to_string(),
28 "Logs".to_string(),
29 ]
30}
31
32fn spec() -> TabsSpec<Msg, impl Fn(usize) -> Msg> {
33 TabsSpec {
34 labels: labels(),
35 active: 1,
36 on_select: |i: usize| Msg::Select(i),
37 content: View::<Msg>::new(Style::default()),
38 tab_height: 32.0,
39 palette: TabsPalette::from_theme(&llimphi_theme::Theme::default()),
40 tab_width: None,
41 on_close: None,
42 on_reorder: None,
43 strip_offset: 0.0,
44 on_strip_scroll: None,
45 }
46}
47
48/// ¿Hay un nodo con este rect (tolerancia de medio píxel)?
49fn hay_rect(ui: &Ui<Msg>, x: f32, w: f32, h: f32) -> bool {
50 ui.all().any(|n| {
51 let r = n.rect();
52 (r.x - x).abs() < 0.5 && (r.w - w).abs() < 0.5 && (r.h - h).abs() < 0.1
53 })
54}
55
56#[test]
57fn el_realce_aterriza_donde_dice_la_aritmetica() {
58 let anchos = tab_widths(&labels(), None, false);
59 // A mitad de camino entre el tab corto y el largo: ni una posición ni un
60 // ancho que coincidan con ningún tab, así que si el nodo está ahí es porque
61 // interpoló de verdad.
62 let (left, w) = indicator_bounds_px(&anchos, 0.5);
63 let ui = Ui::new(tabs_view_sliding(spec(), 0.5), 1200.0, 300.0);
64 assert!(
65 hay_rect(&ui, left, w, 2.0),
66 "no hay realce en ({left:.1}, {w:.1}):\n{}",
67 ui.dump()
68 );
69}
70
71#[test]
72fn el_modo_clasico_no_pinta_ningun_realce_flotante() {
73 let anchos = tab_widths(&labels(), None, false);
74 let (left, w) = indicator_bounds_px(&anchos, 0.5);
75 let ui = Ui::new(tabs_view(spec()), 1200.0, 300.0);
76 assert!(
77 !hay_rect(&ui, left, w, 2.0),
78 "el modo clásico no debería tener nada a mitad de camino entre dos tabs"
79 );
80}
81
82#[test]
83fn con_t_en_el_tab_activo_el_realce_lo_cubre_exacto() {
84 // El caso del host que no anima: `indicator_t = active`. El realce tiene
85 // que quedar clavado sobre el tab, no aproximado.
86 let anchos = tab_widths(&labels(), None, false);
87 let (left, w) = indicator_bounds_px(&anchos, 1.0);
88 let ui = Ui::new(tabs_view_sliding(spec(), 1.0), 1200.0, 300.0);
89 assert!(hay_rect(&ui, left, w, 2.0), "{}", ui.dump());
90 // Y ese ancho es exactamente el del tab activo, no una fracción del track.
91 assert!((w - anchos[1]).abs() < 0.01);
92}
93
94#[test]
95fn con_overflow_el_realce_viaja_con_la_tira() {
96 // Con `strip_offset` la fila interna está desplazada; el realce vive
97 // adentro, así que se corre con ella. Si el host tuviera que restarle el
98 // offset por su cuenta, este test lo delataría.
99 let anchos = tab_widths(&labels(), None, false);
100 let (left, w) = indicator_bounds_px(&anchos, 1.0);
101 let desplazamiento = 40.0_f32;
102 let mut s = spec();
103 s.strip_offset = desplazamiento;
104 s.on_strip_scroll = Some(std::sync::Arc::new(|_d: f32| Msg::Select(0)));
105 let ui = Ui::new(tabs_view_sliding(s, 1.0), 600.0, 300.0);
106 assert!(
107 hay_rect(&ui, left - desplazamiento, w, 2.0),
108 "el realce tendría que haberse corrido {desplazamiento} px:\n{}",
109 ui.dump()
110 );
111}