8fc1d99ddf
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>
879 lines
36 KiB
Rust
879 lines
36 KiB
Rust
// =============================================================================
|
||
// 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, ¶ms)
|
||
.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,
|
||
}
|
||
}
|