Files
brahman/renaser/kernel/src/compositor.rs
T
sergio 8fc1d99ddf feat(renaser): Fase 13 — ratón, puntero y arrastre de flotantes
renaser dialogaba sólo con el teclado; las ventanas flotantes nacían
en cascada y allí se quedaban. La Fase 13 trae el ratón.

- Driver `drivers/raton`: el ratón PS/2 cuelga del dispositivo
  auxiliar del 8042 + IRQ12. El driver despierta el aux, programa
  su IRQ, le ordena reportar, ensambla paquetes de 3 bytes con
  guarda del bit-3. Posición como atómicos, eventos como cola
  lock-free — el mismo guardarraíl que el teclado.
- El puntero, capa de PRESENTACIÓN: `Pantalla::estampar_puntero`
  pinta un sprite de flecha 12×18 sobre el framebuffer después de
  copiar el lienzo. El lienzo nunca lo contiene — hace de
  save-under natural—.
- Compositor: `atender_raton` drena eventos. Botón bajando es un
  clic-para-enfocar consistente con `mover_foco` (silencia bocina,
  alza si flota). Si la enfocada flota, arranca un arrastre con el
  desfase de agarre; el botón sostenido la sigue al puntero; al
  soltar, termina.
- `refrescar_puntero` reestampa el framebuffer si el puntero se
  movió en una vuelta tranquila en que ninguna app pintó.

Verificado en QEMU (mouse_move / mouse_button del monitor): el
puntero aparece al arrancar, se mueve por la pantalla, un clic
sobre pulso le da el foco, y un arrastre con el botón sostenido
mueve la flotante de la cascada al centro-abajo.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-05-22 23:21:06 +00:00

