feat(renaser): Fase 14 — identidad del escritorio (nombres + barra de tareas)

Las ventanas eran anónimas: el escritorio no sabía nombrar lo que
mostraba. Esta fase le pone un nombre a cada cuarto y una barra al
pie con la lista de quienes lo habitan.

- Cada `Ventana` lleva un `nombre: String` —del manifiesto, o del
  orquestador al engendrarla en vivo—. `Plantilla` lo guarda para las
  copias que `Alt+N` instancia.
- Franja `FRANJA_TASKBAR=40px` reservada al pie. `area_apps` la
  descuenta — las ventanas teselan y flotan sin tapar la barra.
- `consola`: tipos `Taskbar` / `CeldaTaskbar` + métodos `pintar_taskbar`
  y `pintar_etiqueta` (rasteriza una cadena en (x, base_y) sobre un
  fondo conocido, sin tocar la pluma). La pestaña enfocada se pinta con
  el índigo del foco, las desalojadas con su color de baliza, el resto
  con el slate del panel.
- `compositor::recomponer` arma las celdas y las pasa junto a las capas
  a la consola; un único repintado, una única presentación.
- `atender_raton`: si el clic cae en la franja de la barra,
  `celda_taskbar_en` localiza la pestaña pulsada y la enfoca (sin
  iniciar arrastre).

