tawasuyu

tawasuyu/brahman

Rust 582TOML 222Markdown 73JSON 198635 Definiciones

feat(renaser): Fase 9 — orden-Z y ventanas flotantes

Segundo modelo de composición sobre el teselado de la Fase 8: el
SOLAPAMIENTO. Una ventana puede abandonar el teselado y FLOTAR sobre
las demás.

- `Escritorio` gana `flotantes: Vec<usize>` — la pila orden-Z, de
  atrás hacia adelante; con `orden` forma una partición de las
  ventanas.
- Mando `Flotar` (`Alt+F`): alterna la ventana enfocada entre
  teselada y flotante; una flotante nace con marco propio en cascada,
  al frente del orden-Z.
- `compositor::recomponer` + `consola::recomponer` (tipos `Capa` /
  `Contenido`): con flotantes vivas el escritorio se repinta entero,
  capa a capa de atrás hacia adelante — el solapamiento se resuelve
  por el orden del pintado. Sin flotantes, camino rápido de la Fase 8.
- El foco recorre todas las ventanas y alza al frente la flotante
  enfocada.

Verificado en QEMU (sendkey): flotar, cascada, alzado-Z y regreso al
teselado.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>

2523652e2250835d1521b0c3f3fc8374f6f1ae15 · por sergio · (hace 5 meses) · padre 6e30dc2d7 · ver el árbol en este commit

Delta semántico · 29

renaser/kernel/src/async_system/teclado.rs

renaser/kernel/src/compositor.rs

renaser/kernel/src/consola.rs

7 archivos · +393 −115

diff --git a/renaser/CHANGELOG.md b/renaser/CHANGELOG.md
index ab2d57c..a150fc9 100644
--- a/renaser/CHANGELOG.md
+++ b/renaser/CHANGELOG.md
@@ -760,3 +760,48 @@ identidad de las ventanas, y el teclado promueve y reordena.
   ventana maestra y `hola` baja a la pila; `Alt+L` la devuelve a la pila y
   `hola` recupera la maestra. El foco —el borde índigo— viaja siempre con la
   ventana, no con la celda.
+
+## Fase 9 — Orden-Z y ventanas flotantes — 2026-05-22
+
+Hasta la 8d el escritorio era TESELADO puro: las ventanas repartían la pantalla
+sin solaparse jamás. La Fase 9 introduce un segundo modelo de composición —el
+SOLAPAMIENTO—: una ventana puede abandonar el teselado y FLOTAR, con un marco
+propio y libre, por encima de las demás.
+
+### Añadido
+- `Escritorio` gana `flotantes: Vec<usize>` — la pila de ventanas flotantes en
+  orden-Z, de atrás hacia adelante; `flotantes.last()` es la frontal. Junto con
+  `orden` forma una partición de las ventanas: cada una está teselada o flota,
+  nunca en ambas ni en ninguna.
+- Mando `Flotar` (`Alt+F`): alterna la ventana enfocada entre teselada y
+  flotante. Al flotar, abandona el teselado —que se recalcula para las que
+  quedan—, recibe un marco propio (su lienzo natural más un reborde de cromo)
+  colocado en CASCADA, y sube al frente del orden-Z. Al volver, se reincorpora
+  al final del orden de teselado.
+- `compositor::recomponer` — recompone el escritorio entero de una pasada: arma
+  la lista de capas (las teseladas al fondo; las flotantes encima, de atrás
+  hacia adelante) y se la entrega a la consola.
+- `consola::recomponer` y los tipos `Capa` / `Contenido`: la consola funde las
+  capas EN ORDEN sobre el lienzo; el solapamiento se resuelve por el orden del
+  pintado, sin recortes ni máscaras. Una sola presentación cierra la pasada.
+- `consola::componer_fotograma` — el volcado de un fotograma natural centrado
+  en su marco, extraído de `volcar_marco` para compartirlo entre el camino
+  rápido y la recomposición.
+
+### Cambiado
+- `presentar_fotograma` y `desalojar` bifurcan: sin ventanas flotantes
+  conservan el camino RÁPIDO de la Fase 8 —pintar sólo la ventana que cambia—;
+  con flotantes vivas recomponen el escritorio entero respetando el orden-Z.
+- `mover_foco` recorre ahora TODAS las ventanas —las teseladas y las
+  flotantes—; al enfocar una flotante, la alza al frente del orden-Z: la
+  flotante con el foco está siempre delante.
+- `componer_escenario`, `ciclar_layout`, `promover` y `mover_ventana` delegan
+  el repintado en `recomponer`; desaparecen `redibujar_todo`,
+  `redibujar_ventana` y `pintar_escenario`.
+
+### Verificado
+- QEMU (`sendkey`): `Alt+F` saca la ventana maestra del teselado y la deja
+  flotando sobre las demás, que se re-teselan. Un segundo `Alt+F` sobre otra
+  ventana la flota en cascada, solapando a la primera. `Alt+K` devuelve el foco
+  a la ventana grande y ésta sube al frente, tapando por completo a la pequeña.
+  Un `Alt+F` final la reintegra al teselado.
diff --git a/renaser/CLAUDE.md b/renaser/CLAUDE.md
index ea499f5..728958e 100644
--- a/renaser/CLAUDE.md
+++ b/renaser/CLAUDE.md
@@ -74,10 +74,11 @@ QEMU 11, OVMF en `/usr/share/edk2/x64/OVMF.4m.fd` (sin módulo KVM → TCG).
 ## Estado
 
 Fases 1 a 5, 6.0, 6.1, 6.2, la Fase 7 COMPLETA —el userspace nace del grafo de