879 lines
36 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// =============================================================================
// 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
// de brahman—, enlazado por `path` cruzando la frontera de workspace.
//
// FASE 8b/8c :: el compositor cobra vida. Mantiene un ESCRITORIO —el registro
// de todas las ventanas— y, por cada una, una CACHE de respaldo con su ultimo
// fotograma. Gracias a esa cache, el teclado puede re-teselar el escritorio en
// caliente —o mover el foco— y el kernel recompone cada ventana en su marco
// 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.
//
// FASE 10 :: el escritorio deja de ser un censo fijo. Una ventana puede
// CERRARSE en vivo (`Alt+Q`): se la marca, su app concluye su tarea por su
// voluntad y el teselado reclama su espacio. Y puede NACER una ventana nueva
// (`Alt+N`): `nacer_ventana` la añade al censo y devuelve su indice al
// orquestador, que instancia su WASM y engendra su tarea. El censo de
// ventanas solo crece —los indices son la IDENTIDAD, jamas se reciclan—; una
// ventana cerrada queda como una ranura inerte, fuera del orden y del foco.
//
// FASE 13 :: el raton entra en juego. Hay un PUNTERO en pantalla y el
// compositor gana dos gestos: clic-para-enfocar (sobre cualquier ventana viva)
// y ARRASTRAR una flotante con el boton izquierdo sostenido. Como el teclado
// y la bocina, los eventos del raton vienen del manejador de IRQ12 por una
// cola lock-free; `atender_raton` los drena cooperativamente y, al detectar
// un boton que baja o un arrastre en curso, mueve el foco o el marco. Los
// cuartos flotantes dejan, por fin, de estar clavados en su cascada.
//
// 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
// un canal estrecho y a prueba de interbloqueos: dos atomicos —el foco y el
// estado de Alt— y una cola lock-free de mandos. Ningun cerrojo que la IRQ
// pudiera disputar a una tarea cooperativa.
// =============================================================================
use core::sync::atomic::{AtomicUsize, Ordering};
use alloc::vec;
use alloc::vec::Vec;
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.
/// La consola conserva ahi su registro de arranque completo —seis lineas,
/// hasta la sonda asincrona de disco— legible sobre el teselado.
const FRANJA_CONSOLA: usize = 296;
/// El modo de teselado con que arranca el escritorio. El teclado lo cicla.
const MODO_INICIAL: LayoutMode = LayoutMode::MasterStack;
/// Margen entre ventanas teseladas, en pixeles.
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)]
pub enum Mando {
/// Ciclar al siguiente modo de teselado de `mirada-layout`.
CiclarLayout,
/// Mover el foco a la siguiente ventana viva.
FocoSiguiente,
/// Mover el foco a la ventana viva anterior.
FocoAnterior,
/// Promover la ventana enfocada a la posicion maestra del teselado.
Promover,
/// Mover la ventana enfocada una posicion adelante en el orden de teselado.
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,
/// Cerrar la aplicacion enfocada — una baja limpia, en vivo (Fase 10).
Cerrar,
/// Lanzar una aplicacion nueva — un alta en vivo (Fase 10).
Lanzar,
}
/// Un arrastre EN CURSO (Fase 13): el indice de la ventana flotante asida con
/// el raton y el desfase con que se asio —para que la ventana no salte al
/// agarrarla, sino que siga al puntero como si lo llevara cogido por ahi—.
#[derive(Clone, Copy)]
struct Arrastre {
ventana: usize,
agarre_dx: usize,
agarre_dy: usize,
}
/// Una ventana del escritorio: una app, su geometria y su ultimo fotograma.
struct Ventana {
/// Tamaño natural del lienzo de la app — lo que sabe pintar, fijo.
natural_ancho: usize,
natural_alto: usize,
/// 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 ×
/// 4`— y se reserva UNA sola vez, al fundar el escritorio: jamas crece. El
/// re-teselado recompone la ventana desde aqui, sin molestar a la app.
cache: Vec<u8>,
/// ¿Ha enviado la app al menos un fotograma? Hasta entonces, su cache es
/// solo ceros y no se recompone.
pintada: bool,
/// Si el kernel desalojo la app, el color de su baliza. `None` mientras
/// vive; `Some(color)` la marca como muerta y la excluye del foco.
baliza: Option<Color>,
/// ¿Se ha pedido cerrar esta ventana en vivo (Fase 10)? Una vez `true`, su
/// app concluye su tarea, la ranura queda inerte —fuera del orden, del
/// orden-Z y del foco— y el teselado reclama su espacio.
cerrada: bool,
}
/// El escritorio: el registro de todas las ventanas y el modo de teselado.
/// Lo tocan SOLO tareas cooperativas — nunca el manejador de IRQ1.
struct Escritorio {
modo: LayoutMode,
ancho: usize,
alto: usize,
/// 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. 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>,
/// ¿Estaba el boton izquierdo del raton pulsado en el evento anterior?
/// Para detectar las transiciones —el momento exacto del clic o de soltar—.
raton_izq: bool,
/// Arrastre en curso, si lo hay (Fase 13).
arrastre: Option<Arrastre>,
}
/// El escritorio global. Se funda una sola vez, en el arranque.
static ESCRITORIO: Once<Mutex<Escritorio>> = Once::new();
/// El indice de la ventana ENFOCADA. Atomico —no un campo del `Escritorio`—
/// porque el manejador de IRQ1 lo LEE para enrutar el teclado, y un atomico no
/// se puede disputar: jamas hay interbloqueo entre la IRQ y una tarea.
static FOCO: AtomicUsize = AtomicUsize::new(0);
/// La cola de mandos: el manejador de IRQ1 deposita aqui las ordenes del
/// teclado (lock-free, segura en contexto de interrupcion); la tarea del
/// compositor las drena desde el reactor cooperativo.
static MANDOS: Once<ArrayQueue<Mando>> = Once::new();
/// Cuantos lanzamientos de aplicacion (Fase 10) aguardan. Lo incrementa
/// `atender_mandos` al recibir un `Mando::Lanzar`; lo drena `partos_pendientes`,
/// que lo lee el orquestador del kernel —el unico que sabe instanciar un WASM—.
/// Atomico: el compositor lo escribe, el orquestador lo lee y lo pone a cero.
static PARTOS: AtomicUsize = AtomicUsize::new(0);
// =============================================================================
// Fundacion y consulta — el arranque
// =============================================================================
/// Funda el escritorio: crea una ventana por app, con su marco teselado inicial
/// y su cache de respaldo ya reservada al tamaño natural. `naturales` da el
/// `(ancho, alto)` del lienzo de cada app, en el orden del manifiesto.
pub fn fundar(ancho: usize, alto: usize, naturales: &[(usize, usize)]) {
MANDOS.call_once(|| ArrayQueue::new(CAPACIDAD_MANDOS));
let mut ventanas = Vec::with_capacity(naturales.len());
for &(nat_ancho, nat_alto) in naturales {
ventanas.push(Ventana {
natural_ancho: nat_ancho,
natural_alto: nat_alto,
// Marco provisional; `aplicar_teselado` lo fija enseguida.
marco: RegionPantalla {
x: 0,
y: 0,
ancho: 0,
alto: 0,
},
// La cache: reservada UNA vez, acotada al lienzo natural.
cache: vec![0u8; nat_ancho.saturating_mul(nat_alto).saturating_mul(4)],
pintada: false,
baliza: None,
cerrada: false,
});
}
// El orden de teselado arranca como la identidad: la ventana `i` ocupa la
// 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,
ancho,
alto,
ventanas,
orden,
flotantes: Vec::new(),
raton_izq: false,
arrastre: None,
};
aplicar_teselado(&mut escritorio);
ESCRITORIO.call_once(|| Mutex::new(escritorio));
}
/// 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(),
escritorio.ancho,
escritorio.alto,
escritorio.modo,
);
for (slot, marco) in marcos.into_iter().enumerate() {
let ventana = escritorio.orden[slot];
escritorio.ventanas[ventana].marco = marco;
}
}
/// 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();
recomponer(&escritorio);
}
/// El indice de la ventana enfocada. Lo LEE el manejador de IRQ1 para enrutar
/// cada tecla — por eso es una simple lectura atomica, sin cerrojo alguno.
pub fn foco() -> usize {
FOCO.load(Ordering::Relaxed)
}
/// Encola un mando del teclado. Lo invoca el manejador de IRQ1: empujar a una
/// cola lock-free es seguro en contexto de interrupcion.
pub fn solicitar(mando: Mando) {
if let Some(mandos) = MANDOS.get() {
// Si la cola se desborda, el mando se pierde en silencio: mas vale
// perder una pulsacion que arriesgar un panico dentro de una IRQ.
let _ = mandos.push(mando);
}
}
// =============================================================================
// El fotograma de una app — cache y composicion
// =============================================================================
/// Recibe el fotograma de la app `indice`: lo copia a su CACHE de respaldo —el
/// 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 mut escritorio = escritorio.lock();
{
let Some(ventana) = escritorio.ventanas.get_mut(indice) else {
return;
};
// Una ventana cerrada (Fase 10) ya no se pinta: su app pudo emitir un
// ultimo fotograma antes de que su tarea advirtiera la baja.
if ventana.cerrada {
return;
}
// Cachear el fotograma. El destino esta acotado al lienzo natural; se
// copia el minimo de ambas longitudes — jamas se desborda la cache.
let n = ventana.cache.len().min(datos.len());
ventana.cache[..n].copy_from_slice(&datos[..n]);
ventana.pintada = true;
}
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.
/// Desde aqui queda excluida del foco — el teclado la salta.
pub fn desalojar(indice: usize, color: Color) {
let Some(escritorio) = ESCRITORIO.get() else {
return;
};
let mut escritorio = escritorio.lock();
{
let Some(ventana) = escritorio.ventanas.get_mut(indice) else {
return;
};
// Una ventana ya cerrada (Fase 10) no recibe baliza: la baja limpia
// gana a un desalojo que llegue tarde, en la misma vuelta.
if ventana.cerrada {
return;
}
ventana.baliza = Some(color);
}
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);
}
}
// =============================================================================
// Los mandos del teclado — el escritorio interactivo
// =============================================================================
/// Atiende los mandos pendientes del teclado. La invoca la tarea del compositor
/// en cada fotograma, desde el reactor cooperativo — el unico contexto donde es
/// seguro bloquear el `ESCRITORIO` y la consola.
pub fn atender_mandos() {
let Some(mandos) = MANDOS.get() else {
return;
};
while let Some(mando) = mandos.pop() {
match mando {
Mando::CiclarLayout => ciclar_layout(),
Mando::FocoSiguiente => mover_foco(true),
Mando::FocoAnterior => mover_foco(false),
Mando::Promover => promover(),
Mando::MoverAdelante => mover_ventana(true),
Mando::MoverAtras => mover_ventana(false),
Mando::Flotar => flotar(),
Mando::Cerrar => cerrar(),
// El alta de una app necesita instanciar un WASM — algo que el
// compositor no sabe hacer—. Solo se cuenta la peticion; el
// orquestador del kernel la atendera (ver `partos_pendientes`).
Mando::Lanzar => {
PARTOS.fetch_add(1, Ordering::Relaxed);
}
}
}
}
/// 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;
};
let mut escritorio = escritorio.lock();
escritorio.modo = escritorio.modo.next();
aplicar_teselado(&mut escritorio);
recomponer(&escritorio);
}
/// 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 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);
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.
let mut nueva_pos = pos;
let mut nuevo = anterior;
for _ in 0..n {
nueva_pos = if adelante {
(nueva_pos + 1) % n
} else {
(nueva_pos + n - 1) % n
};
let candidata = recorrido[nueva_pos];
if escritorio.ventanas[candidata].baliza.is_none() {
nuevo = candidata;
break;
}
}
FOCO.store(nuevo, Ordering::Relaxed);
// La bocina pertenece a la ventana enfocada (Fase 12): al cambiar el foco,
// callar — la nueva dueña la reclamara en su proximo fotograma si quiere.
crate::drivers::altavoz::tono(0);
// 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. 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;
};
let mut escritorio = escritorio.lock();
let foco = FOCO.load(Ordering::Relaxed);
if let Some(pos) = escritorio.orden.iter().position(|&v| v == foco) {
let ventana = escritorio.orden.remove(pos);
escritorio.orden.insert(0, ventana);
aplicar_teselado(&mut escritorio);
recomponer(&escritorio);
}
}
/// Mueve la ventana enfocada una posicion en el orden de teselado,
/// 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;
};
let mut escritorio = escritorio.lock();
let n = escritorio.orden.len();
if n < 2 {
return;
}
let foco = FOCO.load(Ordering::Relaxed);
if let Some(pos) = escritorio.orden.iter().position(|&v| v == foco) {
let destino = if adelante {
(pos + 1) % n
} else {
(pos + n - 1) % n
};
escritorio.orden.swap(pos, destino);
aplicar_teselado(&mut escritorio);
recomponer(&escritorio);
}
}
// =============================================================================
// 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);
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);
}
}
/// 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);
}
// =============================================================================
// FASE 10 — alta y baja de aplicaciones en vivo
// =============================================================================
/// Cierra la aplicacion enfocada (`Alt+Q`): una baja LIMPIA, distinta del
/// desalojo por falla. Marca la ventana como cerrada, libera su cache de
/// respaldo, la saca del teselado y del orden-Z, y traslada el foco a una
/// ventana viva contigua. La app, en su tarea, advertira la baja y concluira.
fn cerrar() {
let Some(escritorio) = ESCRITORIO.get() else {
return;
};
let mut escritorio = escritorio.lock();
let foco = FOCO.load(Ordering::Relaxed);
// Solo se cierra una ventana viva. El foco jamas se posa en una muerta o
// cerrada, pero la guarda lo deja explicito.
match escritorio.ventanas.get(foco) {
Some(v) if v.baliza.is_none() && !v.cerrada => {}
_ => return,
}
// Marcar la baja y liberar el respaldo: la cache de un fotograma puede
// pesar un megabyte — no tiene sentido retenerla en una ranura inerte.
let ventana = &mut escritorio.ventanas[foco];
ventana.cerrada = true;
ventana.pintada = false;
ventana.cache = Vec::new();
// Sacarla del teselado y del orden-Z. El censo conserva la ranura —los
// indices son la identidad, jamas se reciclan—, pero ya nadie la dibuja.
escritorio.orden.retain(|&v| v != foco);
escritorio.flotantes.retain(|&v| v != foco);
// Si la estabamos arrastrando con el raton (Fase 13), soltarla.
if escritorio.arrastre.map(|a| a.ventana) == Some(foco) {
escritorio.arrastre = None;
}
// El foco salta a la primera ventana viva que quede; si no queda ninguna,
// se queda donde estaba —inofensivo: no hay a quien enrutar el teclado—.
let nuevo = escritorio
.orden
.iter()
.chain(escritorio.flotantes.iter())
.copied()
.find(|&v| {
let w = &escritorio.ventanas[v];
w.baliza.is_none() && !w.cerrada
})
.unwrap_or(foco);
FOCO.store(nuevo, Ordering::Relaxed);
// El foco cambia: callar la bocina (Fase 12) — ver `mover_foco`.
crate::drivers::altavoz::tono(0);
alzar_si_flota(&mut escritorio, nuevo);
aplicar_teselado(&mut escritorio);
recomponer(&escritorio);
}
/// Da de alta una ventana NUEVA y devuelve su indice —su identidad—. La crea
/// con su cache de respaldo al tamaño natural, la añade al final del orden de
/// teselado, recalcula el teselado y recompone. La invoca el orquestador del
/// kernel justo antes de instanciar el WASM de la app, que necesita ese indice.
pub fn nacer_ventana(nat_ancho: usize, nat_alto: usize) -> usize {
let Some(escritorio) = ESCRITORIO.get() else {
return 0;
};
let mut escritorio = escritorio.lock();
let indice = escritorio.ventanas.len();
escritorio.ventanas.push(Ventana {
natural_ancho: nat_ancho,
natural_alto: nat_alto,
marco: RegionPantalla {
x: 0,
y: 0,
ancho: 0,
alto: 0,
},
cache: vec![0u8; nat_ancho.saturating_mul(nat_alto).saturating_mul(4)],
pintada: false,
baliza: None,
cerrada: false,
});
escritorio.orden.push(indice);
aplicar_teselado(&mut escritorio);
recomponer(&escritorio);
indice
}
/// ¿Se ha pedido cerrar la ventana `indice`? Cada app la consulta en su tarea,
/// fotograma a fotograma: cuando es `true`, concluye su tarea y se libera. Una
/// ventana inexistente cuenta como cerrada.
pub fn ventana_cerrada(indice: usize) -> bool {
let Some(escritorio) = ESCRITORIO.get() else {
return false;
};
escritorio
.lock()
.ventanas
.get(indice)
.map(|ventana| ventana.cerrada)
.unwrap_or(true)
}
/// Cuantas aplicaciones nuevas se han pedido lanzar desde la ultima consulta —y
/// pone el contador a cero—. La invoca el orquestador del kernel —el unico que
/// sabe instanciar un WASM— en cada fotograma de la tarea del compositor.
pub fn partos_pendientes() -> usize {
PARTOS.swap(0, Ordering::Relaxed)
}
// =============================================================================
// FASE 13 — raton, puntero y arrastre de ventanas flotantes
// =============================================================================
/// La ultima posicion del puntero que el compositor REFRESCO. Si la posicion
/// actual del raton coincide con esta, no hay nada nuevo que estampar; si
/// difiere, la consola debe volver a presentar. Empacada como `y * 65536 + x`,
/// con `usize::MAX` como centinela de «aun no he visto al raton».
static PUNTERO_REFRESCADO: AtomicUsize = AtomicUsize::new(usize::MAX);
/// Drena los eventos del raton y los aplica: clic enfoca la ventana bajo el
/// puntero (y, si flota, inicia un arrastre); el boton sostenido la arrastra;
/// soltarlo termina el gesto. La invoca la tarea del compositor en cada
/// fotograma, desde el reactor cooperativo.
pub fn atender_raton() {
let Some(escritorio) = ESCRITORIO.get() else {
return;
};
let mut escritorio = escritorio.lock();
let mut cambio = false;
while let Some(evento) = crate::drivers::raton::siguiente_evento() {
let izq = evento.botones & 0b001 != 0;
let x = evento.x as usize;
let y = evento.y as usize;
let izq_antes = escritorio.raton_izq;
if izq && !izq_antes {
// Boton bajó: un CLIC sobre el punto (x, y).
if let Some(v) = ventana_en(&escritorio, x, y) {
let viva = {
let w = &escritorio.ventanas[v];
w.baliza.is_none() && !w.cerrada
};
if viva {
// Enfocar como hace `mover_foco`: foco + bocina muda + alza
// si flota.
FOCO.store(v, Ordering::Relaxed);
crate::drivers::altavoz::tono(0);
alzar_si_flota(&mut escritorio, v);
// Si la ventana flota, empezar a arrastrarla.
if escritorio.flotantes.contains(&v) {
let marco = escritorio.ventanas[v].marco;
escritorio.arrastre = Some(Arrastre {
ventana: v,
agarre_dx: x.saturating_sub(marco.x),
agarre_dy: y.saturating_sub(marco.y),
});
}
cambio = true;
}
}
} else if izq && izq_antes {
// Boton sostenido: arrastrar la ventana asida, si la hay.
if let Some(arr) = escritorio.arrastre {
mover_arrastrada(&mut escritorio, arr, x, y);
cambio = true;
}
} else if !izq && izq_antes {
// Boton subió: fin del arrastre.
escritorio.arrastre = None;
}
escritorio.raton_izq = izq;
}
if cambio {
recomponer(&escritorio);
// El recomponer ya presento; sincronizar el centinela para no presentar
// dos veces en la misma vuelta.
PUNTERO_REFRESCADO.store(empacar_puntero(), Ordering::Relaxed);
}
}
/// La ventana topmost que contiene el punto (x, y), si la hay. Recorre el
/// orden-Z de delante hacia atras: primero las flotantes (la ultima es la
/// frontal), despues las teseladas.
fn ventana_en(escritorio: &Escritorio, x: usize, y: usize) -> Option<usize> {
for &v in escritorio.flotantes.iter().rev() {
if contiene(escritorio.ventanas[v].marco, x, y) {
return Some(v);
}
}
for &v in escritorio.orden.iter().rev() {
if contiene(escritorio.ventanas[v].marco, x, y) {
return Some(v);
}
}
None
}
/// ¿Contiene el marco al punto (x, y)?
fn contiene(marco: RegionPantalla, x: usize, y: usize) -> bool {
x >= marco.x && x < marco.x + marco.ancho && y >= marco.y && y < marco.y + marco.alto
}
/// Mueve la ventana arrastrada de modo que el punto del puntero —la asa— siga
/// estando, dentro de la ventana, donde se asio. La ventana queda acotada al
/// area de apps.
fn mover_arrastrada(escritorio: &mut Escritorio, arr: Arrastre, x: usize, y: usize) {
let area = area_apps(escritorio.ancho, escritorio.alto);
let Some(ventana) = escritorio.ventanas.get_mut(arr.ventana) else {
return;
};
let ancho = ventana.marco.ancho;
let alto = ventana.marco.alto;
let mut nx = x.saturating_sub(arr.agarre_dx);
let mut ny = y.saturating_sub(arr.agarre_dy);
// Acotar al area de apps: la ventana entera ha de caber dentro.
if nx + ancho > area.x + area.ancho {
nx = (area.x + area.ancho).saturating_sub(ancho);
}
if ny + alto > area.y + area.alto {
ny = (area.y + area.alto).saturating_sub(alto);
}
nx = nx.max(area.x);
ny = ny.max(area.y);
ventana.marco.x = nx;
ventana.marco.y = ny;
}
/// Empaca la posicion actual del puntero en un solo `usize` —`y * 65536 + x`—
/// para compararla atomicamente con la ultima refrescada. `usize::MAX` indica
/// «el raton no esta vivo».
fn empacar_puntero() -> usize {
match crate::drivers::raton::posicion() {
Some((x, y)) => (y << 16) | (x & 0xFFFF),
None => usize::MAX,
}
}
/// Si el puntero se ha movido desde la ultima presentacion del compositor, le
/// pide a la consola un volcado fresco —para reestampar el puntero en su
/// nuevo lugar—. La invoca la tarea del compositor cada fotograma.
pub fn refrescar_puntero() {
let actual = empacar_puntero();
if actual == usize::MAX {
return;
}
if PUNTERO_REFRESCADO.swap(actual, Ordering::Relaxed) != actual {
crate::consola::refrescar();
}
}
// =============================================================================
// Teselado — la geometria pura de `mirada-layout`
// =============================================================================
/// El area de pantalla que el compositor tesela: toda la pantalla menos la
/// franja de la consola en la cima.
pub fn area_apps(ancho_pantalla: usize, alto_pantalla: usize) -> RegionPantalla {
RegionPantalla {
x: 0,
y: FRANJA_CONSOLA.min(alto_pantalla),
ancho: ancho_pantalla,
alto: alto_pantalla.saturating_sub(FRANJA_CONSOLA),
}
}
/// Tesela el area de apps en `n` marcos con el modo dado. El vector resultante
/// 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(
area.x as i32,
area.y as i32,
area.ancho as i32,
area.alto as i32,
);
let params = LayoutParams {
mode: modo,
gap: MARGEN,
..LayoutParams::default()
};
tile(pantalla, n, &params)
.into_iter()
.map(rect_a_region)
.collect()
}
/// Traduce un `Rect` de `mirada-layout` (`i32`) a la `RegionPantalla` del
/// kernel (`usize`). Un rectangulo degenerado queda en cero.
fn rect_a_region(r: Rect) -> RegionPantalla {
RegionPantalla {
x: r.x.max(0) as usize,
y: r.y.max(0) as usize,
ancho: r.w.max(0) as usize,
alto: r.h.max(0) as usize,
}
}