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"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "d9c4f5dac5e15c24eb999c26181a6ca40b39fe946cbe4c263c7209467bc83af2"
|
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]]
|
[[package]]
|
||||||
name = "fontdue"
|
name = "fontdue"
|
||||||
version = "0.9.3"
|
version = "0.9.3"
|
||||||
@@ -2580,6 +2589,17 @@ version = "0.8.28"
|
|||||||
source = "registry+https://github.com/rust-lang/crates.io-index"
|
source = "registry+https://github.com/rust-lang/crates.io-index"
|
||||||
checksum = "3ae8337f8a065cfc972643663ea4279e04e7256de865aa66fe25cec5fb912d3f"
|
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]]
|
[[package]]
|
||||||
name = "zerocopy"
|
name = "zerocopy"
|
||||||
version = "0.8.31"
|
version = "0.8.31"
|
||||||
@@ -2609,6 +2629,7 @@ dependencies = [
|
|||||||
"bytemuck",
|
"bytemuck",
|
||||||
"dirs",
|
"dirs",
|
||||||
"env_logger",
|
"env_logger",
|
||||||
|
"fontconfig",
|
||||||
"fontdue",
|
"fontdue",
|
||||||
"libc",
|
"libc",
|
||||||
"log",
|
"log",
|
||||||
@@ -2624,4 +2645,5 @@ dependencies = [
|
|||||||
"ttf-parser 0.25.1",
|
"ttf-parser 0.25.1",
|
||||||
"wgpu",
|
"wgpu",
|
||||||
"winit",
|
"winit",
|
||||||
|
"yeslogic-fontconfig-sys",
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -39,6 +39,8 @@ bitflags = "2"
|
|||||||
fontdue = "0.9"
|
fontdue = "0.9"
|
||||||
rustybuzz = "0.20"
|
rustybuzz = "0.20"
|
||||||
ttf-parser = "0.25"
|
ttf-parser = "0.25"
|
||||||
|
fontconfig = "0.10"
|
||||||
|
fontconfig-sys = { package = "yeslogic-fontconfig-sys", version = "6.0" }
|
||||||
|
|
||||||
# Configuration
|
# Configuration
|
||||||
serde = { version = "1", features = ["derive"] }
|
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,
|
pub background_opacity: f32,
|
||||||
/// Number of lines to keep in scrollback buffer.
|
/// Number of lines to keep in scrollback buffer.
|
||||||
pub scrollback_lines: usize,
|
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.
|
/// Keybindings.
|
||||||
pub keybindings: Keybindings,
|
pub keybindings: Keybindings,
|
||||||
}
|
}
|
||||||
@@ -373,6 +378,8 @@ impl Default for Config {
|
|||||||
tab_bar_position: TabBarPosition::Top,
|
tab_bar_position: TabBarPosition::Top,
|
||||||
background_opacity: 1.0,
|
background_opacity: 1.0,
|
||||||
scrollback_lines: 50_000,
|
scrollback_lines: 50_000,
|
||||||
|
inactive_pane_fade_ms: 150,
|
||||||
|
inactive_pane_dim: 0.6,
|
||||||
keybindings: Keybindings::default(),
|
keybindings: Keybindings::default(),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
+110
-26
@@ -6,7 +6,7 @@
|
|||||||
use zterm::config::{Action, Config};
|
use zterm::config::{Action, Config};
|
||||||
use zterm::keyboard::{FunctionalKey, KeyEncoder, KeyEventType, KeyboardState, Modifiers};
|
use zterm::keyboard::{FunctionalKey, KeyEncoder, KeyEventType, KeyboardState, Modifiers};
|
||||||
use zterm::pty::Pty;
|
use zterm::pty::Pty;
|
||||||
use zterm::renderer::Renderer;
|
use zterm::renderer::{PaneRenderInfo, Renderer};
|
||||||
use zterm::terminal::{Terminal, MouseTrackingMode};
|
use zterm::terminal::{Terminal, MouseTrackingMode};
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
@@ -146,6 +146,10 @@ struct Pane {
|
|||||||
is_selecting: bool,
|
is_selecting: bool,
|
||||||
/// Last scrollback length for tracking changes.
|
/// Last scrollback length for tracking changes.
|
||||||
last_scrollback_len: u32,
|
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 {
|
impl Pane {
|
||||||
@@ -170,6 +174,8 @@ impl Pane {
|
|||||||
selection: None,
|
selection: None,
|
||||||
is_selecting: false,
|
is_selecting: false,
|
||||||
last_scrollback_len: 0,
|
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 {
|
fn child_exited(&self) -> bool {
|
||||||
self.pty.child_exited()
|
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.
|
/// 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.
|
/// Collect all pane geometries.
|
||||||
fn collect_geometries(&self, geometries: &mut Vec<(PaneId, PaneGeometry)>) {
|
fn collect_geometries(&self, geometries: &mut Vec<(PaneId, PaneGeometry)>) {
|
||||||
match self {
|
match self {
|
||||||
@@ -507,6 +532,7 @@ impl TabId {
|
|||||||
/// A single tab containing one or more panes arranged in a split tree.
|
/// A single tab containing one or more panes arranged in a split tree.
|
||||||
struct Tab {
|
struct Tab {
|
||||||
/// Unique identifier for this tab.
|
/// Unique identifier for this tab.
|
||||||
|
#[allow(dead_code)]
|
||||||
id: TabId,
|
id: TabId,
|
||||||
/// All panes in this tab, keyed by PaneId.
|
/// All panes in this tab, keyed by PaneId.
|
||||||
panes: HashMap<PaneId, Pane>,
|
panes: HashMap<PaneId, Pane>,
|
||||||
@@ -515,6 +541,7 @@ struct Tab {
|
|||||||
/// Currently active pane ID.
|
/// Currently active pane ID.
|
||||||
active_pane: PaneId,
|
active_pane: PaneId,
|
||||||
/// Tab title (from OSC or shell).
|
/// Tab title (from OSC or shell).
|
||||||
|
#[allow(dead_code)]
|
||||||
title: String,
|
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.
|
/// Get pane by ID.
|
||||||
fn get_pane(&self, pane_id: PaneId) -> Option<&Pane> {
|
fn get_pane(&self, pane_id: PaneId) -> Option<&Pane> {
|
||||||
self.panes.get(&pane_id)
|
self.panes.get(&pane_id)
|
||||||
@@ -1191,6 +1213,8 @@ impl App {
|
|||||||
if let Some(renderer) = &self.renderer {
|
if let Some(renderer) = &self.renderer {
|
||||||
let (cols, rows) = renderer.terminal_size();
|
let (cols, rows) = renderer.terminal_size();
|
||||||
self.create_tab(cols, rows);
|
self.create_tab(cols, rows);
|
||||||
|
// Resize the new tab to calculate pane geometries
|
||||||
|
self.resize_all_panes();
|
||||||
if let Some(window) = &self.window {
|
if let Some(window) = &self.window {
|
||||||
window.request_redraw();
|
window.request_redraw();
|
||||||
}
|
}
|
||||||
@@ -1751,22 +1775,83 @@ impl ApplicationHandler<UserEvent> for App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Render
|
// Render all panes
|
||||||
let render_start = std::time::Instant::now();
|
let render_start = std::time::Instant::now();
|
||||||
let num_tabs = self.tabs.len();
|
let num_tabs = self.tabs.len();
|
||||||
let active_tab_idx = self.active_tab;
|
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(renderer) = &mut self.renderer {
|
||||||
if let Some(tab) = self.tabs.get(active_tab_idx) {
|
if let Some(tab) = self.tabs.get_mut(active_tab_idx) {
|
||||||
if let Some(pane) = tab.active_pane() {
|
// 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 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(_) => {}
|
Ok(_) => {}
|
||||||
Err(wgpu::SurfaceError::Lost) => {
|
Err(wgpu::SurfaceError::Lost) => {
|
||||||
renderer.resize(renderer.width, renderer.height);
|
renderer.resize(renderer.width, renderer.height);
|
||||||
@@ -1781,7 +1866,6 @@ impl ApplicationHandler<UserEvent> for App {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
|
||||||
let render_time = render_start.elapsed();
|
let render_time = render_start.elapsed();
|
||||||
let frame_time = frame_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 {
|
impl ColorPalette {
|
||||||
/// Parse a color specification like "#RRGGBB" or "rgb:RR/GG/BB".
|
/// Parse a color specification like "#RRGGBB" or "rgb:RR/GG/BB".
|
||||||
pub fn parse_color_spec(spec: &str) -> Option<[u8; 3]> {
|
pub fn parse_color_spec(spec: &str) -> Option<[u8; 3]> {
|
||||||
@@ -886,7 +600,10 @@ impl Terminal {
|
|||||||
#[inline]
|
#[inline]
|
||||||
fn clear_grid_row(&mut self, grid_row: usize) {
|
fn clear_grid_row(&mut self, grid_row: usize) {
|
||||||
let blank = self.blank_cell();
|
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).
|
/// Create a blank cell with the current background color (BCE - Background Color Erase).
|
||||||
@@ -930,13 +647,18 @@ impl Terminal {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let old_cols = self.cols;
|
||||||
|
let old_rows = self.rows;
|
||||||
|
|
||||||
// Create new grid
|
// Create new grid
|
||||||
let mut new_grid = vec![vec![Cell::default(); cols]; rows];
|
let mut new_grid = vec![vec![Cell::default(); cols]; rows];
|
||||||
|
|
||||||
// Copy existing content using line_map for correct visual ordering
|
// Copy existing content using line_map for correct visual ordering
|
||||||
for visual_row in 0..rows.min(self.rows) {
|
for visual_row in 0..rows.min(self.rows) {
|
||||||
let old_grid_row = self.line_map[visual_row];
|
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();
|
new_grid[visual_row][col] = self.grid[old_grid_row][col].clone();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -954,6 +676,28 @@ impl Terminal {
|
|||||||
// Adjust cursor position
|
// Adjust cursor position
|
||||||
self.cursor_col = self.cursor_col.min(cols.saturating_sub(1));
|
self.cursor_col = self.cursor_col.min(cols.saturating_sub(1));
|
||||||
self.cursor_row = self.cursor_row.min(rows.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.dirty = true;
|
||||||
self.mark_all_lines_dirty();
|
self.mark_all_lines_dirty();
|
||||||
}
|
}
|
||||||
@@ -1004,11 +748,12 @@ impl Terminal {
|
|||||||
if let Some(saved) = self.alternate_screen.take() {
|
if let Some(saved) = self.alternate_screen.take() {
|
||||||
self.grid = saved.grid;
|
self.grid = saved.grid;
|
||||||
self.line_map = saved.line_map;
|
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.saved_cursor = saved.saved_cursor;
|
||||||
self.scroll_top = saved.scroll_top;
|
self.scroll_top = saved.scroll_top;
|
||||||
self.scroll_bottom = saved.scroll_bottom;
|
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;
|
self.using_alternate_screen = false;
|
||||||
@@ -1486,6 +1231,10 @@ impl Handler for Terminal {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Write character directly using cached grid_row
|
// 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 {
|
self.grid[grid_row][self.cursor_col] = Cell {
|
||||||
character: c,
|
character: c,
|
||||||
fg_color: self.current_fg,
|
fg_color: self.current_fg,
|
||||||
|
|||||||
Reference in New Issue
Block a user