use aura_ui::gpu::{TextAlign, TextStyle, TextSystem, TextTraits, TextTransform};
use aura_ui::view::palette::{blue, slate};
use aura_ui::view::prelude::*;
struct Page(fn() -> AnyElement);
impl Component for Page {
fn view(&mut self, _: &mut ViewContext<Self>) -> impl IntoElement {
(self.0)()
}
}
fn painted(build: fn() -> AnyElement) -> Vec<aura_ui::gpu::Quad> {
let mut window = Window::new(Theme::dark(), Size::new(px(400.0), px(300.0)), 1.0);
window.set_root(Page(build));
window.draw(None);
window.scene().quads.to_vec()
}
fn cell(color: u32) -> Div {
div().bg(Rgba::hex(color)).h_10()
}
#[test]
fn la_cuadricula_reparte_columnas_iguales_y_respeta_el_hueco() {
let quads = painted(|| {
div()
.w(px(320.0))
.grid_cols_3()
.gap_x_2()
.child(cell(1))
.child(cell(2))
.child(cell(3))
.child(cell(4).col_span_2())
.into_element()
});
let near = |value: f32, expected: f32| assert!((value - expected).abs() <= 1.0, "{value} ≈ {expected}");
let track = (320.0 - 2.0 * 8.0) / 3.0;
near(quads[0].bounds[2], track);
near(quads[1].bounds[0], track + 8.0);
near(quads[2].bounds[0], 2.0 * (track + 8.0));
assert_eq!(quads[3].bounds[1], 40.0, "la cuarta celda baja a la segunda fila");
near(quads[3].bounds[2], 2.0 * track + 8.0);
}
#[test]
fn mx_auto_centra_y_flex_row_reverse_invierte() {
let quads = painted(|| div().w(px(400.0)).child(cell(1).w(px(100.0)).mx_auto()).into_element());
assert_eq!(quads[0].bounds[0], 150.0);
let quads =
painted(|| div().w(px(300.0)).flex_row_reverse().child(cell(1).w_20()).child(cell(2).w_20()).into_element());
assert_eq!((quads[0].bounds[0], quads[1].bounds[0]), (220.0, 140.0), "el primero queda a la derecha");
}
#[test]
fn bordes_y_radios_por_lado_y_esquina() {
let quads = painted(|| {
div().size_20().bg(slate(800)).border_t_4().border_b().rounded_t_lg().rounded_br_full().into_element()
});
assert_eq!(quads[0].border_widths, [4.0, 0.0, 1.0, 0.0], "arriba, derecha, abajo, izquierda");
assert_eq!(quads[0].corner_radii, [8.0, 8.0, 40.0, 0.0], "tl, tr, br, bl; `full` se limita a la mitad");
}
#[test]
fn el_anillo_rodea_la_caja_sin_ocupar_espacio() {
let quads = painted(|| div().p_4().child(div().size_10().bg(blue(500)).ring_2().rounded_md()).into_element());
let (ring, body) = (&quads[0], &quads[1]);
assert_eq!(body.bounds, [16.0, 16.0, 40.0, 40.0]);
assert_eq!(ring.bounds, [14.0, 14.0, 44.0, 44.0]);
assert_eq!(ring.border_widths, [2.0; 4]);
assert_eq!(ring.corner_radii, [8.0; 4], "el radio crece con el anillo");
}
#[test]
fn degradado_con_direccion_y_opacidad_en_pasos() {
let quads = painted(|| div().size_10().bg_gradient_to_r(blue(500), blue(900)).opacity_50().into_element());
assert_eq!(quads[0].gradient[0], 90.0_f32.to_radians().sin(), "hacia la derecha = 90°");
assert_eq!(quads[0].background.a, 0.5);
}
fn shape(traits: TextTraits, width: Option<f32>) -> (f32, f32) {
let mut system = TextSystem::new();
let style =
TextStyle { font_size: 14.0, line_height: 20.0, monospace: false, family: None, max_width: width, traits };
let shaped = system.shape("una línea larga de texto que no cabe en cien píxeles", &style);
(shaped.size.width.0, shaped.size.height.0)
}
#[test]
fn truncate_deja_una_linea_y_line_clamp_las_que_se_pidan() {
let free = shape(TextTraits::default(), Some(100.0));
assert!(free.1 > 40.0, "sin recorte se parte en varias líneas: {free:?}");
let one = shape(TextTraits { lines: 1, ..Default::default() }, Some(100.0));
assert_eq!(one.1, 20.0);
assert!(one.0 <= 100.5, "cabe en el ancho: {one:?}");
let two = shape(TextTraits { lines: 2, ..Default::default() }, Some(100.0));
assert_eq!(two.1, 40.0);
}
#[test]
fn tracking_ensancha_y_mayusculas_cambian_el_texto() {
let normal = shape(TextTraits::default(), None);
let wide = shape(TextTraits { tracking: 0.1, ..Default::default() }, None);
assert!(wide.0 > normal.0, "{wide:?} > {normal:?}");
let upper = shape(TextTraits { transform: Some(TextTransform::Upper), ..Default::default() }, None);
assert_ne!(upper.0, normal.0);
}
#[test]
fn la_alineacion_mueve_los_glifos_dentro_del_ancho() {
let first_x = |align| {
let mut system = TextSystem::new();
let style = TextStyle {
font_size: 14.0,
line_height: 20.0,
monospace: true,
family: None,
max_width: Some(300.0),
traits: TextTraits { align, ..Default::default() },
};
system.shape("hola", &style).caret_x(0)
};
assert_eq!(first_x(None), 0.0);
assert!(first_x(Some(TextAlign::Center)) > 100.0);
assert!(first_x(Some(TextAlign::Right)) > first_x(Some(TextAlign::Center)));
}
fn responsive() -> AnyElement {
div()
.w(px(700.0))
.flex_col()
.sm(|s| s.flex_row())
.md(|s| s.flex_col().gap_x_4())
.dark(|s| s.bg(slate(900)))
.light(|s| s.bg(slate(50)))
.child(cell(1).w_10())
.child(cell(2).w_10())
.into_element()
}
#[test]
fn los_puntos_de_corte_miran_el_ancho_de_la_ventana() {
let at = |width: f32| {
let mut window = Window::new(Theme::dark(), Size::new(px(width), px(300.0)), 1.0);
window.set_root(Page(responsive));
window.draw(None);
window.scene().quads.to_vec()
};
let row = at(700.0);
assert!(row[2].bounds[0] > row[1].bounds[0], "los hijos van uno al lado del otro");
let column = at(900.0);
assert_eq!(column[1].bounds[0], column[2].bounds[0]);
assert!(column[2].bounds[1] > column[1].bounds[1]);
assert_eq!(row[0].background, slate(900));
}
#[test]
fn el_ejemplo_del_readme_funciona() {
let painted = painted(|| {
div()
.flex_col()
.gap_4()
.p_6()
.rounded_xl()
.border()
.border_color(palette::slate(200))
.md(|s| s.flex_row().items_center())
.dark(|s| s.bg(palette::slate(900)).border_color(palette::slate(700)))
.child(text("Título").text_2xl().font_semibold().tracking_tight().truncate())
.child(div().w_1_3().h_10().rounded_lg().bg_gradient_to_r(palette::blue(500), palette::indigo(600)))
.into_element()
});
assert!(painted.len() >= 2);
}
struct Clicks(Vec<&'static str>);
impl Component for Clicks {
fn view(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let on = |name: &'static str| {
cx.listener(move |this: &mut Clicks, _: &ClickEvent, cx| {
this.0.push(name);
cx.notify();
})
};
div()
.relative()
.size_full()
.child(
div()
.id("abajo")
.absolute()
.top_0()
.left_0()
.size_20()
.bg(Rgba::hex(0xff0000))
.z_10()
.on_click(on("z-10")),
)
.child(
div().id("arriba").absolute().top_0().left_0().size_20().bg(Rgba::hex(0x0000ff)).on_click(on("normal")),
)
.into_element()
}
}
#[test]
fn z_index_ordena_el_pintado_y_el_raton() {
use aura_ui::core::{InputEvent, MouseButton};
let mut window = Window::new(Theme::dark(), Size::new(px(400.0), px(300.0)), 1.0);
let handle = window.set_root(Clicks(Vec::new()));
window.draw(None);
let colors: Vec<_> = window.scene().quads.iter().map(|quad| quad.background.b > 0.5).collect();
assert_eq!(colors, [true, false], "el azul (sin z) primero; el rojo (z-10) encima");
let position = Point::new(px(10.0), px(10.0));
window.handle_event(&InputEvent::MouseDown { position, button: MouseButton::Left });
window.handle_event(&InputEvent::MouseUp { position, button: MouseButton::Left });
assert_eq!(window.entities.read(handle).map(|view| view.0.clone()), Some(vec!["z-10"]));
}
fn window_with(build: fn() -> AnyElement) -> Window {
let mut window = Window::new(Theme::dark(), Size::new(px(100.0), px(100.0)), 1.0);
window.set_root(Page(build));
window.draw(None);
window
}
#[test]
fn las_transformaciones_viajan_con_su_lote_y_la_identidad_no_cuesta() {
let plain = window_with(|| div().size_10().bg(blue(500)).into_element());
assert!(plain.scene().transforms.is_empty(), "sin transformaciones no se guarda ninguna");
assert!(plain.scene().batches.iter().all(|batch| batch.transform == 0));
let spun = window_with(|| div().size_10().bg(blue(500)).rotate_45().scale_150().into_element());
let scene = spun.scene();
let batch = scene.batches.iter().find(|batch| batch.transform != 0).expect("un lote transformado");
let matrix = scene.transforms[batch.transform as usize];
assert!((matrix.scale_factor() - 1.5).abs() < 1e-4, "gira y escala 1,5×");
assert!(!matrix.is_axis_aligned());
let (x, y) = matrix.apply((20.0, 20.0));
assert!((x - 20.0).abs() < 1e-3 && (y - 20.0).abs() < 1e-3);
}
#[test]
fn trasladar_y_escalar_se_ven_tambien_por_cpu() {
let window = window_with(|| div().size_10().bg(Rgba::hex(0xff0000)).scale(2.0).translate_x_5().into_element());
let frame = aura_ui::gpu::cpu::CpuRenderer::new(None, 100, 100).capture(window.scene(), 1.0, Rgba::hex(0xffffff));
let red = |x: usize, y: usize| frame.rgba[(y * 100 + x) * 4..][..3] == [255, 0, 0];
assert!(red(70, 50) && red(2, 2), "ocupa x de 0 a 80 e y hasta 60");
assert!(!red(90, 50) && !red(40, 70), "y no más allá");
}
fn glass() -> AnyElement {
div()
.size_full()
.flex_row()
.child(div().w_1_2().h_full().bg(Rgba::hex(0x000000)))
.child(div().w_1_2().h_full().bg(Rgba::hex(0xffffff)))
.child(div().absolute().left_10().top_10().size_10().rounded_lg().backdrop_blur_md())
.into_element()
}
#[test]
fn el_desenfoque_de_fondo_difumina_la_frontera_solo_dentro_del_panel() {
let window = window_with(glass);
let scene = window.scene();
assert_eq!(scene.blurs.len(), 1);
assert_eq!(scene.blurs[0].sigma, 12.0);
assert_eq!(scene.blurs[0].bounds, [40.0, 40.0, 40.0, 40.0]);
let frame = aura_ui::gpu::cpu::CpuRenderer::new(None, 100, 100).capture(scene, 1.0, Rgba::hex(0x000000));
let gray = |x: usize, y: usize| frame.rgba[(y * 100 + x) * 4] as i32;
assert!(gray(10, 10) < 5 && gray(90, 10) > 250, "fuera del panel la frontera sigue seca");
assert!(
gray(49, 60) > 40 && gray(49, 60) < 215 && gray(51, 60) > 40,
"dentro del panel es un degradado: {} {}",
gray(49, 60),
gray(51, 60)
);
assert!(gray(49, 50) < gray(51, 50), "pero conserva el sentido: más claro a la derecha");
}
#[test]
fn blur_va_despues_de_los_hijos_y_backdrop_blur_antes_del_fondo() {
let window = window_with(|| {
div().size_10().bg(Rgba::hex(0x00ff00)).blur_sm().child(div().size_4().bg(Rgba::hex(0xff0000))).into_element()
});
let kinds: Vec<_> = window.scene().batches.iter().map(|batch| batch.kind).collect();
use aura_ui::gpu::scene::PrimitiveKind::{Blur, Quad};
assert_eq!(kinds, [Quad, Blur], "fondo e hijo (un solo lote de quads) y luego el desenfoque");
let window = window_with(|| div().size_10().bg(Rgba::hex(0x00ff00)).backdrop_blur_sm().into_element());
let kinds: Vec<_> = window.scene().batches.iter().map(|batch| batch.kind).collect();
assert_eq!(kinds, [Blur, Quad], "el desenfoque va antes del fondo propio");
}