Files
brahman/renaser/kernel/src/compositor.rs
T
sergio 07ab095d42 feat(renaser): Fase 19 — voz del userspace hacia la red (pregon)
Tres capacidades nuevas en wasm/env (12-14):

- sys_net_mac(salida) -> i32: escribe los seis bytes del MAC del
  dispositivo. 0 OK, -1 si no hay red.
- sys_net_enviar(ptr, len) -> i32: envia un frame Ethernet crudo.
  Valida rango contra la memoria lineal del modulo.
- sys_net_recibir(salida, capacidad) -> i32: drena UN paquete por
  llamada hacia el buffer del modulo. Devuelve los bytes copiados, 0
  si nada pendiente, codigos negativos diagnosticos.

Añadida red::recibir_en(buf) -> usize como su contraparte del driver:
gemelo cooperativo de drenar_rx que aterriza en un buffer del usuario.

App nueva pregon (apps/pregon/, 4.2 KiB WASM): lienzo 480x160, tipografia
8x8 (font8x8) escalada x2. Al init pide su MAC y anuncia su presencia
con un broadcast Ethernet — destino FF:FF:FF:FF:FF:FF, EtherType
experimental 0x88B5, payload ASCII 'renaser :: hola desde mi red'. En
cada tick drena un paquete con sys_net_recibir y muestra el titulo, el
MAC propio, las cuentas TX/RX, y los datos del ultimo frame entrante.

GENESIS 8 -> 9 apps (pregon en posicion 2 detras de bitacora);
CELDA_TASKBAR_ANCHO 130 -> 116 px para que las nueve pestañas + lanzador
+ reloj caben holgadas en 1280 px.

tarea_red del kernel ya no drena RX (la cola pertenece al userspace),
conserva solo el envio del ARP de prueba al arrancar.

Verificada en QEMU con -object filter-dump. El pcap captura tres frames
en orden: (1) broadcast 88B5 de pregon con su payload, (2) ARP request
del kernel, (3) ARP reply del gateway 52:55:0a:00:02:02. La consola
anuncia 'manifiesto :: 9 apps nacidas del grafo'.

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

