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:
Sergio
2026-06-18 14:40:00 +00:00
parent e74800d9da
commit ccab39f140
202 changed files with 44034 additions and 1811 deletions
+168 -4
View File
@@ -26,6 +26,16 @@ use crate::diagnostics::{Diagnostic, Severity};
use crate::highlight::{Language, Span, SyntaxPalette, TokenKind};
use crate::state::EditorState;
/// Tope de líneas que la variante embebida (`text_editor_view_colored`)
/// renderiza de una. La virtualización del editor-de-archivos capa a 200 para
/// no generar miles de Views (wgpu rechaza el bind group); pero la variante
/// embebida deja el scroll al contenedor de afuera y necesita pintar TODAS sus
/// líneas (si no, la mitad de abajo queda sin pintar = negro al anclar el panel
/// al fondo). Este tope es sólo la red de seguridad de wgpu — el caller acota el
/// total real (el shell, por su `MAX_VISIBLE = 400`). Probado: ~400 líneas
/// renderizan sin que wgpu rechace nada (el render plano viejo ya lo hacía).
pub const EMBEDDED_LINE_CAP: usize = 512;
/// Paleta del editor. Defaults dark.
#[derive(Debug, Clone, Copy)]
pub struct EditorPalette {
@@ -278,6 +288,7 @@ pub fn text_editor_view_full<Msg: Clone + 'static>(
spans,
&syntax,
match_ranges,
None,
on_pointer,
);
@@ -291,6 +302,53 @@ pub fn text_editor_view_full<Msg: Clone + 'static>(
.children(vec![gutter, content])
}
/// Como [`text_editor_view`] pero el caller provee el color de cada tramo de
/// cada línea (`line_color_runs[n]` = `(byte_start, byte_end, Color)` de la
/// línea `n`), en vez de derivarlo de un `Language`. Para outputs con coloreo
/// semántico propio (un shell que tinta `ls`, paths, urls, números…) sobre el
/// mismo editor read-only (numeración + selección + copiar).
pub fn text_editor_view_colored<Msg: Clone + 'static>(
state: &EditorState,
palette: &EditorPalette,
metrics: EditorMetrics,
visible_lines: usize,
line_color_runs: &[Vec<(usize, usize, Color)>],
on_pointer: impl Fn(PointerEvent) -> Option<Msg> + Send + Sync + Clone + 'static,
) -> View<Msg> {
let caret = state.cursor.caret;
let syntax = crate::syntax_palette_dark(&llimphi_theme::Theme::dark());
// Variante embebida: el contenedor de afuera (el panel de output del shell)
// hace el scroll y reserva alto para TODAS las líneas, así que las pintamos
// completas (cap alto = red de seguridad de wgpu, ver `EMBEDDED_LINE_CAP`).
let visible = visible_lines.max(1).min(EMBEDDED_LINE_CAP);
let line_count = state.line_count();
let scroll = state.scroll_offset.min(line_count.saturating_sub(1));
let end_line = (scroll + visible).min(line_count);
let height = (end_line - scroll) as f32 * metrics.line_height;
let gutter = build_gutter(state, scroll, end_line, caret.line, metrics, palette);
let content = build_content(
state,
palette,
metrics,
height,
scroll,
end_line,
Vec::new(),
&syntax,
&[],
Some(line_color_runs),
on_pointer,
);
View::new(Style {
flex_direction: FlexDirection::Row,
size: Size { width: percent(1.0_f32), height: length(height) },
..Default::default()
})
.fill(palette.bg)
.clip(true)
.children(vec![gutter, content])
}
fn build_gutter<Msg: Clone + 'static>(
state: &EditorState,
scroll: usize,
@@ -336,7 +394,8 @@ fn build_gutter<Msg: Clone + 'static>(
align_items: Some(AlignItems::Center),
..Default::default()
})
.text_aligned(label, metrics.font_size * 0.85, color, Alignment::End),
.text_aligned(label, metrics.font_size * 0.85, color, Alignment::End)
.mono(),
);
}
GutterStyle::Phantom => {
@@ -394,6 +453,7 @@ fn with_alpha(c: Color, alpha: f32) -> Color {
Color::from_rgba8(rgba.r, rgba.g, rgba.b, a)
}
#[allow(clippy::too_many_arguments)]
fn build_content<Msg: Clone + 'static>(
state: &EditorState,
palette: &EditorPalette,
@@ -404,6 +464,11 @@ fn build_content<Msg: Clone + 'static>(
spans_per_line: Vec<Vec<Span>>,
syntax: &SyntaxPalette,
match_ranges: &[(usize, usize)],
// Override de color por línea: `color_runs[n]` son `(byte_start, byte_end,
// Color)` para la línea `n` del buffer (índice absoluto). Cuando es `Some`,
// gana sobre el syntax highlight — para callers que colorean por semántica
// propia (un shell que tinta `ls`, paths, urls…). `None` = highlight normal.
color_runs: Option<&[Vec<(usize, usize, Color)>]>,
on_pointer: impl Fn(PointerEvent) -> Option<Msg> + Send + Sync + Clone + 'static,
) -> View<Msg> {
let caret = state.cursor.caret;
@@ -461,7 +526,9 @@ fn build_content<Msg: Clone + 'static>(
children.push(phantom_guard_divider(local_line, metrics, palette));
continue;
}
if let Some(line_spans) = spans_per_line.get(n) {
if let Some(runs) = color_runs.and_then(|cr| cr.get(n)) {
children.push(line_text_color_runs(local_line, &text, runs, metrics, palette));
} else if let Some(line_spans) = spans_per_line.get(n) {
children.push(line_text_tokens(local_line, &text, line_spans, metrics, palette, syntax));
} else {
children.push(line_text_plain(local_line, text, metrics, palette));
@@ -473,15 +540,33 @@ fn build_content<Msg: Clone + 'static>(
children.extend(diagnostic_underline(d, scroll, end_line, metrics, palette));
}
// 4) Caret — uno por cursor, sólo si visible.
// 4) Caret — uno por cursor, sólo si visible. El caret del cursor
// primario se corre detrás del preedit del IME en composición (el
// texto compuesto se pinta desde `p.col`), para que quede al final
// de lo que el usuario está tecleando.
let preedit_cols = state.preedit.as_ref().map_or(0, |p| p.text.chars().count());
for c in state.all_cursors() {
let p = c.caret;
if p.line >= scroll && p.line < end_line {
let local = crate::cursor::Pos::new(p.line - scroll, p.col);
let is_primary = std::ptr::eq(c, &state.cursor);
let col = if is_primary { p.col + preedit_cols } else { p.col };
let local = crate::cursor::Pos::new(p.line - scroll, col);
children.push(caret_rect(local, metrics, palette));
}
}
// 5) Preedit del IME — texto en composición pintado en el caret con
// subrayado, todavía fuera del buffer. Sólo en el cursor primario y
// si su línea está en viewport. (En mono el ancho es exacto; el
// texto que sigue al caret puede solaparse mientras se compone —
// transitorio y, en el caso típico de acentos, de un solo char.)
if let Some(pre) = state.preedit.as_ref() {
let p = state.cursor.caret;
if p.line >= scroll && p.line < end_line {
children.extend(preedit_views(p.line - scroll, p.col, &pre.text, metrics, palette));
}
}
let click_cb = on_pointer.clone();
let drag_cb = on_pointer;
View::new(Style {
@@ -603,6 +688,7 @@ fn line_text_plain<Msg: Clone + 'static>(
..Default::default()
})
.text_aligned(text, metrics.font_size, palette.fg_text, Alignment::Start)
.mono()
}
/// Renderiza una línea como secuencia de Views absolutos posicionados,
@@ -660,6 +746,38 @@ fn line_text_tokens<Msg: Clone + 'static>(
runs,
Alignment::Start,
)
.mono()
}
/// Como [`line_text_tokens`] pero con `(byte_start, byte_end, Color)`
/// explícitos provistos por el caller (coloreo semántico propio, p. ej. un
/// shell que tinta `ls`/paths/urls). El resto del texto va en `fg_text`.
fn line_text_color_runs<Msg: Clone + 'static>(
line: usize,
text: &str,
runs: &[(usize, usize, Color)],
metrics: EditorMetrics,
palette: &EditorPalette,
) -> View<Msg> {
View::new(Style {
position: Position::Absolute,
inset: Rect {
left: length(4.0_f32),
top: length(line as f32 * metrics.line_height),
right: auto(),
bottom: auto(),
},
size: Size { width: length(2000.0_f32), height: length(metrics.line_height) },
..Default::default()
})
.text_runs(
text.to_string(),
metrics.font_size,
palette.fg_text,
runs.to_vec(),
Alignment::Start,
)
.mono()
}
fn caret_rect<Msg: Clone + 'static>(
@@ -683,6 +801,52 @@ fn caret_rect<Msg: Clone + 'static>(
.fill(palette.caret)
}
/// Pinta el texto en composición del IME en `(local_line, col)`: el texto
/// provisional + un subrayado debajo que lo marca como no-confirmado.
/// Devuelve los dos Views (texto y subrayado). Posición en coords del
/// área de contenido (mismo origen que [`line_text_plain`]).
fn preedit_views<Msg: Clone + 'static>(
local_line: usize,
col: usize,
text: &str,
metrics: EditorMetrics,
palette: &EditorPalette,
) -> Vec<View<Msg>> {
let x = 4.0 + col as f32 * metrics.char_width;
let y = local_line as f32 * metrics.line_height;
let w = (text.chars().count() as f32 * metrics.char_width).max(metrics.char_width);
vec![
// Texto provisional, en el color de texto normal.
View::new(Style {
position: Position::Absolute,
inset: Rect {
left: length(x),
top: length(y),
right: auto(),
bottom: auto(),
},
size: Size { width: length(w), height: length(metrics.line_height) },
align_items: Some(AlignItems::Center),
..Default::default()
})
.text_aligned(text.to_string(), metrics.font_size, palette.fg_text, Alignment::Start)
.mono(),
// Subrayado: una línea fina en el color del caret bajo el texto.
View::new(Style {
position: Position::Absolute,
inset: Rect {
left: length(x),
top: length(y + metrics.line_height - 2.0),
right: auto(),
bottom: auto(),
},
size: Size { width: length(w), height: length(1.5_f32) },
..Default::default()
})
.fill(palette.caret),
]
}
fn bracket_highlight<Msg: Clone + 'static>(
pos: Pos,
metrics: EditorMetrics,