From 2d5062ed75a6961b16b3edfa741fe3319861601a Mon Sep 17 00:00:00 2001 From: TomZz Date: Mon, 15 Jun 2026 01:14:42 +0800 Subject: [PATCH] feat: implement pixel-perfect custom rendering for block elements, powerline and box drawing - Render block elements (U+2580-U+259F), box drawing lines, and powerline characters using precise GPUI vector paths instead of fonts. - Snap logical coordinates to exact physical pixels using window.scale_factor() to perfectly eliminate SDF anti-aliasing gaps and bleeding. - Fixes visual seams in terminal UI layouts. --- src/terminal/custom_blocks.rs | 251 ++++++++++++++++++++++++++++++++++ src/terminal/element.rs | 51 ++++++- src/terminal/mod.rs | 1 + 3 files changed, 300 insertions(+), 3 deletions(-) create mode 100644 src/terminal/custom_blocks.rs diff --git a/src/terminal/custom_blocks.rs b/src/terminal/custom_blocks.rs new file mode 100644 index 0000000..9af6bd1 --- /dev/null +++ b/src/terminal/custom_blocks.rs @@ -0,0 +1,251 @@ +use gpui::{Bounds, Hsla, Path, Pixels, Window, fill, point, px, size}; + +pub fn is_custom_block_supported(c: char) -> bool { + match c as u32 { + 0x2580..=0x258F | 0x2590 | 0x2594..=0x259F => true, // Block Elements + 0x2500 | 0x2502 | 0x250C | 0x2510 | 0x2514 | 0x2518 | 0x251C | 0x2524 | 0x252C | 0x2534 | 0x253C => true, // Light lines + 0x2501 | 0x2503 | 0x250F | 0x2513 | 0x2517 | 0x251B | 0x2523 | 0x252B | 0x2533 | 0x253B | 0x254B => true, // Heavy lines + 0xE0B0..=0xE0B6 => true, // Powerline + _ => false, + } +} + +pub fn paint_custom_block( + window: &mut Window, + c: char, + raw_x: f32, + raw_y: f32, + raw_w: f32, + raw_h: f32, + color: Hsla, +) -> bool { + let scale = window.scale_factor(); + + let x = px((raw_x * scale).round() / scale); + let y = px((raw_y * scale).round() / scale); + let w = px(((raw_x + raw_w) * scale).round() / scale) - x; + let h = px(((raw_y + raw_h) * scale).round() / scale) - y; + + let mut painted = false; + + // Helper to paint a quad snapping its true physical boundaries to integer physical pixels + let mut paint_quad = |qx: Pixels, qy: Pixels, qw: Pixels, qh: Pixels| { + let l = px((qx.as_f32() * scale).round() / scale); + let t = px((qy.as_f32() * scale).round() / scale); + let r = px(((qx + qw).as_f32() * scale).round() / scale); + let b = px(((qy + qh).as_f32() * scale).round() / scale); + window.paint_quad(fill(Bounds::new(point(l, t), size(r - l, b - t)), color)); + painted = true; + }; + + let px_1 = px(1.0); + let px_2 = px(2.0); + + // Box drawing thickness + let light = (w * 0.1).max(px_1); + let heavy = (w * 0.2).max(px_2); + let cx = x + w / 2.0; + let cy = y + h / 2.0; + + match c as u32 { + // --- Block Elements (U+2580..=U+259F) --- + 0x2580 => paint_quad(x, y, w, h / 2.0), + 0x2581 => paint_quad(x, y + h * 7.0 / 8.0, w, h / 8.0), + 0x2582 => paint_quad(x, y + h * 3.0 / 4.0, w, h / 4.0), + 0x2583 => paint_quad(x, y + h * 5.0 / 8.0, w, h * 3.0 / 8.0), + 0x2584 => paint_quad(x, y + h / 2.0, w, h / 2.0), + 0x2585 => paint_quad(x, y + h * 3.0 / 8.0, w, h * 5.0 / 8.0), + 0x2586 => paint_quad(x, y + h / 4.0, w, h * 3.0 / 4.0), + 0x2587 => paint_quad(x, y + h / 8.0, w, h * 7.0 / 8.0), + 0x2588 => paint_quad(x, y, w, h), + 0x2589 => paint_quad(x, y, w * 7.0 / 8.0, h), + 0x258A => paint_quad(x, y, w * 3.0 / 4.0, h), + 0x258B => paint_quad(x, y, w * 5.0 / 8.0, h), + 0x258C => paint_quad(x, y, w / 2.0, h), + 0x258D => paint_quad(x, y, w * 3.0 / 8.0, h), + 0x258E => paint_quad(x, y, w / 4.0, h), + 0x258F => paint_quad(x, y, w / 8.0, h), + 0x2590 => paint_quad(x + w / 2.0, y, w / 2.0, h), + 0x2594 => paint_quad(x, y, w, h / 8.0), + 0x2595 => paint_quad(x + w * 7.0 / 8.0, y, w / 8.0, h), + 0x2596 => paint_quad(x, y + h / 2.0, w / 2.0, h / 2.0), + 0x2597 => paint_quad(x + w / 2.0, y + h / 2.0, w / 2.0, h / 2.0), + 0x2598 => paint_quad(x, y, w / 2.0, h / 2.0), + 0x2599 => { + paint_quad(x, y, w / 2.0, h); + paint_quad(x + w / 2.0, y + h / 2.0, w / 2.0, h / 2.0); + } + 0x259A => { + paint_quad(x, y, w / 2.0, h / 2.0); + paint_quad(x + w / 2.0, y + h / 2.0, w / 2.0, h / 2.0); + } + 0x259B => { + paint_quad(x, y, w, h / 2.0); + paint_quad(x, y + h / 2.0, w / 2.0, h / 2.0); + } + 0x259C => { + paint_quad(x, y, w, h / 2.0); + paint_quad(x + w / 2.0, y + h / 2.0, w / 2.0, h / 2.0); + } + 0x259D => paint_quad(x + w / 2.0, y, w / 2.0, h / 2.0), + 0x259E => { + paint_quad(x + w / 2.0, y, w / 2.0, h / 2.0); + paint_quad(x, y + h / 2.0, w / 2.0, h / 2.0); + } + 0x259F => { + paint_quad(x + w / 2.0, y, w / 2.0, h); + paint_quad(x, y + h / 2.0, w / 2.0, h / 2.0); + } + + // --- Basic Box Drawing (U+2500..=U+257F) --- + // Light lines + 0x2500 => paint_quad(x, cy - light / 2.0, w, light), // Horizontal + 0x2502 => paint_quad(cx - light / 2.0, y, light, h), // Vertical + 0x250C => { // Down + Right + paint_quad(cx - light / 2.0, cy - light / 2.0, light, h / 2.0 + light / 2.0); + paint_quad(cx - light / 2.0, cy - light / 2.0, w / 2.0 + light / 2.0, light); + } + 0x2510 => { // Down + Left + paint_quad(cx - light / 2.0, cy - light / 2.0, light, h / 2.0 + light / 2.0); + paint_quad(x, cy - light / 2.0, w / 2.0 + light / 2.0, light); + } + 0x2514 => { // Up + Right + paint_quad(cx - light / 2.0, y, light, h / 2.0 + light / 2.0); + paint_quad(cx - light / 2.0, cy - light / 2.0, w / 2.0 + light / 2.0, light); + } + 0x2518 => { // Up + Left + paint_quad(cx - light / 2.0, y, light, h / 2.0 + light / 2.0); + paint_quad(x, cy - light / 2.0, w / 2.0 + light / 2.0, light); + } + 0x251C => { // Vertical + Right + paint_quad(cx - light / 2.0, y, light, h); + paint_quad(cx - light / 2.0, cy - light / 2.0, w / 2.0 + light / 2.0, light); + } + 0x2524 => { // Vertical + Left + paint_quad(cx - light / 2.0, y, light, h); + paint_quad(x, cy - light / 2.0, w / 2.0 + light / 2.0, light); + } + 0x252C => { // Horizontal + Down + paint_quad(x, cy - light / 2.0, w, light); + paint_quad(cx - light / 2.0, cy - light / 2.0, light, h / 2.0 + light / 2.0); + } + 0x2534 => { // Horizontal + Up + paint_quad(x, cy - light / 2.0, w, light); + paint_quad(cx - light / 2.0, y, light, h / 2.0 + light / 2.0); + } + 0x253C => { // Vertical + Horizontal (Cross) + paint_quad(x, cy - light / 2.0, w, light); + paint_quad(cx - light / 2.0, y, light, h); + } + + // Heavy lines + 0x2501 => paint_quad(x, cy - heavy / 2.0, w, heavy), // Heavy Horizontal + 0x2503 => paint_quad(cx - heavy / 2.0, y, heavy, h), // Heavy Vertical + 0x250F => { // Heavy Down + Right + paint_quad(cx - heavy / 2.0, cy - heavy / 2.0, heavy, h / 2.0 + heavy / 2.0); + paint_quad(cx - heavy / 2.0, cy - heavy / 2.0, w / 2.0 + heavy / 2.0, heavy); + } + 0x2513 => { // Heavy Down + Left + paint_quad(cx - heavy / 2.0, cy - heavy / 2.0, heavy, h / 2.0 + heavy / 2.0); + paint_quad(x, cy - heavy / 2.0, w / 2.0 + heavy / 2.0, heavy); + } + 0x2517 => { // Heavy Up + Right + paint_quad(cx - heavy / 2.0, y, heavy, h / 2.0 + heavy / 2.0); + paint_quad(cx - heavy / 2.0, cy - heavy / 2.0, w / 2.0 + heavy / 2.0, heavy); + } + 0x251B => { // Heavy Up + Left + paint_quad(cx - heavy / 2.0, y, heavy, h / 2.0 + heavy / 2.0); + paint_quad(x, cy - heavy / 2.0, w / 2.0 + heavy / 2.0, heavy); + } + 0x2523 => { // Heavy Vertical + Right + paint_quad(cx - heavy / 2.0, y, heavy, h); + paint_quad(cx - heavy / 2.0, cy - heavy / 2.0, w / 2.0 + heavy / 2.0, heavy); + } + 0x252B => { // Heavy Vertical + Left + paint_quad(cx - heavy / 2.0, y, heavy, h); + paint_quad(x, cy - heavy / 2.0, w / 2.0 + heavy / 2.0, heavy); + } + 0x2533 => { // Heavy Horizontal + Down + paint_quad(x, cy - heavy / 2.0, w, heavy); + paint_quad(cx - heavy / 2.0, cy - heavy / 2.0, heavy, h / 2.0 + heavy / 2.0); + } + 0x253B => { // Heavy Horizontal + Up + paint_quad(x, cy - heavy / 2.0, w, heavy); + paint_quad(cx - heavy / 2.0, y, heavy, h / 2.0 + heavy / 2.0); + } + 0x254B => { // Heavy Vertical + Horizontal (Cross) + paint_quad(x, cy - heavy / 2.0, w, heavy); + paint_quad(cx - heavy / 2.0, y, heavy, h); + } + + // --- Powerline --- + 0xE0B0 => { // Rightward Solid Arrow + let mut path = Path::new(point(x, y)); + path.line_to(point(x + w, y + h / 2.0)); + path.line_to(point(x, y + h)); + path.line_to(point(x, y)); + window.paint_path(path, color); + painted = true; + } + 0xE0B2 => { // Leftward Solid Arrow + let mut path = Path::new(point(x + w, y)); + path.line_to(point(x, y + h / 2.0)); + path.line_to(point(x + w, y + h)); + path.line_to(point(x + w, y)); + window.paint_path(path, color); + painted = true; + } + 0xE0B1 => { // Rightward Line Arrow + let t = px(1.0); + let mut p = Path::new(point(x, y)); + p.line_to(point(x + w, y + h / 2.0)); + p.line_to(point(x + w - t, y + h / 2.0)); + p.line_to(point(x, y + t)); + p.line_to(point(x, y)); + window.paint_path(p, color); + let mut p2 = Path::new(point(x, y + h)); + p2.line_to(point(x + w, y + h / 2.0)); + p2.line_to(point(x + w - t, y + h / 2.0)); + p2.line_to(point(x, y + h - t)); + p2.line_to(point(x, y + h)); + window.paint_path(p2, color); + painted = true; + } + 0xE0B3 => { // Leftward Line Arrow + let t = px(1.0); + let mut p = Path::new(point(x + w, y)); + p.line_to(point(x, y + h / 2.0)); + p.line_to(point(x + t, y + h / 2.0)); + p.line_to(point(x + w, y + t)); + p.line_to(point(x + w, y)); + window.paint_path(p, color); + let mut p2 = Path::new(point(x + w, y + h)); + p2.line_to(point(x, y + h / 2.0)); + p2.line_to(point(x + t, y + h / 2.0)); + p2.line_to(point(x + w, y + h - t)); + p2.line_to(point(x + w, y + h)); + window.paint_path(p2, color); + painted = true; + } + + // Half arches (Powerline rounded) + 0xE0B4 => { // Rightward Solid Semicircle + let mut path = Path::new(point(x, y)); + path.curve_to(point(x, y + h), point(x + w * 2.0, y + h / 2.0)); + path.line_to(point(x, y)); + window.paint_path(path, color); + painted = true; + } + 0xE0B6 => { // Leftward Solid Semicircle + let mut path = Path::new(point(x + w, y)); + path.curve_to(point(x + w, y + h), point(x - w, y + h / 2.0)); + path.line_to(point(x + w, y)); + window.paint_path(path, color); + painted = true; + } + + _ => {} + } + + painted +} diff --git a/src/terminal/element.rs b/src/terminal/element.rs index e836afd..fe815a8 100644 --- a/src/terminal/element.rs +++ b/src/terminal/element.rs @@ -12,6 +12,7 @@ use gpui_component::ActiveTheme as _; use crate::Ashell; use crate::terminal::{RenderSnapshot, ViewportSelection}; +use crate::terminal::custom_blocks::{paint_custom_block, is_custom_block_supported}; #[derive(Clone, Copy)] struct TerminalMetrics { @@ -138,9 +139,19 @@ pub struct PrepaintState { metrics: TerminalMetrics, rects: Vec, runs: Vec, + custom_blocks: Vec, cursor: Option, } +#[derive(Clone)] +struct LayoutCustomBlock { + c: char, + row: i32, + col: i32, + cells: usize, + color: Hsla, +} + struct TerminalInputHandler { view: Entity, element_bounds: Bounds, @@ -331,9 +342,10 @@ impl TerminalElement { } } - fn layout_grid(&self, cx: &App) -> (Vec, Vec) { + fn layout_grid(&self, cx: &App) -> (Vec, Vec, Vec) { let mut rects = Vec::new(); let mut runs = Vec::new(); + let mut custom_blocks = Vec::new(); let mut current_run: Option = None; for render_cell in &self.snapshot.cells { @@ -371,6 +383,24 @@ impl TerminalElement { } let style = self.cell_run_style(cell, cx); + + // Box Drawing & Block Elements interception + let is_custom_block = is_custom_block_supported(cell.c); + + if is_custom_block { + if let Some(run) = current_run.take() { + runs.push(run); + } + custom_blocks.push(LayoutCustomBlock { + c: cell.c, + row: render_cell.row, + col: render_cell.col, + cells: if cell.flags.contains(Flags::WIDE_CHAR) { 2 } else { 1 }, + color: style.color, + }); + continue; + } + if let Some(run) = current_run.as_mut() { if run.can_append(&style, render_cell.row, render_cell.col) { run.append(cell.c, cell.zerowidth()); @@ -402,7 +432,7 @@ impl TerminalElement { runs.push(run); } - (merge_rects(rects), runs) + (merge_rects(rects), runs, custom_blocks) } fn cursor_layout(&self, cx: &App) -> Option { @@ -458,7 +488,7 @@ impl Element for TerminalElement { cx: &mut App, ) -> Self::PrepaintState { let _ = self.base_text_style(cx); - let (rects, runs) = self.layout_grid(cx); + let (rects, runs, custom_blocks) = self.layout_grid(cx); PrepaintState { bounds, metrics: TerminalMetrics { @@ -467,6 +497,7 @@ impl Element for TerminalElement { }, rects, runs, + custom_blocks, cursor: self.cursor_layout(cx), } } @@ -489,6 +520,20 @@ impl Element for TerminalElement { run.paint(prepaint.bounds.origin, prepaint.metrics, window, cx); } + for block in &prepaint.custom_blocks { + let x = prepaint.bounds.origin.x.as_f32() + block.col as f32 * prepaint.metrics.cell_width.as_f32(); + let y = prepaint.bounds.origin.y.as_f32() + block.row as f32 * prepaint.metrics.line_height.as_f32(); + paint_custom_block( + window, + block.c, + x, + y, + prepaint.metrics.cell_width.as_f32() * block.cells as f32, + prepaint.metrics.line_height.as_f32(), + block.color, + ); + } + window.handle_input( &self.focus_handle, TerminalInputHandler { diff --git a/src/terminal/mod.rs b/src/terminal/mod.rs index 2a79319..401abe8 100644 --- a/src/terminal/mod.rs +++ b/src/terminal/mod.rs @@ -1,5 +1,6 @@ pub mod element; pub mod input; +pub mod custom_blocks; use std::sync::mpsc::Sender;