gioser

tawasuyu/brahman

Rust 572TOML 217Markdown 68JSON 198326 Sutichaykuna

⎇ 8fc1d99ddff618eaa0c2c29f4a51eaaed73443dd renaser/kernel/src/compositor.rs Rust · 878 siq'ikuna · 35.8 KB Ñawpa willay Pi imata llamkarqan ch'uya

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