many fixed and changes
This commit is contained in:
Generated
+22
@@ -514,6 +514,15 @@ version = "0.1.5"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
||||
|
||||
[[package]]
|
||||
name = "fontconfig"
|
||||
version = "0.10.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "b19c4bca8c705ea23bfb3e3403a9e699344d1ee3205b631f03fe4dbf1e52429f"
|
||||
dependencies = [
|
||||
"yeslogic-fontconfig-sys",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "fontdue"
|
||||
version = "0.9.3"
|
||||
@@ -2580,6 +2589,17 @@ version = "0.8.28"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f"
|
||||
|
||||
[[package]]
|
||||
name = "yeslogic-fontconfig-sys"
|
||||
version = "6.0.0"
|
||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||
checksum = "503a066b4c037c440169d995b869046827dbc71263f6e8f3be6d77d4f3229dbd"
|
||||
dependencies = [
|
||||
"dlib",
|
||||
"once_cell",
|
||||
"pkg-config",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "zerocopy"
|
||||
version = "0.8.31"
|
||||
@@ -2609,6 +2629,7 @@ dependencies = [
|
||||
"bytemuck",
|
||||
"dirs",
|
||||
"env_logger",
|
||||
"fontconfig",
|
||||
"fontdue",
|
||||
"libc",
|
||||
"log",
|
||||
@@ -2624,4 +2645,5 @@ dependencies = [
|
||||
"ttf-parser 0.25.1",
|
||||
"wgpu",
|
||||
"winit",
|
||||
"yeslogic-fontconfig-sys",
|
||||
]
|
||||
|
||||
@@ -39,6 +39,8 @@ bitflags = "2"
|
||||
fontdue = "0.9"
|
||||
rustybuzz = "0.20"
|
||||
ttf-parser = "0.25"
|
||||
fontconfig = "0.10"
|
||||
fontconfig-sys = { package = "yeslogic-fontconfig-sys", version = "6.0" }
|
||||
|
||||
# Configuration
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
|
||||
@@ -0,0 +1,31 @@
|
||||
# Maintainer: Zach <zach@brohn.se>
|
||||
pkgname=zterm
|
||||
pkgver=0.1.0
|
||||
pkgrel=1
|
||||
pkgdesc="A GPU-accelerated terminal emulator for Wayland"
|
||||
arch=('x86_64')
|
||||
url="https://github.com/Zacharias-Brohn/zterm"
|
||||
license=('MIT')
|
||||
depends=(
|
||||
'fontconfig'
|
||||
'freetype2'
|
||||
'wayland'
|
||||
'libxkbcommon'
|
||||
'vulkan-icd-loader'
|
||||
)
|
||||
makedepends=('rust' 'cargo')
|
||||
source=()
|
||||
|
||||
build() {
|
||||
cd "$startdir"
|
||||
cargo build --release --locked
|
||||
}
|
||||
|
||||
package() {
|
||||
cd "$startdir"
|
||||
install -Dm755 "target/release/zterm" "$pkgdir/usr/bin/zterm"
|
||||
install -Dm644 "zterm.terminfo" "$pkgdir/usr/share/zterm/zterm.terminfo"
|
||||
|
||||
# Compile and install terminfo
|
||||
tic -x -o "$pkgdir/usr/share/terminfo" zterm.terminfo
|
||||
}
|
||||
@@ -0,0 +1,87 @@
|
||||
//! Test ligature detection - compare individual vs combined shaping
|
||||
use rustybuzz::{Face, UnicodeBuffer, Feature};
|
||||
use ttf_parser::Tag;
|
||||
use fontdue::Font;
|
||||
use std::fs;
|
||||
|
||||
fn main() {
|
||||
let path = "/usr/share/fonts/TTF/0xProtoNerdFontMono-Regular.ttf";
|
||||
println!("Using font: {}", path);
|
||||
|
||||
let font_data = fs::read(path).expect("Failed to read font");
|
||||
let face = Face::from_slice(&font_data, 0).expect("Failed to parse font");
|
||||
let fontdue_font = Font::from_bytes(&font_data[..], fontdue::FontSettings::default()).unwrap();
|
||||
|
||||
let font_size = 16.0;
|
||||
let units_per_em = face.units_per_em() as f32;
|
||||
|
||||
println!("Font units per em: {}", units_per_em);
|
||||
|
||||
// Get cell width from a regular character
|
||||
let (hyphen_metrics, _) = fontdue_font.rasterize('-', font_size);
|
||||
let cell_width = hyphen_metrics.advance_width;
|
||||
println!("Cell width (from '-'): {:.2}px", cell_width);
|
||||
|
||||
let features = vec![
|
||||
Feature::new(Tag::from_bytes(b"liga"), 1, ..),
|
||||
Feature::new(Tag::from_bytes(b"calt"), 1, ..),
|
||||
Feature::new(Tag::from_bytes(b"dlig"), 1, ..),
|
||||
];
|
||||
|
||||
let test_strings = ["->", "=>", "==", "!=", ">=", "<="];
|
||||
|
||||
for s in &test_strings {
|
||||
// Shape combined string
|
||||
let mut buffer = UnicodeBuffer::new();
|
||||
buffer.push_str(s);
|
||||
let combined = rustybuzz::shape(&face, &features, buffer);
|
||||
let combined_infos = combined.glyph_infos();
|
||||
let combined_positions = combined.glyph_positions();
|
||||
|
||||
// Shape each character individually
|
||||
let mut individual_glyphs = Vec::new();
|
||||
for c in s.chars() {
|
||||
let mut buf = UnicodeBuffer::new();
|
||||
buf.push_str(&c.to_string());
|
||||
let shaped = rustybuzz::shape(&face, &features, buf);
|
||||
individual_glyphs.push(shaped.glyph_infos()[0].glyph_id);
|
||||
}
|
||||
|
||||
println!("\n'{}' analysis:", s);
|
||||
println!(" Combined glyphs: {:?}", combined_infos.iter().map(|i| i.glyph_id).collect::<Vec<_>>());
|
||||
println!(" Individual glyphs: {:?}", individual_glyphs);
|
||||
|
||||
// Show advances for each glyph
|
||||
for (i, (info, pos)) in combined_infos.iter().zip(combined_positions.iter()).enumerate() {
|
||||
let advance_px = pos.x_advance as f32 * font_size / units_per_em;
|
||||
println!(" Glyph {}: id={}, advance={} units ({:.2}px)", i, info.glyph_id, pos.x_advance, advance_px);
|
||||
|
||||
// Rasterize and show metrics
|
||||
let (metrics, _) = fontdue_font.rasterize_indexed(info.glyph_id as u16, font_size);
|
||||
println!(" Rasterized: {}x{} px, xmin={}, ymin={}, advance_width={:.2}",
|
||||
metrics.width, metrics.height, metrics.xmin, metrics.ymin, metrics.advance_width);
|
||||
}
|
||||
|
||||
// Check if any glyph was substituted
|
||||
let has_substitution = combined_infos.iter().zip(individual_glyphs.iter())
|
||||
.any(|(combined, &individual)| combined.glyph_id != individual);
|
||||
println!(" Has substitution: {}", has_substitution);
|
||||
}
|
||||
|
||||
// Also test what Kitty does - check glyph names
|
||||
println!("\n=== Checking glyph names via ttf-parser ===");
|
||||
let ttf_face = ttf_parser::Face::parse(&font_data, 0).unwrap();
|
||||
|
||||
// Shape "->" and get glyph names
|
||||
let mut buffer = UnicodeBuffer::new();
|
||||
buffer.push_str("->");
|
||||
let combined = rustybuzz::shape(&face, &features, buffer);
|
||||
for info in combined.glyph_infos() {
|
||||
let glyph_id = ttf_parser::GlyphId(info.glyph_id as u16);
|
||||
if let Some(name) = ttf_face.glyph_name(glyph_id) {
|
||||
println!(" Glyph {} name: {}", info.glyph_id, name);
|
||||
} else {
|
||||
println!(" Glyph {} has no name", info.glyph_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -362,6 +362,11 @@ pub struct Config {
|
||||
pub background_opacity: f32,
|
||||
/// Number of lines to keep in scrollback buffer.
|
||||
pub scrollback_lines: usize,
|
||||
/// Duration in milliseconds for the inactive pane fade animation.
|
||||
/// Set to 0 for instant transitions.
|
||||
pub inactive_pane_fade_ms: u64,
|
||||
/// Dim factor for inactive panes (0.0 = fully dimmed/black, 1.0 = no dimming).
|
||||
pub inactive_pane_dim: f32,
|
||||
/// Keybindings.
|
||||
pub keybindings: Keybindings,
|
||||
}
|
||||
@@ -373,6 +378,8 @@ impl Default for Config {
|
||||
tab_bar_position: TabBarPosition::Top,
|
||||
background_opacity: 1.0,
|
||||
scrollback_lines: 50_000,
|
||||
inactive_pane_fade_ms: 150,
|
||||
inactive_pane_dim: 0.6,
|
||||
keybindings: Keybindings::default(),
|
||||
}
|
||||
}
|
||||
|
||||
+110
-26
@@ -6,7 +6,7 @@
|
||||
use zterm::config::{Action, Config};
|
||||
use zterm::keyboard::{FunctionalKey, KeyEncoder, KeyEventType, KeyboardState, Modifiers};
|
||||
use zterm::pty::Pty;
|
||||
use zterm::renderer::Renderer;
|
||||
use zterm::renderer::{PaneRenderInfo, Renderer};
|
||||
use zterm::terminal::{Terminal, MouseTrackingMode};
|
||||
|
||||
use std::collections::HashMap;
|
||||
@@ -146,6 +146,10 @@ struct Pane {
|
||||
is_selecting: bool,
|
||||
/// Last scrollback length for tracking changes.
|
||||
last_scrollback_len: u32,
|
||||
/// When the focus animation started (for smooth fade).
|
||||
focus_animation_start: std::time::Instant,
|
||||
/// Whether this pane was focused before the current animation.
|
||||
was_focused: bool,
|
||||
}
|
||||
|
||||
impl Pane {
|
||||
@@ -170,6 +174,8 @@ impl Pane {
|
||||
selection: None,
|
||||
is_selecting: false,
|
||||
last_scrollback_len: 0,
|
||||
focus_animation_start: std::time::Instant::now(),
|
||||
was_focused: true, // New panes start as focused
|
||||
})
|
||||
}
|
||||
|
||||
@@ -192,6 +198,36 @@ impl Pane {
|
||||
fn child_exited(&self) -> bool {
|
||||
self.pty.child_exited()
|
||||
}
|
||||
|
||||
/// Calculate the current dim factor based on animation progress.
|
||||
/// Returns a value between `inactive_dim` (for unfocused) and 1.0 (for focused).
|
||||
fn calculate_dim_factor(&mut self, is_focused: bool, fade_duration_ms: u64, inactive_dim: f32) -> f32 {
|
||||
// Detect focus change
|
||||
if is_focused != self.was_focused {
|
||||
self.focus_animation_start = std::time::Instant::now();
|
||||
self.was_focused = is_focused;
|
||||
}
|
||||
|
||||
// If no animation (instant), return target value immediately
|
||||
if fade_duration_ms == 0 {
|
||||
return if is_focused { 1.0 } else { inactive_dim };
|
||||
}
|
||||
|
||||
let elapsed = self.focus_animation_start.elapsed().as_millis() as f32;
|
||||
let duration = fade_duration_ms as f32;
|
||||
let progress = (elapsed / duration).min(1.0);
|
||||
|
||||
// Smooth easing (ease-out cubic)
|
||||
let eased = 1.0 - (1.0 - progress).powi(3);
|
||||
|
||||
if is_focused {
|
||||
// Fading in: from inactive_dim to 1.0
|
||||
inactive_dim + (1.0 - inactive_dim) * eased
|
||||
} else {
|
||||
// Fading out: from 1.0 to inactive_dim
|
||||
1.0 - (1.0 - inactive_dim) * eased
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Geometry of a pane in pixels.
|
||||
@@ -308,17 +344,6 @@ impl SplitNode {
|
||||
}
|
||||
}
|
||||
|
||||
/// Collect all pane IDs.
|
||||
fn collect_pane_ids(&self, ids: &mut Vec<PaneId>) {
|
||||
match self {
|
||||
SplitNode::Leaf { pane_id, .. } => ids.push(*pane_id),
|
||||
SplitNode::Split { first, second, .. } => {
|
||||
first.collect_pane_ids(ids);
|
||||
second.collect_pane_ids(ids);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Collect all pane geometries.
|
||||
fn collect_geometries(&self, geometries: &mut Vec<(PaneId, PaneGeometry)>) {
|
||||
match self {
|
||||
@@ -507,6 +532,7 @@ impl TabId {
|
||||
/// A single tab containing one or more panes arranged in a split tree.
|
||||
struct Tab {
|
||||
/// Unique identifier for this tab.
|
||||
#[allow(dead_code)]
|
||||
id: TabId,
|
||||
/// All panes in this tab, keyed by PaneId.
|
||||
panes: HashMap<PaneId, Pane>,
|
||||
@@ -515,6 +541,7 @@ struct Tab {
|
||||
/// Currently active pane ID.
|
||||
active_pane: PaneId,
|
||||
/// Tab title (from OSC or shell).
|
||||
#[allow(dead_code)]
|
||||
title: String,
|
||||
}
|
||||
|
||||
@@ -637,11 +664,6 @@ impl Tab {
|
||||
}
|
||||
}
|
||||
|
||||
/// Get all pane IDs.
|
||||
fn pane_ids(&self) -> Vec<PaneId> {
|
||||
self.panes.keys().copied().collect()
|
||||
}
|
||||
|
||||
/// Get pane by ID.
|
||||
fn get_pane(&self, pane_id: PaneId) -> Option<&Pane> {
|
||||
self.panes.get(&pane_id)
|
||||
@@ -1191,6 +1213,8 @@ impl App {
|
||||
if let Some(renderer) = &self.renderer {
|
||||
let (cols, rows) = renderer.terminal_size();
|
||||
self.create_tab(cols, rows);
|
||||
// Resize the new tab to calculate pane geometries
|
||||
self.resize_all_panes();
|
||||
if let Some(window) = &self.window {
|
||||
window.request_redraw();
|
||||
}
|
||||
@@ -1751,22 +1775,83 @@ impl ApplicationHandler<UserEvent> for App {
|
||||
}
|
||||
}
|
||||
|
||||
// Render
|
||||
// Render all panes
|
||||
let render_start = std::time::Instant::now();
|
||||
let num_tabs = self.tabs.len();
|
||||
let active_tab_idx = self.active_tab;
|
||||
let fade_duration_ms = self.config.inactive_pane_fade_ms;
|
||||
let inactive_dim = self.config.inactive_pane_dim;
|
||||
|
||||
if let Some(renderer) = &mut self.renderer {
|
||||
if let Some(tab) = self.tabs.get(active_tab_idx) {
|
||||
if let Some(pane) = tab.active_pane() {
|
||||
if let Some(tab) = self.tabs.get_mut(active_tab_idx) {
|
||||
// Collect all pane geometries
|
||||
let geometries = tab.collect_pane_geometries();
|
||||
let active_pane_id = tab.active_pane;
|
||||
|
||||
// First pass: calculate dim factors (needs mutable access)
|
||||
let mut dim_factors: Vec<(PaneId, f32)> = Vec::new();
|
||||
for (pane_id, _) in &geometries {
|
||||
if let Some(pane) = tab.panes.get_mut(pane_id) {
|
||||
let is_active = *pane_id == active_pane_id;
|
||||
let dim_factor = pane.calculate_dim_factor(is_active, fade_duration_ms, inactive_dim);
|
||||
dim_factors.push((*pane_id, dim_factor));
|
||||
}
|
||||
}
|
||||
|
||||
// Build render info for all panes
|
||||
let mut pane_render_data: Vec<(&Terminal, PaneRenderInfo, Option<(usize, usize, usize, usize)>)> = Vec::new();
|
||||
|
||||
for (pane_id, geom) in &geometries {
|
||||
if let Some(pane) = tab.panes.get(pane_id) {
|
||||
let is_active = *pane_id == active_pane_id;
|
||||
let scroll_offset = pane.terminal.scroll_offset;
|
||||
let visible_rows = renderer.terminal_size().1;
|
||||
let renderer_selection = pane.selection.as_ref()
|
||||
.and_then(|sel| sel.to_screen_coords(scroll_offset, visible_rows));
|
||||
|
||||
renderer.set_selection(renderer_selection);
|
||||
// Get pre-calculated dim factor
|
||||
let dim_factor = dim_factors.iter()
|
||||
.find(|(id, _)| id == pane_id)
|
||||
.map(|(_, f)| *f)
|
||||
.unwrap_or(if is_active { 1.0 } else { inactive_dim });
|
||||
|
||||
match renderer.render_from_terminal(&pane.terminal, num_tabs, active_tab_idx) {
|
||||
// Convert selection to screen coords for this pane
|
||||
let selection = if is_active {
|
||||
pane.selection.as_ref()
|
||||
.and_then(|sel| sel.to_screen_coords(scroll_offset, geom.rows))
|
||||
} else {
|
||||
None
|
||||
};
|
||||
|
||||
let render_info = PaneRenderInfo {
|
||||
x: geom.x,
|
||||
y: geom.y,
|
||||
width: geom.width,
|
||||
height: geom.height,
|
||||
cols: geom.cols,
|
||||
rows: geom.rows,
|
||||
is_active,
|
||||
dim_factor,
|
||||
};
|
||||
|
||||
pane_render_data.push((&pane.terminal, render_info, selection));
|
||||
}
|
||||
}
|
||||
|
||||
// Request redraw if any animation is in progress
|
||||
let animation_in_progress = dim_factors.iter().any(|(id, factor)| {
|
||||
let is_active = *id == active_pane_id;
|
||||
if is_active {
|
||||
*factor < 1.0
|
||||
} else {
|
||||
*factor > inactive_dim
|
||||
}
|
||||
});
|
||||
|
||||
if animation_in_progress {
|
||||
if let Some(window) = &self.window {
|
||||
window.request_redraw();
|
||||
}
|
||||
}
|
||||
|
||||
match renderer.render_panes(&pane_render_data, num_tabs, active_tab_idx) {
|
||||
Ok(_) => {}
|
||||
Err(wgpu::SurfaceError::Lost) => {
|
||||
renderer.resize(renderer.width, renderer.height);
|
||||
@@ -1781,7 +1866,6 @@ impl ApplicationHandler<UserEvent> for App {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
let render_time = render_start.elapsed();
|
||||
let frame_time = frame_start.elapsed();
|
||||
|
||||
|
||||
+1508
-1503
File diff suppressed because it is too large
Load Diff
+39
-290
@@ -144,292 +144,6 @@ impl Default for ColorPalette {
|
||||
}
|
||||
}
|
||||
|
||||
/// Packed color value for GPU transfer (Kitty-style encoding).
|
||||
/// Layout: type in low 8 bits, RGB value in upper 24 bits.
|
||||
/// - Type 0: Default (use color table entries 256/257 for fg/bg)
|
||||
/// - Type 1: Indexed (index in bits 8-15, look up in color table)
|
||||
/// - Type 2: RGB (R in bits 8-15, G in bits 16-23, B in bits 24-31)
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
#[repr(transparent)]
|
||||
pub struct PackedColor(pub u32);
|
||||
|
||||
impl PackedColor {
|
||||
/// Color type: default (resolved from color table)
|
||||
pub const TYPE_DEFAULT: u8 = 0;
|
||||
/// Color type: indexed (look up in 256-color palette)
|
||||
pub const TYPE_INDEXED: u8 = 1;
|
||||
/// Color type: direct RGB
|
||||
pub const TYPE_RGB: u8 = 2;
|
||||
|
||||
/// Create a default color (resolved at render time from palette).
|
||||
#[inline]
|
||||
pub const fn default_color() -> Self {
|
||||
Self(Self::TYPE_DEFAULT as u32)
|
||||
}
|
||||
|
||||
/// Create an indexed color (0-255 palette index).
|
||||
#[inline]
|
||||
pub const fn indexed(index: u8) -> Self {
|
||||
Self(Self::TYPE_INDEXED as u32 | ((index as u32) << 8))
|
||||
}
|
||||
|
||||
/// Create a direct RGB color.
|
||||
#[inline]
|
||||
pub const fn rgb(r: u8, g: u8, b: u8) -> Self {
|
||||
Self(Self::TYPE_RGB as u32 | ((r as u32) << 8) | ((g as u32) << 16) | ((b as u32) << 24))
|
||||
}
|
||||
|
||||
/// Get the color type.
|
||||
#[inline]
|
||||
pub const fn color_type(self) -> u8 {
|
||||
(self.0 & 0xFF) as u8
|
||||
}
|
||||
|
||||
/// Get the index for indexed colors.
|
||||
#[inline]
|
||||
pub const fn index(self) -> u8 {
|
||||
((self.0 >> 8) & 0xFF) as u8
|
||||
}
|
||||
|
||||
/// Get RGB components for RGB colors.
|
||||
#[inline]
|
||||
pub const fn rgb_components(self) -> (u8, u8, u8) {
|
||||
(
|
||||
((self.0 >> 8) & 0xFF) as u8,
|
||||
((self.0 >> 16) & 0xFF) as u8,
|
||||
((self.0 >> 24) & 0xFF) as u8,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
impl From<Color> for PackedColor {
|
||||
fn from(color: Color) -> Self {
|
||||
match color {
|
||||
Color::Default => Self::default_color(),
|
||||
Color::Indexed(idx) => Self::indexed(idx),
|
||||
Color::Rgb(r, g, b) => Self::rgb(r, g, b),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<&Color> for PackedColor {
|
||||
fn from(color: &Color) -> Self {
|
||||
(*color).into()
|
||||
}
|
||||
}
|
||||
|
||||
/// Packed cell attributes for GPU transfer (Kitty-style).
|
||||
/// Layout (32-bit bitfield):
|
||||
/// - bits 0-2: decoration (underline style, 0=none, 1=single, 2=double, 3=curly, etc.)
|
||||
/// - bit 3: bold
|
||||
/// - bit 4: italic
|
||||
/// - bit 5: reverse
|
||||
/// - bit 6: strike
|
||||
/// - bit 7: dim
|
||||
/// - bits 8-31: reserved for future use
|
||||
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
|
||||
#[repr(transparent)]
|
||||
pub struct CellAttrs(pub u32);
|
||||
|
||||
impl CellAttrs {
|
||||
pub const DECORATION_MASK: u32 = 0b111;
|
||||
pub const BOLD_BIT: u32 = 1 << 3;
|
||||
pub const ITALIC_BIT: u32 = 1 << 4;
|
||||
pub const REVERSE_BIT: u32 = 1 << 5;
|
||||
pub const STRIKE_BIT: u32 = 1 << 6;
|
||||
pub const DIM_BIT: u32 = 1 << 7;
|
||||
|
||||
/// Decoration values
|
||||
pub const DECO_NONE: u32 = 0;
|
||||
pub const DECO_SINGLE: u32 = 1;
|
||||
pub const DECO_DOUBLE: u32 = 2;
|
||||
pub const DECO_CURLY: u32 = 3;
|
||||
pub const DECO_DOTTED: u32 = 4;
|
||||
pub const DECO_DASHED: u32 = 5;
|
||||
|
||||
#[inline]
|
||||
pub const fn new() -> Self {
|
||||
Self(0)
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub const fn with_underline(self, style: u32) -> Self {
|
||||
Self((self.0 & !Self::DECORATION_MASK) | (style & Self::DECORATION_MASK))
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub const fn with_bold(self, bold: bool) -> Self {
|
||||
if bold {
|
||||
Self(self.0 | Self::BOLD_BIT)
|
||||
} else {
|
||||
Self(self.0 & !Self::BOLD_BIT)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub const fn with_italic(self, italic: bool) -> Self {
|
||||
if italic {
|
||||
Self(self.0 | Self::ITALIC_BIT)
|
||||
} else {
|
||||
Self(self.0 & !Self::ITALIC_BIT)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub const fn with_reverse(self, reverse: bool) -> Self {
|
||||
if reverse {
|
||||
Self(self.0 | Self::REVERSE_BIT)
|
||||
} else {
|
||||
Self(self.0 & !Self::REVERSE_BIT)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub const fn with_strike(self, strike: bool) -> Self {
|
||||
if strike {
|
||||
Self(self.0 | Self::STRIKE_BIT)
|
||||
} else {
|
||||
Self(self.0 & !Self::STRIKE_BIT)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub const fn with_dim(self, dim: bool) -> Self {
|
||||
if dim {
|
||||
Self(self.0 | Self::DIM_BIT)
|
||||
} else {
|
||||
Self(self.0 & !Self::DIM_BIT)
|
||||
}
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub const fn decoration(self) -> u32 {
|
||||
self.0 & Self::DECORATION_MASK
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub const fn is_bold(self) -> bool {
|
||||
(self.0 & Self::BOLD_BIT) != 0
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub const fn is_italic(self) -> bool {
|
||||
(self.0 & Self::ITALIC_BIT) != 0
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub const fn is_reverse(self) -> bool {
|
||||
(self.0 & Self::REVERSE_BIT) != 0
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub const fn is_strike(self) -> bool {
|
||||
(self.0 & Self::STRIKE_BIT) != 0
|
||||
}
|
||||
|
||||
#[inline]
|
||||
pub const fn is_dim(self) -> bool {
|
||||
(self.0 & Self::DIM_BIT) != 0
|
||||
}
|
||||
}
|
||||
|
||||
/// GPU cell data for instanced rendering (Kitty-style).
|
||||
///
|
||||
/// This struct is uploaded directly to the GPU for each cell.
|
||||
/// The shader uses instanced rendering where each cell is one instance.
|
||||
///
|
||||
/// Layout: 20 bytes total
|
||||
/// - fg: 4 bytes (packed color)
|
||||
/// - bg: 4 bytes (packed color)
|
||||
/// - decoration_fg: 4 bytes (packed color for underline/strikethrough)
|
||||
/// - sprite_idx: 4 bytes (glyph atlas index, bit 31 = colored glyph flag)
|
||||
/// - attrs: 4 bytes (packed attributes)
|
||||
#[repr(C)]
|
||||
#[derive(Clone, Copy, Debug, Default, bytemuck::Pod, bytemuck::Zeroable)]
|
||||
pub struct GPUCell {
|
||||
/// Foreground color (packed)
|
||||
pub fg: u32,
|
||||
/// Background color (packed)
|
||||
pub bg: u32,
|
||||
/// Decoration color for underline/strikethrough (packed)
|
||||
pub decoration_fg: u32,
|
||||
/// Sprite index in glyph atlas (bit 31 = colored glyph flag)
|
||||
pub sprite_idx: u32,
|
||||
/// Packed attributes (bold, italic, underline style, etc.)
|
||||
pub attrs: u32,
|
||||
}
|
||||
|
||||
impl GPUCell {
|
||||
/// Flag indicating this glyph is colored (e.g., emoji) and should not be tinted
|
||||
pub const COLORED_GLYPH_FLAG: u32 = 1 << 31;
|
||||
/// Sprite index indicating no glyph (space/empty)
|
||||
pub const NO_GLYPH: u32 = 0;
|
||||
|
||||
/// Create an empty cell (space with default colors)
|
||||
#[inline]
|
||||
pub const fn empty() -> Self {
|
||||
Self {
|
||||
fg: PackedColor::TYPE_DEFAULT as u32,
|
||||
bg: PackedColor::TYPE_DEFAULT as u32,
|
||||
decoration_fg: PackedColor::TYPE_DEFAULT as u32,
|
||||
sprite_idx: Self::NO_GLYPH,
|
||||
attrs: 0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Create a GPUCell from terminal Cell and a sprite index
|
||||
#[inline]
|
||||
pub fn from_cell(cell: &Cell, sprite_idx: u32) -> Self {
|
||||
let fg = PackedColor::from(&cell.fg_color);
|
||||
let bg = PackedColor::from(&cell.bg_color);
|
||||
|
||||
let mut attrs = CellAttrs::new();
|
||||
if cell.bold {
|
||||
attrs = attrs.with_bold(true);
|
||||
}
|
||||
if cell.italic {
|
||||
attrs = attrs.with_italic(true);
|
||||
}
|
||||
if cell.underline {
|
||||
attrs = attrs.with_underline(CellAttrs::DECO_SINGLE);
|
||||
}
|
||||
|
||||
Self {
|
||||
fg: fg.0,
|
||||
bg: bg.0,
|
||||
decoration_fg: fg.0, // Use fg color for decoration by default
|
||||
sprite_idx,
|
||||
attrs: attrs.0,
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the sprite index
|
||||
#[inline]
|
||||
pub fn with_sprite(mut self, idx: u32) -> Self {
|
||||
self.sprite_idx = idx;
|
||||
self
|
||||
}
|
||||
|
||||
/// Mark this glyph as colored (emoji)
|
||||
#[inline]
|
||||
pub fn with_colored_glyph(mut self) -> Self {
|
||||
self.sprite_idx |= Self::COLORED_GLYPH_FLAG;
|
||||
self
|
||||
}
|
||||
|
||||
/// Get the sprite index (without the colored flag)
|
||||
#[inline]
|
||||
pub const fn get_sprite_idx(self) -> u32 {
|
||||
self.sprite_idx & !Self::COLORED_GLYPH_FLAG
|
||||
}
|
||||
|
||||
/// Check if this is a colored glyph
|
||||
#[inline]
|
||||
pub const fn is_colored_glyph(self) -> bool {
|
||||
(self.sprite_idx & Self::COLORED_GLYPH_FLAG) != 0
|
||||
}
|
||||
}
|
||||
|
||||
impl ColorPalette {
|
||||
/// Parse a color specification like "#RRGGBB" or "rgb:RR/GG/BB".
|
||||
pub fn parse_color_spec(spec: &str) -> Option<[u8; 3]> {
|
||||
@@ -886,7 +600,10 @@ impl Terminal {
|
||||
#[inline]
|
||||
fn clear_grid_row(&mut self, grid_row: usize) {
|
||||
let blank = self.blank_cell();
|
||||
self.grid[grid_row].fill(blank);
|
||||
let row = &mut self.grid[grid_row];
|
||||
// Ensure row has correct width (may differ after swap with scrollback post-resize)
|
||||
row.resize(self.cols, blank.clone());
|
||||
row.fill(blank);
|
||||
}
|
||||
|
||||
/// Create a blank cell with the current background color (BCE - Background Color Erase).
|
||||
@@ -930,13 +647,18 @@ impl Terminal {
|
||||
return;
|
||||
}
|
||||
|
||||
let old_cols = self.cols;
|
||||
let old_rows = self.rows;
|
||||
|
||||
// Create new grid
|
||||
let mut new_grid = vec![vec![Cell::default(); cols]; rows];
|
||||
|
||||
// Copy existing content using line_map for correct visual ordering
|
||||
for visual_row in 0..rows.min(self.rows) {
|
||||
let old_grid_row = self.line_map[visual_row];
|
||||
for col in 0..cols.min(self.cols) {
|
||||
// Use actual row length - may differ from self.cols after scrollback swap
|
||||
let old_row_len = self.grid[old_grid_row].len();
|
||||
for col in 0..cols.min(old_row_len) {
|
||||
new_grid[visual_row][col] = self.grid[old_grid_row][col].clone();
|
||||
}
|
||||
}
|
||||
@@ -954,6 +676,28 @@ impl Terminal {
|
||||
// Adjust cursor position
|
||||
self.cursor_col = self.cursor_col.min(cols.saturating_sub(1));
|
||||
self.cursor_row = self.cursor_row.min(rows.saturating_sub(1));
|
||||
|
||||
// Also resize the saved alternate screen if it exists
|
||||
if let Some(ref mut saved) = self.alternate_screen {
|
||||
let mut new_saved_grid = vec![vec![Cell::default(); cols]; rows];
|
||||
for visual_row in 0..rows.min(old_rows) {
|
||||
let old_grid_row = saved.line_map.get(visual_row).copied().unwrap_or(visual_row);
|
||||
if old_grid_row < saved.grid.len() {
|
||||
for col in 0..cols.min(old_cols) {
|
||||
if col < saved.grid[old_grid_row].len() {
|
||||
new_saved_grid[visual_row][col] = saved.grid[old_grid_row][col].clone();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
saved.grid = new_saved_grid;
|
||||
saved.line_map = (0..rows).collect();
|
||||
saved.cursor_col = saved.cursor_col.min(cols.saturating_sub(1));
|
||||
saved.cursor_row = saved.cursor_row.min(rows.saturating_sub(1));
|
||||
saved.scroll_top = 0;
|
||||
saved.scroll_bottom = rows.saturating_sub(1);
|
||||
}
|
||||
|
||||
self.dirty = true;
|
||||
self.mark_all_lines_dirty();
|
||||
}
|
||||
@@ -1004,11 +748,12 @@ impl Terminal {
|
||||
if let Some(saved) = self.alternate_screen.take() {
|
||||
self.grid = saved.grid;
|
||||
self.line_map = saved.line_map;
|
||||
self.cursor_col = saved.cursor_col;
|
||||
self.cursor_row = saved.cursor_row;
|
||||
self.saved_cursor = saved.saved_cursor;
|
||||
self.scroll_top = saved.scroll_top;
|
||||
self.scroll_bottom = saved.scroll_bottom;
|
||||
// Clamp cursor positions to current grid dimensions (defensive)
|
||||
self.cursor_col = saved.cursor_col.min(self.cols.saturating_sub(1));
|
||||
self.cursor_row = saved.cursor_row.min(self.rows.saturating_sub(1));
|
||||
}
|
||||
|
||||
self.using_alternate_screen = false;
|
||||
@@ -1486,6 +1231,10 @@ impl Handler for Terminal {
|
||||
}
|
||||
|
||||
// Write character directly using cached grid_row
|
||||
// Safety: ensure grid row has correct width (may differ after scrollback swap)
|
||||
if self.grid[grid_row].len() != self.cols {
|
||||
self.grid[grid_row].resize(self.cols, Cell::default());
|
||||
}
|
||||
self.grid[grid_row][self.cursor_col] = Cell {
|
||||
character: c,
|
||||
fg_color: self.current_fg,
|
||||
|
||||
Reference in New Issue
Block a user