From b8fce5a272909201b2e5ac92bf90f6db411aa388 Mon Sep 17 00:00:00 2001 From: Lionel DARNIS Date: Fri, 4 Sep 2026 16:53:35 +0200 Subject: [PATCH] yoda: elements de bloc U+2580-259F peints en quads natifs MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Le glyphe de police d'un bloc ne couvre que ~1 em alors que la cellule fait line_height (~1.4 em) : rangées de blocs trouees par l'interligne, bords creneles, rendu flou aux echelles fractionnees. Desormais le caractere est remplace par une espace (aucun glyphe) et le remplissage voyage dans Metadata jusqu'au tracé, ou terminal_box le peint en quad GPU couvrant exactement la fraction de cellule (huitiemes, quadrants, trames alpha). Consecutive cells de meme metadata fusionnent en un seul quad : jointif par construction, aligne sur la grille, net a toute echelle. C'est l'approche de kitty/WezTerm. Tout TUI a blocs profite (bannieres, jauges htop/btop, barres de progression). Couverture totale de la plage + geometrie bornee, 2 tests. --- src/terminal.rs | 135 +++++++++++++++++++++++++++++++++++++++++++- src/terminal_box.rs | 41 +++++++++++++- 2 files changed, 173 insertions(+), 3 deletions(-) diff --git a/src/terminal.rs b/src/terminal.rs index e56f3dd..683ac9a 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -218,11 +218,17 @@ impl TerminalPaneGrid { } } -#[derive(Debug, PartialEq, Eq, Hash, Clone)] +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] pub struct Metadata { pub bg: cosmic_text::Color, pub underline_color: cosmic_text::Color, pub flags: Flags, + /// Remplissage natif (élément de bloc Unicode) — voir [`BlockFill`]. + pub block: Option, + /// Couleur du remplissage natif. Hors bloc, sans effet ; participant à + /// l'égalité pour que deux blocs adjacents de couleurs différentes ne + /// fusionnent pas en un seul quad. + pub block_color: cosmic_text::Color, } impl Metadata { @@ -232,6 +238,8 @@ impl Metadata { bg, underline_color, flags, + block: None, + block_color: underline_color, } } @@ -245,6 +253,73 @@ impl Metadata { fn with_flags(self, flags: Flags) -> Self { Self { flags, ..self } } + + fn with_block(mut self, block: Option, color: cosmic_text::Color) -> Self { + self.block = block; + self.block_color = color; + self + } +} + +/// Élément de bloc Unicode (U+2580–U+259F) rendu nativement. +/// +/// Ces caractères ne passent PAS par le glyphe de la police : le `█` d'une +/// police couvre à peine l'em (≈ 1× la taille de police) alors que la cellule +/// fait `line_height` (≈ 1,4×) — d'où des trous entre les rangées et un rendu +/// crénelé. À la place, chaque bloc est remplacé par une espace (le glyphe +/// disparaît) et peint en quad GPU couvrant exactement la fraction voulue de +/// la cellule : bords alignés sur la grille, jointif par construction, net à +/// toute échelle. C'est l'approche de kitty/WezTerm. +#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +pub enum BlockFill { + /// Un ou deux rectangles en huitièmes de cellule : `(x0, x1, y0, y1)`, + /// abscisse et ordonnée depuis le coin haut-gauche. Un rectangle vide + /// (`x0 >= x1` ou `y0 >= y1`) est ignoré au tracé — deux rectangles + /// suffisent pour toute la famille des quadrants. + Rects([(u8, u8, u8, u8); 2]), + /// Trame (`░▒▓`) : rectangle plein, alpha en quarts (1–3). + Shade(u8), +} + +/// Mappe un caractère vers son remplissage natif, s'il est un élément de bloc. +pub fn block_fill(c: char) -> Option { + use BlockFill::{Rects, Shade}; + const NONE: (u8, u8, u8, u8) = (8, 8, 8, 8); + match c { + '▀' => Some(Rects([(0, 8, 0, 4), NONE])), + '▁' => Some(Rects([(0, 8, 7, 8), NONE])), + '▂' => Some(Rects([(0, 8, 6, 8), NONE])), + '▃' => Some(Rects([(0, 8, 5, 8), NONE])), + '▄' => Some(Rects([(0, 8, 4, 8), NONE])), + '▅' => Some(Rects([(0, 8, 3, 8), NONE])), + '▆' => Some(Rects([(0, 8, 2, 8), NONE])), + '▇' => Some(Rects([(0, 8, 1, 8), NONE])), + '█' => Some(Rects([(0, 8, 0, 8), NONE])), + '▉' => Some(Rects([(0, 7, 0, 8), NONE])), + '▊' => Some(Rects([(0, 6, 0, 8), NONE])), + '▋' => Some(Rects([(0, 5, 0, 8), NONE])), + '▌' => Some(Rects([(0, 4, 0, 8), NONE])), + '▍' => Some(Rects([(0, 3, 0, 8), NONE])), + '▎' => Some(Rects([(0, 2, 0, 8), NONE])), + '▏' => Some(Rects([(0, 1, 0, 8), NONE])), + '▐' => Some(Rects([(4, 8, 0, 8), NONE])), + '░' => Some(Shade(1)), + '▒' => Some(Shade(2)), + '▓' => Some(Shade(3)), + '▔' => Some(Rects([(0, 8, 0, 1), NONE])), + '▕' => Some(Rects([(7, 8, 0, 8), NONE])), + '▖' => Some(Rects([(0, 4, 4, 8), NONE])), + '▗' => Some(Rects([(4, 8, 4, 8), NONE])), + '▘' => Some(Rects([(0, 4, 0, 4), NONE])), + '▝' => Some(Rects([(4, 8, 0, 4), NONE])), + '▙' => Some(Rects([(0, 4, 0, 8), (4, 8, 4, 8)])), + '▚' => Some(Rects([(0, 4, 0, 4), (4, 8, 4, 8)])), + '▛' => Some(Rects([(0, 8, 0, 4), (0, 4, 4, 8)])), + '▜' => Some(Rects([(0, 8, 0, 4), (4, 8, 4, 8)])), + '▞' => Some(Rects([(4, 8, 0, 4), (0, 4, 4, 8)])), + '▟' => Some(Rects([(0, 8, 4, 8), (4, 8, 0, 4)])), + _ => None, + } } pub struct Terminal { @@ -905,9 +980,14 @@ impl Terminal { } let start = text.len(); - // Tab skip/stop is handled by alacritty_terminal + // Tab skip/stop is handled by alacritty_terminal. + // Les éléments de bloc ne deviennent pas des glyphes : la + // police ne couvre pas la cellule (trous d'interligne) ; + // ils sont peints en quads natifs au tracé, via Metadata. + let block = block_fill(indexed.cell.c); text.push(match indexed.cell.c { '\t' => ' ', + c if block.is_some() => ' ', c => c, }); if let Some(zerowidth) = indexed.cell.zerowidth() { @@ -1025,6 +1105,7 @@ impl Terminal { } let metadata = Metadata::new(bg, fg) + .with_block(block, fg) .with_flags(flags) .with_underline_color(underline_color); let (meta_idx, _) = self.metadata_set.insert_full(metadata); @@ -1344,3 +1425,53 @@ impl Drop for Terminal { } } } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn block_fill_couvre_toute_la_plage_unicode() { + // U+2580–U+259F : chaque élément de bloc a sa géométrie native. + for code in 0x2580..=0x259F { + let c = char::from_u32(code).unwrap(); + assert!( + block_fill(c).is_some(), + "{c} (U+{code:04X}) sans remplissage" + ); + } + // Hors de la plage : filets, espaces, lettres — pas de remplissage. + for c in [' ', '─', '│', 'M', '●', '\u{25A0}'] { + assert!( + block_fill(c).is_none(), + "{c} ne doit pas être un bloc natif" + ); + } + } + + #[test] + fn block_fill_geometry_toujours_valide() { + // Fractions croissantes, bornées à la cellule — sinon le quad + // déborderait sur la cellule voisine ou serait rejeté au tracé. + for c in (char::from_u32(0x2580).unwrap()..=char::from_u32(0x259F).unwrap()) + .chain(['░', '▒', '▓'].iter().copied()) + { + let Some(fill) = block_fill(c) else { continue }; + match fill { + BlockFill::Rects(rects) => { + for (x0, x1, y0, y1) in rects { + // Rectangle vide (sentinelle) ou borné à la cellule. + let empty = x0 >= x1 || y0 >= y1; + assert!( + empty || (x1 <= 8 && y1 <= 8), + "{c}: géométrie déborde la cellule" + ); + } + } + BlockFill::Shade(quarters) => { + assert!((1..=3).contains(&quarters), "{c}: alpha de trame invalide") + } + } + } + } +} diff --git a/src/terminal_box.rs b/src/terminal_box.rs index 64b9490..6f7f2d3 100644 --- a/src/terminal_box.rs +++ b/src/terminal_box.rs @@ -46,7 +46,11 @@ use std::{ time::{Duration, Instant}, }; -use crate::{Action, Terminal, TerminalScroll, menu::MenuState, terminal::Metadata}; +use crate::{ + Action, Terminal, TerminalScroll, + menu::MenuState, + terminal::{BlockFill, Metadata}, +}; const AUTOSCROLL_INTERVAL: Duration = Duration::from_millis(100); @@ -590,6 +594,41 @@ where ); } + // Éléments de bloc peints nativement : fractions de la + // cellule (ou de la course fusionnée, à géométrie + // séparable), bords calés sur la grille — zéro glyphe, + // zéro interligne. + if let Some(block) = metadata.block { + let span_w = self.end_x - self.start_x; + let mut color = cosmic_text_to_iced_color(metadata.block_color); + let rects = match block { + BlockFill::Rects(rects) => rects, + BlockFill::Shade(quarters) => { + color.a *= f32::from(quarters) / 4.0; + [(0, 8, 0, 8), (8, 8, 8, 8)] + } + }; + for (x0, x1, y0, y1) in rects { + if x0 >= x1 || y0 >= y1 { + continue; + } + let left = self.start_x + span_w * f32::from(x0) / 8.0; + let right = self.start_x + span_w * f32::from(x1) / 8.0; + let top = self.line_top + self.line_height * f32::from(y0) / 8.0; + let bottom = self.line_top + self.line_height * f32::from(y1) / 8.0; + renderer.fill_quad( + Quad { + bounds: Rectangle::new( + self.view_position + Vector::new(left.floor(), top), + Size::new(right.ceil() - left.floor(), bottom - top), + ), + ..Default::default() + }, + color, + ); + } + } + if !metadata.flags.is_empty() { let style_line_height = (self.glyph_font_size / 10.0).clamp(1.0, 16.0);