-objetos— y la Fase 8 COMPLETA —el compositor teselante e interactivo: teselado
+objetos—, la Fase 8 COMPLETA —el compositor teselante e interactivo: teselado
 con `mirada-layout` (8a), ciclado de layout (8b), foco y enrutamiento selectivo
-del teclado (8c), promoción y reordenación de ventanas (8d)—. Todo verificado
-en QEMU. Ver `ROADMAP.md`.
+del teclado (8c), promoción y reordenación de ventanas (8d)— y la Fase 9
+COMPLETA —orden-Z y ventanas flotantes: composición con solapamiento (`Alt+F`)—.
+Todo verificado en QEMU. Ver `ROADMAP.md`.
 
 ## Flujo de trabajo
 
diff --git a/renaser/DIARIO.md b/renaser/DIARIO.md
index c943df2..abe4211 100644
--- a/renaser/DIARIO.md
+++ b/renaser/DIARIO.md
@@ -390,6 +390,30 @@ dueño de su cajón; sólo se mudó de pared. Por eso, cuando la mirada de la ca
 estaba puesta en él, lo sigue al cuarto nuevo — el foco viaja con la persona,
 nunca se queda mirando una pared vacía.
 
+## Las ventanas que flotan — romper la cuadrícula
+
+Hasta hoy la casa repartía sus cuartos como una cuadrícula perfecta: cada
+inquilino tenía su pieza, y las piezas encajaban unas con otras sin pisarse
+jamás. Era ordenado, sí, pero también rígido — no había forma de sacar un
+cuarto de la rejilla y dejarlo suelto.
+
+Hoy la casa aprendió a soltar. Con una tecla, un cuarto puede desprenderse de
+la cuadrícula y quedar FLOTANDO, libre, por encima de los demás. Los que se
+quedan se reparten de nuevo el espacio que el otro dejó. Y si varios cuartos
+flotan, se colocan en cascada, apenas desplazados unos de otros, como naipes
+sobre una mesa: ninguno tapa del todo al que tiene debajo.
+
+Para que esto funcionara, la casa tuvo que cambiar su manera de pintar. Antes
+retocaba sólo el cuarto que cambiaba —podía permitírselo, porque ninguno tapaba
+a otro—. Ahora, cuando hay cuartos flotando, repinta la escena entera de una
+sola pasada, de atrás hacia adelante, como un pintor que cubre primero el fondo
+y deja para el final lo que ha de quedar delante. Así el solapamiento se
+resuelve solo, sin tijeras ni plantillas.
+
+Y hubo una regla pequeña y elegante: el cuarto flotante en que se posa la
+mirada sube siempre al frente. Mirar algo, en esta casa, es traerlo a primer
+plano.
+
 ---
 
 *El diario continúa. La próxima página la escribirá la próxima jornada.*
diff --git a/renaser/ROADMAP.md b/renaser/ROADMAP.md
index 140939e..745e85e 100644
--- a/renaser/ROADMAP.md
+++ b/renaser/ROADMAP.md
@@ -144,8 +144,27 @@ en `FASE8.md`.
   enfocada a la celda maestra; `Alt+H` / `Alt+L` la reordenan. El foco viaja
   con la ventana.
 
-Líneas abiertas posteriores: orden-Z y ventanas flotantes; más capacidades del
-host (temporización, audio).
+## Fase 9 — orden-Z y ventanas flotantes (completada)
+
+El teselado de la Fase 8 repartía la pantalla sin solapamiento. La Fase 9 suma
+un segundo modelo de composición —el SOLAPAMIENTO—: una ventana puede abandonar
+el teselado y FLOTAR sobre las demás. Verificada en QEMU (`sendkey`).
+
+- El `Escritorio` separa dos capas: las ventanas TESELADAS, al fondo; y las
+  FLOTANTES, encima, apiladas en un orden-Z (`flotantes`, de atrás hacia
+  adelante). Juntas son una partición de las ventanas.
+- `Alt+F` alterna la ventana enfocada entre teselada y flotante. Una flotante
+  nace con un marco propio, en cascada, y al frente del orden-Z; al volver al
+  teselado se reincorpora a la rejilla, que se recalcula.
+- Con flotantes vivas, el kernel deja de pintar ventana a ventana: RECOMPONE el
+  escritorio entero, capa a capa de atrás hacia adelante —el solapamiento se
+  resuelve por el orden del pintado—. Sin flotantes conserva el camino rápido
+  de la Fase 8.
+- El foco recorre todas las ventanas; al posarse en una flotante, la alza al
+  frente: la flotante enfocada está siempre delante.
+
+Líneas abiertas posteriores: alta y baja de aplicaciones en vivo; más
+capacidades del host (temporización, audio).
 
 ## Principios que persisten entre fases
 
diff --git a/renaser/kernel/src/async_system/teclado.rs b/renaser/kernel/src/async_system/teclado.rs
index 8a6cdb8..b28239c 100644
--- a/renaser/kernel/src/async_system/teclado.rs
+++ b/renaser/kernel/src/async_system/teclado.rs
@@ -12,7 +12,8 @@
 //
 //    * La tecla Alt es el MODIFICADOR del sistema. Con Alt pulsada, los make
 //      codes son MANDOS del compositor (ciclar el teselado, mover el foco,
-//      promover y reordenar ventanas): se consumen aqui, jamas llegan a una app.
+//      promover, reordenar y hacer flotar ventanas): se consumen aqui, jamas
+//      llegan a una app.
 //    * Una tecla ordinaria se entrega SOLO a la app ENFOCADA — la que el
 //      compositor senala. El censo de canales se indexa por el `indice_app`,
 //      de modo que el foco —un atomico— elija el canal exacto.
