tawasuyu

tawasuyu/brahman

Rust 572TOML 217Markdown 68JSON 198326 Definitions

⎇ 71ebcea899d0ac71f959ad5461d830d952087735 renaser/kernel/src/compositor.rs Rust · 1082 lines · 44.2 KB History Who touched what raw

feat(renaser): Fase 16 — la barra viva: botón lanzador + reloj 71ebcea89 · sergio ·

1// =============================================================================
2//  renaser :: kernel/src/compositor.rs — el compositor: teselado y flotantes
3// -----------------------------------------------------------------------------
4//  El kernel no coloca las ventanas a mano: las TESELA. El motor es
5//  `mirada-layout` —el mismo nucleo `no_std` que ordena el compositor Wayland
6//  de brahman—, enlazado por `path` cruzando la frontera de workspace.
7//
8//  FASE 8b/8c :: el compositor cobra vida. Mantiene un ESCRITORIO —el registro
9//  de todas las ventanas— y, por cada una, una CACHE de respaldo con su ultimo
10//  fotograma. Gracias a esa cache, el teclado puede re-teselar el escritorio en
11//  caliente —o mover el foco— y el kernel recompone cada ventana en su marco
12//  nuevo SIN despertar a las apps: una app que solo pinto en su `init` conserva
13//  su imagen intacta a traves de cualquier reordenacion.
14//
15//  FASE 9 :: orden-Z y ventanas flotantes. Una ventana puede ABANDONAR el
16//  teselado y FLOTAR —un marco propio, libre, que SOLAPA a las demas—. El
17//  escritorio separa entonces dos capas: las TESELADAS, al fondo, sin
18//  solapamiento entre si; y las FLOTANTES, encima, apiladas por un orden-Z
19//  —`flotantes` ES esa pila, de atras hacia adelante; la ultima es la frontal—.
20//  Con flotantes vivas el kernel deja de pintar cada ventana por separado:
21//  RECOMPONE el escritorio entero, capa a capa, de modo que el solapamiento se
22//  resuelva por el orden del pintado, sin recortes ni mascaras.
23//
24//  FASE 10 :: el escritorio deja de ser un censo fijo. Una ventana puede
25//  CERRARSE en vivo (`Alt+Q`): se la marca, su app concluye su tarea por su
26//  voluntad y el teselado reclama su espacio. Y puede NACER una ventana nueva
27//  (`Alt+N`): `nacer_ventana` la añade al censo y devuelve su indice al
28//  orquestador, que instancia su WASM y engendra su tarea. El censo de
29//  ventanas solo crece —los indices son la IDENTIDAD, jamas se reciclan—; una
30//  ventana cerrada queda como una ranura inerte, fuera del orden y del foco.
31//
32//  FASE 13 :: el raton entra en juego. Hay un PUNTERO en pantalla y el
33//  compositor gana dos gestos: clic-para-enfocar (sobre cualquier ventana viva)
34//  y ARRASTRAR una flotante con el boton izquierdo sostenido. Como el teclado
35//  y la bocina, los eventos del raton vienen del manejador de IRQ12 por una
36//  cola lock-free; `atender_raton` los drena cooperativamente y, al detectar
37//  un boton que baja o un arrastre en curso, mueve el foco o el marco. Los
38//  cuartos flotantes dejan, por fin, de estar clavados en su cascada.
39//
40//  EXCLUSION DE INTERRUPCIONES. El `ESCRITORIO` lo tocan SOLO tareas
41//  cooperativas (el `tick` de una app, la tarea del compositor): el manejador
42//  de IRQ1 jamas lo bloquea. La IRQ se comunica con el mundo cooperativo por
43//  un canal estrecho y a prueba de interbloqueos: dos atomicos —el foco y el
44//  estado de Alt— y una cola lock-free de mandos. Ningun cerrojo que la IRQ
45//  pudiera disputar a una tarea cooperativa.
46// =============================================================================
47
48use core::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
49
50use alloc::string::{String, ToString};
51use alloc::vec;
52use alloc::vec::Vec;
53
54use crossbeam_queue::ArrayQueue;
55use mirada_layout::{tile, LayoutMode, LayoutParams, Rect};
56use spin::{Mutex, Once};
57
58use crate::consola::{self, Capa, CeldaTaskbar, Contenido, Taskbar};
59use crate::grafico::{Color, RegionPantalla};
60
61/// Altura del strip superior reservado a la consola; las apps teselan debajo.
62/// La consola conserva ahi su registro de arranque completo —seis lineas,
63/// hasta la sonda asincrona de disco— legible sobre el teselado.
64const FRANJA_CONSOLA: usize = 296;
65
66/// Altura de la barra de tareas inferior (Fase 14): cada ventana viva tiene
67/// ahi una pestaña con su nombre, que el clic enfoca.
68const FRANJA_TASKBAR: usize = 40;
69
70/// Anchura de cada celda de la barra de tareas, en pixeles.
71const CELDA_TASKBAR_ANCHO: usize = 150;
72/// Hueco entre celdas adyacentes de la barra.
73const CELDA_TASKBAR_HUECO: usize = 6;
74/// Margen izquierdo y derecho de la barra de tareas.
75const CELDA_TASKBAR_MARGEN: usize = 12;
76/// Anchura del boton lanzador («+» a la izquierda de la barra, Fase 16).
77const LAUNCHER_ANCHO: usize = 36;
78/// Hueco entre el lanzador y la primera pestaña.
79const LAUNCHER_HUECO: usize = 8;
80/// Anchura reservada para el reloj a la derecha de la barra (Fase 16).
81const RELOJ_ANCHO: usize = 80;
82/// Hueco entre la ultima pestaña y el reloj.
83const RELOJ_HUECO: usize = 8;
84
85/// El modo de teselado con que arranca el escritorio. El teclado lo cicla.
86const MODO_INICIAL: LayoutMode = LayoutMode::MasterStack;
87
88/// Margen entre ventanas teseladas, en pixeles.
89const MARGEN: i32 = 14;
90
91/// Capacidad de la cola de mandos del compositor — holgada: nadie pulsa tanto.
92const CAPACIDAD_MANDOS: usize = 32;
93
94/// Reborde de cromo de una ventana flotante: el panel que rodea su lienzo
95/// natural, donde se asienta el borde de foco sin tapar el dibujo de la app.
96const CROMO_FLOTANTE: usize = 8;
97
98/// Paso de la cascada con que se colocan las ventanas flotantes nuevas, en
99/// pixeles. Cada flotante se desplaza un paso respecto a la anterior, de modo
100/// que varias no se tapen por completo.
101const PASO_CASCADA: usize = 44;
102
103/// Un mando del compositor — lo emite el teclado desde el contexto de IRQ, lo
104/// atiende la tarea del compositor desde el reactor cooperativo.
105#[derive(Clone, Copy)]
106pub enum Mando {
107    /// Ciclar al siguiente modo de teselado de `mirada-layout`.
108    CiclarLayout,
109    /// Mover el foco a la siguiente ventana viva.
110    FocoSiguiente,
111    /// Mover el foco a la ventana viva anterior.
112    FocoAnterior,
113    /// Promover la ventana enfocada a la posicion maestra del teselado.
114    Promover,
115    /// Mover la ventana enfocada una posicion adelante en el orden de teselado.
116    MoverAdelante,
117    /// Mover la ventana enfocada una posicion atras en el orden de teselado.
118    MoverAtras,
119    /// Alternar la ventana enfocada entre teselada y flotante (Fase 9).
120    Flotar,
121    /// Cerrar la aplicacion enfocada — una baja limpia, en vivo (Fase 10).
122    Cerrar,
123    /// Lanzar una aplicacion nueva — un alta en vivo (Fase 10).
124    Lanzar,
125}
126
127/// Un arrastre EN CURSO (Fase 13): el indice de la ventana flotante asida con
128/// el raton y el desfase con que se asio —para que la ventana no salte al
129/// agarrarla, sino que siga al puntero como si lo llevara cogido por ahi—.
130#[derive(Clone, Copy)]
131struct Arrastre {
132    ventana: usize,
133    agarre_dx: usize,
134    agarre_dy: usize,
135}
136
137/// Una ventana del escritorio: una app, su geometria y su ultimo fotograma.
138struct Ventana {
139    /// Nombre legible de la app — el que dicta su `EntradaApp` del manifiesto.
140    /// Aparece en la pestaña de la barra de tareas (Fase 14).
141    nombre: String,
142    /// Tamaño natural del lienzo de la app — lo que sabe pintar, fijo.
143    natural_ancho: usize,
144    natural_alto: usize,
145    /// El marco actual — donde la app vive en pantalla. Si la ventana esta
146    /// teselada, lo fija el teselado; si flota, es un marco propio y libre.
147    marco: RegionPantalla,
148    /// CACHE de respaldo: el ultimo fotograma exitoso que la app envio. Su
149    /// tamaño esta acotado al lienzo natural —`natural_ancho × natural_alto ×
150    /// 4`— y se reserva UNA sola vez, al fundar el escritorio: jamas crece. El
151    /// re-teselado recompone la ventana desde aqui, sin molestar a la app.
152    cache: Vec<u8>,
153    /// ¿Ha enviado la app al menos un fotograma? Hasta entonces, su cache es
154    /// solo ceros y no se recompone.
155    pintada: bool,
156    /// Si el kernel desalojo la app, el color de su baliza. `None` mientras
157    /// vive; `Some(color)` la marca como muerta y la excluye del foco.
158    baliza: Option<Color>,
159    /// ¿Se ha pedido cerrar esta ventana en vivo (Fase 10)? Una vez `true`, su
160    /// app concluye su tarea, la ranura queda inerte —fuera del orden, del
161    /// orden-Z y del foco— y el teselado reclama su espacio.
162    cerrada: bool,
163}
164
165/// El escritorio: el registro de todas las ventanas y el modo de teselado.
166/// Lo tocan SOLO tareas cooperativas — nunca el manejador de IRQ1.
167struct Escritorio {
168    modo: LayoutMode,
169    ancho: usize,
170    alto: usize,
171    /// Las ventanas, indexadas por `indice_app` — su IDENTIDAD, inmutable.
172    ventanas: Vec<Ventana>,
173    /// El ORDEN de teselado: `orden[slot]` es el `indice_app` de la ventana que
174    /// ocupa esa celda del teselado. Contiene SOLO las ventanas teseladas —las
175    /// flotantes salen de aqui—. Separar el orden de la identidad permite
176    /// promover y reordenar ventanas sin tocar su `indice_app`.
177    orden: Vec<usize>,
178    /// Las ventanas FLOTANTES, en orden-Z (Fase 9): de atras hacia adelante.
179    /// `flotantes.last()` es la ventana frontal. Una ventana esta en `orden` o
180    /// en `flotantes`, jamas en ambos ni en ninguno: juntos son una particion
181    /// de `0..ventanas.len()`.
182    flotantes: Vec<usize>,
183    /// ¿Estaba el boton izquierdo del raton pulsado en el evento anterior?
184    /// Para detectar las transiciones —el momento exacto del clic o de soltar—.
185    raton_izq: bool,
186    /// Arrastre en curso, si lo hay (Fase 13).
187    arrastre: Option<Arrastre>,
188}
189
190/// El escritorio global. Se funda una sola vez, en el arranque.
191static ESCRITORIO: Once<Mutex<Escritorio>> = Once::new();
192
193/// El indice de la ventana ENFOCADA. Atomico —no un campo del `Escritorio`—
194/// porque el manejador de IRQ1 lo LEE para enrutar el teclado, y un atomico no
195/// se puede disputar: jamas hay interbloqueo entre la IRQ y una tarea.
196static FOCO: AtomicUsize = AtomicUsize::new(0);
197
198/// La cola de mandos: el manejador de IRQ1 deposita aqui las ordenes del
199/// teclado (lock-free, segura en contexto de interrupcion); la tarea del
200/// compositor las drena desde el reactor cooperativo.
201static MANDOS: Once<ArrayQueue<Mando>> = Once::new();
202
203/// Cuantos lanzamientos de aplicacion (Fase 10) aguardan. Lo incrementa
204/// `atender_mandos` al recibir un `Mando::Lanzar`; lo drena `partos_pendientes`,
205/// que lo lee el orquestador del kernel —el unico que sabe instanciar un WASM—.
206/// Atomico: el compositor lo escribe, el orquestador lo lee y lo pone a cero.
207static PARTOS: AtomicUsize = AtomicUsize::new(0);
208
209/// El ultimo segundo del reloj monotono que la barra de tareas ha mostrado.
210/// `tick_reloj` lo compara con el actual: si difiere, recompone para pintar el
211/// nuevo. Centinela `u64::MAX` para garantizar que el primer tick fuerza un
212/// repintado y la barra arranca con su reloj a 0:00.
213static ULTIMO_SEGUNDO: AtomicU64 = AtomicU64::new(u64::MAX);
214
215// =============================================================================
216//  Fundacion y consulta — el arranque
217// =============================================================================
218
219/// Funda el escritorio: crea una ventana por app, con su marco teselado inicial
220/// y su cache de respaldo ya reservada al tamaño natural. `naturales` da el
221/// `(ancho, alto)` del lienzo de cada app, en el orden del manifiesto.
222pub fn fundar(ancho: usize, alto: usize, naturales: &[(usize, usize, &str)]) {
223    MANDOS.call_once(|| ArrayQueue::new(CAPACIDAD_MANDOS));
224
225    let mut ventanas = Vec::with_capacity(naturales.len());
226    for &(nat_ancho, nat_alto, nombre) in naturales {
227        ventanas.push(Ventana {
228            nombre: nombre.to_string(),
229            natural_ancho: nat_ancho,
230            natural_alto: nat_alto,
231            // Marco provisional; `aplicar_teselado` lo fija enseguida.
232            marco: RegionPantalla {
233                x: 0,
234                y: 0,
235                ancho: 0,
236                alto: 0,
237            },
238            // La cache: reservada UNA vez, acotada al lienzo natural.
239            cache: vec![0u8; nat_ancho.saturating_mul(nat_alto).saturating_mul(4)],
240            pintada: false,
241            baliza: None,
242            cerrada: false,
243        });
244    }
245
246    // El orden de teselado arranca como la identidad: la ventana `i` ocupa la
247    // celda `i`. Ninguna ventana flota al nacer — el escritorio es puro
248    // teselado hasta que el teclado lo decida (`Alt+F`).
249    let orden = (0..ventanas.len()).collect();
250    let mut escritorio = Escritorio {
251        modo: MODO_INICIAL,
252        ancho,
253        alto,
254        ventanas,
255        orden,
256        flotantes: Vec::new(),
257        raton_izq: false,
258        arrastre: None,
259    };
260    aplicar_teselado(&mut escritorio);
261
262    ESCRITORIO.call_once(|| Mutex::new(escritorio));
263}
264
265/// Recalcula el teselado y asigna a cada ventana TESELADA su marco. La celda
266/// `slot` del teselado va a la ventana `orden[slot]`: manda el orden, no la
267/// identidad. Las ventanas flotantes no estan en `orden` y conservan su marco.
268fn aplicar_teselado(escritorio: &mut Escritorio) {
269    let marcos = teselar(
270        escritorio.orden.len(),
271        escritorio.ancho,
272        escritorio.alto,
273        escritorio.modo,
274    );
275    for (slot, marco) in marcos.into_iter().enumerate() {
276        let ventana = escritorio.orden[slot];
277        escritorio.ventanas[ventana].marco = marco;
278    }
279}
280
281/// Pinta el escenario inicial del compositor. Se invoca una vez, tras `fundar`,
282/// antes de encender las apps: recompone el escritorio con todas las ventanas
283/// aun sin pintar — el teselado se ve como una rejilla de paneles.
284pub fn componer_escenario() {
285    let Some(escritorio) = ESCRITORIO.get() else {
286        return;
287    };
288    let escritorio = escritorio.lock();
289    recomponer(&escritorio);
290}
291
292/// El indice de la ventana enfocada. Lo LEE el manejador de IRQ1 para enrutar
293/// cada tecla — por eso es una simple lectura atomica, sin cerrojo alguno.
294pub fn foco() -> usize {
295    FOCO.load(Ordering::Relaxed)
296}
297
298/// Encola un mando del teclado. Lo invoca el manejador de IRQ1: empujar a una
299/// cola lock-free es seguro en contexto de interrupcion.
300pub fn solicitar(mando: Mando) {
301    if let Some(mandos) = MANDOS.get() {
302        // Si la cola se desborda, el mando se pierde en silencio: mas vale
303        // perder una pulsacion que arriesgar un panico dentro de una IRQ.
304        let _ = mandos.push(mando);
305    }
306}
307
308// =============================================================================
309//  El fotograma de una app — cache y composicion
310// =============================================================================
311
312/// Recibe el fotograma de la app `indice`: lo copia a su CACHE de respaldo —el
313/// kernel asume la persistencia visual— y lo lleva a pantalla. Sin ventanas
314/// flotantes ninguna ventana solapa a otra: basta repintar la que cambio —el
315/// camino RAPIDO—. Con flotantes vivas el solapamiento obliga a RECOMPONER el
316/// escritorio entero, respetando el orden-Z. Lo invoca la capacidad
317/// `sys_render_frame` desde el `tick` cooperativo.
318pub fn presentar_fotograma(indice: usize, datos: &[u8]) {
319    let Some(escritorio) = ESCRITORIO.get() else {
320        return;
321    };
322    let mut escritorio = escritorio.lock();
323    {
324        let Some(ventana) = escritorio.ventanas.get_mut(indice) else {
325            return;
326        };
327        // Una ventana cerrada (Fase 10) ya no se pinta: su app pudo emitir un
328        // ultimo fotograma antes de que su tarea advirtiera la baja.
329        if ventana.cerrada {
330            return;
331        }
332        // Cachear el fotograma. El destino esta acotado al lienzo natural; se
333        // copia el minimo de ambas longitudes — jamas se desborda la cache.
334        let n = ventana.cache.len().min(datos.len());
335        ventana.cache[..n].copy_from_slice(&datos[..n]);
336        ventana.pintada = true;
337    }
338
339    if escritorio.flotantes.is_empty() {
340        // Camino RAPIDO: sin flotantes el escritorio es puro teselado y la app
341        // pinta directamente en su marco, como en la Fase 8.
342        let ventana = &escritorio.ventanas[indice];
343        let marco = ventana.marco;
344        let nat_ancho = ventana.natural_ancho;
345        let nat_alto = ventana.natural_alto;
346        let enfocada = FOCO.load(Ordering::Relaxed) == indice;
347        drop(escritorio);
348        consola::volcar_marco(marco, nat_ancho, nat_alto, datos, enfocada);
349    } else {
350        // Hay ventanas flotantes: el solapamiento obliga a recomponer.
351        recomponer(&escritorio);
352    }
353}
354
355/// Marca la ventana `indice` como desalojada y tatua su marco con la baliza.
356/// Desde aqui queda excluida del foco — el teclado la salta.
357pub fn desalojar(indice: usize, color: Color) {
358    let Some(escritorio) = ESCRITORIO.get() else {
359        return;
360    };
361    let mut escritorio = escritorio.lock();
362    {
363        let Some(ventana) = escritorio.ventanas.get_mut(indice) else {
364            return;
365        };
366        // Una ventana ya cerrada (Fase 10) no recibe baliza: la baja limpia
367        // gana a un desalojo que llegue tarde, en la misma vuelta.
368        if ventana.cerrada {
369            return;
370        }
371        ventana.baliza = Some(color);
372    }
373    // Fase 15: la voz del kernel anuncia el desalojo.
374    crate::drivers::altavoz::agendar(&crate::drivers::altavoz::VOZ_DESALOJO);
375
376    if escritorio.flotantes.is_empty() {
377        let marco = escritorio.ventanas[indice].marco;
378        let enfocada = FOCO.load(Ordering::Relaxed) == indice;
379        drop(escritorio);
380        consola::pintar_desalojo(marco, color, enfocada);
381    } else {
382        recomponer(&escritorio);
383    }
384}
385
386// =============================================================================
387//  Los mandos del teclado — el escritorio interactivo
388// =============================================================================
389
390/// Atiende los mandos pendientes del teclado. La invoca la tarea del compositor
391/// en cada fotograma, desde el reactor cooperativo — el unico contexto donde es
392/// seguro bloquear el `ESCRITORIO` y la consola.
393pub fn atender_mandos() {
394    let Some(mandos) = MANDOS.get() else {
395        return;
396    };
397    while let Some(mando) = mandos.pop() {
398        match mando {
399            Mando::CiclarLayout => ciclar_layout(),
400            Mando::FocoSiguiente => mover_foco(true),
401            Mando::FocoAnterior => mover_foco(false),
402            Mando::Promover => promover(),
403            Mando::MoverAdelante => mover_ventana(true),
404            Mando::MoverAtras => mover_ventana(false),
405            Mando::Flotar => flotar(),
406            Mando::Cerrar => cerrar(),
407            // El alta de una app necesita instanciar un WASM — algo que el
408            // compositor no sabe hacer—. Solo se cuenta la peticion; el
409            // orquestador del kernel la atendera (ver `partos_pendientes`).
410            Mando::Lanzar => {
411                PARTOS.fetch_add(1, Ordering::Relaxed);
412            }
413        }
414    }
415}
416
417/// Cicla al siguiente modo de teselado: recalcula los marcos de las ventanas
418/// teseladas y recompone el escritorio entero desde las caches de respaldo.
419fn ciclar_layout() {
420    let Some(escritorio) = ESCRITORIO.get() else {
421        return;
422    };
423    let mut escritorio = escritorio.lock();
424    escritorio.modo = escritorio.modo.next();
425    aplicar_teselado(&mut escritorio);
426    recomponer(&escritorio);
427}
428
429/// Mueve el foco a la siguiente ventana VIVA. El recorrido abarca TODAS las
430/// ventanas —las teseladas y, tras ellas, las flotantes— saltando las
431/// desalojadas. Si la ventana recien enfocada flota, sube al frente del
432/// orden-Z: la flotante con el foco esta SIEMPRE delante.
433fn mover_foco(adelante: bool) {
434    let Some(escritorio) = ESCRITORIO.get() else {
435        return;
436    };
437    let mut escritorio = escritorio.lock();
438    // El recorrido del foco: las teseladas, luego las flotantes — un orden
439    // estable y visualmente coherente.
440    let recorrido: Vec<usize> = escritorio
441        .orden
442        .iter()
443        .chain(escritorio.flotantes.iter())
444        .copied()
445        .collect();
446    let n = recorrido.len();
447    if n == 0 {
448        return;
449    }
450    let anterior = FOCO.load(Ordering::Relaxed);
451    let pos = recorrido.iter().position(|&v| v == anterior).unwrap_or(0);
452
453    // Avanzar saltando las ventanas desalojadas. Si no hay ninguna viva, tras
454    // `n` pasos se vuelve al punto de partida y el foco no cambia.
455    let mut nueva_pos = pos;
456    let mut nuevo = anterior;
457    for _ in 0..n {
458        nueva_pos = if adelante {
459            (nueva_pos + 1) % n
460        } else {
461            (nueva_pos + n - 1) % n
462        };
463        let candidata = recorrido[nueva_pos];
464        if escritorio.ventanas[candidata].baliza.is_none() {
465            nuevo = candidata;
466            break;
467        }
468    }
469    FOCO.store(nuevo, Ordering::Relaxed);
470    // La bocina pertenece a la ventana enfocada (Fase 12): al cambiar el foco,
471    // callar — la nueva dueña la reclamara en su proximo fotograma si quiere.
472    crate::drivers::altavoz::tono(0);
473    // La ventana recien enfocada, si flota, al frente del orden-Z.
474    alzar_si_flota(&mut escritorio, nuevo);
475    recomponer(&escritorio);
476}
477
478/// Promueve la ventana enfocada a la posicion maestra —la celda 0— del
479/// teselado. Si la ventana enfocada flota, no esta en el orden de teselado y
480/// el mando no hace nada — promover es una operacion del teselado.
481fn promover() {
482    let Some(escritorio) = ESCRITORIO.get() else {
483        return;
484    };
485    let mut escritorio = escritorio.lock();
486    let foco = FOCO.load(Ordering::Relaxed);
487    if let Some(pos) = escritorio.orden.iter().position(|&v| v == foco) {
488        let ventana = escritorio.orden.remove(pos);
489        escritorio.orden.insert(0, ventana);
490        aplicar_teselado(&mut escritorio);
491        recomponer(&escritorio);
492    }
493}
494
495/// Mueve la ventana enfocada una posicion en el orden de teselado,
496/// intercambiandola con su vecina. Una ventana flotante no esta en el orden:
497/// el mando no la afecta.
498fn mover_ventana(adelante: bool) {
499    let Some(escritorio) = ESCRITORIO.get() else {
500        return;
501    };
502    let mut escritorio = escritorio.lock();
503    let n = escritorio.orden.len();
504    if n < 2 {
505        return;
506    }
507    let foco = FOCO.load(Ordering::Relaxed);
508    if let Some(pos) = escritorio.orden.iter().position(|&v| v == foco) {
509        let destino = if adelante {
510            (pos + 1) % n
511        } else {
512            (pos + n - 1) % n
513        };
514        escritorio.orden.swap(pos, destino);
515        aplicar_teselado(&mut escritorio);
516        recomponer(&escritorio);
517    }
518}
519
520// =============================================================================
521//  FASE 9 — orden-Z y ventanas flotantes
522// =============================================================================
523
524/// Alterna la ventana enfocada entre TESELADA y FLOTANTE. Al flotar, la ventana
525/// abandona el teselado —que se recalcula para las que quedan—, recibe un marco
526/// propio en cascada y sube al frente del orden-Z. Al volver al teselado, se
527/// reincorpora al final del orden. El foco no cambia: viaja con la ventana.
528fn flotar() {
529    let Some(escritorio) = ESCRITORIO.get() else {
530        return;
531    };
532    let mut escritorio = escritorio.lock();
533    let foco = FOCO.load(Ordering::Relaxed);
534
535    if let Some(pos) = escritorio.orden.iter().position(|&v| v == foco) {
536        // Teselada -> flotante: se desliga del teselado, recibe su marco
537        // propio en cascada y sube al frente del orden-Z.
538        escritorio.orden.remove(pos);
539        let marco = marco_flotante(&escritorio, foco);
540        escritorio.ventanas[foco].marco = marco;
541        escritorio.flotantes.push(foco);
542        aplicar_teselado(&mut escritorio);
543        recomponer(&escritorio);
544    } else if let Some(pos) = escritorio.flotantes.iter().position(|&v| v == foco) {
545        // Flotante -> teselada: vuelve a la rejilla, al final del orden.
546        escritorio.flotantes.remove(pos);
547        escritorio.orden.push(foco);
548        aplicar_teselado(&mut escritorio);
549        recomponer(&escritorio);
550    }
551}
552
553/// Si la ventana `indice` es flotante, la lleva al frente del orden-Z —al final
554/// de `flotantes`—. Si esta teselada, no hace nada.
555fn alzar_si_flota(escritorio: &mut Escritorio, indice: usize) {
556    if let Some(pos) = escritorio.flotantes.iter().position(|&v| v == indice) {
557        let ventana = escritorio.flotantes.remove(pos);
558        escritorio.flotantes.push(ventana);
559    }
560}
561
562/// El marco de una ventana recien hecha flotante: su lienzo natural mas un
563/// reborde de cromo, colocado en cascada —para que varias flotantes no se
564/// tapen del todo— y acotado al area de apps. Se invoca ANTES de inscribir la
565/// ventana en `flotantes`: su longitud da el escalon de la cascada.
566fn marco_flotante(escritorio: &Escritorio, indice: usize) -> RegionPantalla {
567    let area = area_apps(escritorio.ancho, escritorio.alto);
568    let ventana = &escritorio.ventanas[indice];
569    let ancho = (ventana.natural_ancho + 2 * CROMO_FLOTANTE).min(area.ancho);
570    let alto = (ventana.natural_alto + 2 * CROMO_FLOTANTE).min(area.alto);
571
572    // La cascada: un escalon por cada flotante ya existente.
573    let escalon = escritorio.flotantes.len().saturating_mul(PASO_CASCADA);
574    let mut x = area.x + 48 + escalon;
575    let mut y = area.y + 40 + escalon;
576    // Acotar: la ventana entera ha de caber dentro del area de apps.
577    if x + ancho > area.x + area.ancho {
578        x = area.x + area.ancho.saturating_sub(ancho);
579    }
580    if y + alto > area.y + area.alto {
581        y = area.y + area.alto.saturating_sub(alto);
582    }
583    RegionPantalla {
584        x,
585        y,
586        ancho,
587        alto,
588    }
589}
590
591/// Recompone el escritorio entero respetando el orden-Z. Arma la lista de capas
592/// —primero las ventanas TESELADAS, la capa de fondo; despues las FLOTANTES, de
593/// atras hacia adelante— y se la entrega a la consola, que las funde en ese
594/// orden de una sola pasada. El solapamiento se resuelve por el orden del
595/// pintado. La invocan los mandos del teclado y `presentar_fotograma` cuando
596/// hay flotantes vivas. El llamante sostiene ya el cerrojo del `ESCRITORIO`.
597fn recomponer(escritorio: &Escritorio) {
598    let area = area_apps(escritorio.ancho, escritorio.alto);
599    let foco = FOCO.load(Ordering::Relaxed);
600    let mut capas: Vec<Capa> = Vec::with_capacity(escritorio.ventanas.len());
601    for &indice in escritorio.orden.iter().chain(escritorio.flotantes.iter()) {
602        let ventana = &escritorio.ventanas[indice];
603        let contenido = match ventana.baliza {
604            Some(color) => Contenido::Baliza(color),
605            None if ventana.pintada => Contenido::Fotograma(&ventana.cache),
606            None => Contenido::Panel,
607        };
608        capas.push(Capa {
609            marco: ventana.marco,
610            nat_ancho: ventana.natural_ancho,
611            nat_alto: ventana.natural_alto,
612            contenido,
613            enfocada: indice == foco,
614        });
615    }
616
617    // FASE 14/16 :: armar la barra de tareas. A la izquierda un boton lanzador
618    // («+»); en el medio una pestaña por ventana viva; a la derecha el reloj.
619    let area_bar = area_taskbar(escritorio.ancho, escritorio.alto);
620    let cy = area_bar.y + 4;
621    let calto = area_bar.alto.saturating_sub(8);
622    let launcher = RegionPantalla {
623        x: area_bar.x + CELDA_TASKBAR_MARGEN,
624        y: cy,
625        ancho: LAUNCHER_ANCHO,
626        alto: calto,
627    };
628    let cells_x0 = launcher.x + launcher.ancho + LAUNCHER_HUECO;
629    let cells_x_max =
630        area_bar.x + area_bar.ancho - CELDA_TASKBAR_MARGEN - RELOJ_ANCHO - RELOJ_HUECO;
631    let mut celdas: Vec<CeldaTaskbar> = Vec::new();
632    let mut cx = cells_x0;
633    for (indice, ventana) in escritorio.ventanas.iter().enumerate() {
634        if ventana.cerrada {
635            continue;
636        }
637        if cx + CELDA_TASKBAR_ANCHO > cells_x_max {
638            break;
639        }
640        let fondo = match ventana.baliza {
641            Some(color) => color,
642            None if indice == foco => Color::FOCO,
643            None => Color::PANEL,
644        };
645        celdas.push(CeldaTaskbar {
646            region: RegionPantalla {
647                x: cx,
648                y: cy,
649                ancho: CELDA_TASKBAR_ANCHO,
650                alto: calto,
651            },
652            nombre: &ventana.nombre,
653            fondo,
654            tinta: tinta_para(fondo),
655        });
656        cx += CELDA_TASKBAR_ANCHO + CELDA_TASKBAR_HUECO;
657    }
658    // El reloj: minutos:segundos desde el arranque, alineado a la derecha.
659    let ms = crate::async_system::reloj::milisegundos();
660    let segs = ms / 1000;
661    let reloj_texto = alloc::format!("{}:{:02}", segs / 60, segs % 60);
662    let reloj_region = RegionPantalla {
663        x: area_bar.x + area_bar.ancho - CELDA_TASKBAR_MARGEN - RELOJ_ANCHO,
664        y: cy,
665        ancho: RELOJ_ANCHO,
666        alto: calto,
667    };
668    let taskbar = Taskbar {
669        area: area_bar,
670        launcher,
671        celdas: &celdas,
672        reloj: &reloj_texto,
673        reloj_region,
674    };
675    consola::recomponer(area, &capas, &taskbar);
676    // Recordar el segundo recien mostrado: `tick_reloj` evita repintar de mas
677    // mientras dure este mismo segundo.
678    ULTIMO_SEGUNDO.store(segs, Ordering::Relaxed);
679}
680
681/// Localiza la celda de la barra de tareas bajo la coordenada x: itera las
682/// ventanas vivas en orden de creacion y devuelve la N-esima donde la N es la
683/// posicion en la barra. `None` si el clic cae en el lanzador, en el reloj, en
684/// un hueco entre celdas, o fuera del rango de las pestañas.
685fn celda_taskbar_en(escritorio: &Escritorio, x: usize) -> Option<usize> {
686    let area_bar = area_taskbar(escritorio.ancho, escritorio.alto);
687    // Las pestañas empiezan despues del lanzador.
688    let cells_x0 = area_bar.x + CELDA_TASKBAR_MARGEN + LAUNCHER_ANCHO + LAUNCHER_HUECO;
689    let cells_x_max =
690        area_bar.x + area_bar.ancho - CELDA_TASKBAR_MARGEN - RELOJ_ANCHO - RELOJ_HUECO;
691    if x < cells_x0 || x >= cells_x_max {
692        return None;
693    }
694    let rel = x - cells_x0;
695    let paso = CELDA_TASKBAR_ANCHO + CELDA_TASKBAR_HUECO;
696    let posicion = rel / paso;
697    let offset = rel % paso;
698    if offset >= CELDA_TASKBAR_ANCHO {
699        return None;
700    }
701    let mut k = 0;
702    for (indice, ventana) in escritorio.ventanas.iter().enumerate() {
703        if ventana.cerrada {
704            continue;
705        }
706        if k == posicion {
707            return Some(indice);
708        }
709        k += 1;
710    }
711    None
712}
713
714/// ¿Cae la coordenada x en el boton lanzador («+»)?
715fn clic_en_launcher(escritorio: &Escritorio, x: usize) -> bool {
716    let area_bar = area_taskbar(escritorio.ancho, escritorio.alto);
717    let x0 = area_bar.x + CELDA_TASKBAR_MARGEN;
718    let x1 = x0 + LAUNCHER_ANCHO;
719    x >= x0 && x < x1
720}
721
722// =============================================================================
723//  FASE 10 — alta y baja de aplicaciones en vivo
724// =============================================================================
725
726/// Cierra la aplicacion enfocada (`Alt+Q`): una baja LIMPIA, distinta del
727/// desalojo por falla. Marca la ventana como cerrada, libera su cache de
728/// respaldo, la saca del teselado y del orden-Z, y traslada el foco a una
729/// ventana viva contigua. La app, en su tarea, advertira la baja y concluira.
730fn cerrar() {
731    let Some(escritorio) = ESCRITORIO.get() else {
732        return;
733    };
734    let mut escritorio = escritorio.lock();
735    let foco = FOCO.load(Ordering::Relaxed);
736    // Solo se cierra una ventana viva. El foco jamas se posa en una muerta o
737    // cerrada, pero la guarda lo deja explicito.
738    match escritorio.ventanas.get(foco) {
739        Some(v) if v.baliza.is_none() && !v.cerrada => {}
740        _ => return,
741    }
742    // Fase 15: el kernel se despide de la app con un repique descendente.
743    crate::drivers::altavoz::agendar(&crate::drivers::altavoz::VOZ_CERRAR);
744    // Marcar la baja y liberar el respaldo: la cache de un fotograma puede
745    // pesar un megabyte — no tiene sentido retenerla en una ranura inerte.
746    let ventana = &mut escritorio.ventanas[foco];
747    ventana.cerrada = true;
748    ventana.pintada = false;
749    ventana.cache = Vec::new();
750    // Sacarla del teselado y del orden-Z. El censo conserva la ranura —los
751    // indices son la identidad, jamas se reciclan—, pero ya nadie la dibuja.
752    escritorio.orden.retain(|&v| v != foco);
753    escritorio.flotantes.retain(|&v| v != foco);
754    // Si la estabamos arrastrando con el raton (Fase 13), soltarla.
755    if escritorio.arrastre.map(|a| a.ventana) == Some(foco) {
756        escritorio.arrastre = None;
757    }
758    // El foco salta a la primera ventana viva que quede; si no queda ninguna,
759    // se queda donde estaba —inofensivo: no hay a quien enrutar el teclado—.
760    let nuevo = escritorio
761        .orden
762        .iter()
763        .chain(escritorio.flotantes.iter())
764        .copied()
765        .find(|&v| {
766            let w = &escritorio.ventanas[v];
767            w.baliza.is_none() && !w.cerrada
768        })
769        .unwrap_or(foco);
770    FOCO.store(nuevo, Ordering::Relaxed);
771    // El foco cambia: callar la bocina (Fase 12) — ver `mover_foco`.
772    crate::drivers::altavoz::tono(0);
773    alzar_si_flota(&mut escritorio, nuevo);
774    aplicar_teselado(&mut escritorio);
775    recomponer(&escritorio);
776}
777
778/// Da de alta una ventana NUEVA y devuelve su indice —su identidad—. La crea
779/// con su cache de respaldo al tamaño natural, la añade al final del orden de
780/// teselado, recalcula el teselado y recompone. La invoca el orquestador del
781/// kernel justo antes de instanciar el WASM de la app, que necesita ese indice.
782pub fn nacer_ventana(nat_ancho: usize, nat_alto: usize, nombre: &str) -> usize {
783    let Some(escritorio) = ESCRITORIO.get() else {
784        return 0;
785    };
786    let mut escritorio = escritorio.lock();
787    let indice = escritorio.ventanas.len();
788    escritorio.ventanas.push(Ventana {
789        nombre: nombre.to_string(),
790        natural_ancho: nat_ancho,
791        natural_alto: nat_alto,
792        marco: RegionPantalla {
793            x: 0,
794            y: 0,
795            ancho: 0,
796            alto: 0,
797        },
798        cache: vec![0u8; nat_ancho.saturating_mul(nat_alto).saturating_mul(4)],
799        pintada: false,
800        baliza: None,
801        cerrada: false,
802    });
803    escritorio.orden.push(indice);
804    aplicar_teselado(&mut escritorio);
805    recomponer(&escritorio);
806    // Fase 15: el kernel saluda al nacimiento con un repique ascendente.
807    crate::drivers::altavoz::agendar(&crate::drivers::altavoz::VOZ_LANZAR);
808    indice
809}
810
811/// ¿Se ha pedido cerrar la ventana `indice`? Cada app la consulta en su tarea,
812/// fotograma a fotograma: cuando es `true`, concluye su tarea y se libera. Una
813/// ventana inexistente cuenta como cerrada.
814pub fn ventana_cerrada(indice: usize) -> bool {
815    let Some(escritorio) = ESCRITORIO.get() else {
816        return false;
817    };
818    escritorio
819        .lock()
820        .ventanas
821        .get(indice)
822        .map(|ventana| ventana.cerrada)
823        .unwrap_or(true)
824}
825
826/// Cuantas aplicaciones nuevas se han pedido lanzar desde la ultima consulta —y
827/// pone el contador a cero—. La invoca el orquestador del kernel —el unico que
828/// sabe instanciar un WASM— en cada fotograma de la tarea del compositor.
829pub fn partos_pendientes() -> usize {
830    PARTOS.swap(0, Ordering::Relaxed)
831}
832
833/// Avanza el reloj de la barra de tareas (Fase 16): si el segundo del reloj
834/// monotono cambio respecto al ultimo mostrado, recompone para refrescar la
835/// pantalla. Si el segundo es el mismo, vuelve sin hacer nada — un fotograma
836/// barato—. La invoca la tarea del compositor cada fotograma.
837pub fn tick_reloj() {
838    let actual = crate::async_system::reloj::milisegundos() / 1000;
839    if ULTIMO_SEGUNDO.load(Ordering::Relaxed) == actual {
840        return;
841    }
842    let Some(escritorio) = ESCRITORIO.get() else {
843        return;
844    };
845    let escritorio = escritorio.lock();
846    recomponer(&escritorio);
847}
848
849// =============================================================================
850//  FASE 13 — raton, puntero y arrastre de ventanas flotantes
851// =============================================================================
852
853/// La ultima posicion del puntero que el compositor REFRESCO. Si la posicion
854/// actual del raton coincide con esta, no hay nada nuevo que estampar; si
855/// difiere, la consola debe volver a presentar. Empacada como `y * 65536 + x`,
856/// con `usize::MAX` como centinela de «aun no he visto al raton».
857static PUNTERO_REFRESCADO: AtomicUsize = AtomicUsize::new(usize::MAX);
858
859/// Drena los eventos del raton y los aplica: clic enfoca la ventana bajo el
860/// puntero (y, si flota, inicia un arrastre); el boton sostenido la arrastra;
861/// soltarlo termina el gesto. La invoca la tarea del compositor en cada
862/// fotograma, desde el reactor cooperativo.
863pub fn atender_raton() {
864    let Some(escritorio) = ESCRITORIO.get() else {
865        return;
866    };
867    let mut escritorio = escritorio.lock();
868    let mut cambio = false;
869    while let Some(evento) = crate::drivers::raton::siguiente_evento() {
870        let izq = evento.botones & 0b001 != 0;
871        let x = evento.x as usize;
872        let y = evento.y as usize;
873        let izq_antes = escritorio.raton_izq;
874        if izq && !izq_antes {
875            // Boton bajó: un CLIC. Si cae en la barra de tareas, enfocar la
876            // pestaña pulsada SIN iniciar arrastre. Si no, comportamiento
877            // habitual: enfocar la ventana topmost bajo el puntero.
878            let area_bar = area_taskbar(escritorio.ancho, escritorio.alto);
879            if y >= area_bar.y && y < area_bar.y + area_bar.alto {
880                if clic_en_launcher(&escritorio, x) {
881                    // El boton «+» equivale a `Alt+N`: solicita un parto. La
882                    // tarea del compositor lo recogera en su proxima vuelta.
883                    PARTOS.fetch_add(1, Ordering::Relaxed);
884                } else if let Some(v) = celda_taskbar_en(&escritorio, x) {
885                    let viva = {
886                        let w = &escritorio.ventanas[v];
887                        w.baliza.is_none() && !w.cerrada
888                    };
889                    if viva {
890                        FOCO.store(v, Ordering::Relaxed);
891                        crate::drivers::altavoz::tono(0);
892                        alzar_si_flota(&mut escritorio, v);
893                        cambio = true;
894                    }
895                }
896            } else if let Some(v) = ventana_en(&escritorio, x, y) {
897                let viva = {
898                    let w = &escritorio.ventanas[v];
899                    w.baliza.is_none() && !w.cerrada
900                };
901                if viva {
902                    // Enfocar como hace `mover_foco`: foco + bocina muda + alza
903                    // si flota.
904                    FOCO.store(v, Ordering::Relaxed);
905                    crate::drivers::altavoz::tono(0);
906                    alzar_si_flota(&mut escritorio, v);
907                    // Si la ventana flota, empezar a arrastrarla.
908                    if escritorio.flotantes.contains(&v) {
909                        let marco = escritorio.ventanas[v].marco;
910                        escritorio.arrastre = Some(Arrastre {
911                            ventana: v,
912                            agarre_dx: x.saturating_sub(marco.x),
913                            agarre_dy: y.saturating_sub(marco.y),
914                        });
915                    }
916                    cambio = true;
917                }
918            }
919        } else if izq && izq_antes {
920            // Boton sostenido: arrastrar la ventana asida, si la hay.
921            if let Some(arr) = escritorio.arrastre {
922                mover_arrastrada(&mut escritorio, arr, x, y);
923                cambio = true;
924            }
925        } else if !izq && izq_antes {
926            // Boton subió: fin del arrastre.
927            escritorio.arrastre = None;
928        }
929        escritorio.raton_izq = izq;
930    }
931    if cambio {
932        recomponer(&escritorio);
933        // El recomponer ya presento; sincronizar el centinela para no presentar
934        // dos veces en la misma vuelta.
935        PUNTERO_REFRESCADO.store(empacar_puntero(), Ordering::Relaxed);
936    }
937}
938
939/// La ventana topmost que contiene el punto (x, y), si la hay. Recorre el
940/// orden-Z de delante hacia atras: primero las flotantes (la ultima es la
941/// frontal), despues las teseladas.
942fn ventana_en(escritorio: &Escritorio, x: usize, y: usize) -> Option<usize> {
943    for &v in escritorio.flotantes.iter().rev() {
944        if contiene(escritorio.ventanas[v].marco, x, y) {
945            return Some(v);
946        }
947    }
948    for &v in escritorio.orden.iter().rev() {
949        if contiene(escritorio.ventanas[v].marco, x, y) {
950            return Some(v);
951        }
952    }
953    None
954}
955
956/// ¿Contiene el marco al punto (x, y)?
957fn contiene(marco: RegionPantalla, x: usize, y: usize) -> bool {
958    x >= marco.x && x < marco.x + marco.ancho && y >= marco.y && y < marco.y + marco.alto
959}
960
961/// Mueve la ventana arrastrada de modo que el punto del puntero —la asa— siga
962/// estando, dentro de la ventana, donde se asio. La ventana queda acotada al
963/// area de apps.
964fn mover_arrastrada(escritorio: &mut Escritorio, arr: Arrastre, x: usize, y: usize) {
965    let area = area_apps(escritorio.ancho, escritorio.alto);
966    let Some(ventana) = escritorio.ventanas.get_mut(arr.ventana) else {
967        return;
968    };
969    let ancho = ventana.marco.ancho;
970    let alto = ventana.marco.alto;
971    let mut nx = x.saturating_sub(arr.agarre_dx);
972    let mut ny = y.saturating_sub(arr.agarre_dy);
973    // Acotar al area de apps: la ventana entera ha de caber dentro.
974    if nx + ancho > area.x + area.ancho {
975        nx = (area.x + area.ancho).saturating_sub(ancho);
976    }
977    if ny + alto > area.y + area.alto {
978        ny = (area.y + area.alto).saturating_sub(alto);
979    }
980    nx = nx.max(area.x);
981    ny = ny.max(area.y);
982    ventana.marco.x = nx;
983    ventana.marco.y = ny;
984}
985
986/// Empaca la posicion actual del puntero en un solo `usize` —`y * 65536 + x`—
987/// para compararla atomicamente con la ultima refrescada. `usize::MAX` indica
988/// «el raton no esta vivo».
989fn empacar_puntero() -> usize {
990    match crate::drivers::raton::posicion() {
991        Some((x, y)) => (y << 16) | (x & 0xFFFF),
992        None => usize::MAX,
993    }
994}
995
996/// Si el puntero se ha movido desde la ultima presentacion del compositor, le
997/// pide a la consola un volcado fresco —para reestampar el puntero en su
998/// nuevo lugar—. La invoca la tarea del compositor cada fotograma.
999pub fn refrescar_puntero() {
1000    let actual = empacar_puntero();
1001    if actual == usize::MAX {
1002        return;
1003    }
1004    if PUNTERO_REFRESCADO.swap(actual, Ordering::Relaxed) != actual {
1005        crate::consola::refrescar();
1006    }
1007}
1008
1009// =============================================================================
1010//  Teselado — la geometria pura de `mirada-layout`
1011// =============================================================================
1012
1013/// El area de pantalla que el compositor tesela: toda la pantalla menos la
1014/// franja de la consola en la cima y la barra de tareas al pie.
1015pub fn area_apps(ancho_pantalla: usize, alto_pantalla: usize) -> RegionPantalla {
1016    let cabeza = FRANJA_CONSOLA.min(alto_pantalla);
1017    let pie = FRANJA_TASKBAR.min(alto_pantalla.saturating_sub(cabeza));
1018    RegionPantalla {
1019        x: 0,
1020        y: cabeza,
1021        ancho: ancho_pantalla,
1022        alto: alto_pantalla.saturating_sub(cabeza).saturating_sub(pie),
1023    }
1024}
1025
1026/// El area de la barra de tareas: una franja al pie de la pantalla.
1027fn area_taskbar(ancho_pantalla: usize, alto_pantalla: usize) -> RegionPantalla {
1028    let pie = FRANJA_TASKBAR.min(alto_pantalla);
1029    RegionPantalla {
1030        x: 0,
1031        y: alto_pantalla.saturating_sub(pie),
1032        ancho: ancho_pantalla,
1033        alto: pie,
1034    }
1035}
1036
1037/// El color de tinta —oscuro o claro— que da contraste legible sobre `fondo`.
1038/// Sin esto, la pestaña amarilla palida del desalojo por memoria quedaba con
1039/// texto blanco sobre crema: ilegible. La regla de luminancia ITU-R BT.601 fija
1040/// el umbral: fondos claros llevan tinta oscura, fondos oscuros la clara.
1041fn tinta_para(fondo: Color) -> Color {
1042    let brillo =
1043        (fondo.r as u32 * 299 + fondo.g as u32 * 587 + fondo.b as u32 * 114) / 1000;
1044    if brillo > 160 {
1045        // Fondo claro: tinta del reposo del lienzo, casi negra.
1046        Color::LIENZO_EN_REPOSO
1047    } else {
1048        Color::TEXTO
1049    }
1050}
1051
1052/// Tesela el area de apps en `n` marcos con el modo dado. El vector resultante
1053/// tiene exactamente `n` elementos, en el orden de las celdas del teselado.
1054fn teselar(n: usize, ancho: usize, alto: usize, modo: LayoutMode) -> Vec<RegionPantalla> {
1055    let area = area_apps(ancho, alto);
1056    let pantalla = Rect::new(
1057        area.x as i32,
1058        area.y as i32,
1059        area.ancho as i32,
1060        area.alto as i32,
1061    );
1062    let params = LayoutParams {
1063        mode: modo,
1064        gap: MARGEN,
1065        ..LayoutParams::default()
1066    };
1067    tile(pantalla, n, &params)
1068        .into_iter()
1069        .map(rect_a_region)
1070        .collect()
1071}
1072
1073/// Traduce un `Rect` de `mirada-layout` (`i32`) a la `RegionPantalla` del
1074/// kernel (`usize`). Un rectangulo degenerado queda en cero.
1075fn rect_a_region(r: Rect) -> RegionPantalla {
1076    RegionPantalla {
1077        x: r.x.max(0) as usize,
1078        y: r.y.max(0) as usize,
1079        ancho: r.w.max(0) as usize,
1080        alto: r.h.max(0) as usize,
1081    }
1082}