tawasuyu

tawasuyu/brahman

Rust 334TOML 129Markdown 39JSON 174745 Definiciones

⎇ 2523652e2250835d1521b0c3f3fc8374f6f1ae15 renaser/kernel/src/compositor.rs Rust · 564 líneas · 22.7 KB Historia Quién tocó qué crudo

feat(renaser): Fase 9 — orden-Z y ventanas flotantes 2523652e2 · sergio ·

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//  EXCLUSION DE INTERRUPCIONES. El `ESCRITORIO` lo tocan SOLO tareas
25//  cooperativas (el `tick` de una app, la tarea del compositor): el manejador
26//  de IRQ1 jamas lo bloquea. La IRQ se comunica con el mundo cooperativo por
27//  un canal estrecho y a prueba de interbloqueos: dos atomicos —el foco y el
28//  estado de Alt— y una cola lock-free de mandos. Ningun cerrojo que la IRQ
29//  pudiera disputar a una tarea cooperativa.
30// =============================================================================
31
32use core::sync::atomic::{AtomicUsize, Ordering};
33
34use alloc::vec;
35use alloc::vec::Vec;
36
37use crossbeam_queue::ArrayQueue;
38use mirada_layout::{tile, LayoutMode, LayoutParams, Rect};
39use spin::{Mutex, Once};
40
41use crate::consola::{self, Capa, Contenido};
42use crate::grafico::{Color, RegionPantalla};
43
44/// Altura del strip superior reservado a la consola; las apps teselan debajo.
45/// La consola conserva ahi su registro de arranque completo —seis lineas,
46/// hasta la sonda asincrona de disco— legible sobre el teselado.
47const FRANJA_CONSOLA: usize = 296;
48
49/// El modo de teselado con que arranca el escritorio. El teclado lo cicla.
50const MODO_INICIAL: LayoutMode = LayoutMode::MasterStack;
51
52/// Margen entre ventanas teseladas, en pixeles.
53const MARGEN: i32 = 14;
54
55/// Capacidad de la cola de mandos del compositor — holgada: nadie pulsa tanto.
56const CAPACIDAD_MANDOS: usize = 32;
57
58/// Reborde de cromo de una ventana flotante: el panel que rodea su lienzo
59/// natural, donde se asienta el borde de foco sin tapar el dibujo de la app.
60const CROMO_FLOTANTE: usize = 8;
61
62/// Paso de la cascada con que se colocan las ventanas flotantes nuevas, en
63/// pixeles. Cada flotante se desplaza un paso respecto a la anterior, de modo
64/// que varias no se tapen por completo.
65const PASO_CASCADA: usize = 44;
66
67/// Un mando del compositor — lo emite el teclado desde el contexto de IRQ, lo
68/// atiende la tarea del compositor desde el reactor cooperativo.
69#[derive(Clone, Copy)]
70pub enum Mando {
71    /// Ciclar al siguiente modo de teselado de `mirada-layout`.
72    CiclarLayout,
73    /// Mover el foco a la siguiente ventana viva.
74    FocoSiguiente,
75    /// Mover el foco a la ventana viva anterior.
76    FocoAnterior,
77    /// Promover la ventana enfocada a la posicion maestra del teselado.
78    Promover,
79    /// Mover la ventana enfocada una posicion adelante en el orden de teselado.
80    MoverAdelante,
81    /// Mover la ventana enfocada una posicion atras en el orden de teselado.
82    MoverAtras,
83    /// Alternar la ventana enfocada entre teselada y flotante (Fase 9).
84    Flotar,
85}
86
87/// Una ventana del escritorio: una app, su geometria y su ultimo fotograma.
88struct Ventana {
89    /// Tamaño natural del lienzo de la app — lo que sabe pintar, fijo.
90    natural_ancho: usize,
91    natural_alto: usize,
92    /// El marco actual — donde la app vive en pantalla. Si la ventana esta
93    /// teselada, lo fija el teselado; si flota, es un marco propio y libre.
94    marco: RegionPantalla,
95    /// CACHE de respaldo: el ultimo fotograma exitoso que la app envio. Su
96    /// tamaño esta acotado al lienzo natural —`natural_ancho × natural_alto ×
97    /// 4`— y se reserva UNA sola vez, al fundar el escritorio: jamas crece. El
98    /// re-teselado recompone la ventana desde aqui, sin molestar a la app.
99    cache: Vec<u8>,
100    /// ¿Ha enviado la app al menos un fotograma? Hasta entonces, su cache es
101    /// solo ceros y no se recompone.
102    pintada: bool,
103    /// Si el kernel desalojo la app, el color de su baliza. `None` mientras
104    /// vive; `Some(color)` la marca como muerta y la excluye del foco.
105    baliza: Option<Color>,
106}
107
108/// El escritorio: el registro de todas las ventanas y el modo de teselado.
109/// Lo tocan SOLO tareas cooperativas — nunca el manejador de IRQ1.
110struct Escritorio {
111    modo: LayoutMode,
112    ancho: usize,
113    alto: usize,
114    /// Las ventanas, indexadas por `indice_app` — su IDENTIDAD, inmutable.
115    ventanas: Vec<Ventana>,
116    /// El ORDEN de teselado: `orden[slot]` es el `indice_app` de la ventana que
117    /// ocupa esa celda del teselado. Contiene SOLO las ventanas teseladas —las
118    /// flotantes salen de aqui—. Separar el orden de la identidad permite
119    /// promover y reordenar ventanas sin tocar su `indice_app`.
120    orden: Vec<usize>,
121    /// Las ventanas FLOTANTES, en orden-Z (Fase 9): de atras hacia adelante.
122    /// `flotantes.last()` es la ventana frontal. Una ventana esta en `orden` o
123    /// en `flotantes`, jamas en ambos ni en ninguno: juntos son una particion
124    /// de `0..ventanas.len()`.
125    flotantes: Vec<usize>,
126}
127
128/// El escritorio global. Se funda una sola vez, en el arranque.
129static ESCRITORIO: Once<Mutex<Escritorio>> = Once::new();
130
131/// El indice de la ventana ENFOCADA. Atomico —no un campo del `Escritorio`—
132/// porque el manejador de IRQ1 lo LEE para enrutar el teclado, y un atomico no
133/// se puede disputar: jamas hay interbloqueo entre la IRQ y una tarea.
134static FOCO: AtomicUsize = AtomicUsize::new(0);
135
136/// La cola de mandos: el manejador de IRQ1 deposita aqui las ordenes del
137/// teclado (lock-free, segura en contexto de interrupcion); la tarea del
138/// compositor las drena desde el reactor cooperativo.
139static MANDOS: Once<ArrayQueue<Mando>> = Once::new();
140
141// =============================================================================
142//  Fundacion y consulta — el arranque
143// =============================================================================
144
145/// Funda el escritorio: crea una ventana por app, con su marco teselado inicial
146/// y su cache de respaldo ya reservada al tamaño natural. `naturales` da el
147/// `(ancho, alto)` del lienzo de cada app, en el orden del manifiesto.
148pub fn fundar(ancho: usize, alto: usize, naturales: &[(usize, usize)]) {
149    MANDOS.call_once(|| ArrayQueue::new(CAPACIDAD_MANDOS));
150
151    let mut ventanas = Vec::with_capacity(naturales.len());
152    for &(nat_ancho, nat_alto) in naturales {
153        ventanas.push(Ventana {
154            natural_ancho: nat_ancho,
155            natural_alto: nat_alto,
156            // Marco provisional; `aplicar_teselado` lo fija enseguida.
157            marco: RegionPantalla {
158                x: 0,
159                y: 0,
160                ancho: 0,
161                alto: 0,
162            },
163            // La cache: reservada UNA vez, acotada al lienzo natural.
164            cache: vec![0u8; nat_ancho.saturating_mul(nat_alto).saturating_mul(4)],
165            pintada: false,
166            baliza: None,
167        });
168    }
169
170    // El orden de teselado arranca como la identidad: la ventana `i` ocupa la
171    // celda `i`. Ninguna ventana flota al nacer — el escritorio es puro
172    // teselado hasta que el teclado lo decida (`Alt+F`).
173    let orden = (0..ventanas.len()).collect();
174    let mut escritorio = Escritorio {
175        modo: MODO_INICIAL,
176        ancho,
177        alto,
178        ventanas,
179        orden,
180        flotantes: Vec::new(),
181    };
182    aplicar_teselado(&mut escritorio);
183
184    ESCRITORIO.call_once(|| Mutex::new(escritorio));
185}
186
187/// Recalcula el teselado y asigna a cada ventana TESELADA su marco. La celda
188/// `slot` del teselado va a la ventana `orden[slot]`: manda el orden, no la
189/// identidad. Las ventanas flotantes no estan en `orden` y conservan su marco.
190fn aplicar_teselado(escritorio: &mut Escritorio) {
191    let marcos = teselar(
192        escritorio.orden.len(),
193        escritorio.ancho,
194        escritorio.alto,
195        escritorio.modo,
196    );
197    for (slot, marco) in marcos.into_iter().enumerate() {
198        let ventana = escritorio.orden[slot];
199        escritorio.ventanas[ventana].marco = marco;
200    }
201}
202
203/// Pinta el escenario inicial del compositor. Se invoca una vez, tras `fundar`,
204/// antes de encender las apps: recompone el escritorio con todas las ventanas
205/// aun sin pintar — el teselado se ve como una rejilla de paneles.
206pub fn componer_escenario() {
207    let Some(escritorio) = ESCRITORIO.get() else {
208        return;
209    };
210    let escritorio = escritorio.lock();
211    recomponer(&escritorio);
212}
213
214/// El indice de la ventana enfocada. Lo LEE el manejador de IRQ1 para enrutar
215/// cada tecla — por eso es una simple lectura atomica, sin cerrojo alguno.
216pub fn foco() -> usize {
217    FOCO.load(Ordering::Relaxed)
218}
219
220/// Encola un mando del teclado. Lo invoca el manejador de IRQ1: empujar a una
221/// cola lock-free es seguro en contexto de interrupcion.
222pub fn solicitar(mando: Mando) {
223    if let Some(mandos) = MANDOS.get() {
224        // Si la cola se desborda, el mando se pierde en silencio: mas vale
225        // perder una pulsacion que arriesgar un panico dentro de una IRQ.
226        let _ = mandos.push(mando);
227    }
228}
229
230// =============================================================================
231//  El fotograma de una app — cache y composicion
232// =============================================================================
233
234/// Recibe el fotograma de la app `indice`: lo copia a su CACHE de respaldo —el
235/// kernel asume la persistencia visual— y lo lleva a pantalla. Sin ventanas
236/// flotantes ninguna ventana solapa a otra: basta repintar la que cambio —el
237/// camino RAPIDO—. Con flotantes vivas el solapamiento obliga a RECOMPONER el
238/// escritorio entero, respetando el orden-Z. Lo invoca la capacidad
239/// `sys_render_frame` desde el `tick` cooperativo.
240pub fn presentar_fotograma(indice: usize, datos: &[u8]) {
241    let Some(escritorio) = ESCRITORIO.get() else {
242        return;
243    };
244    let mut escritorio = escritorio.lock();
245    {
246        let Some(ventana) = escritorio.ventanas.get_mut(indice) else {
247            return;
248        };
249        // Cachear el fotograma. El destino esta acotado al lienzo natural; se
250        // copia el minimo de ambas longitudes — jamas se desborda la cache.
251        let n = ventana.cache.len().min(datos.len());
252        ventana.cache[..n].copy_from_slice(&datos[..n]);
253        ventana.pintada = true;
254    }
255
256    if escritorio.flotantes.is_empty() {
257        // Camino RAPIDO: sin flotantes el escritorio es puro teselado y la app
258        // pinta directamente en su marco, como en la Fase 8.
259        let ventana = &escritorio.ventanas[indice];
260        let marco = ventana.marco;
261        let nat_ancho = ventana.natural_ancho;
262        let nat_alto = ventana.natural_alto;
263        let enfocada = FOCO.load(Ordering::Relaxed) == indice;
264        drop(escritorio);
265        consola::volcar_marco(marco, nat_ancho, nat_alto, datos, enfocada);
266    } else {
267        // Hay ventanas flotantes: el solapamiento obliga a recomponer.
268        recomponer(&escritorio);
269    }
270}
271
272/// Marca la ventana `indice` como desalojada y tatua su marco con la baliza.
273/// Desde aqui queda excluida del foco — el teclado la salta.
274pub fn desalojar(indice: usize, color: Color) {
275    let Some(escritorio) = ESCRITORIO.get() else {
276        return;
277    };
278    let mut escritorio = escritorio.lock();
279    {
280        let Some(ventana) = escritorio.ventanas.get_mut(indice) else {
281            return;
282        };
283        ventana.baliza = Some(color);
284    }
285
286    if escritorio.flotantes.is_empty() {
287        let marco = escritorio.ventanas[indice].marco;
288        let enfocada = FOCO.load(Ordering::Relaxed) == indice;
289        drop(escritorio);
290        consola::pintar_desalojo(marco, color, enfocada);
291    } else {
292        recomponer(&escritorio);
293    }
294}
295
296// =============================================================================
297//  Los mandos del teclado — el escritorio interactivo
298// =============================================================================
299
300/// Atiende los mandos pendientes del teclado. La invoca la tarea del compositor
301/// en cada fotograma, desde el reactor cooperativo — el unico contexto donde es
302/// seguro bloquear el `ESCRITORIO` y la consola.
303pub fn atender_mandos() {
304    let Some(mandos) = MANDOS.get() else {
305        return;
306    };
307    while let Some(mando) = mandos.pop() {
308        match mando {
309            Mando::CiclarLayout => ciclar_layout(),
310            Mando::FocoSiguiente => mover_foco(true),
311            Mando::FocoAnterior => mover_foco(false),
312            Mando::Promover => promover(),
313            Mando::MoverAdelante => mover_ventana(true),
314            Mando::MoverAtras => mover_ventana(false),
315            Mando::Flotar => flotar(),
316        }
317    }
318}
319
320/// Cicla al siguiente modo de teselado: recalcula los marcos de las ventanas
321/// teseladas y recompone el escritorio entero desde las caches de respaldo.
322fn ciclar_layout() {
323    let Some(escritorio) = ESCRITORIO.get() else {
324        return;
325    };
326    let mut escritorio = escritorio.lock();
327    escritorio.modo = escritorio.modo.next();
328    aplicar_teselado(&mut escritorio);
329    recomponer(&escritorio);
330}
331
332/// Mueve el foco a la siguiente ventana VIVA. El recorrido abarca TODAS las
333/// ventanas —las teseladas y, tras ellas, las flotantes— saltando las
334/// desalojadas. Si la ventana recien enfocada flota, sube al frente del
335/// orden-Z: la flotante con el foco esta SIEMPRE delante.
336fn mover_foco(adelante: bool) {
337    let Some(escritorio) = ESCRITORIO.get() else {
338        return;
339    };
340    let mut escritorio = escritorio.lock();
341    // El recorrido del foco: las teseladas, luego las flotantes — un orden
342    // estable y visualmente coherente.
343    let recorrido: Vec<usize> = escritorio
344        .orden
345        .iter()
346        .chain(escritorio.flotantes.iter())
347        .copied()
348        .collect();
349    let n = recorrido.len();
350    if n == 0 {
351        return;
352    }
353    let anterior = FOCO.load(Ordering::Relaxed);
354    let pos = recorrido.iter().position(|&v| v == anterior).unwrap_or(0);
355
356    // Avanzar saltando las ventanas desalojadas. Si no hay ninguna viva, tras
357    // `n` pasos se vuelve al punto de partida y el foco no cambia.
358    let mut nueva_pos = pos;
359    let mut nuevo = anterior;
360    for _ in 0..n {
361        nueva_pos = if adelante {
362            (nueva_pos + 1) % n
363        } else {
364            (nueva_pos + n - 1) % n
365        };
366        let candidata = recorrido[nueva_pos];
367        if escritorio.ventanas[candidata].baliza.is_none() {
368            nuevo = candidata;
369            break;
370        }
371    }
372    FOCO.store(nuevo, Ordering::Relaxed);
373    // La ventana recien enfocada, si flota, al frente del orden-Z.
374    alzar_si_flota(&mut escritorio, nuevo);
375    recomponer(&escritorio);
376}
377
378/// Promueve la ventana enfocada a la posicion maestra —la celda 0— del
379/// teselado. Si la ventana enfocada flota, no esta en el orden de teselado y
380/// el mando no hace nada — promover es una operacion del teselado.
381fn promover() {
382    let Some(escritorio) = ESCRITORIO.get() else {
383        return;
384    };
385    let mut escritorio = escritorio.lock();
386    let foco = FOCO.load(Ordering::Relaxed);
387    if let Some(pos) = escritorio.orden.iter().position(|&v| v == foco) {
388        let ventana = escritorio.orden.remove(pos);
389        escritorio.orden.insert(0, ventana);
390        aplicar_teselado(&mut escritorio);
391        recomponer(&escritorio);
392    }
393}
394
395/// Mueve la ventana enfocada una posicion en el orden de teselado,
396/// intercambiandola con su vecina. Una ventana flotante no esta en el orden:
397/// el mando no la afecta.
398fn mover_ventana(adelante: bool) {
399    let Some(escritorio) = ESCRITORIO.get() else {
400        return;
401    };
402    let mut escritorio = escritorio.lock();
403    let n = escritorio.orden.len();
404    if n < 2 {
405        return;
406    }
407    let foco = FOCO.load(Ordering::Relaxed);
408    if let Some(pos) = escritorio.orden.iter().position(|&v| v == foco) {
409        let destino = if adelante {
410            (pos + 1) % n
411        } else {
412            (pos + n - 1) % n
413        };
414        escritorio.orden.swap(pos, destino);
415        aplicar_teselado(&mut escritorio);
416        recomponer(&escritorio);
417    }
418}
419
420// =============================================================================
421//  FASE 9 — orden-Z y ventanas flotantes
422// =============================================================================
423
424/// Alterna la ventana enfocada entre TESELADA y FLOTANTE. Al flotar, la ventana
425/// abandona el teselado —que se recalcula para las que quedan—, recibe un marco
426/// propio en cascada y sube al frente del orden-Z. Al volver al teselado, se
427/// reincorpora al final del orden. El foco no cambia: viaja con la ventana.
428fn flotar() {
429    let Some(escritorio) = ESCRITORIO.get() else {
430        return;
431    };
432    let mut escritorio = escritorio.lock();
433    let foco = FOCO.load(Ordering::Relaxed);
434
435    if let Some(pos) = escritorio.orden.iter().position(|&v| v == foco) {
436        // Teselada -> flotante: se desliga del teselado, recibe su marco
437        // propio en cascada y sube al frente del orden-Z.
438        escritorio.orden.remove(pos);
439        let marco = marco_flotante(&escritorio, foco);
440        escritorio.ventanas[foco].marco = marco;
441        escritorio.flotantes.push(foco);
442        aplicar_teselado(&mut escritorio);
443        recomponer(&escritorio);
444    } else if let Some(pos) = escritorio.flotantes.iter().position(|&v| v == foco) {
445        // Flotante -> teselada: vuelve a la rejilla, al final del orden.
446        escritorio.flotantes.remove(pos);
447        escritorio.orden.push(foco);
448        aplicar_teselado(&mut escritorio);
449        recomponer(&escritorio);
450    }
451}
452
453/// Si la ventana `indice` es flotante, la lleva al frente del orden-Z —al final
454/// de `flotantes`—. Si esta teselada, no hace nada.
455fn alzar_si_flota(escritorio: &mut Escritorio, indice: usize) {
456    if let Some(pos) = escritorio.flotantes.iter().position(|&v| v == indice) {
457        let ventana = escritorio.flotantes.remove(pos);
458        escritorio.flotantes.push(ventana);
459    }
460}
461
462/// El marco de una ventana recien hecha flotante: su lienzo natural mas un
463/// reborde de cromo, colocado en cascada —para que varias flotantes no se
464/// tapen del todo— y acotado al area de apps. Se invoca ANTES de inscribir la
465/// ventana en `flotantes`: su longitud da el escalon de la cascada.
466fn marco_flotante(escritorio: &Escritorio, indice: usize) -> RegionPantalla {
467    let area = area_apps(escritorio.ancho, escritorio.alto);
468    let ventana = &escritorio.ventanas[indice];
469    let ancho = (ventana.natural_ancho + 2 * CROMO_FLOTANTE).min(area.ancho);
470    let alto = (ventana.natural_alto + 2 * CROMO_FLOTANTE).min(area.alto);
471
472    // La cascada: un escalon por cada flotante ya existente.
473    let escalon = escritorio.flotantes.len().saturating_mul(PASO_CASCADA);
474    let mut x = area.x + 48 + escalon;
475    let mut y = area.y + 40 + escalon;
476    // Acotar: la ventana entera ha de caber dentro del area de apps.
477    if x + ancho > area.x + area.ancho {
478        x = area.x + area.ancho.saturating_sub(ancho);
479    }
480    if y + alto > area.y + area.alto {
481        y = area.y + area.alto.saturating_sub(alto);
482    }
483    RegionPantalla {
484        x,
485        y,
486        ancho,
487        alto,
488    }
489}
490
491/// Recompone el escritorio entero respetando el orden-Z. Arma la lista de capas
492/// —primero las ventanas TESELADAS, la capa de fondo; despues las FLOTANTES, de
493/// atras hacia adelante— y se la entrega a la consola, que las funde en ese
494/// orden de una sola pasada. El solapamiento se resuelve por el orden del
495/// pintado. La invocan los mandos del teclado y `presentar_fotograma` cuando
496/// hay flotantes vivas. El llamante sostiene ya el cerrojo del `ESCRITORIO`.
497fn recomponer(escritorio: &Escritorio) {
498    let area = area_apps(escritorio.ancho, escritorio.alto);
499    let foco = FOCO.load(Ordering::Relaxed);
500    let mut capas: Vec<Capa> = Vec::with_capacity(escritorio.ventanas.len());
501    for &indice in escritorio.orden.iter().chain(escritorio.flotantes.iter()) {
502        let ventana = &escritorio.ventanas[indice];
503        let contenido = match ventana.baliza {
504            Some(color) => Contenido::Baliza(color),
505            None if ventana.pintada => Contenido::Fotograma(&ventana.cache),
506            None => Contenido::Panel,
507        };
508        capas.push(Capa {
509            marco: ventana.marco,
510            nat_ancho: ventana.natural_ancho,
511            nat_alto: ventana.natural_alto,
512            contenido,
513            enfocada: indice == foco,
514        });
515    }
516    consola::recomponer(area, &capas);
517}
518
519// =============================================================================
520//  Teselado — la geometria pura de `mirada-layout`
521// =============================================================================
522
523/// El area de pantalla que el compositor tesela: toda la pantalla menos la
524/// franja de la consola en la cima.
525pub fn area_apps(ancho_pantalla: usize, alto_pantalla: usize) -> RegionPantalla {
526    RegionPantalla {
527        x: 0,
528        y: FRANJA_CONSOLA.min(alto_pantalla),
529        ancho: ancho_pantalla,
530        alto: alto_pantalla.saturating_sub(FRANJA_CONSOLA),
531    }
532}
533
534/// Tesela el area de apps en `n` marcos con el modo dado. El vector resultante
535/// tiene exactamente `n` elementos, en el orden de las celdas del teselado.
536fn teselar(n: usize, ancho: usize, alto: usize, modo: LayoutMode) -> Vec<RegionPantalla> {
537    let area = area_apps(ancho, alto);
538    let pantalla = Rect::new(
539        area.x as i32,
540        area.y as i32,
541        area.ancho as i32,
542        area.alto as i32,
543    );
544    let params = LayoutParams {
545        mode: modo,
546        gap: MARGEN,
547        ..LayoutParams::default()
548    };
549    tile(pantalla, n, &params)
550        .into_iter()
551        .map(rect_a_region)
552        .collect()
553}
554
555/// Traduce un `Rect` de `mirada-layout` (`i32`) a la `RegionPantalla` del
556/// kernel (`usize`). Un rectangulo degenerado queda en cero.
557fn rect_a_region(r: Rect) -> RegionPantalla {
558    RegionPantalla {
559        x: r.x.max(0) as usize,
560        y: r.y.max(0) as usize,
561        ancho: r.w.max(0) as usize,
562        alto: r.h.max(0) as usize,
563    }
564}