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);