refresh: stack al día (vello 0.7 / wgpu 27 / parley 0.6) + motor 3D voxel
Re-sincroniza las fuentes desde el monorepo (estaba en vello 0.5/wgpu 24 y con la estructura vieja de eventloop) y suma el 3D: - bump del workspace a vello 0.7 / wgpu 27 / parley 0.6, + accesskit 0.24 / accesskit_winit 0.33 / vello_hybrid 0.0.9. - nuevos crates: llimphi-3d (voxels ray-march + mallas en un depth compartido, montable dentro de un View 2D vía set_viewport+scissor) y llimphi-voxel (world-gen, personajes, director de escenas) + shared/foreign-vox (puente .vox). - README: sección "Not just 2D — a 3D voxel engine" + GIF (docs/llimphi_voxel.gif). - excluido modules/allichay (arrastra deps fuera del alcance del front-door). - cargo check --workspace: verde. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -15,7 +15,7 @@
|
||||
#![forbid(unsafe_code)]
|
||||
|
||||
use llimphi_ui::llimphi_layout::taffy::{
|
||||
prelude::{length, percent, Size, Style},
|
||||
prelude::{auto, length, percent, Size, Style},
|
||||
AlignItems, Rect,
|
||||
};
|
||||
use llimphi_ui::llimphi_raster::peniko::Color;
|
||||
@@ -33,6 +33,10 @@ pub struct TextInputPalette {
|
||||
pub border_focus: Color,
|
||||
pub fg_text: Color,
|
||||
pub fg_placeholder: Color,
|
||||
/// Color del caret (cursor de inserción) que se pinta cuando el input
|
||||
/// está focado. Default = `fg_text` (sigue al texto, como `caret-color:
|
||||
/// auto` en CSS).
|
||||
pub caret: Color,
|
||||
}
|
||||
|
||||
impl Default for TextInputPalette {
|
||||
@@ -51,6 +55,7 @@ impl TextInputPalette {
|
||||
border_focus: t.border_focus,
|
||||
fg_text: t.fg_text,
|
||||
fg_placeholder: t.fg_placeholder,
|
||||
caret: t.fg_text,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -133,7 +138,11 @@ impl TextInputState {
|
||||
}
|
||||
|
||||
/// Compone el input box: borde de 1 px (rect padre coloreado), relleno
|
||||
/// interno, texto o placeholder, caret simulado al final si está focado.
|
||||
/// interno, texto o placeholder, y el caret (cursor de inserción) sobre el
|
||||
/// texto si está focado. Caret v3 (Fase 7.1255): cuando está focado la hoja
|
||||
/// pinta texto+caret en un `paint_over` con **scroll horizontal** — el texto
|
||||
/// se desplaza para mantener el caret a la vista cuando desborda la caja, y se
|
||||
/// recorta al área de contenido. Sin foco usa un nodo-hijo de texto (sin caret).
|
||||
/// Click sobre el box emite `on_focus` (típicamente `Msg::Focus(Field)`).
|
||||
pub fn text_input_view<Msg: Clone + 'static>(
|
||||
state: &TextInputState,
|
||||
@@ -151,9 +160,18 @@ pub fn text_input_view<Msg: Clone + 'static>(
|
||||
} else {
|
||||
raw
|
||||
};
|
||||
// El cambio de bg al focus ya transmite "este es el activo"; sin
|
||||
// caret glyph (la fuente default rendea cuadrados de fallback).
|
||||
let display = shown;
|
||||
// Prefijo del texto visible hasta el caret (cursor de inserción), para
|
||||
// medir su ancho y posicionar la barra del caret. La columna es índice de
|
||||
// carácter (single-line ⇒ `line == 0`); `take(col)` sobre el texto MOSTRADO
|
||||
// (placeholder/`•`/crudo) alinea el caret con lo que se ve. Cuando el input
|
||||
// está vacío el `col` es 0 ⇒ prefijo vacío ⇒ caret al inicio (no se mide el
|
||||
// placeholder).
|
||||
let caret_prefix: String = if focused {
|
||||
display.chars().take(state.editor().cursor.caret.col).collect()
|
||||
} else {
|
||||
String::new()
|
||||
};
|
||||
let text_color = if is_empty {
|
||||
palette.fg_placeholder
|
||||
} else {
|
||||
@@ -165,7 +183,7 @@ pub fn text_input_view<Msg: Clone + 'static>(
|
||||
(palette.bg, palette.border)
|
||||
};
|
||||
|
||||
let inner = View::new(Style {
|
||||
let mut inner = View::new(Style {
|
||||
size: Size {
|
||||
width: percent(1.0_f32),
|
||||
height: percent(1.0_f32),
|
||||
@@ -173,15 +191,90 @@ pub fn text_input_view<Msg: Clone + 'static>(
|
||||
padding: Rect {
|
||||
left: length(10.0_f32),
|
||||
right: length(10.0_f32),
|
||||
top: length(6.0_f32),
|
||||
bottom: length(6.0_f32),
|
||||
top: length(0.0_f32),
|
||||
bottom: length(0.0_f32),
|
||||
},
|
||||
align_items: Some(AlignItems::Center),
|
||||
..Default::default()
|
||||
})
|
||||
.fill(bg)
|
||||
.radius(3.0)
|
||||
.text_aligned(display, 13.0, text_color, Alignment::Start);
|
||||
.radius(3.0);
|
||||
let inner = if focused {
|
||||
// Caret v3 — scroll horizontal (Fase 7.1255). Cuando el input está
|
||||
// focado, la propia hoja pinta el texto Y el caret en un solo `paint_over`
|
||||
// (pasada vello FINAL): así puede DESPLAZAR el texto a la izquierda cuando
|
||||
// el cursor se saldría por el borde derecho, manteniéndolo visible — el
|
||||
// clásico scroll del caret de los `<input>`. El offset (`scroll`) depende
|
||||
// del ancho de layout y de la posición del caret, ambos conocidos sólo en
|
||||
// tiempo de pintado (acá `rect.w` ya está resuelto y `ts` puede medir), no
|
||||
// en `view()` — por eso no se hace con un nodo hijo + `transform` estático.
|
||||
// Sin foco se usa el camino de nodo-hijo de abajo (sin caret, sin scroll).
|
||||
let caret_color = palette.caret;
|
||||
let display_c = display;
|
||||
let caret_prefix_c = caret_prefix;
|
||||
let tcolor = text_color;
|
||||
inner.paint_over(move |scene, ts, rect| {
|
||||
use llimphi_ui::llimphi_raster::kurbo::{Affine, Rect as KRect};
|
||||
use llimphi_ui::llimphi_raster::peniko::{BlendMode, Fill};
|
||||
use llimphi_ui::llimphi_text::{draw_layout, measurement, Alignment};
|
||||
let pad = 10.0_f64;
|
||||
// Ancho visible interno (entre los dos paddings de 10 px).
|
||||
let vis_w = (rect.w as f64 - 2.0 * pad).max(0.0);
|
||||
// Layout del texto completo en una sola línea (sin wrap).
|
||||
let layout = ts.layout(
|
||||
&display_c, 13.0, None, Alignment::Start, 1.2, false, None, 400.0, false, false,
|
||||
0.0, 0.0,
|
||||
);
|
||||
let th = measurement(&layout).height as f64;
|
||||
// Ancho del prefijo hasta el caret = posición x del caret en el texto.
|
||||
let caret_w = if caret_prefix_c.is_empty() {
|
||||
0.0
|
||||
} else {
|
||||
let lp = ts.layout(
|
||||
&caret_prefix_c, 13.0, None, Alignment::Start, 1.2, false, None, 400.0, false,
|
||||
false, 0.0, 0.0,
|
||||
);
|
||||
measurement(&lp).width as f64
|
||||
};
|
||||
// Scroll: si el caret cae más allá del ancho visible, corre el texto a
|
||||
// la izquierda lo justo para que el caret quede al borde (con 2 px de
|
||||
// aire). Texto que entra ⇒ scroll 0 (anclado al padding-left).
|
||||
let scroll = (caret_w - vis_w + 2.0).max(0.0);
|
||||
let cx0 = rect.x as f64 + pad;
|
||||
// Recorte al área de contenido para que el texto desplazado no se
|
||||
// derrame sobre el padding ni fuera de la caja.
|
||||
let clip = KRect::new(
|
||||
cx0,
|
||||
rect.y as f64,
|
||||
rect.x as f64 + rect.w as f64 - pad,
|
||||
rect.y as f64 + rect.h as f64,
|
||||
);
|
||||
scene.push_layer(Fill::NonZero, BlendMode::default(), 1.0, Affine::IDENTITY, &clip);
|
||||
let oy = rect.y as f64 + (rect.h as f64 - th) * 0.5;
|
||||
draw_layout(scene, &layout, tcolor, (cx0 - scroll, oy));
|
||||
scene.pop_layer();
|
||||
// Caret: barra vertical en la posición del caret, desplazada por el
|
||||
// mismo scroll. Fuera del clip para que nunca se recorte en el borde.
|
||||
let x = cx0 + caret_w - scroll;
|
||||
let h = 16.0_f64;
|
||||
let cy = rect.y as f64 + rect.h as f64 * 0.5;
|
||||
let bar = KRect::new(x, cy - h * 0.5, x + 1.5, cy + h * 0.5);
|
||||
scene.fill(Fill::NonZero, Affine::IDENTITY, caret_color, None, &bar);
|
||||
})
|
||||
} else {
|
||||
// Sin foco: el texto va en un nodo HIJO de alto automático, centrado
|
||||
// verticalmente por el contenedor (`align_items: Center`). (`align_items`
|
||||
// no centra el texto PROPIO de un nodo — por eso el hijo.)
|
||||
let texto = View::new(Style {
|
||||
size: Size {
|
||||
width: percent(1.0_f32),
|
||||
height: auto(),
|
||||
},
|
||||
..Default::default()
|
||||
})
|
||||
.text_aligned(display, 13.0, text_color, Alignment::Start);
|
||||
inner.children(vec![texto])
|
||||
};
|
||||
|
||||
View::new(Style {
|
||||
size: Size {
|
||||
@@ -198,7 +291,17 @@ pub fn text_input_view<Msg: Clone + 'static>(
|
||||
})
|
||||
.fill(border)
|
||||
.radius(4.0)
|
||||
// Semántica: input de texto + el valor crudo como `value` (no el "•"
|
||||
// del modo masked — los lectores no deben dictar la contraseña en
|
||||
// voz alta; AccessKit ya marca el control como TextInput y el lector
|
||||
// sustituye por "punto" cuando el contexto lo requiere). El
|
||||
// placeholder va como `description` cuando el campo está vacío para
|
||||
// que el lector lo enuncie como pista. `value` queda vacío en masked.
|
||||
.role(llimphi_ui::Role::TextInput)
|
||||
.aria_value(if state.masked { String::new() } else { state.text() })
|
||||
.aria_description(if is_empty { placeholder.to_string() } else { String::new() })
|
||||
.on_click(on_focus)
|
||||
.cursor(llimphi_ui::Cursor::Text)
|
||||
.children(vec![inner])
|
||||
}
|
||||
|
||||
@@ -217,6 +320,43 @@ mod tests {
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn palette_caret_default_sigue_al_texto() {
|
||||
// El caret por default sigue al color del texto (`caret-color: auto`):
|
||||
// `from_theme` y `Default` lo igualan a `fg_text`.
|
||||
let t = llimphi_theme::Theme::dark();
|
||||
let pal = TextInputPalette::from_theme(&t);
|
||||
assert_eq!(pal.caret, pal.fg_text);
|
||||
assert_eq!(pal.caret, t.fg_text);
|
||||
assert_eq!(TextInputPalette::default().caret, TextInputPalette::default().fg_text);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn caret_se_registra_como_over_painter_solo_focado() {
|
||||
// Caret v2 (Fase 7.1249): el caret se pinta con `paint_over` (pasada
|
||||
// FINAL sobre el glifo). Verificamos el wiring montando la vista: con
|
||||
// foco hay un over-painter registrado; sin foco no hay ninguno.
|
||||
use llimphi_ui::llimphi_layout::LayoutTree;
|
||||
use llimphi_ui::{has_over_painter, mount};
|
||||
let mut st = TextInputState::new();
|
||||
st.set_text("hola");
|
||||
let pal = TextInputPalette::default();
|
||||
|
||||
let mut lt = LayoutTree::new();
|
||||
let focado = mount(&mut lt, text_input_view(&st, "ph", true, &pal, ()));
|
||||
assert!(
|
||||
has_over_painter(&focado),
|
||||
"input focado debe registrar el caret como over-painter"
|
||||
);
|
||||
|
||||
let mut lt2 = LayoutTree::new();
|
||||
let sin_foco = mount(&mut lt2, text_input_view(&st, "ph", false, &pal, ()));
|
||||
assert!(
|
||||
!has_over_painter(&sin_foco),
|
||||
"input sin foco no pinta caret"
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn apply_key_inserts_printable_chars() {
|
||||
let mut s = TextInputState::new();
|
||||
|
||||
Reference in New Issue
Block a user