@@ -51,6 +52,8 @@ const TECLA_H: u8 = 0x23;
 const TECLA_L: u8 = 0x26;
 /// Tecla Enter — `Alt + Enter` promueve la ventana enfocada a maestra.
 const ENTER: u8 = 0x1C;
+/// Tecla F — `Alt + F` alterna la ventana enfocada entre teselada y flotante.
+const TECLA_F: u8 = 0x21;
 
 /// Un canal de teclado: la cola lock-free de scancodes de UNA aplicacion.
 pub type CanalTeclado = Arc<ArrayQueue<u8>>;
@@ -136,6 +139,7 @@ pub fn recibir_scancode(scancode: u8) {
             ENTER => compositor::solicitar(Mando::Promover),
             TECLA_L => compositor::solicitar(Mando::MoverAdelante),
             TECLA_H => compositor::solicitar(Mando::MoverAtras),
+            TECLA_F => compositor::solicitar(Mando::Flotar),
             _ => {}
         }
         return;
diff --git a/renaser/kernel/src/compositor.rs b/renaser/kernel/src/compositor.rs
index 8469809..9ee5ce6 100644
--- a/renaser/kernel/src/compositor.rs
+++ b/renaser/kernel/src/compositor.rs
@@ -1,5 +1,5 @@
 // =============================================================================
-//  renaser :: kernel/src/compositor.rs — Fase 8 :: el compositor teselante
+//  renaser :: kernel/src/compositor.rs — el compositor: teselado y flotantes
 // -----------------------------------------------------------------------------
 //  El kernel no coloca las ventanas a mano: las TESELA. El motor es
 //  `mirada-layout` —el mismo nucleo `no_std` que ordena el compositor Wayland
@@ -12,6 +12,15 @@
 //  nuevo SIN despertar a las apps: una app que solo pinto en su `init` conserva
 //  su imagen intacta a traves de cualquier reordenacion.
 //
+//  FASE 9 :: orden-Z y ventanas flotantes. Una ventana puede ABANDONAR el
+//  teselado y FLOTAR —un marco propio, libre, que SOLAPA a las demas—. El
+//  escritorio separa entonces dos capas: las TESELADAS, al fondo, sin
+//  solapamiento entre si; y las FLOTANTES, encima, apiladas por un orden-Z
+//  —`flotantes` ES esa pila, de atras hacia adelante; la ultima es la frontal—.
+//  Con flotantes vivas el kernel deja de pintar cada ventana por separado:
+//  RECOMPONE el escritorio entero, capa a capa, de modo que el solapamiento se
+//  resuelva por el orden del pintado, sin recortes ni mascaras.
+//
 //  EXCLUSION DE INTERRUPCIONES. El `ESCRITORIO` lo tocan SOLO tareas
 //  cooperativas (el `tick` de una app, la tarea del compositor): el manejador
 //  de IRQ1 jamas lo bloquea. La IRQ se comunica con el mundo cooperativo por
@@ -29,6 +38,7 @@ use crossbeam_queue::ArrayQueue;
 use mirada_layout::{tile, LayoutMode, LayoutParams, Rect};
 use spin::{Mutex, Once};
 
+use crate::consola::{self, Capa, Contenido};
 use crate::grafico::{Color, RegionPantalla};
 
 /// Altura del strip superior reservado a la consola; las apps teselan debajo.
@@ -45,6 +55,15 @@ const MARGEN: i32 = 14;
 /// Capacidad de la cola de mandos del compositor — holgada: nadie pulsa tanto.
 const CAPACIDAD_MANDOS: usize = 32;
 
+/// Reborde de cromo de una ventana flotante: el panel que rodea su lienzo
+/// natural, donde se asienta el borde de foco sin tapar el dibujo de la app.
+const CROMO_FLOTANTE: usize = 8;
+
+/// Paso de la cascada con que se colocan las ventanas flotantes nuevas, en
+/// pixeles. Cada flotante se desplaza un paso respecto a la anterior, de modo
+/// que varias no se tapen por completo.
+const PASO_CASCADA: usize = 44;
+
 /// Un mando del compositor — lo emite el teclado desde el contexto de IRQ, lo
 /// atiende la tarea del compositor desde el reactor cooperativo.
 #[derive(Clone, Copy)]
@@ -61,6 +80,8 @@ pub enum Mando {
     MoverAdelante,
     /// Mover la ventana enfocada una posicion atras en el orden de teselado.
     MoverAtras,
+    /// Alternar la ventana enfocada entre teselada y flotante (Fase 9).
+    Flotar,
 }
 
 /// Una ventana del escritorio: una app, su geometria y su ultimo fotograma.
