⎇ 71ebcea899d0ac71f959ad5461d830d952087735
renaser/kernel/src/compositor.rs
History
Who touched what
raw
feat(renaser): Fase 16 — la barra viva: botón lanzador + reloj
1// =============================================================================
2// renaser :: kernel/src/compositor.rs — el compositor: teselado y flotantes
3// -----------------------------------------------------------------------------
4// El kernel no coloca las ventanas a mano: las TESELA. El motor es
5// `mirada-layout` —el mismo nucleo `no_std` que ordena el compositor Wayland
6// de brahman—, enlazado por `path` cruzando la frontera de workspace.
7//
8// FASE 8b/8c :: el compositor cobra vida. Mantiene un ESCRITORIO —el registro
9// de todas las ventanas— y, por cada una, una CACHE de respaldo con su ultimo
10// fotograma. Gracias a esa cache, el teclado puede re-teselar el escritorio en
11// caliente —o mover el foco— y el kernel recompone cada ventana en su marco
12// nuevo SIN despertar a las apps: una app que solo pinto en su `init` conserva
13// su imagen intacta a traves de cualquier reordenacion.
14//
15// FASE 9 :: orden-Z y ventanas flotantes. Una ventana puede ABANDONAR el
16// teselado y FLOTAR —un marco propio, libre, que SOLAPA a las demas—. El
17// escritorio separa entonces dos capas: las TESELADAS, al fondo, sin
18// solapamiento entre si; y las FLOTANTES, encima, apiladas por un orden-Z
19// —`flotantes` ES esa pila, de atras hacia adelante; la ultima es la frontal—.
20// Con flotantes vivas el kernel deja de pintar cada ventana por separado:
21// RECOMPONE el escritorio entero, capa a capa, de modo que el solapamiento se
22// resuelva por el orden del pintado, sin recortes ni mascaras.
23//
24// FASE 10 :: el escritorio deja de ser un censo fijo. Una ventana puede
25// CERRARSE en vivo (`Alt+Q`): se la marca, su app concluye su tarea por su
26// voluntad y el teselado reclama su espacio. Y puede NACER una ventana nueva
27// (`Alt+N`): `nacer_ventana` la añade al censo y devuelve su indice al
28// orquestador, que instancia su WASM y engendra su tarea. El censo de
29// ventanas solo crece —los indices son la IDENTIDAD, jamas se reciclan—; una
30// ventana cerrada queda como una ranura inerte, fuera del orden y del foco.
31//
32// FASE 13 :: el raton entra en juego. Hay un PUNTERO en pantalla y el
33// compositor gana dos gestos: clic-para-enfocar (sobre cualquier ventana viva)
34// y ARRASTRAR una flotante con el boton izquierdo sostenido. Como el teclado
35// y la bocina, los eventos del raton vienen del manejador de IRQ12 por una
36// cola lock-free; `atender_raton` los drena cooperativamente y, al detectar
37// un boton que baja o un arrastre en curso, mueve el foco o el marco. Los
38// cuartos flotantes dejan, por fin, de estar clavados en su cascada.
39//
40// EXCLUSION DE INTERRUPCIONES. El `ESCRITORIO` lo tocan SOLO tareas
41// cooperativas (el `tick` de una app, la tarea del compositor): el manejador
42// de IRQ1 jamas lo bloquea. La IRQ se comunica con el mundo cooperativo por
43// un canal estrecho y a prueba de interbloqueos: dos atomicos —el foco y el
44// estado de Alt— y una cola lock-free de mandos. Ningun cerrojo que la IRQ
45// pudiera disputar a una tarea cooperativa.
46// =============================================================================
47
48use core::sync::atomic::{AtomicU64, AtomicUsize, Ordering};
49
50use alloc::string::{String, ToString};
51use alloc::vec;
52use alloc::vec::Vec;
53
54use crossbeam_queue::ArrayQueue;
55use mirada_layout::{tile, LayoutMode, LayoutParams, Rect};
56use spin::{Mutex, Once};
57
58use crate::consola::{self, Capa, CeldaTaskbar, Contenido, Taskbar};
59use crate::grafico::{Color, RegionPantalla};
60
61/// Altura del strip superior reservado a la consola; las apps teselan debajo.
62/// La consola conserva ahi su registro de arranque completo —seis lineas,
63/// hasta la sonda asincrona de disco— legible sobre el teselado.
64const FRANJA_CONSOLA: usize = 296;
65
66/// Altura de la barra de tareas inferior (Fase 14): cada ventana viva tiene
67/// ahi una pestaña con su nombre, que el clic enfoca.
68const FRANJA_TASKBAR: usize = 40;
69
70/// Anchura de cada celda de la barra de tareas, en pixeles.
71const CELDA_TASKBAR_ANCHO: usize = 150;
72/// Hueco entre celdas adyacentes de la barra.
73const CELDA_TASKBAR_HUECO: usize = 6;
74/// Margen izquierdo y derecho de la barra de tareas.
75const CELDA_TASKBAR_MARGEN: usize = 12;
76/// Anchura del boton lanzador («+» a la izquierda de la barra, Fase 16).
77const LAUNCHER_ANCHO: usize = 36;
78/// Hueco entre el lanzador y la primera pestaña.
79const LAUNCHER_HUECO: usize = 8;
80/// Anchura reservada para el reloj a la derecha de la barra (Fase 16).
81const RELOJ_ANCHO: usize = 80;
82/// Hueco entre la ultima pestaña y el reloj.
83const RELOJ_HUECO: usize = 8;
84
85/// El modo de teselado con que arranca el escritorio. El teclado lo cicla.
86const MODO_INICIAL: LayoutMode = LayoutMode::MasterStack;
87
88/// Margen entre ventanas teseladas, en pixeles.
89const MARGEN: i32 = 14;
90
91/// Capacidad de la cola de mandos del compositor — holgada: nadie pulsa tanto.
92const CAPACIDAD_MANDOS: usize = 32;
93
94/// Reborde de cromo de una ventana flotante: el panel que rodea su lienzo
95/// natural, donde se asienta el borde de foco sin tapar el dibujo de la app.
96const CROMO_FLOTANTE: usize = 8;
97
98/// Paso de la cascada con que se colocan las ventanas flotantes nuevas, en
99/// pixeles. Cada flotante se desplaza un paso respecto a la anterior, de modo
100/// que varias no se tapen por completo.
101const PASO_CASCADA: usize = 44;
102
103/// Un mando del compositor — lo emite el teclado desde el contexto de IRQ, lo
104/// atiende la tarea del compositor desde el reactor cooperativo.
105#[derive(Clone, Copy)]
106pub enum Mando {
107 /// Ciclar al siguiente modo de teselado de `mirada-layout`.
108 CiclarLayout,
109 /// Mover el foco a la siguiente ventana viva.
110 FocoSiguiente,
111 /// Mover el foco a la ventana viva anterior.
112 FocoAnterior,
113 /// Promover la ventana enfocada a la posicion maestra del teselado.
114 Promover,
115 /// Mover la ventana enfocada una posicion adelante en el orden de teselado.
116 MoverAdelante,
117 /// Mover la ventana enfocada una posicion atras en el orden de teselado.
118 MoverAtras,
119 /// Alternar la ventana enfocada entre teselada y flotante (Fase 9).
120 Flotar,
121 /// Cerrar la aplicacion enfocada — una baja limpia, en vivo (Fase 10).
122 Cerrar,
123 /// Lanzar una aplicacion nueva — un alta en vivo (Fase 10).
124 Lanzar,
125}
126
127/// Un arrastre EN CURSO (Fase 13): el indice de la ventana flotante asida con
128/// el raton y el desfase con que se asio —para que la ventana no salte al
129/// agarrarla, sino que siga al puntero como si lo llevara cogido por ahi—.
130#[derive(Clone, Copy)]
131struct Arrastre {
132 ventana: usize,
133 agarre_dx: usize,
134 agarre_dy: usize,
135}
136
137/// Una ventana del escritorio: una app, su geometria y su ultimo fotograma.
138struct Ventana {
139 /// Nombre legible de la app — el que dicta su `EntradaApp` del manifiesto.
140 /// Aparece en la pestaña de la barra de tareas (Fase 14).
141 nombre: String,
142 /// Tamaño natural del lienzo de la app — lo que sabe pintar, fijo.
143 natural_ancho: usize,
144 natural_alto: usize,
145 /// El marco actual — donde la app vive en pantalla. Si la ventana esta
146 /// teselada, lo fija el teselado; si flota, es un marco propio y libre.
147 marco: RegionPantalla,
148 /// CACHE de respaldo: el ultimo fotograma exitoso que la app envio. Su
149 /// tamaño esta acotado al lienzo natural —`natural_ancho × natural_alto ×
150 /// 4`— y se reserva UNA sola vez, al fundar el escritorio: jamas crece. El
151 /// re-teselado recompone la ventana desde aqui, sin molestar a la app.
152 cache: Vec<u8>,
153 /// ¿Ha enviado la app al menos un fotograma? Hasta entonces, su cache es
154 /// solo ceros y no se recompone.
155 pintada: bool,
156 /// Si el kernel desalojo la app, el color de su baliza. `None` mientras
157 /// vive; `Some(color)` la marca como muerta y la excluye del foco.
158 baliza: Option<Color>,
159 /// ¿Se ha pedido cerrar esta ventana en vivo (Fase 10)? Una vez `true`, su
160 /// app concluye su tarea, la ranura queda inerte —fuera del orden, del
161 /// orden-Z y del foco— y el teselado reclama su espacio.
162 cerrada: bool,
163}
164
165/// El escritorio: el registro de todas las ventanas y el modo de teselado.
166/// Lo tocan SOLO tareas cooperativas — nunca el manejador de IRQ1.
167struct Escritorio {
168 modo: LayoutMode,
169 ancho: usize,
170 alto: usize,
171 /// Las ventanas, indexadas por `indice_app` — su IDENTIDAD, inmutable.
172 ventanas: Vec<Ventana>,
173 /// El ORDEN de teselado: `orden[slot]` es el `indice_app` de la ventana que
174 /// ocupa esa celda del teselado. Contiene SOLO las ventanas teseladas —las
175 /// flotantes salen de aqui—. Separar el orden de la identidad permite
176 /// promover y reordenar ventanas sin tocar su `indice_app`.
177 orden: Vec<usize>,
178 /// Las ventanas FLOTANTES, en orden-Z (Fase 9): de atras hacia adelante.
179 /// `flotantes.last()` es la ventana frontal. Una ventana esta en `orden` o
180 /// en `flotantes`, jamas en ambos ni en ninguno: juntos son una particion
181 /// de `0..ventanas.len()`.
182 flotantes: Vec<usize>,
183 /// ¿Estaba el boton izquierdo del raton pulsado en el evento anterior?
184 /// Para detectar las transiciones —el momento exacto del clic o de soltar—.
185 raton_izq: bool,
186 /// Arrastre en curso, si lo hay (Fase 13).
187 arrastre: Option<Arrastre>,
188}
189
190/// El escritorio global. Se funda una sola vez, en el arranque.
191static ESCRITORIO: Once<Mutex<Escritorio>> = Once::new();
192
193/// El indice de la ventana ENFOCADA. Atomico —no un campo del `Escritorio`—
194/// porque el manejador de IRQ1 lo LEE para enrutar el teclado, y un atomico no
195/// se puede disputar: jamas hay interbloqueo entre la IRQ y una tarea.
196static FOCO: AtomicUsize = AtomicUsize::new(0);
197
198/// La cola de mandos: el manejador de IRQ1 deposita aqui las ordenes del
199/// teclado (lock-free, segura en contexto de interrupcion); la tarea del
200/// compositor las drena desde el reactor cooperativo.
201static MANDOS: Once<ArrayQueue<Mando>> = Once::new();
202
203/// Cuantos lanzamientos de aplicacion (Fase 10) aguardan. Lo incrementa
204/// `atender_mandos` al recibir un `Mando::Lanzar`; lo drena `partos_pendientes`,
205/// que lo lee el orquestador del kernel —el unico que sabe instanciar un WASM—.
206/// Atomico: el compositor lo escribe, el orquestador lo lee y lo pone a cero.
207static PARTOS: AtomicUsize = AtomicUsize::new(0);
208
209/// El ultimo segundo del reloj monotono que la barra de tareas ha mostrado.
210/// `tick_reloj` lo compara con el actual: si difiere, recompone para pintar el
211/// nuevo. Centinela `u64::MAX` para garantizar que el primer tick fuerza un
212/// repintado y la barra arranca con su reloj a 0:00.
213static ULTIMO_SEGUNDO: AtomicU64 = AtomicU64::new(u64::MAX);
214
215// =============================================================================
216// Fundacion y consulta — el arranque
217// =============================================================================
218
219/// Funda el escritorio: crea una ventana por app, con su marco teselado inicial
220/// y su cache de respaldo ya reservada al tamaño natural. `naturales` da el
221/// `(ancho, alto)` del lienzo de cada app, en el orden del manifiesto.
222pub fn fundar(ancho: usize, alto: usize, naturales: &[(usize, usize, &str)]) {
223 MANDOS.call_once(|| ArrayQueue::new(CAPACIDAD_MANDOS));
224
225 let mut ventanas = Vec::with_capacity(naturales.len());
226 for &(nat_ancho, nat_alto, nombre) in naturales {
227 ventanas.push(Ventana {
228 nombre: nombre.to_string(),
229 natural_ancho: nat_ancho,
230 natural_alto: nat_alto,
231 // Marco provisional; `aplicar_teselado` lo fija enseguida.
232 marco: RegionPantalla {
233 x: 0,
234 y: 0,
235 ancho: 0,
236 alto: 0,
237 },
238 // La cache: reservada UNA vez, acotada al lienzo natural.
239 cache: vec![0u8; nat_ancho.saturating_mul(nat_alto).saturating_mul(4)],
240 pintada: false,
241 baliza: None,
242 cerrada: false,
243 });
244 }
245
246 // El orden de teselado arranca como la identidad: la ventana `i` ocupa la
247 // celda `i`. Ninguna ventana flota al nacer — el escritorio es puro
248 // teselado hasta que el teclado lo decida (`Alt+F`).
249 let orden = (0..ventanas.len()).collect();
250 let mut escritorio = Escritorio {
251 modo: MODO_INICIAL,
252 ancho,
253 alto,
254 ventanas,
255 orden,
256 flotantes: Vec::new(),
257 raton_izq: false,
258 arrastre: None,
259 };
260 aplicar_teselado(&mut escritorio);
261
262 ESCRITORIO.call_once(|| Mutex::new(escritorio));
263}
264
265/// Recalcula el teselado y asigna a cada ventana TESELADA su marco. La celda
266/// `slot` del teselado va a la ventana `orden[slot]`: manda el orden, no la
267/// identidad. Las ventanas flotantes no estan en `orden` y conservan su marco.
268fn aplicar_teselado(escritorio: &mut Escritorio) {
269 let marcos = teselar(
270 escritorio.orden.len(),
271 escritorio.ancho,
272 escritorio.alto,
273 escritorio.modo,
274 );
275 for (slot, marco) in marcos.into_iter().enumerate() {
276 let ventana = escritorio.orden[slot];
277 escritorio.ventanas[ventana].marco = marco;
278 }
279}
280
281/// Pinta el escenario inicial del compositor. Se invoca una vez, tras `fundar`,
282/// antes de encender las apps: recompone el escritorio con todas las ventanas
283/// aun sin pintar — el teselado se ve como una rejilla de paneles.
284pub fn componer_escenario() {
285 let Some(escritorio) = ESCRITORIO.get() else {
286 return;
287 };
288 let escritorio = escritorio.lock();
289 recomponer(&escritorio);
290}
291
292/// El indice de la ventana enfocada. Lo LEE el manejador de IRQ1 para enrutar
293/// cada tecla — por eso es una simple lectura atomica, sin cerrojo alguno.
294pub fn foco() -> usize {
295 FOCO.load(Ordering::Relaxed)
296}
297
298/// Encola un mando del teclado. Lo invoca el manejador de IRQ1: empujar a una
299/// cola lock-free es seguro en contexto de interrupcion.
300pub fn solicitar(mando: Mando) {
301 if let Some(mandos) = MANDOS.get() {
302 // Si la cola se desborda, el mando se pierde en silencio: mas vale
303 // perder una pulsacion que arriesgar un panico dentro de una IRQ.
304 let _ = mandos.push(mando);
305 }
306}
307
308// =============================================================================
309// El fotograma de una app — cache y composicion
310// =============================================================================
311
312/// Recibe el fotograma de la app `indice`: lo copia a su CACHE de respaldo —el
313/// kernel asume la persistencia visual— y lo lleva a pantalla. Sin ventanas
314/// flotantes ninguna ventana solapa a otra: basta repintar la que cambio —el
315/// camino RAPIDO—. Con flotantes vivas el solapamiento obliga a RECOMPONER el
316/// escritorio entero, respetando el orden-Z. Lo invoca la capacidad
317/// `sys_render_frame` desde el `tick` cooperativo.
318pub fn presentar_fotograma(indice: usize, datos: &[u8]) {
319 let Some(escritorio) = ESCRITORIO.get() else {
320 return;
321 };
322 let mut escritorio = escritorio.lock();
323 {
324 let Some(ventana) = escritorio.ventanas.get_mut(indice) else {
325 return;
326 };
327 // Una ventana cerrada (Fase 10) ya no se pinta: su app pudo emitir un
328 // ultimo fotograma antes de que su tarea advirtiera la baja.
329 if ventana.cerrada {
330 return;
331 }
332 // Cachear el fotograma. El destino esta acotado al lienzo natural; se
333 // copia el minimo de ambas longitudes — jamas se desborda la cache.
334 let n = ventana.cache.len().min(datos.len());
335 ventana.cache[..n].copy_from_slice(&datos[..n]);
336 ventana.pintada = true;
337 }
338
339 if escritorio.flotantes.is_empty() {
340 // Camino RAPIDO: sin flotantes el escritorio es puro teselado y la app
341 // pinta directamente en su marco, como en la Fase 8.
342 let ventana = &escritorio.ventanas[indice];
343 let marco = ventana.marco;
344 let nat_ancho = ventana.natural_ancho;
345 let nat_alto = ventana.natural_alto;
346 let enfocada = FOCO.load(Ordering::Relaxed) == indice;
347 drop(escritorio);
348 consola::volcar_marco(marco, nat_ancho, nat_alto, datos, enfocada);
349 } else {
350 // Hay ventanas flotantes: el solapamiento obliga a recomponer.
351 recomponer(&escritorio);
352 }
353}
354
355/// Marca la ventana `indice` como desalojada y tatua su marco con la baliza.
356/// Desde aqui queda excluida del foco — el teclado la salta.
357pub fn desalojar(indice: usize, color: Color) {
358 let Some(escritorio) = ESCRITORIO.get() else {
359 return;
360 };
361 let mut escritorio = escritorio.lock();
362 {
363 let Some(ventana) = escritorio.ventanas.get_mut(indice) else {
364 return;
365 };
366 // Una ventana ya cerrada (Fase 10) no recibe baliza: la baja limpia
367 // gana a un desalojo que llegue tarde, en la misma vuelta.
368 if ventana.cerrada {
369 return;
370 }
371 ventana.baliza = Some(color);
372 }
373 // Fase 15: la voz del kernel anuncia el desalojo.
374 crate::drivers::altavoz::agendar(&crate::drivers::altavoz::VOZ_DESALOJO);
375
376 if escritorio.flotantes.is_empty() {
377 let marco = escritorio.ventanas[indice].marco;
378 let enfocada = FOCO.load(Ordering::Relaxed) == indice;
379 drop(escritorio);
380 consola::pintar_desalojo(marco, color, enfocada);
381 } else {
382 recomponer(&escritorio);
383 }
384}
385
386// =============================================================================
387// Los mandos del teclado — el escritorio interactivo
388// =============================================================================
389
390/// Atiende los mandos pendientes del teclado. La invoca la tarea del compositor
391/// en cada fotograma, desde el reactor cooperativo — el unico contexto donde es
392/// seguro bloquear el `ESCRITORIO` y la consola.
393pub fn atender_mandos() {
394 let Some(mandos) = MANDOS.get() else {
395 return;
396 };
397 while let Some(mando) = mandos.pop() {
398 match mando {
399 Mando::CiclarLayout => ciclar_layout(),
400 Mando::FocoSiguiente => mover_foco(true),
401 Mando::FocoAnterior => mover_foco(false),
402 Mando::Promover => promover(),
403 Mando::MoverAdelante => mover_ventana(true),
404 Mando::MoverAtras => mover_ventana(false),
405 Mando::Flotar => flotar(),
406 Mando::Cerrar => cerrar(),
407 // El alta de una app necesita instanciar un WASM — algo que el
408 // compositor no sabe hacer—. Solo se cuenta la peticion; el
409 // orquestador del kernel la atendera (ver `partos_pendientes`).
410 Mando::Lanzar => {
411 PARTOS.fetch_add(1, Ordering::Relaxed);
412 }
413 }
414 }
415}
416
417/// Cicla al siguiente modo de teselado: recalcula los marcos de las ventanas
418/// teseladas y recompone el escritorio entero desde las caches de respaldo.
419fn ciclar_layout() {
420 let Some(escritorio) = ESCRITORIO.get() else {
421 return;
422 };
423 let mut escritorio = escritorio.lock();
424 escritorio.modo = escritorio.modo.next();
425 aplicar_teselado(&mut escritorio);
426 recomponer(&escritorio);
427}
428
429/// Mueve el foco a la siguiente ventana VIVA. El recorrido abarca TODAS las
430/// ventanas —las teseladas y, tras ellas, las flotantes— saltando las
431/// desalojadas. Si la ventana recien enfocada flota, sube al frente del
432/// orden-Z: la flotante con el foco esta SIEMPRE delante.
433fn mover_foco(adelante: bool) {
434 let Some(escritorio) = ESCRITORIO.get() else {
435 return;
436 };
437 let mut escritorio = escritorio.lock();
438 // El recorrido del foco: las teseladas, luego las flotantes — un orden
439 // estable y visualmente coherente.
440 let recorrido: Vec<usize> = escritorio
441 .orden
442 .iter()
443 .chain(escritorio.flotantes.iter())
444 .copied()
445 .collect();
446 let n = recorrido.len();
447 if n == 0 {
448 return;
449 }
450 let anterior = FOCO.load(Ordering::Relaxed);
451 let pos = recorrido.iter().position(|&v| v == anterior).unwrap_or(0);
452
453 // Avanzar saltando las ventanas desalojadas. Si no hay ninguna viva, tras
454 // `n` pasos se vuelve al punto de partida y el foco no cambia.
455 let mut nueva_pos = pos;
456 let mut nuevo = anterior;
457 for _ in 0..n {
458 nueva_pos = if adelante {
459 (nueva_pos + 1) % n
460 } else {
461 (nueva_pos + n - 1) % n
462 };
463 let candidata = recorrido[nueva_pos];
464 if escritorio.ventanas[candidata].baliza.is_none() {
465 nuevo = candidata;
466 break;
467 }
468 }
469 FOCO.store(nuevo, Ordering::Relaxed);
470 // La bocina pertenece a la ventana enfocada (Fase 12): al cambiar el foco,
471 // callar — la nueva dueña la reclamara en su proximo fotograma si quiere.
472 crate::drivers::altavoz::tono(0);
473 // La ventana recien enfocada, si flota, al frente del orden-Z.
474 alzar_si_flota(&mut escritorio, nuevo);
475 recomponer(&escritorio);
476}
477
478/// Promueve la ventana enfocada a la posicion maestra —la celda 0— del
479/// teselado. Si la ventana enfocada flota, no esta en el orden de teselado y
480/// el mando no hace nada — promover es una operacion del teselado.
481fn promover() {
482 let Some(escritorio) = ESCRITORIO.get() else {
483 return;
484 };
485 let mut escritorio = escritorio.lock();
486 let foco = FOCO.load(Ordering::Relaxed);
487 if let Some(pos) = escritorio.orden.iter().position(|&v| v == foco) {
488 let ventana = escritorio.orden.remove(pos);
489 escritorio.orden.insert(0, ventana);
490 aplicar_teselado(&mut escritorio);
491 recomponer(&escritorio);
492 }
493}
494
495/// Mueve la ventana enfocada una posicion en el orden de teselado,
496/// intercambiandola con su vecina. Una ventana flotante no esta en el orden:
497/// el mando no la afecta.
498fn mover_ventana(adelante: bool) {
499 let Some(escritorio) = ESCRITORIO.get() else {
500 return;
501 };
502 let mut escritorio = escritorio.lock();
503 let n = escritorio.orden.len();
504 if n < 2 {
505 return;
506 }
507 let foco = FOCO.load(Ordering::Relaxed);
508 if let Some(pos) = escritorio.orden.iter().position(|&v| v == foco) {
509 let destino = if adelante {
510 (pos + 1) % n
511 } else {
512 (pos + n - 1) % n
513 };
514 escritorio.orden.swap(pos, destino);
515 aplicar_teselado(&mut escritorio);
516 recomponer(&escritorio);
517 }
518}
519
520// =============================================================================
521// FASE 9 — orden-Z y ventanas flotantes
522// =============================================================================
523
524/// Alterna la ventana enfocada entre TESELADA y FLOTANTE. Al flotar, la ventana
525/// abandona el teselado —que se recalcula para las que quedan—, recibe un marco
526/// propio en cascada y sube al frente del orden-Z. Al volver al teselado, se
527/// reincorpora al final del orden. El foco no cambia: viaja con la ventana.
528fn flotar() {
529 let Some(escritorio) = ESCRITORIO.get() else {
530 return;
531 };
532 let mut escritorio = escritorio.lock();
533 let foco = FOCO.load(Ordering::Relaxed);
534
535 if let Some(pos) = escritorio.orden.iter().position(|&v| v == foco) {
536 // Teselada -> flotante: se desliga del teselado, recibe su marco
537 // propio en cascada y sube al frente del orden-Z.
538 escritorio.orden.remove(pos);
539 let marco = marco_flotante(&escritorio, foco);
540 escritorio.ventanas[foco].marco = marco;
541 escritorio.flotantes.push(foco);
542 aplicar_teselado(&mut escritorio);
543 recomponer(&escritorio);
544 } else if let Some(pos) = escritorio.flotantes.iter().position(|&v| v == foco) {
545 // Flotante -> teselada: vuelve a la rejilla, al final del orden.
546 escritorio.flotantes.remove(pos);
547 escritorio.orden.push(foco);
548 aplicar_teselado(&mut escritorio);
549 recomponer(&escritorio);
550 }
551}
552
553/// Si la ventana `indice` es flotante, la lleva al frente del orden-Z —al final
554/// de `flotantes`—. Si esta teselada, no hace nada.
555fn alzar_si_flota(escritorio: &mut Escritorio, indice: usize) {
556 if let Some(pos) = escritorio.flotantes.iter().position(|&v| v == indice) {
557 let ventana = escritorio.flotantes.remove(pos);
558 escritorio.flotantes.push(ventana);
559 }
560}
561
562/// El marco de una ventana recien hecha flotante: su lienzo natural mas un
563/// reborde de cromo, colocado en cascada —para que varias flotantes no se
564/// tapen del todo— y acotado al area de apps. Se invoca ANTES de inscribir la
565/// ventana en `flotantes`: su longitud da el escalon de la cascada.
566fn marco_flotante(escritorio: &Escritorio, indice: usize) -> RegionPantalla {
567 let area = area_apps(escritorio.ancho, escritorio.alto);
568 let ventana = &escritorio.ventanas[indice];
569 let ancho = (ventana.natural_ancho + 2 * CROMO_FLOTANTE).min(area.ancho);
570 let alto = (ventana.natural_alto + 2 * CROMO_FLOTANTE).min(area.alto);
571
572 // La cascada: un escalon por cada flotante ya existente.
573 let escalon = escritorio.flotantes.len().saturating_mul(PASO_CASCADA);
574 let mut x = area.x + 48 + escalon;
575 let mut y = area.y + 40 + escalon;
576 // Acotar: la ventana entera ha de caber dentro del area de apps.
577 if x + ancho > area.x + area.ancho {
578 x = area.x + area.ancho.saturating_sub(ancho);
579 }
580 if y + alto > area.y + area.alto {
581 y = area.y + area.alto.saturating_sub(alto);
582 }
583 RegionPantalla {
584 x,
585 y,
586 ancho,
587 alto,
588 }
589}
590
591/// Recompone el escritorio entero respetando el orden-Z. Arma la lista de capas
592/// —primero las ventanas TESELADAS, la capa de fondo; despues las FLOTANTES, de
593/// atras hacia adelante— y se la entrega a la consola, que las funde en ese
594/// orden de una sola pasada. El solapamiento se resuelve por el orden del
595/// pintado. La invocan los mandos del teclado y `presentar_fotograma` cuando
596/// hay flotantes vivas. El llamante sostiene ya el cerrojo del `ESCRITORIO`.
597fn recomponer(escritorio: &Escritorio) {
598 let area = area_apps(escritorio.ancho, escritorio.alto);
599 let foco = FOCO.load(Ordering::Relaxed);
600 let mut capas: Vec<Capa> = Vec::with_capacity(escritorio.ventanas.len());
601 for &indice in escritorio.orden.iter().chain(escritorio.flotantes.iter()) {
602 let ventana = &escritorio.ventanas[indice];
603 let contenido = match ventana.baliza {
604 Some(color) => Contenido::Baliza(color),
605 None if ventana.pintada => Contenido::Fotograma(&ventana.cache),
606 None => Contenido::Panel,
607 };
608 capas.push(Capa {
609 marco: ventana.marco,
610 nat_ancho: ventana.natural_ancho,
611 nat_alto: ventana.natural_alto,
612 contenido,
613 enfocada: indice == foco,
614 });
615 }
616
617 // FASE 14/16 :: armar la barra de tareas. A la izquierda un boton lanzador
618 // («+»); en el medio una pestaña por ventana viva; a la derecha el reloj.
619 let area_bar = area_taskbar(escritorio.ancho, escritorio.alto);
620 let cy = area_bar.y + 4;
621 let calto = area_bar.alto.saturating_sub(8);
622 let launcher = RegionPantalla {
623 x: area_bar.x + CELDA_TASKBAR_MARGEN,
624 y: cy,
625 ancho: LAUNCHER_ANCHO,
626 alto: calto,
627 };
628 let cells_x0 = launcher.x + launcher.ancho + LAUNCHER_HUECO;
629 let cells_x_max =
630 area_bar.x + area_bar.ancho - CELDA_TASKBAR_MARGEN - RELOJ_ANCHO - RELOJ_HUECO;
631 let mut celdas: Vec<CeldaTaskbar> = Vec::new();
632 let mut cx = cells_x0;
633 for (indice, ventana) in escritorio.ventanas.iter().enumerate() {
634 if ventana.cerrada {
635 continue;
636 }
637 if cx + CELDA_TASKBAR_ANCHO > cells_x_max {
638 break;
639 }
640 let fondo = match ventana.baliza {
641 Some(color) => color,
642 None if indice == foco => Color::FOCO,
643 None => Color::PANEL,
644 };
645 celdas.push(CeldaTaskbar {
646 region: RegionPantalla {
647 x: cx,
648 y: cy,
649 ancho: CELDA_TASKBAR_ANCHO,
650 alto: calto,
651 },
652 nombre: &ventana.nombre,
653 fondo,
654 tinta: tinta_para(fondo),
655 });
656 cx += CELDA_TASKBAR_ANCHO + CELDA_TASKBAR_HUECO;
657 }
658 // El reloj: minutos:segundos desde el arranque, alineado a la derecha.
659 let ms = crate::async_system::reloj::milisegundos();
660 let segs = ms / 1000;
661 let reloj_texto = alloc::format!("{}:{:02}", segs / 60, segs % 60);
662 let reloj_region = RegionPantalla {
663 x: area_bar.x + area_bar.ancho - CELDA_TASKBAR_MARGEN - RELOJ_ANCHO,
664 y: cy,
665 ancho: RELOJ_ANCHO,
666 alto: calto,
667 };
668 let taskbar = Taskbar {
669 area: area_bar,
670 launcher,
671 celdas: &celdas,
672 reloj: &reloj_texto,
673 reloj_region,
674 };
675 consola::recomponer(area, &capas, &taskbar);
676 // Recordar el segundo recien mostrado: `tick_reloj` evita repintar de mas
677 // mientras dure este mismo segundo.
678 ULTIMO_SEGUNDO.store(segs, Ordering::Relaxed);
679}
680
681/// Localiza la celda de la barra de tareas bajo la coordenada x: itera las
682/// ventanas vivas en orden de creacion y devuelve la N-esima donde la N es la
683/// posicion en la barra. `None` si el clic cae en el lanzador, en el reloj, en
684/// un hueco entre celdas, o fuera del rango de las pestañas.
685fn celda_taskbar_en(escritorio: &Escritorio, x: usize) -> Option<usize> {
686 let area_bar = area_taskbar(escritorio.ancho, escritorio.alto);
687 // Las pestañas empiezan despues del lanzador.
688 let cells_x0 = area_bar.x + CELDA_TASKBAR_MARGEN + LAUNCHER_ANCHO + LAUNCHER_HUECO;
689 let cells_x_max =
690 area_bar.x + area_bar.ancho - CELDA_TASKBAR_MARGEN - RELOJ_ANCHO - RELOJ_HUECO;
691 if x < cells_x0 || x >= cells_x_max {
692 return None;
693 }
694 let rel = x - cells_x0;
695 let paso = CELDA_TASKBAR_ANCHO + CELDA_TASKBAR_HUECO;
696 let posicion = rel / paso;
697 let offset = rel % paso;
698 if offset >= CELDA_TASKBAR_ANCHO {
699 return None;
700 }
701 let mut k = 0;
702 for (indice, ventana) in escritorio.ventanas.iter().enumerate() {
703 if ventana.cerrada {
704 continue;
705 }
706 if k == posicion {
707 return Some(indice);
708 }
709 k += 1;
710 }
711 None
712}
713
714/// ¿Cae la coordenada x en el boton lanzador («+»)?
715fn clic_en_launcher(escritorio: &Escritorio, x: usize) -> bool {
716 let area_bar = area_taskbar(escritorio.ancho, escritorio.alto);
717 let x0 = area_bar.x + CELDA_TASKBAR_MARGEN;
718 let x1 = x0 + LAUNCHER_ANCHO;
719 x >= x0 && x < x1
720}
721
722// =============================================================================
723// FASE 10 — alta y baja de aplicaciones en vivo
724// =============================================================================
725
726/// Cierra la aplicacion enfocada (`Alt+Q`): una baja LIMPIA, distinta del
727/// desalojo por falla. Marca la ventana como cerrada, libera su cache de
728/// respaldo, la saca del teselado y del orden-Z, y traslada el foco a una
729/// ventana viva contigua. La app, en su tarea, advertira la baja y concluira.
730fn cerrar() {
731 let Some(escritorio) = ESCRITORIO.get() else {
732 return;
733 };
734 let mut escritorio = escritorio.lock();
735 let foco = FOCO.load(Ordering::Relaxed);
736 // Solo se cierra una ventana viva. El foco jamas se posa en una muerta o
737 // cerrada, pero la guarda lo deja explicito.
738 match escritorio.ventanas.get(foco) {
739 Some(v) if v.baliza.is_none() && !v.cerrada => {}
740 _ => return,
741 }
742 // Fase 15: el kernel se despide de la app con un repique descendente.
743 crate::drivers::altavoz::agendar(&crate::drivers::altavoz::VOZ_CERRAR);
744 // Marcar la baja y liberar el respaldo: la cache de un fotograma puede
745 // pesar un megabyte — no tiene sentido retenerla en una ranura inerte.
746 let ventana = &mut escritorio.ventanas[foco];
747 ventana.cerrada = true;
748 ventana.pintada = false;
749 ventana.cache = Vec::new();
750 // Sacarla del teselado y del orden-Z. El censo conserva la ranura —los
751 // indices son la identidad, jamas se reciclan—, pero ya nadie la dibuja.
752 escritorio.orden.retain(|&v| v != foco);
753 escritorio.flotantes.retain(|&v| v != foco);
754 // Si la estabamos arrastrando con el raton (Fase 13), soltarla.
755 if escritorio.arrastre.map(|a| a.ventana) == Some(foco) {
756 escritorio.arrastre = None;
757 }
758 // El foco salta a la primera ventana viva que quede; si no queda ninguna,
759 // se queda donde estaba —inofensivo: no hay a quien enrutar el teclado—.
760 let nuevo = escritorio
761 .orden
762 .iter()
763 .chain(escritorio.flotantes.iter())
764 .copied()
765 .find(|&v| {
766 let w = &escritorio.ventanas[v];
767 w.baliza.is_none() && !w.cerrada
768 })
769 .unwrap_or(foco);
770 FOCO.store(nuevo, Ordering::Relaxed);
771 // El foco cambia: callar la bocina (Fase 12) — ver `mover_foco`.
772 crate::drivers::altavoz::tono(0);
773 alzar_si_flota(&mut escritorio, nuevo);
774 aplicar_teselado(&mut escritorio);
775 recomponer(&escritorio);
776}
777
778/// Da de alta una ventana NUEVA y devuelve su indice —su identidad—. La crea
779/// con su cache de respaldo al tamaño natural, la añade al final del orden de
780/// teselado, recalcula el teselado y recompone. La invoca el orquestador del
781/// kernel justo antes de instanciar el WASM de la app, que necesita ese indice.
782pub fn nacer_ventana(nat_ancho: usize, nat_alto: usize, nombre: &str) -> usize {
783 let Some(escritorio) = ESCRITORIO.get() else {
784 return 0;
785 };
786 let mut escritorio = escritorio.lock();
787 let indice = escritorio.ventanas.len();
788 escritorio.ventanas.push(Ventana {
789 nombre: nombre.to_string(),
790 natural_ancho: nat_ancho,
791 natural_alto: nat_alto,
792 marco: RegionPantalla {
793 x: 0,
794 y: 0,
795 ancho: 0,
796 alto: 0,
797 },
798 cache: vec![0u8; nat_ancho.saturating_mul(nat_alto).saturating_mul(4)],
799 pintada: false,
800 baliza: None,
801 cerrada: false,
802 });
803 escritorio.orden.push(indice);
804 aplicar_teselado(&mut escritorio);
805 recomponer(&escritorio);
806 // Fase 15: el kernel saluda al nacimiento con un repique ascendente.
807 crate::drivers::altavoz::agendar(&crate::drivers::altavoz::VOZ_LANZAR);
808 indice
809}
810
811/// ¿Se ha pedido cerrar la ventana `indice`? Cada app la consulta en su tarea,
812/// fotograma a fotograma: cuando es `true`, concluye su tarea y se libera. Una
813/// ventana inexistente cuenta como cerrada.
814pub fn ventana_cerrada(indice: usize) -> bool {
815 let Some(escritorio) = ESCRITORIO.get() else {
816 return false;
817 };
818 escritorio
819 .lock()
820 .ventanas
821 .get(indice)
822 .map(|ventana| ventana.cerrada)
823 .unwrap_or(true)
824}
825
826/// Cuantas aplicaciones nuevas se han pedido lanzar desde la ultima consulta —y
827/// pone el contador a cero—. La invoca el orquestador del kernel —el unico que
828/// sabe instanciar un WASM— en cada fotograma de la tarea del compositor.
829pub fn partos_pendientes() -> usize {
830 PARTOS.swap(0, Ordering::Relaxed)
831}
832
833/// Avanza el reloj de la barra de tareas (Fase 16): si el segundo del reloj
834/// monotono cambio respecto al ultimo mostrado, recompone para refrescar la
835/// pantalla. Si el segundo es el mismo, vuelve sin hacer nada — un fotograma
836/// barato—. La invoca la tarea del compositor cada fotograma.
837pub fn tick_reloj() {
838 let actual = crate::async_system::reloj::milisegundos() / 1000;
839 if ULTIMO_SEGUNDO.load(Ordering::Relaxed) == actual {
840 return;
841 }
842 let Some(escritorio) = ESCRITORIO.get() else {
843 return;
844 };
845 let escritorio = escritorio.lock();
846 recomponer(&escritorio);
847}
848
849// =============================================================================
850// FASE 13 — raton, puntero y arrastre de ventanas flotantes
851// =============================================================================
852
853/// La ultima posicion del puntero que el compositor REFRESCO. Si la posicion
854/// actual del raton coincide con esta, no hay nada nuevo que estampar; si
855/// difiere, la consola debe volver a presentar. Empacada como `y * 65536 + x`,
856/// con `usize::MAX` como centinela de «aun no he visto al raton».
857static PUNTERO_REFRESCADO: AtomicUsize = AtomicUsize::new(usize::MAX);
858
859/// Drena los eventos del raton y los aplica: clic enfoca la ventana bajo el
860/// puntero (y, si flota, inicia un arrastre); el boton sostenido la arrastra;
861/// soltarlo termina el gesto. La invoca la tarea del compositor en cada
862/// fotograma, desde el reactor cooperativo.
863pub fn atender_raton() {
864 let Some(escritorio) = ESCRITORIO.get() else {
865 return;
866 };
867 let mut escritorio = escritorio.lock();
868 let mut cambio = false;
869 while let Some(evento) = crate::drivers::raton::siguiente_evento() {
870 let izq = evento.botones & 0b001 != 0;
871 let x = evento.x as usize;
872 let y = evento.y as usize;
873 let izq_antes = escritorio.raton_izq;
874 if izq && !izq_antes {
875 // Boton bajó: un CLIC. Si cae en la barra de tareas, enfocar la
876 // pestaña pulsada SIN iniciar arrastre. Si no, comportamiento
877 // habitual: enfocar la ventana topmost bajo el puntero.
878 let area_bar = area_taskbar(escritorio.ancho, escritorio.alto);
879 if y >= area_bar.y && y < area_bar.y + area_bar.alto {
880 if clic_en_launcher(&escritorio, x) {
881 // El boton «+» equivale a `Alt+N`: solicita un parto. La
882 // tarea del compositor lo recogera en su proxima vuelta.
883 PARTOS.fetch_add(1, Ordering::Relaxed);
884 } else if let Some(v) = celda_taskbar_en(&escritorio, x) {
885 let viva = {
886 let w = &escritorio.ventanas[v];
887 w.baliza.is_none() && !w.cerrada
888 };
889 if viva {
890 FOCO.store(v, Ordering::Relaxed);
891 crate::drivers::altavoz::tono(0);
892 alzar_si_flota(&mut escritorio, v);
893 cambio = true;
894 }
895 }
896 } else if let Some(v) = ventana_en(&escritorio, x, y) {
897 let viva = {
898 let w = &escritorio.ventanas[v];
899 w.baliza.is_none() && !w.cerrada
900 };
901 if viva {
902 // Enfocar como hace `mover_foco`: foco + bocina muda + alza
903 // si flota.
904 FOCO.store(v, Ordering::Relaxed);
905 crate::drivers::altavoz::tono(0);
906 alzar_si_flota(&mut escritorio, v);
907 // Si la ventana flota, empezar a arrastrarla.
908 if escritorio.flotantes.contains(&v) {
909 let marco = escritorio.ventanas[v].marco;
910 escritorio.arrastre = Some(Arrastre {
911 ventana: v,
912 agarre_dx: x.saturating_sub(marco.x),
913 agarre_dy: y.saturating_sub(marco.y),
914 });
915 }
916 cambio = true;
917 }
918 }
919 } else if izq && izq_antes {
920 // Boton sostenido: arrastrar la ventana asida, si la hay.
921 if let Some(arr) = escritorio.arrastre {
922 mover_arrastrada(&mut escritorio, arr, x, y);
923 cambio = true;
924 }
925 } else if !izq && izq_antes {
926 // Boton subió: fin del arrastre.
927 escritorio.arrastre = None;
928 }
929 escritorio.raton_izq = izq;
930 }
931 if cambio {
932 recomponer(&escritorio);
933 // El recomponer ya presento; sincronizar el centinela para no presentar
934 // dos veces en la misma vuelta.
935 PUNTERO_REFRESCADO.store(empacar_puntero(), Ordering::Relaxed);
936 }
937}
938
939/// La ventana topmost que contiene el punto (x, y), si la hay. Recorre el
940/// orden-Z de delante hacia atras: primero las flotantes (la ultima es la
941/// frontal), despues las teseladas.
942fn ventana_en(escritorio: &Escritorio, x: usize, y: usize) -> Option<usize> {
943 for &v in escritorio.flotantes.iter().rev() {
944 if contiene(escritorio.ventanas[v].marco, x, y) {
945 return Some(v);
946 }
947 }
948 for &v in escritorio.orden.iter().rev() {
949 if contiene(escritorio.ventanas[v].marco, x, y) {
950 return Some(v);
951 }
952 }
953 None
954}
955
956/// ¿Contiene el marco al punto (x, y)?
957fn contiene(marco: RegionPantalla, x: usize, y: usize) -> bool {
958 x >= marco.x && x < marco.x + marco.ancho && y >= marco.y && y < marco.y + marco.alto
959}
960
961/// Mueve la ventana arrastrada de modo que el punto del puntero —la asa— siga
962/// estando, dentro de la ventana, donde se asio. La ventana queda acotada al
963/// area de apps.
964fn mover_arrastrada(escritorio: &mut Escritorio, arr: Arrastre, x: usize, y: usize) {
965 let area = area_apps(escritorio.ancho, escritorio.alto);
966 let Some(ventana) = escritorio.ventanas.get_mut(arr.ventana) else {
967 return;
968 };
969 let ancho = ventana.marco.ancho;
970 let alto = ventana.marco.alto;
971 let mut nx = x.saturating_sub(arr.agarre_dx);
972 let mut ny = y.saturating_sub(arr.agarre_dy);
973 // Acotar al area de apps: la ventana entera ha de caber dentro.
974 if nx + ancho > area.x + area.ancho {
975 nx = (area.x + area.ancho).saturating_sub(ancho);
976 }
977 if ny + alto > area.y + area.alto {
978 ny = (area.y + area.alto).saturating_sub(alto);
979 }
980 nx = nx.max(area.x);
981 ny = ny.max(area.y);
982 ventana.marco.x = nx;
983 ventana.marco.y = ny;
984}
985
986/// Empaca la posicion actual del puntero en un solo `usize` —`y * 65536 + x`—
987/// para compararla atomicamente con la ultima refrescada. `usize::MAX` indica
988/// «el raton no esta vivo».
989fn empacar_puntero() -> usize {
990 match crate::drivers::raton::posicion() {
991 Some((x, y)) => (y << 16) | (x & 0xFFFF),
992 None => usize::MAX,
993 }
994}
995
996/// Si el puntero se ha movido desde la ultima presentacion del compositor, le
997/// pide a la consola un volcado fresco —para reestampar el puntero en su
998/// nuevo lugar—. La invoca la tarea del compositor cada fotograma.
999pub fn refrescar_puntero() {
1000 let actual = empacar_puntero();
1001 if actual == usize::MAX {
1002 return;
1003 }
1004 if PUNTERO_REFRESCADO.swap(actual, Ordering::Relaxed) != actual {
1005 crate::consola::refrescar();
1006 }
1007}
1008
1009// =============================================================================
1010// Teselado — la geometria pura de `mirada-layout`
1011// =============================================================================
1012
1013/// El area de pantalla que el compositor tesela: toda la pantalla menos la
1014/// franja de la consola en la cima y la barra de tareas al pie.
1015pub fn area_apps(ancho_pantalla: usize, alto_pantalla: usize) -> RegionPantalla {
1016 let cabeza = FRANJA_CONSOLA.min(alto_pantalla);
1017 let pie = FRANJA_TASKBAR.min(alto_pantalla.saturating_sub(cabeza));
1018 RegionPantalla {
1019 x: 0,
1020 y: cabeza,
1021 ancho: ancho_pantalla,
1022 alto: alto_pantalla.saturating_sub(cabeza).saturating_sub(pie),
1023 }
1024}
1025
1026/// El area de la barra de tareas: una franja al pie de la pantalla.
1027fn area_taskbar(ancho_pantalla: usize, alto_pantalla: usize) -> RegionPantalla {
1028 let pie = FRANJA_TASKBAR.min(alto_pantalla);
1029 RegionPantalla {
1030 x: 0,
1031 y: alto_pantalla.saturating_sub(pie),
1032 ancho: ancho_pantalla,
1033 alto: pie,
1034 }
1035}
1036
1037/// El color de tinta —oscuro o claro— que da contraste legible sobre `fondo`.
1038/// Sin esto, la pestaña amarilla palida del desalojo por memoria quedaba con
1039/// texto blanco sobre crema: ilegible. La regla de luminancia ITU-R BT.601 fija
1040/// el umbral: fondos claros llevan tinta oscura, fondos oscuros la clara.
1041fn tinta_para(fondo: Color) -> Color {
1042 let brillo =
1043 (fondo.r as u32 * 299 + fondo.g as u32 * 587 + fondo.b as u32 * 114) / 1000;
1044 if brillo > 160 {
1045 // Fondo claro: tinta del reposo del lienzo, casi negra.
1046 Color::LIENZO_EN_REPOSO
1047 } else {
1048 Color::TEXTO
1049 }
1050}
1051
1052/// Tesela el area de apps en `n` marcos con el modo dado. El vector resultante
1053/// tiene exactamente `n` elementos, en el orden de las celdas del teselado.
1054fn teselar(n: usize, ancho: usize, alto: usize, modo: LayoutMode) -> Vec<RegionPantalla> {
1055 let area = area_apps(ancho, alto);
1056 let pantalla = Rect::new(
1057 area.x as i32,
1058 area.y as i32,
1059 area.ancho as i32,
1060 area.alto as i32,
1061 );
1062 let params = LayoutParams {
1063 mode: modo,
1064 gap: MARGEN,
1065 ..LayoutParams::default()
1066 };
1067 tile(pantalla, n, ¶ms)
1068 .into_iter()
1069 .map(rect_a_region)
1070 .collect()
1071}
1072
1073/// Traduce un `Rect` de `mirada-layout` (`i32`) a la `RegionPantalla` del
1074/// kernel (`usize`). Un rectangulo degenerado queda en cero.
1075fn rect_a_region(r: Rect) -> RegionPantalla {
1076 RegionPantalla {
1077 x: r.x.max(0) as usize,
1078 y: r.y.max(0) as usize,
1079 ancho: r.w.max(0) as usize,
1080 alto: r.h.max(0) as usize,
1081 }
1082}