feat: llimphi standalone — framework UI soberano extraído del monorepo

Motor gráfico Llimphi como workspace independiente: bucle Elm
(input→update→view→layout→raster→present) sobre wgpu+vello+taffy+parley.
Núcleo (hal/raster/layout/text/ui/theme/surface/motion/icons) + ~40 widgets
+ módulos, sin dependencias al resto del monorepo. cargo check --workspace
pasa (64 crates). Puerta de entrada: cargo run -p llimphi-ui --example counter.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-04 04:23:42 +00:00
commit e65e9cc623
286 changed files with 46136 additions and 0 deletions
+155
View File
@@ -0,0 +1,155 @@
//! Smoke tests del cómputo de filas y el routing de teclas. Sin
//! backend gráfico — pruebas puras sobre `compute_rows` y `apply`.
use llimphi_module_diff_viewer::{
self as diff, DiffAction, DiffKind, DiffMsg, DiffState,
};
#[test]
fn diff_basico_inserts_y_deletes() {
let before = "a\nb\nc\n";
let after = "a\nB\nc\nd\n";
let (rows, stats) = diff::compute_rows(before, after);
// El diff esperado:
// = a / a
// - b
// + B
// = c / c
// + d
assert_eq!(stats.equals, 2);
assert_eq!(stats.deletes, 1);
assert_eq!(stats.inserts, 2);
assert_eq!(rows[0].kind, DiffKind::Equal);
assert_eq!(rows[0].left.as_ref().unwrap().text, "a");
assert_eq!(rows[0].right.as_ref().unwrap().text, "a");
// El primer cambio debe ser un Delete o Insert (similar agrupa);
// verificamos que B aparezca y b no.
let texts_left: Vec<&str> = rows
.iter()
.filter_map(|r| r.left.as_ref().map(|c| c.text.as_str()))
.collect();
let texts_right: Vec<&str> = rows
.iter()
.filter_map(|r| r.right.as_ref().map(|c| c.text.as_str()))
.collect();
assert!(texts_left.contains(&"b"));
assert!(texts_right.contains(&"B"));
assert!(texts_right.contains(&"d"));
}
#[test]
fn numeros_de_linea_son_correctos() {
let before = "alpha\nbeta\ngamma\n";
let after = "alpha\nBETA\ngamma\ndelta\n";
let (rows, _) = diff::compute_rows(before, after);
// alpha en línea 1 de ambos.
let alpha_row = rows.iter().find(|r| {
r.left.as_ref().map(|c| c.text == "alpha").unwrap_or(false)
}).unwrap();
assert_eq!(alpha_row.left.as_ref().unwrap().line_no, 1);
assert_eq!(alpha_row.right.as_ref().unwrap().line_no, 1);
// beta (delete) en línea 2 izquierda.
let beta_row = rows.iter().find(|r| {
r.left.as_ref().map(|c| c.text == "beta").unwrap_or(false)
}).unwrap();
assert_eq!(beta_row.left.as_ref().unwrap().line_no, 2);
assert!(beta_row.right.is_none());
// delta (insert) en línea 4 derecha.
let delta_row = rows.iter().find(|r| {
r.right.as_ref().map(|c| c.text == "delta").unwrap_or(false)
}).unwrap();
assert_eq!(delta_row.right.as_ref().unwrap().line_no, 4);
assert!(delta_row.left.is_none());
}
#[test]
fn textos_identicos_solo_equal() {
let text = "uno\ndos\ntres\n";
let (rows, stats) = diff::compute_rows(text, text);
assert_eq!(rows.len(), 3);
assert!(rows.iter().all(|r| r.kind == DiffKind::Equal));
assert_eq!(stats.inserts, 0);
assert_eq!(stats.deletes, 0);
assert_eq!(stats.equals, 3);
}
#[test]
fn scroll_no_excede_los_limites() {
let before = (0..50).map(|i| i.to_string()).collect::<Vec<_>>().join("\n");
let after = before.clone(); // identical → 50 Equal rows
let mut state = DiffState::new("a", "b", &before, &after);
assert_eq!(state.scroll, 0);
// Scroll grande hacia abajo: tope = 50 - visible_rows.
diff::apply(&mut state, DiffMsg::Scroll(1000), 10);
assert_eq!(state.scroll, 40);
// Scroll arriba: tope mínimo 0.
diff::apply(&mut state, DiffMsg::Scroll(-1000), 10);
assert_eq!(state.scroll, 0);
}
#[test]
fn next_hunk_salta_a_la_proxima_diferencia() {
// 20 líneas iguales + 2 cambios + 20 más. visible_rows=5 deja
// espacio real para scrollear.
let mut before = String::new();
let mut after = String::new();
for i in 0..20 {
before.push_str(&format!("eq{i}\n"));
after.push_str(&format!("eq{i}\n"));
}
before.push_str("DEL\n");
after.push_str("INS\n");
for i in 20..40 {
before.push_str(&format!("eq{i}\n"));
after.push_str(&format!("eq{i}\n"));
}
let mut state = DiffState::new("a", "b", &before, &after);
assert_eq!(state.scroll, 0);
diff::apply(&mut state, DiffMsg::NextHunk, 5);
assert!(state.scroll > 0, "scroll quedó en 0 — no saltó al hunk");
let row = &state.rows[state.scroll];
assert!(
!matches!(row.kind, DiffKind::Equal),
"esperaba aterrizar en un hunk, vi {:?}",
row.kind
);
// PrevHunk: vuelve al inicio (no hay hunk antes del primer cambio).
diff::apply(&mut state, DiffMsg::PrevHunk, 5);
// Puede quedarse en el mismo hunk si era el único accesible hacia
// atrás, o saltar más arriba. Lo único que verificamos es que no
// hubo panic ni scroll fuera de rango.
assert!(state.scroll < state.rows.len());
}
#[test]
fn escape_cierra() {
let mut state = DiffState::new("a", "b", "x\n", "y\n");
let action = diff::apply(&mut state, DiffMsg::Close, 10);
assert_eq!(action, DiffAction::Close);
}
#[test]
fn open_shortcut_es_ctrl_shift_d() {
use llimphi_ui::{Key, KeyEvent, KeyState, Modifiers};
let mk = |ctrl: bool, shift: bool, c: &str| KeyEvent {
key: Key::Character(c.into()),
state: KeyState::Pressed,
text: Some(c.into()),
modifiers: Modifiers { ctrl, shift, ..Modifiers::default() },
repeat: false,
};
assert!(diff::open_shortcut(&mk(true, true, "d")));
assert!(diff::open_shortcut(&mk(true, true, "D")));
assert!(!diff::open_shortcut(&mk(true, false, "d")));
assert!(!diff::open_shortcut(&mk(false, true, "d")));
}