@@ -68,8 +89,8 @@ struct Ventana {
     /// Tamaño natural del lienzo de la app — lo que sabe pintar, fijo.
     natural_ancho: usize,
     natural_alto: usize,
-    /// El marco teselado actual — donde la app vive en pantalla. Cambia con
-    /// cada re-teselado.
+    /// El marco actual — donde la app vive en pantalla. Si la ventana esta
+    /// teselada, lo fija el teselado; si flota, es un marco propio y libre.
     marco: RegionPantalla,
     /// CACHE de respaldo: el ultimo fotograma exitoso que la app envio. Su
     /// tamaño esta acotado al lienzo natural —`natural_ancho × natural_alto ×
@@ -93,10 +114,15 @@ struct Escritorio {
     /// Las ventanas, indexadas por `indice_app` — su IDENTIDAD, inmutable.
     ventanas: Vec<Ventana>,
     /// El ORDEN de teselado: `orden[slot]` es el `indice_app` de la ventana que
-    /// ocupa esa celda del teselado. Una permutacion de `0..ventanas.len()`.
-    /// Separar el orden de la identidad permite promover y reordenar ventanas
-    /// sin tocar su `indice_app` —su canal de teclado, su ranura de estado—.
+    /// ocupa esa celda del teselado. Contiene SOLO las ventanas teseladas —las
+    /// flotantes salen de aqui—. Separar el orden de la identidad permite
+    /// promover y reordenar ventanas sin tocar su `indice_app`.
     orden: Vec<usize>,
+    /// Las ventanas FLOTANTES, en orden-Z (Fase 9): de atras hacia adelante.
+    /// `flotantes.last()` es la ventana frontal. Una ventana esta en `orden` o
+    /// en `flotantes`, jamas en ambos ni en ninguno: juntos son una particion
+    /// de `0..ventanas.len()`.
+    flotantes: Vec<usize>,
 }
 
 /// El escritorio global. Se funda una sola vez, en el arranque.
@@ -142,7 +168,8 @@ pub fn fundar(ancho: usize, alto: usize, naturales: &[(usize, usize)]) {
     }
 
     // El orden de teselado arranca como la identidad: la ventana `i` ocupa la
-    // celda `i`. Los mandos del teclado lo permutaran.
+    // celda `i`. Ninguna ventana flota al nacer — el escritorio es puro
+    // teselado hasta que el teclado lo decida (`Alt+F`).
     let orden = (0..ventanas.len()).collect();
     let mut escritorio = Escritorio {
         modo: MODO_INICIAL,
@@ -150,14 +177,16 @@ pub fn fundar(ancho: usize, alto: usize, naturales: &[(usize, usize)]) {
         alto,
         ventanas,
         orden,
+        flotantes: Vec::new(),
     };
     aplicar_teselado(&mut escritorio);
 
     ESCRITORIO.call_once(|| Mutex::new(escritorio));
 }
 
-/// Recalcula el teselado y asigna a cada ventana su marco. La celda `slot` del
-/// teselado va a la ventana `orden[slot]`: manda el orden, no la identidad.
+/// Recalcula el teselado y asigna a cada ventana TESELADA su marco. La celda
+/// `slot` del teselado va a la ventana `orden[slot]`: manda el orden, no la
+/// identidad. Las ventanas flotantes no estan en `orden` y conservan su marco.
 fn aplicar_teselado(escritorio: &mut Escritorio) {
     let marcos = teselar(
         escritorio.orden.len(),
@@ -171,16 +200,15 @@ fn aplicar_teselado(escritorio: &mut Escritorio) {
     }
 }
 
-/// Pinta el escenario inicial del compositor: el area de apps y sus marcos
-/// teselados. Se invoca una vez, tras `fundar`, antes de encender las apps.
+/// Pinta el escenario inicial del compositor. Se invoca una vez, tras `fundar`,
+/// antes de encender las apps: recompone el escritorio con todas las ventanas
+/// aun sin pintar — el teselado se ve como una rejilla de paneles.
 pub fn componer_escenario() {
     let Some(escritorio) = ESCRITORIO.get() else {
         return;
     };
     let escritorio = escritorio.lock();
-    let area = area_apps(escritorio.ancho, escritorio.alto);
-    let marcos: Vec<RegionPantalla> = escritorio.ventanas.iter().map(|v| v.marco).collect();
-    crate::consola::pintar_escenario(area, &marcos);
+    recomponer(&escritorio);
 }
 
 /// El indice de la ventana enfocada. Lo LEE el manejador de IRQ1 para enrutar
@@ -204,14 +232,17 @@ pub fn solicitar(mando: Mando) {
 // =============================================================================
 
 /// Recibe el fotograma de la app `indice`: lo copia a su CACHE de respaldo —el
-/// kernel asume la persistencia visual— y lo compone, centrado, en su marco.
-/// Lo invoca la capacidad `sys_render_frame` desde el `tick` cooperativo.
+/// kernel asume la persistencia visual— y lo lleva a pantalla. Sin ventanas
+/// flotantes ninguna ventana solapa a otra: basta repintar la que cambio —el
+/// camino RAPIDO—. Con flotantes vivas el solapamiento obliga a RECOMPONER el
+/// escritorio entero, respetando el orden-Z. Lo invoca la capacidad
+/// `sys_render_frame` desde el `tick` cooperativo.
 pub fn presentar_fotograma(indice: usize, datos: &[u8]) {
     let Some(escritorio) = ESCRITORIO.get() else {
         return;
     };
-    let (marco, nat_ancho, nat_alto) = {
-        let mut escritorio = escritorio.lock();
+    let mut escritorio = escritorio.lock();
+    {
         let Some(ventana) = escritorio.ventanas.get_mut(indice) else {
             return;
         };
@@ -220,10 +251,22 @@ pub fn presentar_fotograma(indice: usize, datos: &[u8]) {
         let n = ventana.cache.len().min(datos.len());
         ventana.cache[..n].copy_from_slice(&datos[..n]);
         ventana.pintada = true;
-        (ventana.marco, ventana.natural_ancho, ventana.natural_alto)
-    };
-    let enfocada = FOCO.load(Ordering::Relaxed) == indice;
-    crate::consola::volcar_marco(marco, nat_ancho, nat_alto, datos, enfocada);
+    }
+
+    if escritorio.flotantes.is_empty() {
+        // Camino RAPIDO: sin flotantes el escritorio es puro teselado y la app
+        // pinta directamente en su marco, como en la Fase 8.
+        let ventana = &escritorio.ventanas[indice];
+        let marco = ventana.marco;
+        let nat_ancho = ventana.natural_ancho;
+        let nat_alto = ventana.natural_alto;
+        let enfocada = FOCO.load(Ordering::Relaxed) == indice;
+        drop(escritorio);
+        consola::volcar_marco(marco, nat_ancho, nat_alto, datos, enfocada);
+    } else {
+        // Hay ventanas flotantes: el solapamiento obliga a recomponer.
+        recomponer(&escritorio);
+    }
 }
 
 /// Marca la ventana `indice` como desalojada y tatua su marco con la baliza.
@@ -232,16 +275,22 @@ pub fn desalojar(indice: usize, color: Color) {
     let Some(escritorio) = ESCRITORIO.get() else {
         return;
     };
-    let marco = {
-        let mut escritorio = escritorio.lock();
+    let mut escritorio = escritorio.lock();
+    {
         let Some(ventana) = escritorio.ventanas.get_mut(indice) else {
             return;
         };
         ventana.baliza = Some(color);
-        ventana.marco
-    };
-    let enfocada = FOCO.load(Ordering::Relaxed) == indice;
-    crate::consola::pintar_desalojo(marco, color, enfocada);
+    }
+
+    if escritorio.flotantes.is_empty() {
+        let marco = escritorio.ventanas[indice].marco;
+        let enfocada = FOCO.load(Ordering::Relaxed) == indice;
+        drop(escritorio);
+        consola::pintar_desalojo(marco, color, enfocada);
+    } else {
+        recomponer(&escritorio);
+    }
 }
 
 // =============================================================================
@@ -263,12 +312,13 @@ pub fn atender_mandos() {
             Mando::Promover => promover(),
             Mando::MoverAdelante => mover_ventana(true),
             Mando::MoverAtras => mover_ventana(false),
+            Mando::Flotar => flotar(),
         }
     }
 }
 
-/// Cicla al siguiente modo de teselado: recalcula los marcos de todas las
-/// ventanas y recompone el escritorio entero desde las caches de respaldo.
+/// Cicla al siguiente modo de teselado: recalcula los marcos de las ventanas
+/// teseladas y recompone el escritorio entero desde las caches de respaldo.
 fn ciclar_layout() {
     let Some(escritorio) = ESCRITORIO.get() else {
         return;
@@ -276,28 +326,32 @@ fn ciclar_layout() {
     let mut escritorio = escritorio.lock();
     escritorio.modo = escritorio.modo.next();
     aplicar_teselado(&mut escritorio);
-    redibujar_todo(&escritorio);
+    recomponer(&escritorio);
 }
 
-/// Mueve el foco a la siguiente ventana VIVA, recorriendo el ORDEN de teselado
-/// —de modo que el foco salte entre ventanas visualmente contiguas— y saltando
-/// las desalojadas. Redibuja la ventana que pierde el foco y la que lo gana.
+/// Mueve el foco a la siguiente ventana VIVA. El recorrido abarca TODAS las
+/// ventanas —las teseladas y, tras ellas, las flotantes— saltando las
+/// desalojadas. Si la ventana recien enfocada flota, sube al frente del
+/// orden-Z: la flotante con el foco esta SIEMPRE delante.
 fn mover_foco(adelante: bool) {
     let Some(escritorio) = ESCRITORIO.get() else {
         return;
     };
-    let escritorio = escritorio.lock();
-    let n = escritorio.orden.len();
+    let mut escritorio = escritorio.lock();
+    // El recorrido del foco: las teseladas, luego las flotantes — un orden
+    // estable y visualmente coherente.
+    let recorrido: Vec<usize> = escritorio
+        .orden
+        .iter()
+        .chain(escritorio.flotantes.iter())
+        .copied()
+        .collect();
+    let n = recorrido.len();
     if n == 0 {
         return;
     }
     let anterior = FOCO.load(Ordering::Relaxed);
-    // La posicion del foco en el orden de teselado — el punto de partida.
-    let pos = escritorio
-        .orden
-        .iter()
-        .position(|&v| v == anterior)
-        .unwrap_or(0);
+    let pos = recorrido.iter().position(|&v| v == anterior).unwrap_or(0);
 
     // Avanzar saltando las ventanas desalojadas. Si no hay ninguna viva, tras
     // `n` pasos se vuelve al punto de partida y el foco no cambia.
@@ -309,23 +363,21 @@ fn mover_foco(adelante: bool) {
         } else {
             (nueva_pos + n - 1) % n
         };
-        let candidata = escritorio.orden[nueva_pos];
+        let candidata = recorrido[nueva_pos];
         if escritorio.ventanas[candidata].baliza.is_none() {
             nuevo = candidata;
             break;
         }
     }
     FOCO.store(nuevo, Ordering::Relaxed);
-
-    if let Some(ventana) = escritorio.ventanas.get(anterior) {
-        redibujar_ventana(ventana, false);
-    }
-    redibujar_ventana(&escritorio.ventanas[nuevo], true);
+    // La ventana recien enfocada, si flota, al frente del orden-Z.
+    alzar_si_flota(&mut escritorio, nuevo);
+    recomponer(&escritorio);
 }
 
 /// Promueve la ventana enfocada a la posicion maestra —la celda 0— del
-/// teselado: la saca de su lugar en el orden y la inserta al frente. Las demas
-/// se desplazan una posicion. El escritorio entero se recompone.
+/// teselado. Si la ventana enfocada flota, no esta en el orden de teselado y
+/// el mando no hace nada — promover es una operacion del teselado.
 fn promover() {
     let Some(escritorio) = ESCRITORIO.get() else {
         return;
@@ -336,13 +388,13 @@ fn promover() {
         let ventana = escritorio.orden.remove(pos);
         escritorio.orden.insert(0, ventana);
         aplicar_teselado(&mut escritorio);
-        redibujar_todo(&escritorio);
+        recomponer(&escritorio);
     }
 }
 
 /// Mueve la ventana enfocada una posicion en el orden de teselado,
-/// intercambiandola con su vecina. El foco viaja con la ventana — el indice no
-/// cambia, solo su lugar en el orden.
+/// intercambiandola con su vecina. Una ventana flotante no esta en el orden:
+/// el mando no la afecta.
 fn mover_ventana(adelante: bool) {
     let Some(escritorio) = ESCRITORIO.get() else {
         return;
@@ -361,41 +413,107 @@ fn mover_ventana(adelante: bool) {
         };
         escritorio.orden.swap(pos, destino);
         aplicar_teselado(&mut escritorio);
-        redibujar_todo(&escritorio);
+        recomponer(&escritorio);
     }
 }
 
-/// Recompone el escritorio entero: repinta el escenario —area y paneles— con
-/// los marcos nuevos y, sobre el, cada ventana desde su cache de respaldo.
-fn redibujar_todo(escritorio: &Escritorio) {
-    let area = area_apps(escritorio.ancho, escritorio.alto);
-    let marcos: Vec<RegionPantalla> = escritorio.ventanas.iter().map(|v| v.marco).collect();
-    crate::consola::pintar_escenario(area, &marcos);
+// =============================================================================
+//  FASE 9 — orden-Z y ventanas flotantes
+// =============================================================================
 
+/// Alterna la ventana enfocada entre TESELADA y FLOTANTE. Al flotar, la ventana
+/// abandona el teselado —que se recalcula para las que quedan—, recibe un marco
+/// propio en cascada y sube al frente del orden-Z. Al volver al teselado, se
+/// reincorpora al final del orden. El foco no cambia: viaja con la ventana.
+fn flotar() {
+    let Some(escritorio) = ESCRITORIO.get() else {
+        return;
+    };
+    let mut escritorio = escritorio.lock();
     let foco = FOCO.load(Ordering::Relaxed);
-    for (i, ventana) in escritorio.ventanas.iter().enumerate() {
-        redibujar_ventana(ventana, i == foco);
+
+    if let Some(pos) = escritorio.orden.iter().position(|&v| v == foco) {
+        // Teselada -> flotante: se desliga del teselado, recibe su marco
+        // propio en cascada y sube al frente del orden-Z.
+        escritorio.orden.remove(pos);
+        let marco = marco_flotante(&escritorio, foco);
+        escritorio.ventanas[foco].marco = marco;
+        escritorio.flotantes.push(foco);
+        aplicar_teselado(&mut escritorio);
+        recomponer(&escritorio);
+    } else if let Some(pos) = escritorio.flotantes.iter().position(|&v| v == foco) {
+        // Flotante -> teselada: vuelve a la rejilla, al final del orden.
+        escritorio.flotantes.remove(pos);
+        escritorio.orden.push(foco);
+        aplicar_teselado(&mut escritorio);
+        recomponer(&escritorio);
     }
 }
 
-/// Redibuja UNA ventana en su marco actual: si fue desalojada, su baliza; si ya
-/// pinto, su ultimo fotograma desde la cache; si aun no pinto, nada —el panel
-/// del escenario ya esta puesto—.
-fn redibujar_ventana(ventana: &Ventana, enfocada: bool) {
-    match ventana.baliza {
-        Some(color) => crate::consola::pintar_desalojo(ventana.marco, color, enfocada),
-        None => {
-            if ventana.pintada {
-                crate::consola::volcar_marco(
-                    ventana.marco,
-                    ventana.natural_ancho,
-                    ventana.natural_alto,
-                    &ventana.cache,
-                    enfocada,
-                );
-            }
-        }
+/// Si la ventana `indice` es flotante, la lleva al frente del orden-Z —al final
+/// de `flotantes`—. Si esta teselada, no hace nada.
+fn alzar_si_flota(escritorio: &mut Escritorio, indice: usize) {
+    if let Some(pos) = escritorio.flotantes.iter().position(|&v| v == indice) {
+        let ventana = escritorio.flotantes.remove(pos);
+        escritorio.flotantes.push(ventana);
+    }
+}
+
+/// El marco de una ventana recien hecha flotante: su lienzo natural mas un
+/// reborde de cromo, colocado en cascada —para que varias flotantes no se
+/// tapen del todo— y acotado al area de apps. Se invoca ANTES de inscribir la
+/// ventana en `flotantes`: su longitud da el escalon de la cascada.
+fn marco_flotante(escritorio: &Escritorio, indice: usize) -> RegionPantalla {
+    let area = area_apps(escritorio.ancho, escritorio.alto);
+    let ventana = &escritorio.ventanas[indice];
+    let ancho = (ventana.natural_ancho + 2 * CROMO_FLOTANTE).min(area.ancho);
+    let alto = (ventana.natural_alto + 2 * CROMO_FLOTANTE).min(area.alto);
+
+    // La cascada: un escalon por cada flotante ya existente.
+    let escalon = escritorio.flotantes.len().saturating_mul(PASO_CASCADA);
+    let mut x = area.x + 48 + escalon;
+    let mut y = area.y + 40 + escalon;
+    // Acotar: la ventana entera ha de caber dentro del area de apps.
+    if x + ancho > area.x + area.ancho {
+        x = area.x + area.ancho.saturating_sub(ancho);
+    }
+    if y + alto > area.y + area.alto {
+        y = area.y + area.alto.saturating_sub(alto);
+    }
+    RegionPantalla {
+        x,
+        y,
+        ancho,
+        alto,
+    }
+}
+
+/// Recompone el escritorio entero respetando el orden-Z. Arma la lista de capas
+/// —primero las ventanas TESELADAS, la capa de fondo; despues las FLOTANTES, de
+/// atras hacia adelante— y se la entrega a la consola, que las funde en ese
+/// orden de una sola pasada. El solapamiento se resuelve por el orden del
+/// pintado. La invocan los mandos del teclado y `presentar_fotograma` cuando
+/// hay flotantes vivas. El llamante sostiene ya el cerrojo del `ESCRITORIO`.
+fn recomponer(escritorio: &Escritorio) {
+    let area = area_apps(escritorio.ancho, escritorio.alto);
+    let foco = FOCO.load(Ordering::Relaxed);
+    let mut capas: Vec<Capa> = Vec::with_capacity(escritorio.ventanas.len());
+    for &indice in escritorio.orden.iter().chain(escritorio.flotantes.iter()) {
+        let ventana = &escritorio.ventanas[indice];
+        let contenido = match ventana.baliza {
+            Some(color) => Contenido::Baliza(color),
+            None if ventana.pintada => Contenido::Fotograma(&ventana.cache),
+            None => Contenido::Panel,
+        };
+        capas.push(Capa {
+            marco: ventana.marco,
+            nat_ancho: ventana.natural_ancho,
+            nat_alto: ventana.natural_alto,
+            contenido,
+            enfocada: indice == foco,
+        });
     }
+    consola::recomponer(area, &capas);
 }
 
 // =============================================================================
@@ -414,7 +532,7 @@ pub fn area_apps(ancho_pantalla: usize, alto_pantalla: usize) -> RegionPantalla
 }
 
 /// Tesela el area de apps en `n` marcos con el modo dado. El vector resultante
-/// tiene exactamente `n` elementos, en el orden de las apps del manifiesto.
+/// tiene exactamente `n` elementos, en el orden de las celdas del teselado.
 fn teselar(n: usize, ancho: usize, alto: usize, modo: LayoutMode) -> Vec<RegionPantalla> {
     let area = area_apps(ancho, alto);
     let pantalla = Rect::new(
diff --git a/renaser/kernel/src/consola.rs b/renaser/kernel/src/consola.rs
index cd29ec0..6850ea1 100644
--- a/renaser/kernel/src/consola.rs
+++ b/renaser/kernel/src/consola.rs
@@ -34,6 +34,39 @@ fn mezclar(fondo: Color, tinta: Color, cobertura: u8) -> Color {
     }
 }
 
+// =============================================================================
+//  CAPAS — la descripcion de una recomposicion del escritorio (Fase 9)
+// -----------------------------------------------------------------------------
+//  Cuando hay ventanas flotantes, el escritorio no se pinta ventana a ventana:
+//  el compositor entrega la lista de CAPAS —ordenada de atras hacia adelante— y
+//  la consola las funde en ese orden de una sola pasada. El solapamiento de las
+//  ventanas se resuelve solo, por el orden del pintado.
+// =============================================================================
+
+/// El contenido visible de una capa al recomponer el escritorio.
+pub(crate) enum Contenido<'a> {
+    /// La ventana aun no ha pintado: solo se ve su panel de reposo.
+    Panel,
+    /// El ultimo fotograma de la ventana — su lienzo natural crudo.
+    Fotograma(&'a [u8]),
+    /// La ventana fue desalojada: su baliza, un color plano.
+    Baliza(Color),
+}
+
+/// Una capa del escritorio: una ventana, su marco y lo que muestra. El
+/// compositor arma con ellas una lista ordenada de atras hacia adelante.
+pub(crate) struct Capa<'a> {
+    /// El marco donde la ventana vive en pantalla.
+    pub(crate) marco: RegionPantalla,
+    /// El tamaño natural del lienzo de la app — su fotograma mide esto.
+    pub(crate) nat_ancho: usize,
+    pub(crate) nat_alto: usize,
+    /// Lo que la capa muestra.
+    pub(crate) contenido: Contenido<'a>,
+    /// ¿Tiene esta ventana el foco del compositor?
+    pub(crate) enfocada: bool,
+}
+
 /// La consola grafica de renaser: doble bufer, pantalla fisica y pluma.
 pub(crate) struct Consola {
     lienzo: Lienzo,
@@ -120,17 +153,17 @@ impl Consola {
 
     /// Compone un fotograma crudo del userspace WASM —pixeles `0x00RRGGBB`, con
     /// sus limites ya verificados por el host— dentro del MARCO que el
-    /// compositor (Fase 8) asigno a su aplicacion. El fotograma mide el tamaño
-    /// NATURAL de la app (`nat_ancho × nat_alto`); se CENTRA en el marco —que el
-    /// teselado pudo hacer mayor o menor que ese natural— y se recorta con
-    /// firmeza a sus bordes. Una app jamas pinta un pixel fuera de su marco.
-    fn volcar_marco(
+    /// compositor asigno a su aplicacion. El fotograma mide el tamaño NATURAL
+    /// de la app (`nat_ancho × nat_alto`); se CENTRA en el marco —que pudo
+    /// hacerse mayor o menor que ese natural— y se recorta con firmeza a sus
+    /// bordes. Una app jamas pinta un pixel fuera de su marco. NO traza borde
+    /// ni presenta: de eso se encargan `volcar_marco` y `recomponer`.
+    fn componer_fotograma(
         &mut self,
         marco: RegionPantalla,
         nat_ancho: usize,
         nat_alto: usize,
         datos: &[u8],
-        enfocada: bool,
     ) {
         if nat_ancho == 0 || nat_alto == 0 {
             return;
@@ -166,11 +199,62 @@ impl Consola {
             self.lienzo.pixeles[y * self.lienzo.ancho + x] =
                 codificar(self.lienzo.formato, color);
         }
+    }
+
+    /// Compone el fotograma de una app en su marco, le traza el borde de foco y
+    /// presenta. El camino RAPIDO del compositor: sin ventanas flotantes
+    /// ninguna ventana solapa a otra y basta repintar la que cambia.
+    fn volcar_marco(
+        &mut self,
+        marco: RegionPantalla,
+        nat_ancho: usize,
+        nat_alto: usize,
+        datos: &[u8],
+        enfocada: bool,
+    ) {
+        self.componer_fotograma(marco, nat_ancho, nat_alto, datos);
         // El borde del compositor: delata, de un vistazo, quien tiene el foco.
         self.dibujar_borde(marco, enfocada);
         self.presentar();
     }
 
+    /// Recompone el escritorio entero de una sola pasada (Fase 9). Inunda el
+    /// area de apps con el reposo del lienzo y funde sobre ella cada capa, EN
+    /// ORDEN —de atras hacia adelante—: asi el solapamiento de las ventanas
+    /// flotantes se resuelve por si solo, sin recortes ni mascaras. Cada capa
+    /// pinta primero su panel —el cromo de la ventana— y, encima, su contenido;
+    /// una sola presentacion cierra la pasada.
+    fn recomponer(&mut self, area: RegionPantalla, capas: &[Capa]) {
+        self.lienzo.rellenar_rect(
+            area.x,
+            area.y,
+            area.ancho,
+            area.alto,
+            Color::LIENZO_EN_REPOSO,
+        );
+        for capa in capas {
+            let m = capa.marco;
+            match &capa.contenido {
+                Contenido::Panel => {
+                    self.lienzo
+                        .rellenar_rect(m.x, m.y, m.ancho, m.alto, Color::PANEL);
+                }
+                Contenido::Fotograma(datos) => {
+                    // El panel primero —el cromo que rodea el lienzo— y el
+                    // fotograma natural centrado encima.
+                    self.lienzo
+                        .rellenar_rect(m.x, m.y, m.ancho, m.alto, Color::PANEL);
+                    self.componer_fotograma(m, capa.nat_ancho, capa.nat_alto, datos);
+                }
+                Contenido::Baliza(color) => {
+                    self.lienzo.rellenar_rect(m.x, m.y, m.ancho, m.alto, *color);
+                }
+            }
+            self.dibujar_borde(m, capa.enfocada);
+        }
+        self.presentar();
+    }
+
     /// Inunda una region entera con un color plano —la baliza de desalojo: una
     /// app que falla tatua su marco de purpura— y le traza su borde de foco.
     fn pintar_region(&mut self, region: RegionPantalla, color: Color, enfocada: bool) {
@@ -207,25 +291,6 @@ impl Consola {
         );
     }
 
-    /// Pinta el escenario del compositor (Fase 8): inunda el area de apps con
-    /// el reposo del lienzo —borrando cuanto hubiera debajo— y, sobre ella,
-    /// tiñe cada marco teselado con el color de panel. Asi el teselado se ve
-    /// como una rejilla de paneles aun antes de que sus apps pinten nada.
-    fn pintar_escenario(&mut self, area: RegionPantalla, marcos: &[RegionPantalla]) {
-        self.lienzo.rellenar_rect(
-            area.x,
-            area.y,
-            area.ancho,
-            area.alto,
-            Color::LIENZO_EN_REPOSO,
-        );
-        for marco in marcos {
-            self.lienzo
-                .rellenar_rect(marco.x, marco.y, marco.ancho, marco.alto, Color::PANEL);
-        }
-        self.presentar();
-    }
-
     /// Vuelca el lienzo sobre la pantalla fisica.
     pub(crate) fn presentar(&mut self) {
         self.pantalla.presentar(&self.lienzo);
@@ -253,11 +318,13 @@ pub(crate) fn volcar_marco(
     }
 }
 
-/// Pinta el escenario del compositor (Fase 8): el area de apps y, sobre ella,
-/// cada marco teselado. La invoca `compositor` al arrancar y al re-teselar.
-pub(crate) fn pintar_escenario(area: RegionPantalla, marcos: &[RegionPantalla]) {
+/// Recompone el escritorio entero respetando el orden-Z de sus capas (Fase 9).
+/// La invoca `compositor` al arrancar y siempre que hay ventanas flotantes: el
+/// solapamiento obliga a repintar el escritorio en bloque, no ventana a
+/// ventana. Las capas llegan ya ordenadas de atras hacia adelante.
+pub(crate) fn recomponer(area: RegionPantalla, capas: &[Capa]) {
     if let Some(consola) = CONSOLA.get() {
-        consola.lock().pintar_escenario(area, marcos);
+        consola.lock().recomponer(area, capas);
     }
 }