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` — 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>; @@ -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, /// 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, + /// 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, } /// 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 = 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 = 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,43 +413,109 @@ 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 = 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 = 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` // ============================================================================= @@ -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 { 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); } }