1085 lines
44 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::{AtomicU64, AtomicUsize, Ordering};
use alloc::string::{String, ToString};
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, CeldaTaskbar, Contenido, Taskbar};
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;
/// Altura de la barra de tareas inferior (Fase 14): cada ventana viva tiene
/// ahi una pestaña con su nombre, que el clic enfoca.
const FRANJA_TASKBAR: usize = 40;
/// Anchura de cada celda de la barra de tareas, en pixeles. Dimensionada para
/// que las nueve apps de genesis (Fase 19 anexa `pregon`) + el lanzador + el
/// reloj caben holgados en una pantalla de 1280 px.
const CELDA_TASKBAR_ANCHO: usize = 116;
/// Hueco entre celdas adyacentes de la barra.
const CELDA_TASKBAR_HUECO: usize = 6;
/// Margen izquierdo y derecho de la barra de tareas.
const CELDA_TASKBAR_MARGEN: usize = 12;
/// Anchura del boton lanzador («+» a la izquierda de la barra, Fase 16).
const LAUNCHER_ANCHO: usize = 36;
/// Hueco entre el lanzador y la primera pestaña.
const LAUNCHER_HUECO: usize = 8;
/// Anchura reservada para el reloj a la derecha de la barra (Fase 16).
const RELOJ_ANCHO: usize = 80;
/// Hueco entre la ultima pestaña y el reloj.
const RELOJ_HUECO: usize = 8;
/// 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 {
/// Nombre legible de la app — el que dicta su `EntradaApp` del manifiesto.
/// Aparece en la pestaña de la barra de tareas (Fase 14).
nombre: String,
/// 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);
/// El ultimo segundo del reloj monotono que la barra de tareas ha mostrado.
/// `tick_reloj` lo compara con el actual: si difiere, recompone para pintar el
/// nuevo. Centinela `u64::MAX` para garantizar que el primer tick fuerza un
/// repintado y la barra arranca con su reloj a 0:00.
static ULTIMO_SEGUNDO: AtomicU64 = AtomicU64::new(u64::MAX);
// =============================================================================
// 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, &str)]) {
MANDOS.call_once(|| ArrayQueue::new(CAPACIDAD_MANDOS));
let mut ventanas = Vec::with_capacity(naturales.len());
for &(nat_ancho, nat_alto, nombre) in naturales {
ventanas.push(Ventana {
nombre: nombre.to_string(),
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);
}
// Fase 15: la voz del kernel anuncia el desalojo.
crate::drivers::altavoz::agendar(&crate::drivers::altavoz::VOZ_DESALOJO);
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,
});
}
// FASE 14/16 :: armar la barra de tareas. A la izquierda un boton lanzador
// («+»); en el medio una pestaña por ventana viva; a la derecha el reloj.
let area_bar = area_taskbar(escritorio.ancho, escritorio.alto);
let cy = area_bar.y + 4;
let calto = area_bar.alto.saturating_sub(8);
let launcher = RegionPantalla {
x: area_bar.x + CELDA_TASKBAR_MARGEN,
y: cy,
ancho: LAUNCHER_ANCHO,
alto: calto,
};
let cells_x0 = launcher.x + launcher.ancho + LAUNCHER_HUECO;
let cells_x_max =
area_bar.x + area_bar.ancho - CELDA_TASKBAR_MARGEN - RELOJ_ANCHO - RELOJ_HUECO;
let mut celdas: Vec<CeldaTaskbar> = Vec::new();
let mut cx = cells_x0;
for (indice, ventana) in escritorio.ventanas.iter().enumerate() {
if ventana.cerrada {
continue;
}
if cx + CELDA_TASKBAR_ANCHO > cells_x_max {
break;
}
let fondo = match ventana.baliza {
Some(color) => color,
None if indice == foco => Color::FOCO,
None => Color::PANEL,
};
celdas.push(CeldaTaskbar {
region: RegionPantalla {
x: cx,
y: cy,
ancho: CELDA_TASKBAR_ANCHO,
alto: calto,
},
nombre: &ventana.nombre,
fondo,
tinta: tinta_para(fondo),
});
cx += CELDA_TASKBAR_ANCHO + CELDA_TASKBAR_HUECO;
}
// El reloj: minutos:segundos desde el arranque, alineado a la derecha.
let ms = crate::async_system::reloj::milisegundos();
let segs = ms / 1000;
let reloj_texto = alloc::format!("{}:{:02}", segs / 60, segs % 60);
let reloj_region = RegionPantalla {
x: area_bar.x + area_bar.ancho - CELDA_TASKBAR_MARGEN - RELOJ_ANCHO,
y: cy,
ancho: RELOJ_ANCHO,
alto: calto,
};
let taskbar = Taskbar {
area: area_bar,
launcher,
celdas: &celdas,
reloj: &reloj_texto,
reloj_region,
};
consola::recomponer(area, &capas, &taskbar);
// Recordar el segundo recien mostrado: `tick_reloj` evita repintar de mas
// mientras dure este mismo segundo.
ULTIMO_SEGUNDO.store(segs, Ordering::Relaxed);
}
/// Localiza la celda de la barra de tareas bajo la coordenada x: itera las
/// ventanas vivas en orden de creacion y devuelve la N-esima donde la N es la
/// posicion en la barra. `None` si el clic cae en el lanzador, en el reloj, en
/// un hueco entre celdas, o fuera del rango de las pestañas.
fn celda_taskbar_en(escritorio: &Escritorio, x: usize) -> Option<usize> {
let area_bar = area_taskbar(escritorio.ancho, escritorio.alto);
// Las pestañas empiezan despues del lanzador.
let cells_x0 = area_bar.x + CELDA_TASKBAR_MARGEN + LAUNCHER_ANCHO + LAUNCHER_HUECO;
let cells_x_max =
area_bar.x + area_bar.ancho - CELDA_TASKBAR_MARGEN - RELOJ_ANCHO - RELOJ_HUECO;
if x < cells_x0 || x >= cells_x_max {
return None;
}
let rel = x - cells_x0;
let paso = CELDA_TASKBAR_ANCHO + CELDA_TASKBAR_HUECO;
let posicion = rel / paso;
let offset = rel % paso;
if offset >= CELDA_TASKBAR_ANCHO {
return None;
}
let mut k = 0;
for (indice, ventana) in escritorio.ventanas.iter().enumerate() {
if ventana.cerrada {
continue;
}
if k == posicion {
return Some(indice);
}
k += 1;
}
None
}
/// ¿Cae la coordenada x en el boton lanzador («+»)?
fn clic_en_launcher(escritorio: &Escritorio, x: usize) -> bool {
let area_bar = area_taskbar(escritorio.ancho, escritorio.alto);
let x0 = area_bar.x + CELDA_TASKBAR_MARGEN;
let x1 = x0 + LAUNCHER_ANCHO;
x >= x0 && x < x1
}
// =============================================================================
// 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,
}
// Fase 15: el kernel se despide de la app con un repique descendente.
crate::drivers::altavoz::agendar(&crate::drivers::altavoz::VOZ_CERRAR);
// 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, nombre: &str) -> usize {
let Some(escritorio) = ESCRITORIO.get() else {
return 0;
};
let mut escritorio = escritorio.lock();
let indice = escritorio.ventanas.len();
escritorio.ventanas.push(Ventana {
nombre: nombre.to_string(),
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);
// Fase 15: el kernel saluda al nacimiento con un repique ascendente.
crate::drivers::altavoz::agendar(&crate::drivers::altavoz::VOZ_LANZAR);
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)
}
/// Avanza el reloj de la barra de tareas (Fase 16): si el segundo del reloj
/// monotono cambio respecto al ultimo mostrado, recompone para refrescar la
/// pantalla. Si el segundo es el mismo, vuelve sin hacer nada — un fotograma
/// barato—. La invoca la tarea del compositor cada fotograma.
pub fn tick_reloj() {
let actual = crate::async_system::reloj::milisegundos() / 1000;
if ULTIMO_SEGUNDO.load(Ordering::Relaxed) == actual {
return;
}
let Some(escritorio) = ESCRITORIO.get() else {
return;
};
let escritorio = escritorio.lock();
recomponer(&escritorio);
}
// =============================================================================
// 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. Si cae en la barra de tareas, enfocar la
// pestaña pulsada SIN iniciar arrastre. Si no, comportamiento
// habitual: enfocar la ventana topmost bajo el puntero.
let area_bar = area_taskbar(escritorio.ancho, escritorio.alto);
if y >= area_bar.y && y < area_bar.y + area_bar.alto {
if clic_en_launcher(&escritorio, x) {
// El boton «+» equivale a `Alt+N`: solicita un parto. La
// tarea del compositor lo recogera en su proxima vuelta.
PARTOS.fetch_add(1, Ordering::Relaxed);
} else if let Some(v) = celda_taskbar_en(&escritorio, x) {
let viva = {
let w = &escritorio.ventanas[v];
w.baliza.is_none() && !w.cerrada
};
if viva {
FOCO.store(v, Ordering::Relaxed);
crate::drivers::altavoz::tono(0);
alzar_si_flota(&mut escritorio, v);
cambio = true;
}
}
} else 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 y la barra de tareas al pie.
pub fn area_apps(ancho_pantalla: usize, alto_pantalla: usize) -> RegionPantalla {
let cabeza = FRANJA_CONSOLA.min(alto_pantalla);
let pie = FRANJA_TASKBAR.min(alto_pantalla.saturating_sub(cabeza));
RegionPantalla {
x: 0,
y: cabeza,
ancho: ancho_pantalla,
alto: alto_pantalla.saturating_sub(cabeza).saturating_sub(pie),
}
}
/// El area de la barra de tareas: una franja al pie de la pantalla.
fn area_taskbar(ancho_pantalla: usize, alto_pantalla: usize) -> RegionPantalla {
let pie = FRANJA_TASKBAR.min(alto_pantalla);
RegionPantalla {
x: 0,
y: alto_pantalla.saturating_sub(pie),
ancho: ancho_pantalla,
alto: pie,
}
}
/// El color de tinta —oscuro o claro— que da contraste legible sobre `fondo`.
/// Sin esto, la pestaña amarilla palida del desalojo por memoria quedaba con
/// texto blanco sobre crema: ilegible. La regla de luminancia ITU-R BT.601 fija
/// el umbral: fondos claros llevan tinta oscura, fondos oscuros la clara.
fn tinta_para(fondo: Color) -> Color {
let brillo =
(fondo.r as u32 * 299 + fondo.g as u32 * 587 + fondo.b as u32 * 114) / 1000;
if brillo > 160 {
// Fondo claro: tinta del reposo del lienzo, casi negra.
Color::LIENZO_EN_REPOSO
} else {
Color::TEXTO
}
}
/// 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,
}
}