fix: image appearing over alternate screen
This commit is contained in:
+14
-1
@@ -4,6 +4,19 @@
|
|||||||
//! All structures use `#[repr(C)]` and implement `bytemuck::Pod` for GPU compatibility.
|
//! All structures use `#[repr(C)]` and implement `bytemuck::Pod` for GPU compatibility.
|
||||||
|
|
||||||
use bytemuck::{Pod, Zeroable};
|
use bytemuck::{Pod, Zeroable};
|
||||||
|
use std::sync::atomic::{AtomicU64, Ordering};
|
||||||
|
|
||||||
|
/// Unique identifier for a pane.
|
||||||
|
#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash)]
|
||||||
|
pub struct PaneId(pub u64);
|
||||||
|
|
||||||
|
impl PaneId {
|
||||||
|
/// Generate a new unique pane ID.
|
||||||
|
pub fn new() -> Self {
|
||||||
|
static COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0);
|
||||||
|
Self(COUNTER.fetch_add(1, std::sync::atomic::Ordering::SeqCst))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
// CONSTANTS
|
// CONSTANTS
|
||||||
@@ -148,8 +161,8 @@ pub struct ImageUniforms {
|
|||||||
pub src_y: f32,
|
pub src_y: f32,
|
||||||
pub src_width: f32,
|
pub src_width: f32,
|
||||||
pub src_height: f32,
|
pub src_height: f32,
|
||||||
|
pub dim_factor: f32,
|
||||||
pub _padding1: f32,
|
pub _padding1: f32,
|
||||||
pub _padding2: f32,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|||||||
+35
-3
@@ -137,10 +137,18 @@ pub struct GraphicsCommand {
|
|||||||
pub delete_target: DeleteTarget,
|
pub delete_target: DeleteTarget,
|
||||||
/// Unicode placeholder (virtual placement).
|
/// Unicode placeholder (virtual placement).
|
||||||
pub unicode_placeholder: bool,
|
pub unicode_placeholder: bool,
|
||||||
|
/// Parent image ID (for relative placement).
|
||||||
|
pub parent_image_id: Option<u32>,
|
||||||
|
/// Parent placement ID (for relative placement).
|
||||||
|
pub parent_placement_id: Option<u32>,
|
||||||
|
/// Horizontal cell displacement from parent.
|
||||||
|
pub h_offset: i32,
|
||||||
|
/// Vertical cell displacement from parent.
|
||||||
|
pub v_offset: i32,
|
||||||
/// Parent image ID (for animation frames).
|
/// Parent image ID (for animation frames).
|
||||||
pub parent_id: Option<u32>,
|
pub parent_id: Option<u32>,
|
||||||
/// Parent placement ID (for animation frames).
|
/// Parent placement ID (for animation frames).
|
||||||
pub parent_placement_id: Option<u32>,
|
pub parent_placement_id_anim: Option<u32>,
|
||||||
/// Frame number (for animation).
|
/// Frame number (for animation).
|
||||||
pub frame_number: Option<u32>,
|
pub frame_number: Option<u32>,
|
||||||
/// Frame gap in milliseconds (z key for animation frames).
|
/// Frame gap in milliseconds (z key for animation frames).
|
||||||
@@ -256,6 +264,17 @@ impl GraphicsCommand {
|
|||||||
cmd.height = value.parse().ok();
|
cmd.height = value.parse().ok();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
"P" => {
|
||||||
|
if is_animation {
|
||||||
|
// P = parent_frame_index for animation
|
||||||
|
cmd.base_frame = value.parse().ok();
|
||||||
|
} else {
|
||||||
|
cmd.parent_image_id = value.parse().ok();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
"Q" => cmd.parent_placement_id = value.parse().ok(),
|
||||||
|
"H" => cmd.h_offset = value.parse().unwrap_or(0),
|
||||||
|
"V" => cmd.v_offset = value.parse().unwrap_or(0),
|
||||||
"x" => cmd.src_x = value.parse().unwrap_or(0),
|
"x" => cmd.src_x = value.parse().unwrap_or(0),
|
||||||
"y" => cmd.src_y = value.parse().unwrap_or(0),
|
"y" => cmd.src_y = value.parse().unwrap_or(0),
|
||||||
"w" => cmd.src_width = value.parse().unwrap_or(0),
|
"w" => cmd.src_width = value.parse().unwrap_or(0),
|
||||||
@@ -1611,6 +1630,19 @@ impl ImageStorage {
|
|||||||
cmd.rows as usize
|
cmd.rows as usize
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// Handle relative positioning
|
||||||
|
let (final_col, final_row) = if let (Some(p_id), Some(q_id)) = (cmd.parent_image_id, cmd.parent_placement_id) {
|
||||||
|
if let Some(parent) = self.placements.iter().find(|p| p.image_id == p_id && p.placement_id == q_id) {
|
||||||
|
let col = parent.col as i32 + cmd.h_offset;
|
||||||
|
let row = parent.row as i32 + cmd.v_offset;
|
||||||
|
(col.max(0) as usize, row.max(0) as usize)
|
||||||
|
} else {
|
||||||
|
(cursor_col, cursor_row)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
(cursor_col, cursor_row)
|
||||||
|
};
|
||||||
|
|
||||||
// Don't create actual placement for virtual placements (U=1)
|
// Don't create actual placement for virtual placements (U=1)
|
||||||
// Virtual placements are referenced by Unicode placeholders
|
// Virtual placements are referenced by Unicode placeholders
|
||||||
if cmd.unicode_placeholder {
|
if cmd.unicode_placeholder {
|
||||||
@@ -1626,8 +1658,8 @@ impl ImageStorage {
|
|||||||
let placement = ImagePlacement {
|
let placement = ImagePlacement {
|
||||||
image_id: id,
|
image_id: id,
|
||||||
placement_id: cmd.placement_id.unwrap_or(0),
|
placement_id: cmd.placement_id.unwrap_or(0),
|
||||||
col: cursor_col,
|
col: final_col,
|
||||||
row: cursor_row,
|
row: final_row,
|
||||||
cols,
|
cols,
|
||||||
rows,
|
rows,
|
||||||
z_index: cmd.z_index,
|
z_index: cmd.z_index,
|
||||||
|
|||||||
+121
-67
@@ -4,7 +4,7 @@
|
|||||||
//! supporting the Kitty Graphics Protocol for inline image display.
|
//! supporting the Kitty Graphics Protocol for inline image display.
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use crate::gpu_types::ImageUniforms;
|
use crate::gpu_types::{ImageUniforms, PaneId};
|
||||||
use crate::graphics::{ImageData, ImagePlacement, ImageStorage};
|
use crate::graphics::{ImageData, ImagePlacement, ImageStorage};
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
@@ -15,7 +15,6 @@ use crate::graphics::{ImageData, ImagePlacement, ImageStorage};
|
|||||||
pub struct GpuImage {
|
pub struct GpuImage {
|
||||||
pub texture: wgpu::Texture,
|
pub texture: wgpu::Texture,
|
||||||
pub view: wgpu::TextureView,
|
pub view: wgpu::TextureView,
|
||||||
pub uniform_buffer: wgpu::Buffer,
|
|
||||||
pub bind_group: wgpu::BindGroup,
|
pub bind_group: wgpu::BindGroup,
|
||||||
pub width: u32,
|
pub width: u32,
|
||||||
pub height: u32,
|
pub height: u32,
|
||||||
@@ -28,14 +27,23 @@ pub struct GpuImage {
|
|||||||
/// Manages GPU resources for image rendering.
|
/// Manages GPU resources for image rendering.
|
||||||
/// Handles uploading, caching, and preparing images for rendering.
|
/// Handles uploading, caching, and preparing images for rendering.
|
||||||
pub struct ImageRenderer {
|
pub struct ImageRenderer {
|
||||||
/// Bind group layout for image rendering.
|
/// Bind group layout for uniforms.
|
||||||
bind_group_layout: wgpu::BindGroupLayout,
|
uniform_layout: wgpu::BindGroupLayout,
|
||||||
|
/// Bind group layout for textures.
|
||||||
|
texture_layout: wgpu::BindGroupLayout,
|
||||||
/// Sampler for image textures.
|
/// Sampler for image textures.
|
||||||
sampler: wgpu::Sampler,
|
sampler: wgpu::Sampler,
|
||||||
/// Cached GPU textures for images, keyed by image ID.
|
/// Cached GPU textures for images, keyed by (pane_id, image_id).
|
||||||
textures: HashMap<u32, GpuImage>,
|
textures: HashMap<(PaneId, u32), GpuImage>,
|
||||||
|
/// Global uniform buffer for image renders.
|
||||||
|
pub uniform_buffer: wgpu::Buffer,
|
||||||
|
/// Bind group for image uniforms.
|
||||||
|
uniform_bind_group: wgpu::BindGroup,
|
||||||
|
/// Minimum offset alignment for uniform buffers.
|
||||||
|
pub alignment: u64,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
impl ImageRenderer {
|
impl ImageRenderer {
|
||||||
/// Create a new ImageRenderer with the necessary GPU resources.
|
/// Create a new ImageRenderer with the necessary GPU resources.
|
||||||
pub fn new(device: &wgpu::Device) -> Self {
|
pub fn new(device: &wgpu::Device) -> Self {
|
||||||
@@ -51,20 +59,25 @@ impl ImageRenderer {
|
|||||||
..Default::default()
|
..Default::default()
|
||||||
});
|
});
|
||||||
|
|
||||||
// Create bind group layout for images
|
// Create bind group layout for uniforms (binding 0)
|
||||||
let bind_group_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
let uniform_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||||
label: Some("Image Bind Group Layout"),
|
label: Some("Image Uniform Layout"),
|
||||||
entries: &[
|
entries: &[wgpu::BindGroupLayoutEntry {
|
||||||
wgpu::BindGroupLayoutEntry {
|
binding: 0,
|
||||||
binding: 0,
|
visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
|
||||||
visibility: wgpu::ShaderStages::VERTEX | wgpu::ShaderStages::FRAGMENT,
|
ty: wgpu::BindingType::Buffer {
|
||||||
ty: wgpu::BindingType::Buffer {
|
ty: wgpu::BufferBindingType::Uniform,
|
||||||
ty: wgpu::BufferBindingType::Uniform,
|
has_dynamic_offset: true,
|
||||||
has_dynamic_offset: false,
|
min_binding_size: None,
|
||||||
min_binding_size: None,
|
|
||||||
},
|
|
||||||
count: None,
|
|
||||||
},
|
},
|
||||||
|
count: None,
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create bind group layout for textures (binding 1, 2)
|
||||||
|
let texture_layout = device.create_bind_group_layout(&wgpu::BindGroupLayoutDescriptor {
|
||||||
|
label: Some("Image Texture Layout"),
|
||||||
|
entries: &[
|
||||||
wgpu::BindGroupLayoutEntry {
|
wgpu::BindGroupLayoutEntry {
|
||||||
binding: 1,
|
binding: 1,
|
||||||
visibility: wgpu::ShaderStages::FRAGMENT,
|
visibility: wgpu::ShaderStages::FRAGMENT,
|
||||||
@@ -84,31 +97,71 @@ impl ImageRenderer {
|
|||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
|
// Create a large uniform buffer for all image renders in a frame
|
||||||
|
// Max 256 images per frame (65536 / 256)
|
||||||
|
let buffer_size = 65536;
|
||||||
|
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
||||||
|
label: Some("Image Uniform Buffer"),
|
||||||
|
size: buffer_size,
|
||||||
|
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
||||||
|
mapped_at_creation: false,
|
||||||
|
});
|
||||||
|
|
||||||
|
// Create the uniform bind group
|
||||||
|
let uniform_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||||
|
label: Some("Image Uniform Bind Group"),
|
||||||
|
layout: &uniform_layout,
|
||||||
|
entries: &[wgpu::BindGroupEntry {
|
||||||
|
binding: 0,
|
||||||
|
resource: wgpu::BindingResource::Buffer(wgpu::BufferBinding {
|
||||||
|
buffer: &uniform_buffer,
|
||||||
|
offset: 0,
|
||||||
|
size: std::num::NonZeroU64::new(std::mem::size_of::<ImageUniforms>() as u64),
|
||||||
|
}),
|
||||||
|
}],
|
||||||
|
});
|
||||||
|
|
||||||
|
let alignment = device.limits().min_uniform_buffer_offset_alignment as u64;
|
||||||
|
|
||||||
Self {
|
Self {
|
||||||
bind_group_layout,
|
uniform_layout,
|
||||||
|
texture_layout,
|
||||||
sampler,
|
sampler,
|
||||||
textures: HashMap::new(),
|
textures: HashMap::new(),
|
||||||
|
uniform_buffer,
|
||||||
|
uniform_bind_group,
|
||||||
|
alignment,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get the bind group layout for creating the image pipeline.
|
/// Get the uniform bind group layout.
|
||||||
pub fn bind_group_layout(&self) -> &wgpu::BindGroupLayout {
|
pub fn uniform_layout(&self) -> &wgpu::BindGroupLayout {
|
||||||
&self.bind_group_layout
|
&self.uniform_layout
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the texture bind group layout.
|
||||||
|
pub fn texture_layout(&self) -> &wgpu::BindGroupLayout {
|
||||||
|
&self.texture_layout
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Get the uniform bind group.
|
||||||
|
pub fn uniform_bind_group(&self) -> &wgpu::BindGroup {
|
||||||
|
&self.uniform_bind_group
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Get a GPU image by ID.
|
/// Get a GPU image by ID.
|
||||||
pub fn get(&self, image_id: &u32) -> Option<&GpuImage> {
|
pub fn get(&self, pane_id: PaneId, image_id: &u32) -> Option<&GpuImage> {
|
||||||
self.textures.get(image_id)
|
self.textures.get(&(pane_id, *image_id))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Upload an image to the GPU, creating or updating its texture.
|
/// Upload an image to the GPU, creating or updating its texture.
|
||||||
pub fn upload_image(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, image: &ImageData) {
|
pub fn upload_image(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, pane_id: PaneId, image: &ImageData) {
|
||||||
log::debug!("upload_image: id={}, width={}, height={}, data_len={}", image.id, image.width, image.height, image.data.len());
|
log::debug!("upload_image: pane_id={:?}, id={}, width={}, height={}, data_len={}", pane_id, image.id, image.width, image.height, image.data.len());
|
||||||
// Get current frame data (handles animation frames automatically)
|
// Get current frame data (handles animation frames automatically)
|
||||||
let data = image.current_frame_data();
|
let data = image.current_frame_data();
|
||||||
|
|
||||||
// Check if we already have this image
|
// Check if we already have this image
|
||||||
if let Some(existing) = self.textures.get(&image.id) {
|
if let Some(existing) = self.textures.get(&(pane_id, image.id)) {
|
||||||
if existing.width == image.width && existing.height == image.height {
|
if existing.width == image.width && existing.height == image.height {
|
||||||
// Same dimensions, just update the data
|
// Same dimensions, just update the data
|
||||||
queue.write_texture(
|
queue.write_texture(
|
||||||
@@ -137,7 +190,7 @@ impl ImageRenderer {
|
|||||||
|
|
||||||
// Create new texture
|
// Create new texture
|
||||||
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
let texture = device.create_texture(&wgpu::TextureDescriptor {
|
||||||
label: Some(&format!("Image {}", image.id)),
|
label: Some(&format!("Image {} (pane {:?})", image.id, pane_id)),
|
||||||
size: wgpu::Extent3d {
|
size: wgpu::Extent3d {
|
||||||
width: image.width,
|
width: image.width,
|
||||||
height: image.height,
|
height: image.height,
|
||||||
@@ -174,23 +227,10 @@ impl ImageRenderer {
|
|||||||
|
|
||||||
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
|
||||||
|
|
||||||
// Create per-image uniform buffer
|
|
||||||
let uniform_buffer = device.create_buffer(&wgpu::BufferDescriptor {
|
|
||||||
label: Some(&format!("Image {} Uniform Buffer", image.id)),
|
|
||||||
size: std::mem::size_of::<ImageUniforms>() as u64,
|
|
||||||
usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
|
|
||||||
mapped_at_creation: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
// Create bind group for this image with its own uniform buffer
|
|
||||||
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
let bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
|
||||||
label: Some(&format!("Image {} Bind Group", image.id)),
|
label: Some(&format!("Image {} (pane {:?}) Bind Group", image.id, pane_id)),
|
||||||
layout: &self.bind_group_layout,
|
layout: &self.texture_layout,
|
||||||
entries: &[
|
entries: &[
|
||||||
wgpu::BindGroupEntry {
|
|
||||||
binding: 0,
|
|
||||||
resource: uniform_buffer.as_entire_binding(),
|
|
||||||
},
|
|
||||||
wgpu::BindGroupEntry {
|
wgpu::BindGroupEntry {
|
||||||
binding: 1,
|
binding: 1,
|
||||||
resource: wgpu::BindingResource::TextureView(&view),
|
resource: wgpu::BindingResource::TextureView(&view),
|
||||||
@@ -201,16 +241,16 @@ impl ImageRenderer {
|
|||||||
},
|
},
|
||||||
],
|
],
|
||||||
});
|
});
|
||||||
|
|
||||||
self.textures.insert(image.id, GpuImage {
|
self.textures.insert((pane_id, image.id), GpuImage {
|
||||||
texture,
|
texture,
|
||||||
view,
|
view,
|
||||||
uniform_buffer,
|
|
||||||
bind_group,
|
bind_group,
|
||||||
width: image.width,
|
width: image.width,
|
||||||
height: image.height,
|
height: image.height,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
|
||||||
log::debug!(
|
log::debug!(
|
||||||
"Uploaded image {} ({}x{}) to GPU",
|
"Uploaded image {} ({}x{}) to GPU",
|
||||||
image.id,
|
image.id,
|
||||||
@@ -220,52 +260,61 @@ impl ImageRenderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Remove an image from the GPU.
|
/// Remove an image from the GPU.
|
||||||
pub fn remove_image(&mut self, image_id: u32) {
|
pub fn remove_image(&mut self, pane_id: PaneId, image_id: u32) {
|
||||||
if self.textures.remove(&image_id).is_some() {
|
if self.textures.remove(&(pane_id, image_id)).is_some() {
|
||||||
log::debug!("Removed image {} from GPU", image_id);
|
log::debug!("Removed image {} (pane {:?}) from GPU", image_id, pane_id);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Sync images from terminal's image storage to GPU.
|
/// Sync images from terminal's image storage to GPU.
|
||||||
/// Uploads new/changed images and removes deleted ones.
|
/// Uploads new/changed images and removes deleted ones.
|
||||||
/// Also updates animation frames.
|
/// Also updates animation frames.
|
||||||
pub fn sync_images(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, storage: &mut ImageStorage) {
|
pub fn sync_images(&mut self, device: &wgpu::Device, queue: &wgpu::Queue, pane_id: PaneId, storage: &mut ImageStorage) {
|
||||||
// Update animations and get list of changed image IDs
|
// Update animations and get list of changed image IDs
|
||||||
let changed_ids = storage.update_animations();
|
let changed_ids = storage.update_animations();
|
||||||
log::debug!("Sync images: changed_ids={:?}, dirty={}", changed_ids, storage.dirty);
|
log::debug!("Sync images: pane_id={:?}, changed_ids={:?}, dirty={}", pane_id, changed_ids, storage.dirty);
|
||||||
|
|
||||||
// Re-upload frames that changed due to animation
|
// Re-upload frames that changed due to animation
|
||||||
for id in &changed_ids {
|
for id in &changed_ids {
|
||||||
if let Some(image) = storage.get_image(*id) {
|
if let Some(image) = storage.get_image(*id) {
|
||||||
self.upload_image(device, queue, image);
|
self.upload_image(device, queue, pane_id, image);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !storage.dirty && changed_ids.is_empty() {
|
if !storage.dirty && changed_ids.is_empty() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Upload all images (upload_image handles deduplication)
|
// Upload all images (upload_image handles deduplication)
|
||||||
for image in storage.images().values() {
|
for image in storage.images().values() {
|
||||||
self.upload_image(device, queue, image);
|
self.upload_image(device, queue, pane_id, image);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
storage.clear_dirty();
|
||||||
|
}
|
||||||
|
|
||||||
// Remove textures for deleted images
|
|
||||||
let current_ids: std::collections::HashSet<u32> = storage.images().keys().copied().collect();
|
/// Remove images from the GPU that are not present in the provided set of active images.
|
||||||
let gpu_ids: Vec<u32> = self.textures.keys().copied().collect();
|
/// active_images is a set of (pane_id, image_id) tuples.
|
||||||
|
pub fn gc_images(&mut self, active_images: &std::collections::HashSet<(PaneId, u32)>) {
|
||||||
|
let gpu_ids: Vec<(PaneId, u32)> = self.textures.keys().copied().collect();
|
||||||
|
let mut removed_count = 0;
|
||||||
for id in gpu_ids {
|
for id in gpu_ids {
|
||||||
if !current_ids.contains(&id) {
|
if !active_images.contains(&id) {
|
||||||
self.remove_image(id);
|
self.remove_image(id.0, id.1);
|
||||||
|
removed_count += 1;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
if removed_count > 0 {
|
||||||
storage.clear_dirty();
|
log::debug!("GC images: removed {} unused textures", removed_count);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Prepare image renders for a pane.
|
/// Prepare image renders for a pane.
|
||||||
/// Returns a Vec of (image_id, uniforms) for deferred rendering.
|
/// Returns a Vec of (image_id, uniforms) for deferred rendering.
|
||||||
pub fn prepare_image_renders(
|
pub fn prepare_image_renders(
|
||||||
&self,
|
&self,
|
||||||
|
pane_id: PaneId,
|
||||||
placements: &[ImagePlacement],
|
placements: &[ImagePlacement],
|
||||||
pane_x: f32,
|
pane_x: f32,
|
||||||
pane_y: f32,
|
pane_y: f32,
|
||||||
@@ -276,14 +325,19 @@ impl ImageRenderer {
|
|||||||
scrollback_len: usize,
|
scrollback_len: usize,
|
||||||
scroll_offset: usize,
|
scroll_offset: usize,
|
||||||
visible_rows: usize,
|
visible_rows: usize,
|
||||||
|
dim_factor: f32,
|
||||||
) -> Vec<(u32, ImageUniforms)> {
|
) -> Vec<(u32, ImageUniforms)> {
|
||||||
|
log::debug!("prepare_image_renders: pane={:?}, placements={}, scrollback={}, offset={}, rows={}", pane_id, placements.len(), scrollback_len, scroll_offset, visible_rows);
|
||||||
let mut renders = Vec::new();
|
let mut renders = Vec::new();
|
||||||
|
|
||||||
for placement in placements {
|
for placement in placements {
|
||||||
// Check if we have the GPU texture for this image
|
// Check if we have the GPU texture for this image
|
||||||
let gpu_image = match self.textures.get(&placement.image_id) {
|
let gpu_image = match self.textures.get(&(pane_id, placement.image_id)) {
|
||||||
Some(img) => img,
|
Some(img) => img,
|
||||||
None => continue, // Skip if not uploaded yet
|
None => {
|
||||||
|
log::debug!("Image {} not found in GPU cache for pane {:?}", placement.image_id, pane_id);
|
||||||
|
continue;
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
// Convert absolute row to visible screen row
|
// Convert absolute row to visible screen row
|
||||||
@@ -338,8 +392,8 @@ impl ImageRenderer {
|
|||||||
src_y,
|
src_y,
|
||||||
src_width,
|
src_width,
|
||||||
src_height,
|
src_height,
|
||||||
|
dim_factor,
|
||||||
_padding1: 0.0,
|
_padding1: 0.0,
|
||||||
_padding2: 0.0,
|
|
||||||
};
|
};
|
||||||
|
|
||||||
renders.push((placement.image_id, uniforms));
|
renders.push((placement.image_id, uniforms));
|
||||||
|
|||||||
@@ -16,18 +16,19 @@ struct ImageUniforms {
|
|||||||
src_y: f32,
|
src_y: f32,
|
||||||
src_width: f32,
|
src_width: f32,
|
||||||
src_height: f32,
|
src_height: f32,
|
||||||
|
// Dim factor for unfocused panes (1.0 = bright, 0.0 = dimmed)
|
||||||
|
dim_factor: f32,
|
||||||
// Padding for alignment
|
// Padding for alignment
|
||||||
_padding1: f32,
|
_padding1: f32,
|
||||||
_padding2: f32,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@group(0) @binding(0)
|
@group(0) @binding(0)
|
||||||
var<uniform> uniforms: ImageUniforms;
|
var<uniform> uniforms: ImageUniforms;
|
||||||
|
|
||||||
@group(0) @binding(1)
|
@group(1) @binding(1)
|
||||||
var image_texture: texture_2d<f32>;
|
var image_texture: texture_2d<f32>;
|
||||||
|
|
||||||
@group(0) @binding(2)
|
@group(1) @binding(2)
|
||||||
var image_sampler: sampler;
|
var image_sampler: sampler;
|
||||||
|
|
||||||
struct VertexOutput {
|
struct VertexOutput {
|
||||||
@@ -87,6 +88,9 @@ fn fs_main(in: VertexOutput) -> @location(0) vec4<f32> {
|
|||||||
// Sample the image texture
|
// Sample the image texture
|
||||||
let color = textureSample(image_texture, image_sampler, in.uv);
|
let color = textureSample(image_texture, image_sampler, in.uv);
|
||||||
|
|
||||||
|
// Apply dimming factor to RGB channels
|
||||||
|
let dimmed_rgb = color.rgb * uniforms.dim_factor;
|
||||||
|
|
||||||
// Return with premultiplied alpha for proper blending
|
// Return with premultiplied alpha for proper blending
|
||||||
return vec4<f32>(color.rgb * color.a, color.a);
|
return vec4<f32>(dimmed_rgb * color.a, color.a);
|
||||||
}
|
}
|
||||||
|
|||||||
+37
-20
@@ -3,6 +3,8 @@
|
|||||||
//! Single-process architecture: owns PTY, terminal state, and rendering.
|
//! Single-process architecture: owns PTY, terminal state, and rendering.
|
||||||
//! Supports window close/reopen without losing terminal state.
|
//! Supports window close/reopen without losing terminal state.
|
||||||
|
|
||||||
|
use zterm::graphics::ImageStorage;
|
||||||
|
use zterm::vt_parser::SharedParser;
|
||||||
use zterm::config::{Action, Config};
|
use zterm::config::{Action, Config};
|
||||||
use zterm::keyboard::{
|
use zterm::keyboard::{
|
||||||
FunctionalKey, KeyEncoder, KeyEventType, KeyboardState, Modifiers,
|
FunctionalKey, KeyEncoder, KeyEventType, KeyboardState, Modifiers,
|
||||||
@@ -15,7 +17,7 @@ use zterm::renderer::{
|
|||||||
use zterm::terminal::{
|
use zterm::terminal::{
|
||||||
Direction, MouseTrackingMode, Terminal, TerminalCommand,
|
Direction, MouseTrackingMode, Terminal, TerminalCommand,
|
||||||
};
|
};
|
||||||
use zterm::vt_parser::SharedParser;
|
use zterm::gpu_types::PaneId;
|
||||||
|
|
||||||
use std::collections::HashMap;
|
use std::collections::HashMap;
|
||||||
use std::io::Write;
|
use std::io::Write;
|
||||||
@@ -40,17 +42,9 @@ use winit::keyboard::{Key, NamedKey};
|
|||||||
use winit::platform::wayland::EventLoopBuilderExtWayland;
|
use winit::platform::wayland::EventLoopBuilderExtWayland;
|
||||||
use winit::window::{Window, WindowId};
|
use winit::window::{Window, WindowId};
|
||||||
|
|
||||||
/// Unique identifier for a pane.
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
// PANE
|
||||||
struct PaneId(u64);
|
// ═══════════════════════════════════════════════════════════════════════════════
|
||||||
|
|
||||||
impl PaneId {
|
|
||||||
fn new() -> Self {
|
|
||||||
use std::sync::atomic::AtomicU64;
|
|
||||||
static NEXT_ID: AtomicU64 = AtomicU64::new(1);
|
|
||||||
Self(NEXT_ID.fetch_add(1, Ordering::Relaxed))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/// A single pane containing a terminal and its PTY.
|
/// A single pane containing a terminal and its PTY.
|
||||||
struct Pane {
|
struct Pane {
|
||||||
@@ -2026,9 +2020,12 @@ impl App {
|
|||||||
// Check if any images have running animations
|
// Check if any images have running animations
|
||||||
let image_animation_in_progress = tab.panes.values().any(|p| {
|
let image_animation_in_progress = tab.panes.values().any(|p| {
|
||||||
p.terminal.image_storage.has_animations()
|
p.terminal.image_storage.has_animations()
|
||||||
|
|| p.terminal.alternate_screen.as_ref().map_or(false, |alt| alt.image_storage.has_animations())
|
||||||
});
|
});
|
||||||
|
|
||||||
if !has_dirty_content && !self.needs_redraw && self.edge_glows.is_empty() && !fade_in_progress && !has_selection && !has_pending_redraw && !image_animation_in_progress {
|
let has_dirty_terminal = tab.panes.values().any(|p| p.terminal.dirty);
|
||||||
|
|
||||||
|
if !has_dirty_content && !has_dirty_terminal && !self.needs_redraw && self.edge_glows.is_empty() && !fade_in_progress && !has_selection && !has_pending_redraw && !image_animation_in_progress {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -2046,12 +2043,31 @@ impl App {
|
|||||||
);
|
);
|
||||||
dim_factors.push((*pane_id, dim_factor));
|
dim_factors.push((*pane_id, dim_factor));
|
||||||
|
|
||||||
// Sync terminal images to GPU (Kitty graphics protocol)
|
// Sync terminal images to GPU (Kitty graphics protocol)
|
||||||
renderer.sync_images(&mut pane.terminal.image_storage);
|
let storage = if pane.terminal.using_alternate_screen {
|
||||||
}
|
pane.terminal.alternate_screen.as_mut().map(|alt| &mut alt.image_storage).unwrap_or(&mut pane.terminal.image_storage)
|
||||||
}
|
} else {
|
||||||
|
&mut pane.terminal.image_storage
|
||||||
|
};
|
||||||
|
renderer.sync_images(*pane_id, storage);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Garbage collect unused images across all panes
|
||||||
|
let mut image_storages = Vec::new();
|
||||||
|
for (id, _) in &geometries {
|
||||||
|
if let Some(pane) = tab.panes.get(id) {
|
||||||
|
image_storages.push((*id, &pane.terminal.image_storage));
|
||||||
|
if let Some(alt) = &pane.terminal.alternate_screen {
|
||||||
|
image_storages.push((*id, &alt.image_storage));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
renderer.gc_images(&image_storages);
|
||||||
|
|
||||||
|
|
||||||
|
// Clear custom statusline if the foreground process is no longer neovim/vim
|
||||||
|
|
||||||
// Clear custom statusline if the foreground process is no longer neovim/vim
|
|
||||||
if let Some(pane) = tab.panes.get_mut(&active_pane_id) {
|
if let Some(pane) = tab.panes.get_mut(&active_pane_id) {
|
||||||
if pane.custom_statusline.is_some() {
|
if pane.custom_statusline.is_some() {
|
||||||
if let Some(proc_name) =
|
if let Some(proc_name) =
|
||||||
@@ -2171,8 +2187,9 @@ impl App {
|
|||||||
// Clear dirty lines after successful render (like Kitty's linebuf_mark_line_clean)
|
// Clear dirty lines after successful render (like Kitty's linebuf_mark_line_clean)
|
||||||
for (pane_id, _) in &geometries {
|
for (pane_id, _) in &geometries {
|
||||||
if let Some(pane) = tab.panes.get_mut(pane_id) {
|
if let Some(pane) = tab.panes.get_mut(pane_id) {
|
||||||
pane.terminal.clear_dirty_lines();
|
pane.terminal.clear_dirty_lines();
|
||||||
}
|
pane.terminal.dirty = false;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// Clear pending redraw and needs_redraw
|
// Clear pending redraw and needs_redraw
|
||||||
if let Some(renderer) = &mut self.renderer {
|
if let Some(renderer) = &mut self.renderer {
|
||||||
|
|||||||
+53
-19
@@ -798,7 +798,10 @@ impl Renderer {
|
|||||||
// Create pipeline layout for images
|
// Create pipeline layout for images
|
||||||
let image_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
let image_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
|
||||||
label: Some("Image Pipeline Layout"),
|
label: Some("Image Pipeline Layout"),
|
||||||
bind_group_layouts: &[image_renderer.bind_group_layout()],
|
bind_group_layouts: &[
|
||||||
|
image_renderer.uniform_layout(),
|
||||||
|
image_renderer.texture_layout(),
|
||||||
|
],
|
||||||
immediate_size: 0,
|
immediate_size: 0,
|
||||||
});
|
});
|
||||||
|
|
||||||
@@ -4903,25 +4906,50 @@ impl Renderer {
|
|||||||
// ═══════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
// PREPARE IMAGE RENDERS (Kitty Graphics Protocol)
|
// PREPARE IMAGE RENDERS (Kitty Graphics Protocol)
|
||||||
// ═══════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
let mut image_renders: Vec<(u32, ImageUniforms)> = Vec::new();
|
let mut image_renders: Vec<(crate::gpu_types::PaneId, u32, u64, [u32; 4])> = Vec::new();
|
||||||
|
let mut current_uniform_offset = 0;
|
||||||
|
|
||||||
for (terminal, info, _) in panes {
|
for (terminal, info, _) in panes {
|
||||||
// Apply grid centering offsets to pane position
|
// Apply grid centering offsets to pane position
|
||||||
let pane_x = grid_x_offset + info.x;
|
let pane_x = grid_x_offset + info.x;
|
||||||
let pane_y = terminal_y_offset + grid_y_offset + info.y;
|
let pane_y = terminal_y_offset + grid_y_offset + info.y;
|
||||||
|
|
||||||
|
// Compute scissor rect to clip image to pane boundaries
|
||||||
|
let scissor_x = (pane_x.round() as i32).max(0) as u32;
|
||||||
|
let scissor_y = (pane_y.round() as i32).max(0) as u32;
|
||||||
|
let scissor_w = (info.width.round() as i32).max(0).min((width as i32) - (scissor_x as i32)) as u32;
|
||||||
|
let scissor_h = (info.height.round() as i32).max(0).min((height as i32) - (scissor_y as i32)) as u32;
|
||||||
|
let scissor = [scissor_x, scissor_y, scissor_w, scissor_h];
|
||||||
|
|
||||||
let renders = self.image_renderer.prepare_image_renders(
|
let renders = self.image_renderer.prepare_image_renders(
|
||||||
terminal.image_storage.placements(),
|
crate::gpu_types::PaneId(info.pane_id),
|
||||||
|
if terminal.using_alternate_screen {
|
||||||
|
terminal.alternate_screen.as_ref().map(|alt| alt.image_storage.placements()).unwrap_or_default()
|
||||||
|
} else {
|
||||||
|
terminal.image_storage.placements()
|
||||||
|
},
|
||||||
pane_x,
|
pane_x,
|
||||||
pane_y,
|
pane_y,
|
||||||
self.cell_metrics.cell_width as f32,
|
self.cell_metrics.cell_width as f32,
|
||||||
self.cell_metrics.cell_height as f32,
|
self.cell_metrics.cell_height as f32,
|
||||||
width,
|
width,
|
||||||
height,
|
height,
|
||||||
terminal.scrollback.len(),
|
if terminal.using_alternate_screen { 0 } else { terminal.scrollback.len() },
|
||||||
terminal.scroll_offset,
|
if terminal.using_alternate_screen { 0 } else { terminal.scroll_offset },
|
||||||
info.rows,
|
info.rows,
|
||||||
|
info.dim_factor,
|
||||||
);
|
);
|
||||||
image_renders.extend(renders);
|
for (id, uniforms) in renders {
|
||||||
|
self.queue.write_buffer(
|
||||||
|
&self.image_renderer.uniform_buffer,
|
||||||
|
current_uniform_offset,
|
||||||
|
bytemuck::cast_slice(&[uniforms]),
|
||||||
|
);
|
||||||
|
image_renders.push((crate::gpu_types::PaneId(info.pane_id), id, current_uniform_offset, scissor));
|
||||||
|
|
||||||
|
// Align offset to device's min_uniform_buffer_offset_alignment
|
||||||
|
current_uniform_offset += self.image_renderer.alignment;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// ═══════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
@@ -5164,16 +5192,9 @@ impl Renderer {
|
|||||||
// IMAGE PASS (Kitty Graphics Protocol images, after glyph rendering)
|
// IMAGE PASS (Kitty Graphics Protocol images, after glyph rendering)
|
||||||
// Each image is rendered with its own draw call using separate bind groups
|
// Each image is rendered with its own draw call using separate bind groups
|
||||||
// ═══════════════════════════════════════════════════════════════════
|
// ═══════════════════════════════════════════════════════════════════
|
||||||
for (image_id, uniforms) in &image_renders {
|
for (pane_id, image_id, offset, scissor) in &image_renders {
|
||||||
// Check if we have the GPU texture for this image
|
// Check if we have the GPU texture for this image
|
||||||
if let Some(gpu_image) = self.image_renderer.get(image_id) {
|
if let Some(gpu_image) = self.image_renderer.get(*pane_id, image_id) {
|
||||||
// Upload uniforms to this image's dedicated uniform buffer
|
|
||||||
self.queue.write_buffer(
|
|
||||||
&gpu_image.uniform_buffer,
|
|
||||||
0,
|
|
||||||
bytemuck::cast_slice(&[*uniforms]),
|
|
||||||
);
|
|
||||||
|
|
||||||
// Create a render pass for this image (load existing content)
|
// Create a render pass for this image (load existing content)
|
||||||
let mut image_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
let mut image_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
|
||||||
label: Some("Image Pass"),
|
label: Some("Image Pass"),
|
||||||
@@ -5192,8 +5213,10 @@ impl Renderer {
|
|||||||
multiview_mask: None,
|
multiview_mask: None,
|
||||||
});
|
});
|
||||||
|
|
||||||
|
image_pass.set_scissor_rect(scissor[0], scissor[1], scissor[2], scissor[3]);
|
||||||
image_pass.set_pipeline(&self.image_pipeline);
|
image_pass.set_pipeline(&self.image_pipeline);
|
||||||
image_pass.set_bind_group(0, &gpu_image.bind_group, &[]);
|
image_pass.set_bind_group(0, self.image_renderer.uniform_bind_group(), &[*offset as u32]);
|
||||||
|
image_pass.set_bind_group(1, &gpu_image.bind_group, &[]);
|
||||||
image_pass.draw(0..4, 0..1); // Triangle strip quad
|
image_pass.draw(0..4, 0..1); // Triangle strip quad
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -5245,10 +5268,21 @@ impl Renderer {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Sync images from terminal's image storage to GPU.
|
/// Sync images from terminal's image storage to GPU.
|
||||||
/// Uploads new/changed images and removes deleted ones.
|
/// Uploads new/changed images.
|
||||||
/// Also updates animation frames.
|
/// Also updates animation frames.
|
||||||
pub fn sync_images(&mut self, storage: &mut ImageStorage) {
|
pub fn sync_images(&mut self, pane_id: crate::gpu_types::PaneId, storage: &mut ImageStorage) {
|
||||||
self.image_renderer.sync_images(&self.device, &self.queue, storage);
|
self.image_renderer.sync_images(&self.device, &self.queue, pane_id, storage);
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Remove images from the GPU that are not present in any of the provided storages.
|
||||||
|
pub fn gc_images(&mut self, storages: &[(crate::gpu_types::PaneId, &ImageStorage)]) {
|
||||||
|
let mut active_ids = std::collections::HashSet::new();
|
||||||
|
for (pane_id, storage) in storages {
|
||||||
|
for id in storage.images().keys() {
|
||||||
|
active_ids.insert((*pane_id, *id));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
self.image_renderer.gc_images(&active_ids);
|
||||||
}
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
+76
-48
@@ -287,8 +287,7 @@ struct SavedCursor {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/// Alternate screen buffer state.
|
/// Alternate screen buffer state.
|
||||||
#[derive(Clone)]
|
pub struct AlternateScreen {
|
||||||
struct AlternateScreen {
|
|
||||||
grid: Vec<Vec<Cell>>,
|
grid: Vec<Vec<Cell>>,
|
||||||
line_map: Vec<usize>,
|
line_map: Vec<usize>,
|
||||||
cursor_col: usize,
|
cursor_col: usize,
|
||||||
@@ -296,6 +295,7 @@ struct AlternateScreen {
|
|||||||
saved_cursor: SavedCursor,
|
saved_cursor: SavedCursor,
|
||||||
scroll_top: usize,
|
scroll_top: usize,
|
||||||
scroll_bottom: usize,
|
scroll_bottom: usize,
|
||||||
|
pub image_storage: ImageStorage,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Kitty-style ring buffer for scrollback history.
|
/// Kitty-style ring buffer for scrollback history.
|
||||||
@@ -472,7 +472,7 @@ pub struct Terminal {
|
|||||||
/// Saved cursor state (DECSC/DECRC).
|
/// Saved cursor state (DECSC/DECRC).
|
||||||
saved_cursor: SavedCursor,
|
saved_cursor: SavedCursor,
|
||||||
/// Alternate screen buffer (for fullscreen apps like vim, less).
|
/// Alternate screen buffer (for fullscreen apps like vim, less).
|
||||||
alternate_screen: Option<AlternateScreen>,
|
pub alternate_screen: Option<AlternateScreen>,
|
||||||
/// Whether we're currently using the alternate screen.
|
/// Whether we're currently using the alternate screen.
|
||||||
pub using_alternate_screen: bool,
|
pub using_alternate_screen: bool,
|
||||||
/// Application cursor keys mode (DECCKM) - arrows send ESC O instead of ESC [.
|
/// Application cursor keys mode (DECCKM) - arrows send ESC O instead of ESC [.
|
||||||
@@ -775,16 +775,19 @@ impl Terminal {
|
|||||||
return; // Already in alternate screen
|
return; // Already in alternate screen
|
||||||
}
|
}
|
||||||
|
|
||||||
// Save main screen state
|
// Create alternate screen if it doesn't exist, otherwise reuse it
|
||||||
self.alternate_screen = Some(AlternateScreen {
|
if self.alternate_screen.is_none() {
|
||||||
grid: self.grid.clone(),
|
self.alternate_screen = Some(AlternateScreen {
|
||||||
line_map: self.line_map.clone(),
|
grid: self.grid.clone(),
|
||||||
cursor_col: self.cursor_col,
|
line_map: self.line_map.clone(),
|
||||||
cursor_row: self.cursor_row,
|
cursor_col: self.cursor_col,
|
||||||
saved_cursor: self.saved_cursor.clone(),
|
cursor_row: self.cursor_row,
|
||||||
scroll_top: self.scroll_top,
|
saved_cursor: self.saved_cursor.clone(),
|
||||||
scroll_bottom: self.scroll_bottom,
|
scroll_top: self.scroll_top,
|
||||||
});
|
scroll_bottom: self.scroll_bottom,
|
||||||
|
image_storage: ImageStorage::new(),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
// Clear the screen for alternate buffer
|
// Clear the screen for alternate buffer
|
||||||
self.grid = vec![vec![Cell::default(); self.cols]; self.rows];
|
self.grid = vec![vec![Cell::default(); self.cols]; self.rows];
|
||||||
@@ -812,10 +815,10 @@ impl Terminal {
|
|||||||
return; // Not in alternate screen
|
return; // Not in alternate screen
|
||||||
}
|
}
|
||||||
|
|
||||||
if let Some(saved) = self.alternate_screen.take() {
|
if let Some(saved) = self.alternate_screen.as_ref() {
|
||||||
self.grid = saved.grid;
|
self.grid = saved.grid.clone();
|
||||||
self.line_map = saved.line_map;
|
self.line_map = saved.line_map.clone();
|
||||||
self.saved_cursor = saved.saved_cursor;
|
self.saved_cursor = saved.saved_cursor.clone();
|
||||||
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)
|
// Clamp cursor positions to current grid dimensions (defensive)
|
||||||
@@ -2721,51 +2724,76 @@ impl Terminal {
|
|||||||
|
|
||||||
// Convert cursor_row to absolute row (accounting for scrollback)
|
// Convert cursor_row to absolute row (accounting for scrollback)
|
||||||
// This allows images to scroll with terminal content
|
// This allows images to scroll with terminal content
|
||||||
let absolute_row = self.scrollback.len() + self.cursor_row;
|
let absolute_row = if self.using_alternate_screen {
|
||||||
|
self.cursor_row
|
||||||
|
} else {
|
||||||
|
self.scrollback.len() + self.cursor_row
|
||||||
|
};
|
||||||
|
|
||||||
// Process the command
|
log::debug!(
|
||||||
let (response, placement_result) =
|
"Routing image command to {}: cursor_col={}, absolute_row={}, using_alt={}",
|
||||||
|
if self.using_alternate_screen { "alternate" } else { "main" },
|
||||||
|
self.cursor_col,
|
||||||
|
absolute_row,
|
||||||
|
self.using_alternate_screen
|
||||||
|
);
|
||||||
|
let (response, placement_result) = if self.using_alternate_screen {
|
||||||
|
if let Some(ref mut alt) = self.alternate_screen {
|
||||||
|
alt.image_storage.process_command(
|
||||||
|
cmd,
|
||||||
|
self.cursor_col,
|
||||||
|
absolute_row,
|
||||||
|
self.cell_width,
|
||||||
|
self.cell_height,
|
||||||
|
)
|
||||||
|
} else {
|
||||||
|
// This should not happen if using_alternate_screen is true
|
||||||
|
(None, None)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
self.image_storage.process_command(
|
self.image_storage.process_command(
|
||||||
cmd,
|
cmd,
|
||||||
self.cursor_col,
|
self.cursor_col,
|
||||||
absolute_row,
|
absolute_row,
|
||||||
self.cell_width,
|
self.cell_width,
|
||||||
self.cell_height,
|
self.cell_height,
|
||||||
);
|
)
|
||||||
|
};
|
||||||
|
|
||||||
// Queue the response to send back to the application
|
// Queue the response to send back to the application
|
||||||
if let Some(resp) = response {
|
if let Some(resp) = response {
|
||||||
self.response_queue.extend_from_slice(resp.as_bytes());
|
self.response_queue.extend_from_slice(resp.as_bytes());
|
||||||
}
|
}
|
||||||
|
|
||||||
// Move cursor after image placement per Kitty protocol spec:
|
// Move cursor after image placement per Kitty protocol spec:
|
||||||
// "After placing an image on the screen the cursor must be moved to the
|
// "After placing an image on the screen the cursor must be moved to the
|
||||||
// right by the number of cols in the image placement rectangle and down
|
// right by the number of cols in the image placement rectangle and down
|
||||||
// by the number of rows in the image placement rectangle."
|
// by the number of rows in the image placement rectangle."
|
||||||
// However, if C=1 was specified, don't move the cursor.
|
// However, if C=1 was specified, don't move the cursor.
|
||||||
if let Some(placement) = placement_result {
|
if let Some(placement) = placement_result {
|
||||||
if !placement.suppress_cursor_move
|
self.dirty = true;
|
||||||
&& !placement.virtual_placement
|
if !placement.suppress_cursor_move
|
||||||
{
|
&& !placement.virtual_placement
|
||||||
// Move cursor to the right and down by the image dimensions
|
{
|
||||||
self.cursor_col += placement.cols;
|
// Move cursor to the right and and down by the image dimensions
|
||||||
let new_row = self.cursor_row + placement.rows;
|
self.cursor_col += placement.cols;
|
||||||
if new_row >= self.rows {
|
let new_row = self.cursor_row + placement.rows;
|
||||||
// Need to scroll
|
if new_row >= self.rows {
|
||||||
let scroll_amount = new_row - self.rows + 1;
|
// Need to scroll
|
||||||
self.scroll_up(scroll_amount);
|
let scroll_amount = new_row - self.rows + 1;
|
||||||
self.cursor_row = self.rows - 1;
|
self.scroll_up(scroll_amount);
|
||||||
} else {
|
self.cursor_row = self.rows - 1;
|
||||||
self.cursor_row = new_row;
|
} else {
|
||||||
|
self.cursor_row = new_row;
|
||||||
|
}
|
||||||
|
// If cursor is now beyond the right edge, it will be handled by the normal
|
||||||
|
// cursor movement logic (wrapping/scrolling) if applicable.
|
||||||
|
log::debug!(
|
||||||
|
"Cursor moved after image placement: col={}, row={} (moved {}x{} cells)",
|
||||||
|
self.cursor_col, self.cursor_row, placement.cols, placement.rows
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// If cursor is now beyond the right edge, it will be handled by the normal
|
|
||||||
// cursor movement logic (wrapping/scrolling) if applicable.
|
|
||||||
log::debug!(
|
|
||||||
"Cursor moved after image placement: col={}, row={} (moved {}x{} cells)",
|
|
||||||
self.cursor_col, self.cursor_row, placement.cols, placement.rows
|
|
||||||
);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user