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>
This commit is contained in:
sergio
2026-05-22 19:42:51 +00:00
parent 6e30dc2d72
commit 2523652e22
7 changed files with 393 additions and 115 deletions
+45
View File
@@ -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 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 `hola` recupera la maestra. El foco —el borde índigo— viaja siempre con la
ventana, no con la celda. 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.
+4 -3
View File
@@ -74,10 +74,11 @@ QEMU 11, OVMF en `/usr/share/edk2/x64/OVMF.4m.fd` (sin módulo KVM → TCG).
## Estado ## Estado
Fases 1 a 5, 6.0, 6.1, 6.2, la Fase 7 COMPLETA —el userspace nace del grafo de 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 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 del teclado (8c), promoción y reordenación de ventanas (8d)— y la Fase 9
en QEMU. Ver `ROADMAP.md`. COMPLETA —orden-Z y ventanas flotantes: composición con solapamiento (`Alt+F`)—.
Todo verificado en QEMU. Ver `ROADMAP.md`.
## Flujo de trabajo ## Flujo de trabajo
+24
View File
@@ -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, estaba puesta en él, lo sigue al cuarto nuevo — el foco viaja con la persona,
nunca se queda mirando una pared vacía. 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.* *El diario continúa. La próxima página la escribirá la próxima jornada.*
+21 -2
View File
@@ -144,8 +144,27 @@ en `FASE8.md`.
enfocada a la celda maestra; `Alt+H` / `Alt+L` la reordenan. El foco viaja enfocada a la celda maestra; `Alt+H` / `Alt+L` la reordenan. El foco viaja
con la ventana. con la ventana.
Líneas abiertas posteriores: orden-Z y ventanas flotantes; más capacidades del ## Fase 9 — orden-Z y ventanas flotantes (completada)
host (temporización, audio).
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 ## Principios que persisten entre fases
+5 -1
View File
@@ -12,7 +12,8 @@
// //
// * La tecla Alt es el MODIFICADOR del sistema. Con Alt pulsada, los make // * La tecla Alt es el MODIFICADOR del sistema. Con Alt pulsada, los make
// codes son MANDOS del compositor (ciclar el teselado, mover el foco, // 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 // * 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`, // compositor senala. El censo de canales se indexa por el `indice_app`,
// de modo que el foco —un atomico— elija el canal exacto. // 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; const TECLA_L: u8 = 0x26;
/// Tecla Enter — `Alt + Enter` promueve la ventana enfocada a maestra. /// Tecla Enter — `Alt + Enter` promueve la ventana enfocada a maestra.
const ENTER: u8 = 0x1C; 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. /// Un canal de teclado: la cola lock-free de scancodes de UNA aplicacion.
pub type CanalTeclado = Arc<ArrayQueue<u8>>; pub type CanalTeclado = Arc<ArrayQueue<u8>>;
@@ -136,6 +139,7 @@ pub fn recibir_scancode(scancode: u8) {
ENTER => compositor::solicitar(Mando::Promover), ENTER => compositor::solicitar(Mando::Promover),
TECLA_L => compositor::solicitar(Mando::MoverAdelante), TECLA_L => compositor::solicitar(Mando::MoverAdelante),
TECLA_H => compositor::solicitar(Mando::MoverAtras), TECLA_H => compositor::solicitar(Mando::MoverAtras),
TECLA_F => compositor::solicitar(Mando::Flotar),
_ => {} _ => {}
} }
return; return;
+198 -80
View File
@@ -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 // 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 // `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 // nuevo SIN despertar a las apps: una app que solo pinto en su `init` conserva
// su imagen intacta a traves de cualquier reordenacion. // 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 // EXCLUSION DE INTERRUPCIONES. El `ESCRITORIO` lo tocan SOLO tareas
// cooperativas (el `tick` de una app, la tarea del compositor): el manejador // 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 // 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 mirada_layout::{tile, LayoutMode, LayoutParams, Rect};
use spin::{Mutex, Once}; use spin::{Mutex, Once};
use crate::consola::{self, Capa, Contenido};
use crate::grafico::{Color, RegionPantalla}; use crate::grafico::{Color, RegionPantalla};
/// Altura del strip superior reservado a la consola; las apps teselan debajo. /// 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. /// Capacidad de la cola de mandos del compositor — holgada: nadie pulsa tanto.
const CAPACIDAD_MANDOS: usize = 32; 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 /// Un mando del compositor — lo emite el teclado desde el contexto de IRQ, lo
/// atiende la tarea del compositor desde el reactor cooperativo. /// atiende la tarea del compositor desde el reactor cooperativo.
#[derive(Clone, Copy)] #[derive(Clone, Copy)]
@@ -61,6 +80,8 @@ pub enum Mando {
MoverAdelante, MoverAdelante,
/// Mover la ventana enfocada una posicion atras en el orden de teselado. /// Mover la ventana enfocada una posicion atras en el orden de teselado.
MoverAtras, MoverAtras,
/// Alternar la ventana enfocada entre teselada y flotante (Fase 9).
Flotar,
} }
/// Una ventana del escritorio: una app, su geometria y su ultimo fotograma. /// 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. /// Tamaño natural del lienzo de la app — lo que sabe pintar, fijo.
natural_ancho: usize, natural_ancho: usize,
natural_alto: usize, natural_alto: usize,
/// El marco teselado actual — donde la app vive en pantalla. Cambia con /// El marco actual — donde la app vive en pantalla. Si la ventana esta
/// cada re-teselado. /// teselada, lo fija el teselado; si flota, es un marco propio y libre.
marco: RegionPantalla, marco: RegionPantalla,
/// CACHE de respaldo: el ultimo fotograma exitoso que la app envio. Su /// CACHE de respaldo: el ultimo fotograma exitoso que la app envio. Su
/// tamaño esta acotado al lienzo natural —`natural_ancho × natural_alto × /// 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. /// Las ventanas, indexadas por `indice_app` — su IDENTIDAD, inmutable.
ventanas: Vec<Ventana>, ventanas: Vec<Ventana>,
/// El ORDEN de teselado: `orden[slot]` es el `indice_app` de la ventana que /// 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()`. /// ocupa esa celda del teselado. Contiene SOLO las ventanas teseladas —las
/// Separar el orden de la identidad permite promover y reordenar ventanas /// flotantes salen de aqui—. Separar el orden de la identidad permite
/// sin tocar su `indice_app` —su canal de teclado, su ranura de estado—. /// promover y reordenar ventanas sin tocar su `indice_app`.
orden: Vec<usize>, 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. /// 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 // 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 orden = (0..ventanas.len()).collect();
let mut escritorio = Escritorio { let mut escritorio = Escritorio {
modo: MODO_INICIAL, modo: MODO_INICIAL,
@@ -150,14 +177,16 @@ pub fn fundar(ancho: usize, alto: usize, naturales: &[(usize, usize)]) {
alto, alto,
ventanas, ventanas,
orden, orden,
flotantes: Vec::new(),
}; };
aplicar_teselado(&mut escritorio); aplicar_teselado(&mut escritorio);
ESCRITORIO.call_once(|| Mutex::new(escritorio)); ESCRITORIO.call_once(|| Mutex::new(escritorio));
} }
/// Recalcula el teselado y asigna a cada ventana su marco. La celda `slot` del /// Recalcula el teselado y asigna a cada ventana TESELADA su marco. La celda
/// teselado va a la ventana `orden[slot]`: manda el orden, no la identidad. /// `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) { fn aplicar_teselado(escritorio: &mut Escritorio) {
let marcos = teselar( let marcos = teselar(
escritorio.orden.len(), 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 /// Pinta el escenario inicial del compositor. Se invoca una vez, tras `fundar`,
/// teselados. Se invoca una vez, tras `fundar`, antes de encender las apps. /// 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() { pub fn componer_escenario() {
let Some(escritorio) = ESCRITORIO.get() else { let Some(escritorio) = ESCRITORIO.get() else {
return; return;
}; };
let escritorio = escritorio.lock(); let escritorio = escritorio.lock();
let area = area_apps(escritorio.ancho, escritorio.alto); recomponer(&escritorio);
let marcos: Vec<RegionPantalla> = escritorio.ventanas.iter().map(|v| v.marco).collect();
crate::consola::pintar_escenario(area, &marcos);
} }
/// El indice de la ventana enfocada. Lo LEE el manejador de IRQ1 para enrutar /// 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 /// 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. /// kernel asume la persistencia visual— y lo lleva a pantalla. Sin ventanas
/// Lo invoca la capacidad `sys_render_frame` desde el `tick` cooperativo. /// 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]) { pub fn presentar_fotograma(indice: usize, datos: &[u8]) {
let Some(escritorio) = ESCRITORIO.get() else { let Some(escritorio) = ESCRITORIO.get() else {
return; 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 { let Some(ventana) = escritorio.ventanas.get_mut(indice) else {
return; return;
}; };
@@ -220,10 +251,22 @@ pub fn presentar_fotograma(indice: usize, datos: &[u8]) {
let n = ventana.cache.len().min(datos.len()); let n = ventana.cache.len().min(datos.len());
ventana.cache[..n].copy_from_slice(&datos[..n]); ventana.cache[..n].copy_from_slice(&datos[..n]);
ventana.pintada = true; ventana.pintada = true;
(ventana.marco, ventana.natural_ancho, ventana.natural_alto) }
};
let enfocada = FOCO.load(Ordering::Relaxed) == indice; if escritorio.flotantes.is_empty() {
crate::consola::volcar_marco(marco, nat_ancho, nat_alto, datos, enfocada); // 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. /// 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 { let Some(escritorio) = ESCRITORIO.get() else {
return; return;
}; };
let marco = { let mut escritorio = escritorio.lock();
let mut escritorio = escritorio.lock(); {
let Some(ventana) = escritorio.ventanas.get_mut(indice) else { let Some(ventana) = escritorio.ventanas.get_mut(indice) else {
return; return;
}; };
ventana.baliza = Some(color); ventana.baliza = Some(color);
ventana.marco }
};
let enfocada = FOCO.load(Ordering::Relaxed) == indice; if escritorio.flotantes.is_empty() {
crate::consola::pintar_desalojo(marco, color, enfocada); 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::Promover => promover(),
Mando::MoverAdelante => mover_ventana(true), Mando::MoverAdelante => mover_ventana(true),
Mando::MoverAtras => mover_ventana(false), Mando::MoverAtras => mover_ventana(false),
Mando::Flotar => flotar(),
} }
} }
} }
/// Cicla al siguiente modo de teselado: recalcula los marcos de todas las /// Cicla al siguiente modo de teselado: recalcula los marcos de las ventanas
/// ventanas y recompone el escritorio entero desde las caches de respaldo. /// teseladas y recompone el escritorio entero desde las caches de respaldo.
fn ciclar_layout() { fn ciclar_layout() {
let Some(escritorio) = ESCRITORIO.get() else { let Some(escritorio) = ESCRITORIO.get() else {
return; return;
@@ -276,28 +326,32 @@ fn ciclar_layout() {
let mut escritorio = escritorio.lock(); let mut escritorio = escritorio.lock();
escritorio.modo = escritorio.modo.next(); escritorio.modo = escritorio.modo.next();
aplicar_teselado(&mut escritorio); aplicar_teselado(&mut escritorio);
redibujar_todo(&escritorio); recomponer(&escritorio);
} }
/// Mueve el foco a la siguiente ventana VIVA, recorriendo el ORDEN de teselado /// Mueve el foco a la siguiente ventana VIVA. El recorrido abarca TODAS las
/// —de modo que el foco salte entre ventanas visualmente contiguas— y saltando /// ventanas —las teseladas y, tras ellas, las flotantes— saltando las
/// las desalojadas. Redibuja la ventana que pierde el foco y la que lo gana. /// 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) { fn mover_foco(adelante: bool) {
let Some(escritorio) = ESCRITORIO.get() else { let Some(escritorio) = ESCRITORIO.get() else {
return; return;
}; };
let escritorio = escritorio.lock(); let mut escritorio = escritorio.lock();
let n = escritorio.orden.len(); // 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 { if n == 0 {
return; return;
} }
let anterior = FOCO.load(Ordering::Relaxed); let anterior = FOCO.load(Ordering::Relaxed);
// La posicion del foco en el orden de teselado — el punto de partida. let pos = recorrido.iter().position(|&v| v == anterior).unwrap_or(0);
let pos = escritorio
.orden
.iter()
.position(|&v| v == anterior)
.unwrap_or(0);
// Avanzar saltando las ventanas desalojadas. Si no hay ninguna viva, tras // Avanzar saltando las ventanas desalojadas. Si no hay ninguna viva, tras
// `n` pasos se vuelve al punto de partida y el foco no cambia. // `n` pasos se vuelve al punto de partida y el foco no cambia.
@@ -309,23 +363,21 @@ fn mover_foco(adelante: bool) {
} else { } else {
(nueva_pos + n - 1) % n (nueva_pos + n - 1) % n
}; };
let candidata = escritorio.orden[nueva_pos]; let candidata = recorrido[nueva_pos];
if escritorio.ventanas[candidata].baliza.is_none() { if escritorio.ventanas[candidata].baliza.is_none() {
nuevo = candidata; nuevo = candidata;
break; break;
} }
} }
FOCO.store(nuevo, Ordering::Relaxed); FOCO.store(nuevo, Ordering::Relaxed);
// La ventana recien enfocada, si flota, al frente del orden-Z.
if let Some(ventana) = escritorio.ventanas.get(anterior) { alzar_si_flota(&mut escritorio, nuevo);
redibujar_ventana(ventana, false); recomponer(&escritorio);
}
redibujar_ventana(&escritorio.ventanas[nuevo], true);
} }
/// Promueve la ventana enfocada a la posicion maestra —la celda 0— del /// 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 /// teselado. Si la ventana enfocada flota, no esta en el orden de teselado y
/// se desplazan una posicion. El escritorio entero se recompone. /// el mando no hace nada — promover es una operacion del teselado.
fn promover() { fn promover() {
let Some(escritorio) = ESCRITORIO.get() else { let Some(escritorio) = ESCRITORIO.get() else {
return; return;
@@ -336,13 +388,13 @@ fn promover() {
let ventana = escritorio.orden.remove(pos); let ventana = escritorio.orden.remove(pos);
escritorio.orden.insert(0, ventana); escritorio.orden.insert(0, ventana);
aplicar_teselado(&mut escritorio); aplicar_teselado(&mut escritorio);
redibujar_todo(&escritorio); recomponer(&escritorio);
} }
} }
/// Mueve la ventana enfocada una posicion en el orden de teselado, /// Mueve la ventana enfocada una posicion en el orden de teselado,
/// intercambiandola con su vecina. El foco viaja con la ventana — el indice no /// intercambiandola con su vecina. Una ventana flotante no esta en el orden:
/// cambia, solo su lugar en el orden. /// el mando no la afecta.
fn mover_ventana(adelante: bool) { fn mover_ventana(adelante: bool) {
let Some(escritorio) = ESCRITORIO.get() else { let Some(escritorio) = ESCRITORIO.get() else {
return; return;
@@ -361,43 +413,109 @@ fn mover_ventana(adelante: bool) {
}; };
escritorio.orden.swap(pos, destino); escritorio.orden.swap(pos, destino);
aplicar_teselado(&mut escritorio); 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. // FASE 9 — orden-Z y ventanas flotantes
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);
/// 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); 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 /// Si la ventana `indice` es flotante, la lleva al frente del orden-Z —al final
/// pinto, su ultimo fotograma desde la cache; si aun no pinto, nada —el panel /// de `flotantes`—. Si esta teselada, no hace nada.
/// del escenario ya esta puesto—. fn alzar_si_flota(escritorio: &mut Escritorio, indice: usize) {
fn redibujar_ventana(ventana: &Ventana, enfocada: bool) { if let Some(pos) = escritorio.flotantes.iter().position(|&v| v == indice) {
match ventana.baliza { let ventana = escritorio.flotantes.remove(pos);
Some(color) => crate::consola::pintar_desalojo(ventana.marco, color, enfocada), escritorio.flotantes.push(ventana);
None => {
if ventana.pintada {
crate::consola::volcar_marco(
ventana.marco,
ventana.natural_ancho,
ventana.natural_alto,
&ventana.cache,
enfocada,
);
}
}
} }
} }
/// 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);
}
// ============================================================================= // =============================================================================
// Teselado — la geometria pura de `mirada-layout` // Teselado — la geometria pura de `mirada-layout`
// ============================================================================= // =============================================================================
@@ -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 /// 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> { fn teselar(n: usize, ancho: usize, alto: usize, modo: LayoutMode) -> Vec<RegionPantalla> {
let area = area_apps(ancho, alto); let area = area_apps(ancho, alto);
let pantalla = Rect::new( let pantalla = Rect::new(
+96 -29
View File
@@ -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. /// La consola grafica de renaser: doble bufer, pantalla fisica y pluma.
pub(crate) struct Consola { pub(crate) struct Consola {
lienzo: Lienzo, lienzo: Lienzo,
@@ -120,17 +153,17 @@ impl Consola {
/// Compone un fotograma crudo del userspace WASM —pixeles `0x00RRGGBB`, con /// Compone un fotograma crudo del userspace WASM —pixeles `0x00RRGGBB`, con
/// sus limites ya verificados por el host— dentro del MARCO que el /// 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 /// compositor asigno a su aplicacion. El fotograma mide el tamaño NATURAL
/// NATURAL de la app (`nat_ancho × nat_alto`); se CENTRA en el marco —que el /// de la app (`nat_ancho × nat_alto`); se CENTRA en el marco —que pudo
/// teselado pudo hacer mayor o menor que ese natural— y se recorta con /// hacerse mayor o menor que ese natural— y se recorta con firmeza a sus
/// firmeza a sus bordes. Una app jamas pinta un pixel fuera de su marco. /// bordes. Una app jamas pinta un pixel fuera de su marco. NO traza borde
fn volcar_marco( /// ni presenta: de eso se encargan `volcar_marco` y `recomponer`.
fn componer_fotograma(
&mut self, &mut self,
marco: RegionPantalla, marco: RegionPantalla,
nat_ancho: usize, nat_ancho: usize,
nat_alto: usize, nat_alto: usize,
datos: &[u8], datos: &[u8],
enfocada: bool,
) { ) {
if nat_ancho == 0 || nat_alto == 0 { if nat_ancho == 0 || nat_alto == 0 {
return; return;
@@ -166,11 +199,62 @@ impl Consola {
self.lienzo.pixeles[y * self.lienzo.ancho + x] = self.lienzo.pixeles[y * self.lienzo.ancho + x] =
codificar(self.lienzo.formato, color); 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. // El borde del compositor: delata, de un vistazo, quien tiene el foco.
self.dibujar_borde(marco, enfocada); self.dibujar_borde(marco, enfocada);
self.presentar(); 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 /// 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. /// 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) { 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. /// Vuelca el lienzo sobre la pantalla fisica.
pub(crate) fn presentar(&mut self) { pub(crate) fn presentar(&mut self) {
self.pantalla.presentar(&self.lienzo); 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, /// Recompone el escritorio entero respetando el orden-Z de sus capas (Fase 9).
/// cada marco teselado. La invoca `compositor` al arrancar y al re-teselar. /// La invoca `compositor` al arrancar y siempre que hay ventanas flotantes: el
pub(crate) fn pintar_escenario(area: RegionPantalla, marcos: &[RegionPantalla]) { /// 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() { if let Some(consola) = CONSOLA.get() {
consola.lock().pintar_escenario(area, marcos); consola.lock().recomponer(area, capas);
} }
} }