gioser

tawasuyu/brahman

Rust 534TOML 203Markdown 58JSON 197690 Definiciones

⎇ 6a29152feb85e67dc2ee001c158c62f20dd41380 renaser/kernel/src/compositor.rs Rust · 994 líneas · 40.2 KB Historia Quién tocó qué crudo

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