Verificado en QEMU: al arrancar, la barra al pie muestra las 7
pestañas con sus nombres; `tonada` enfocada en índigo, `discola` y
`glotona` en sus colores de baliza. Un clic sobre `pulso` traslada el
foco al instante — el borde del compositor envuelve `pulso` y su
pestaña se ilumina.

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
sergio
2026-05-23 02:05:51 +00:00
parent be4de986be
commit 6a29152feb
7 changed files with 328 additions and 19 deletions
+126 -10
View File
@@ -47,6 +47,7 @@
use core::sync::atomic::{AtomicUsize, Ordering};
use alloc::string::{String, ToString};
use alloc::vec;
use alloc::vec::Vec;
@@ -54,7 +55,7 @@ use crossbeam_queue::ArrayQueue;
use mirada_layout::{tile, LayoutMode, LayoutParams, Rect};
use spin::{Mutex, Once};
use crate::consola::{self, Capa, Contenido};
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.
@@ -62,6 +63,17 @@ use crate::grafico::{Color, RegionPantalla};
/// 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.
const CELDA_TASKBAR_ANCHO: usize = 156;
/// Hueco entre celdas adyacentes de la barra.
const CELDA_TASKBAR_HUECO: usize = 6;
/// Margen izquierdo de la primera celda.
const CELDA_TASKBAR_MARGEN: usize = 16;
/// El modo de teselado con que arranca el escritorio. El teclado lo cicla.
const MODO_INICIAL: LayoutMode = LayoutMode::MasterStack;
@@ -116,6 +128,9 @@ struct Arrastre {
/// 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,
@@ -190,12 +205,13 @@ static PARTOS: AtomicUsize = AtomicUsize::new(0);
/// 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)]) {
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) in naturales {
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.
@@ -581,7 +597,77 @@ fn recomponer(escritorio: &Escritorio) {
enfocada: indice == foco,
});
}
consola::recomponer(area, &capas);
// FASE 14 :: armar las celdas de la barra de tareas. Una pestaña por
// ventana viva (no cerrada), de izquierda a derecha, con el nombre de la
// app; la enfocada lleva el color indigo del foco, las desalojadas su
// baliza, las demas el slate del panel. El clic sobre una pestaña enfoca
// su ventana.
let area_bar = area_taskbar(escritorio.ancho, escritorio.alto);
let mut celdas: Vec<CeldaTaskbar> = Vec::new();
let mut cx = area_bar.x + CELDA_TASKBAR_MARGEN;
let cy = area_bar.y + 4;
let calto = area_bar.alto.saturating_sub(8);
for (indice, ventana) in escritorio.ventanas.iter().enumerate() {
if ventana.cerrada {
continue;
}
if cx + CELDA_TASKBAR_ANCHO > area_bar.x + area_bar.ancho {
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: Color::TEXTO,
});
cx += CELDA_TASKBAR_ANCHO + CELDA_TASKBAR_HUECO;
}
let taskbar = Taskbar {
area: area_bar,
celdas: &celdas,
};
consola::recomponer(area, &capas, &taskbar);
}
/// 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 un hueco entre celdas, antes
/// del margen, o pasada la ultima.
fn celda_taskbar_en(escritorio: &Escritorio, x: usize) -> Option<usize> {
let area_bar = area_taskbar(escritorio.ancho, escritorio.alto);
let margen_izq = area_bar.x + CELDA_TASKBAR_MARGEN;
if x < margen_izq {
return None;
}
let rel = x - margen_izq;
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
}
// =============================================================================
@@ -642,13 +728,14 @@ fn cerrar() {
/// 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 {
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 {
@@ -716,8 +803,24 @@ pub fn atender_raton() {
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) {
// 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 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
@@ -835,13 +938,26 @@ pub fn refrescar_puntero() {
// =============================================================================
/// El area de pantalla que el compositor tesela: toda la pantalla menos la
/// franja de la consola en la cima.
/// 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: FRANJA_CONSOLA.min(alto_pantalla),
y: cabeza,
ancho: ancho_pantalla,
alto: alto_pantalla.saturating_sub(FRANJA_CONSOLA),
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,
}
}
+84 -3
View File
@@ -67,6 +67,24 @@ pub(crate) struct Capa<'a> {
pub(crate) enfocada: bool,
}
/// Una pestaña de la barra de tareas (Fase 14): una región de pantalla, el
/// nombre de la app y los colores con que pintarla. Las arma el compositor
/// con una pestaña por ventana viva.
pub(crate) struct CeldaTaskbar<'a> {
pub(crate) region: RegionPantalla,
pub(crate) nombre: &'a str,
/// Color de fondo: indigo del foco, slate del panel, o color de baliza.
pub(crate) fondo: Color,
/// Color de la tinta del texto.
pub(crate) tinta: Color,
}
/// La barra de tareas del escritorio (Fase 14): su area y sus pestañas.
pub(crate) struct Taskbar<'a> {
pub(crate) area: RegionPantalla,
pub(crate) celdas: &'a [CeldaTaskbar<'a>],
}
/// La consola grafica de renaser: doble bufer, pantalla fisica y pluma.
pub(crate) struct Consola {
lienzo: Lienzo,
@@ -224,7 +242,7 @@ impl Consola {
/// flotantes se resuelve por si solo, sin recortes ni mascaras. Cada capa
/// pinta primero su panel —el cromo de la ventana— y, encima, su contenido;
/// una sola presentacion cierra la pasada.
fn recomponer(&mut self, area: RegionPantalla, capas: &[Capa]) {
fn recomponer(&mut self, area: RegionPantalla, capas: &[Capa], taskbar: &Taskbar) {
self.lienzo.rellenar_rect(
area.x,
area.y,
@@ -252,9 +270,72 @@ impl Consola {
}
self.dibujar_borde(m, capa.enfocada);
}
self.pintar_taskbar(taskbar);
self.presentar();
}
/// Pinta la barra de tareas como ultima capa del escritorio (Fase 14): el
/// fondo de la franja, una linea fina arriba que la separa de las apps, y
/// las pestañas —cada una su rectángulo y su nombre—.
fn pintar_taskbar(&mut self, taskbar: &Taskbar) {
// Fondo de la barra y linea de separacion.
self.lienzo.rellenar_rect(
taskbar.area.x,
taskbar.area.y,
taskbar.area.ancho,
taskbar.area.alto,
Color::PANEL,
);
self.lienzo
.rellenar_rect(taskbar.area.x, taskbar.area.y, taskbar.area.ancho, 1, Color::SIN_FOCO);
// Las pestañas.
for celda in taskbar.celdas {
let r = celda.region;
self.lienzo
.rellenar_rect(r.x, r.y, r.ancho, r.alto, celda.fondo);
// El nombre, alineado a la izquierda de la pestaña, vertical-
// centrado a la altura visible de la franja.
let base_y = r.y + (r.alto + 14) / 2;
self.pintar_etiqueta(r.x + 10, base_y, celda.nombre, 16.0, celda.fondo, celda.tinta);
}
}
/// Rasteriza una cadena de texto a un tamaño dado, en (x, base_y), sobre
/// un fondo conocido —del que toma la mezcla por cobertura del glifo—. Es
/// la version sin estado de la pluma: el llamante decide donde escribir.
fn pintar_etiqueta(
&mut self,
x: usize,
base_y: usize,
texto: &str,
tamaño: f32,
fondo: Color,
tinta: Color,
) {
let mut cursor = x;
for caracter in texto.chars() {
let (metricas, cobertura) = texto::rasterizar(caracter, tamaño);
let inicio_x = cursor as isize + metricas.xmin as isize;
let inicio_y = base_y as isize - metricas.ymin as isize - metricas.height as isize;
for fila in 0..metricas.height {
for col in 0..metricas.width {
let opacidad = cobertura[fila * metricas.width + col];
if opacidad == 0 {
continue;
}
let px = inicio_x + col as isize;
let py = inicio_y + fila as isize;
if px < 0 || py < 0 {
continue;
}
let color = mezclar(fondo, tinta, opacidad);
self.lienzo.pintar_pixel(px as usize, py as usize, color);
}
}
cursor += metricas.advance_width as usize;
}
}
/// Inunda una region entera con un color plano —la baliza de desalojo: una
/// app que falla tatua su marco de purpura— y le traza su borde de foco.
fn pintar_region(&mut self, region: RegionPantalla, color: Color, enfocada: bool) {
@@ -328,9 +409,9 @@ pub(crate) fn volcar_marco(
/// La invoca `compositor` al arrancar y siempre que hay ventanas flotantes: el
/// solapamiento obliga a repintar el escritorio en bloque, no ventana a
/// ventana. Las capas llegan ya ordenadas de atras hacia adelante.
pub(crate) fn recomponer(area: RegionPantalla, capas: &[Capa]) {
pub(crate) fn recomponer(area: RegionPantalla, capas: &[Capa], taskbar: &Taskbar) {
if let Some(consola) = CONSOLA.get() {
consola.lock().recomponer(area, capas);
consola.lock().recomponer(area, capas, taskbar);
}
}
+7 -3
View File
@@ -105,6 +105,9 @@ fn traza(rotulo: &str) {
/// su bytecode —cacheado en RAM al arrancar, para no volver al disco despues—
/// y la geometria y la cuota de memoria con que instanciarla.
struct Plantilla {
/// Nombre legible de la app — el del manifiesto, que la barra de tareas
/// (Fase 14) muestra en la pestaña.
nombre: alloc::string::String,
bytecode: Vec<u8>,
nat_ancho: usize,
nat_alto: usize,
@@ -227,6 +230,7 @@ fn encender_app(
// un molde en RAM con que `Alt+N` instanciara copias en vivo, sin volver al
// disco —que la E/S por sondeo en mitad del reactor seria un mal vecino—.
Some(Plantilla {
nombre: entrada.nombre.clone(),
bytecode,
nat_ancho: natural.ancho,
nat_alto: natural.alto,
@@ -252,7 +256,7 @@ fn lanzar_app() {
// La ventana nace primero: el compositor le entrega su indice —su
// identidad—, que el WASM necesita para hallar su ventana y su canal.
let indice = compositor::nacer_ventana(plantilla.nat_ancho, plantilla.nat_alto);
let indice = compositor::nacer_ventana(plantilla.nat_ancho, plantilla.nat_alto, &plantilla.nombre);
match wasm::AplicacionWasm::cargar(
&plantilla.bytecode,
plantilla.nat_ancho,
@@ -318,10 +322,10 @@ fn cargar_userspace(ejecutor: &mut Executor, ancho_pantalla: usize, alto_pantall
// con su cache de respaldo y su marco teselado por `mirada-layout`— y
// pintar el escenario antes de encender las apps: el teselado se ve
// aunque alguna app no llegue a pintar su primer fotograma.
let naturales: Vec<(usize, usize)> = m
let naturales: Vec<(usize, usize, &str)> = m
.apps
.iter()
.map(|e| (e.region_ancho as usize, e.region_alto as usize))
.map(|e| (e.region_ancho as usize, e.region_alto as usize, e.nombre.as_str()))
.collect();
compositor::fundar(ancho_pantalla, alto_pantalla, &naturales);
compositor::componer_escenario();