fixed glow + dimming

This commit is contained in:
Zacharias-Brohn
2025-12-18 23:43:31 +01:00
parent d47ee0d1ef
commit ad055d0326
6 changed files with 414 additions and 77 deletions
+53 -7
View File
@@ -219,6 +219,43 @@ const CURSOR_BLOCK: u32 = 0u;
const CURSOR_UNDERLINE: u32 = 1u; const CURSOR_UNDERLINE: u32 = 1u;
const CURSOR_BAR: u32 = 2u; const CURSOR_BAR: u32 = 2u;
// Check if a cell is within the selection range
// Selection is specified as (start_col, start_row) to (end_col, end_row), normalized
// so start <= end in reading order
fn is_cell_selected(col: u32, row: u32) -> bool {
// Check if selection is active (-1 values mean no selection)
if grid_params.selection_start_col < 0 || grid_params.selection_start_row < 0 {
return false;
}
let sel_start_col = u32(grid_params.selection_start_col);
let sel_start_row = u32(grid_params.selection_start_row);
let sel_end_col = u32(grid_params.selection_end_col);
let sel_end_row = u32(grid_params.selection_end_row);
// Check if cell is within row range
if row < sel_start_row || row > sel_end_row {
return false;
}
// Single row selection
if sel_start_row == sel_end_row {
return col >= sel_start_col && col <= sel_end_col;
}
// Multi-row selection
if row == sel_start_row {
// First row: from start_col to end of line
return col >= sel_start_col;
} else if row == sel_end_row {
// Last row: from start of line to end_col
return col <= sel_end_col;
} else {
// Middle rows: entire row is selected
return true;
}
}
// Vertex output for instanced cell rendering // Vertex output for instanced cell rendering
struct CellVertexOutput { struct CellVertexOutput {
@builtin(position) clip_position: vec4<f32>, @builtin(position) clip_position: vec4<f32>,
@@ -327,8 +364,8 @@ fn vs_cell_bg(
bg = tmp; bg = tmp;
} }
// Check if this cell is selected (per-cell flag set by CPU, respects xlimit) // Check if this cell is selected using GridParams selection range
let is_selected = (attrs & ATTR_SELECTED_BIT) != 0u; let is_selected = is_cell_selected(col, row);
if is_selected { if is_selected {
fg = vec4<f32>(0.0, 0.0, 0.0, 1.0); // Black foreground fg = vec4<f32>(0.0, 0.0, 0.0, 1.0); // Black foreground
bg = vec4<f32>(1.0, 1.0, 1.0, 1.0); // White background bg = vec4<f32>(1.0, 1.0, 1.0, 1.0); // White background
@@ -346,9 +383,18 @@ fn vs_cell_bg(
bg.a = 0.0; bg.a = 0.0;
} }
// Calculate cursor color - use fg color (inverted from bg) for visibility // Calculate cursor color
// For block cursor, we'll use fg as the cursor background // If the cell is empty (no glyph), use default foreground color for cursor
var cursor_color = fg; // Otherwise use the cell's foreground color
var cursor_color: vec4<f32>;
let sprite_idx = cell.sprite_idx & ~COLORED_GLYPH_FLAG;
if sprite_idx == 0u {
// Empty cell - use default foreground color for cursor
cursor_color = color_table.colors[256]; // default_fg
} else {
// Cell has a glyph - use its foreground color
cursor_color = fg;
}
cursor_color.a = 1.0; cursor_color.a = 1.0;
var out: CellVertexOutput; var out: CellVertexOutput;
@@ -444,8 +490,8 @@ fn vs_cell_glyph(
bg = tmp; bg = tmp;
} }
// Check if this cell is selected (per-cell flag set by CPU, respects xlimit) // Check if this cell is selected using GridParams selection range
let is_selected = (attrs & ATTR_SELECTED_BIT) != 0u; let is_selected = is_cell_selected(col, row);
if is_selected { if is_selected {
fg = vec4<f32>(0.0, 0.0, 0.0, 1.0); // Black foreground fg = vec4<f32>(0.0, 0.0, 0.0, 1.0); // Black foreground
bg = vec4<f32>(1.0, 1.0, 1.0, 1.0); // White background bg = vec4<f32>(1.0, 1.0, 1.0, 1.0); // White background
+44 -21
View File
@@ -270,19 +270,23 @@ impl Pane {
/// Create a new pane with its own terminal and PTY. /// Create a new pane with its own terminal and PTY.
fn new(cols: usize, rows: usize, scrollback_lines: usize) -> Result<Self, String> { fn new(cols: usize, rows: usize, scrollback_lines: usize) -> Result<Self, String> {
let terminal = Terminal::new(cols, rows, scrollback_lines); let terminal = Terminal::new(cols, rows, scrollback_lines);
let pty = Pty::spawn(None).map_err(|e| format!("Failed to spawn PTY: {}", e))?;
// Set terminal size (use default cell size estimate for initial pixel dimensions) // Calculate pixel dimensions (use default cell size estimate)
let default_cell_width = 10u16; let default_cell_width = 10u16;
let default_cell_height = 20u16; let default_cell_height = 20u16;
if let Err(e) = pty.resize( let width_px = cols as u16 * default_cell_width;
let height_px = rows as u16 * default_cell_height;
// Spawn PTY with initial size - this sets the size BEFORE forking,
// so the shell inherits the correct terminal dimensions immediately.
// This prevents race conditions where .zshrc runs before resize().
let pty = Pty::spawn(
None,
cols as u16, cols as u16,
rows as u16, rows as u16,
cols as u16 * default_cell_width, width_px,
rows as u16 * default_cell_height, height_px
) { ).map_err(|e| format!("Failed to spawn PTY: {}", e))?;
log::warn!("Failed to set initial PTY size: {}", e);
}
let pty_fd = pty.as_raw_fd(); let pty_fd = pty.as_raw_fd();
@@ -302,12 +306,20 @@ impl Pane {
} }
/// Resize the terminal and PTY. /// Resize the terminal and PTY.
/// Only sends SIGWINCH to the PTY if the size actually changed.
fn resize(&mut self, cols: usize, rows: usize, width_px: u16, height_px: u16) { fn resize(&mut self, cols: usize, rows: usize, width_px: u16, height_px: u16) {
// Check if size actually changed before sending SIGWINCH
// This prevents spurious signals that can interrupt programs like fastfetch
let size_changed = cols != self.terminal.cols || rows != self.terminal.rows;
self.terminal.resize(cols, rows); self.terminal.resize(cols, rows);
if size_changed {
if let Err(e) = self.pty.resize(cols as u16, rows as u16, width_px, height_px) { if let Err(e) = self.pty.resize(cols as u16, rows as u16, width_px, height_px) {
log::warn!("Failed to resize PTY: {}", e); log::warn!("Failed to resize PTY: {}", e);
} }
} }
}
/// Write data to the PTY. /// Write data to the PTY.
fn write_to_pty(&mut self, data: &[u8]) { fn write_to_pty(&mut self, data: &[u8]) {
@@ -439,17 +451,19 @@ impl SplitNode {
// Calculate how many cells fit // Calculate how many cells fit
let cols = (width / cell_width).floor() as usize; let cols = (width / cell_width).floor() as usize;
let rows = (height / cell_height).floor() as usize; let rows = (height / cell_height).floor() as usize;
// Store actual cell-aligned dimensions (not allocated space) // Store the full allocated dimensions (not just cell-aligned)
let actual_width = cols.max(1) as f32 * cell_width; // This ensures edge glow and pane dimming cover the full pane area
let actual_height = rows.max(1) as f32 * cell_height;
*geometry = PaneGeometry { *geometry = PaneGeometry {
x, x,
y, y,
width: actual_width, width, // Full allocated width
height: actual_height, height, // Full allocated height
cols: cols.max(1), cols: cols.max(1),
rows: rows.max(1), rows: rows.max(1),
}; };
// Return cell-aligned dimensions for layout calculations
let actual_width = cols.max(1) as f32 * cell_width;
let actual_height = rows.max(1) as f32 * cell_height;
(actual_width, actual_height) (actual_width, actual_height)
} }
SplitNode::Split { horizontal, ratio, first, second } => { SplitNode::Split { horizontal, ratio, first, second } => {
@@ -1954,14 +1968,17 @@ impl App {
if !navigated { if !navigated {
// No neighbor in that direction - trigger edge glow animation // No neighbor in that direction - trigger edge glow animation
// Add to existing glows (don't replace) so multiple can be visible // Use renderer's helper to calculate proper screen-space glow bounds
if let Some(geom) = active_pane_geom { if let (Some(geom), Some(renderer)) = (active_pane_geom, &self.renderer) {
let (glow_x, glow_y, glow_width, glow_height) =
renderer.calculate_edge_glow_bounds(geom.x, geom.y, geom.width, geom.height);
self.edge_glows.push(EdgeGlow::new( self.edge_glows.push(EdgeGlow::new(
direction, direction,
geom.x, glow_x,
geom.y, glow_y,
geom.width, glow_width,
geom.height, glow_height,
)); ));
} }
} }
@@ -2380,6 +2397,7 @@ impl ApplicationHandler<UserEvent> for App {
let scroll_offset = self.get_scroll_offset(); let scroll_offset = self.get_scroll_offset();
let content_row = screen_row as isize - scroll_offset as isize; let content_row = screen_row as isize - scroll_offset as isize;
let pos = CellPosition { col, row: content_row }; let pos = CellPosition { col, row: content_row };
log::debug!("Selection started at col={}, content_row={}, screen_row={}, scroll_offset={}", col, content_row, screen_row, scroll_offset);
if let Some(tab) = self.active_tab_mut() { if let Some(tab) = self.active_tab_mut() {
if let Some(pane) = tab.active_pane_mut() { if let Some(pane) = tab.active_pane_mut() {
pane.selection = Some(Selection { start: pos, end: pos }); pane.selection = Some(Selection { start: pos, end: pos });
@@ -2505,8 +2523,13 @@ impl ApplicationHandler<UserEvent> for App {
// Convert selection to screen coords for this pane // Convert selection to screen coords for this pane
let selection = if is_active { let selection = if is_active {
pane.selection.as_ref() let sel = pane.selection.as_ref()
.and_then(|sel| sel.to_screen_coords(scroll_offset, geom.rows)) .and_then(|sel| sel.to_screen_coords(scroll_offset, geom.rows));
if pane.selection.is_some() {
log::debug!("Render: pane.selection={:?}, scroll_offset={}, rows={}, screen_coords={:?}",
pane.selection, scroll_offset, geom.rows, sel);
}
sel
} else { } else {
None None
}; };
+17 -1
View File
@@ -35,7 +35,8 @@ pub struct Pty {
impl Pty { impl Pty {
/// Creates a new PTY and spawns a shell process. /// Creates a new PTY and spawns a shell process.
pub fn spawn(shell: Option<&str>) -> Result<Self, PtyError> { /// The initial terminal size should be provided so the shell starts with the correct dimensions.
pub fn spawn(shell: Option<&str>, cols: u16, rows: u16, xpixel: u16, ypixel: u16) -> Result<Self, PtyError> {
// Open the PTY master // Open the PTY master
let master = openpt(OpenptFlags::RDWR | OpenptFlags::NOCTTY | OpenptFlags::CLOEXEC) let master = openpt(OpenptFlags::RDWR | OpenptFlags::NOCTTY | OpenptFlags::CLOEXEC)
.map_err(PtyError::OpenMaster)?; .map_err(PtyError::OpenMaster)?;
@@ -50,6 +51,21 @@ impl Pty {
// Get the slave name // Get the slave name
let slave_name = ptsname(&master, Vec::new()).map_err(PtyError::PtsName)?; let slave_name = ptsname(&master, Vec::new()).map_err(PtyError::PtsName)?;
// Set the terminal size BEFORE forking so the child inherits the correct size.
// This prevents race conditions where the shell's .zshrc runs before the parent
// can call resize(), causing programs like fastfetch to get wrong dimensions.
let winsize = libc::winsize {
ws_row: rows,
ws_col: cols,
ws_xpixel: xpixel,
ws_ypixel: ypixel,
};
let fd = std::os::fd::AsRawFd::as_raw_fd(&master);
let result = unsafe { libc::ioctl(fd, libc::TIOCSWINSZ, &winsize) };
if result == -1 {
return Err(PtyError::Io(std::io::Error::last_os_error()));
}
// Fork the process // Fork the process
// SAFETY: We're careful to only use async-signal-safe functions in the child // SAFETY: We're careful to only use async-signal-safe functions in the child
let fork_result = unsafe { libc::fork() }; let fork_result = unsafe { libc::fork() };
+275 -36
View File
@@ -786,6 +786,10 @@ pub struct Renderer {
/// GPU quads for overlay rendering (rendered on top of everything). /// GPU quads for overlay rendering (rendered on top of everything).
overlay_quads: Vec<Quad>, overlay_quads: Vec<Quad>,
/// GPU buffer for overlay quad instances (separate from main quads).
overlay_quad_buffer: wgpu::Buffer,
/// Bind group for overlay quad rendering.
overlay_quad_bind_group: wgpu::BindGroup,
} }
// ═══════════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════════
@@ -2747,6 +2751,30 @@ impl Renderer {
], ],
}); });
// Overlay quad buffer for instance data (separate from main quads)
let overlay_quad_buffer = device.create_buffer(&wgpu::BufferDescriptor {
label: Some("Overlay Quad Buffer"),
size: (max_quads * std::mem::size_of::<Quad>()) as u64,
usage: wgpu::BufferUsages::STORAGE | wgpu::BufferUsages::COPY_DST,
mapped_at_creation: false,
});
// Bind group for overlay quad rendering (uses same params buffer but different quad buffer)
let overlay_quad_bind_group = device.create_bind_group(&wgpu::BindGroupDescriptor {
label: Some("Overlay Quad Bind Group"),
layout: &quad_bind_group_layout,
entries: &[
wgpu::BindGroupEntry {
binding: 0,
resource: quad_params_buffer.as_entire_binding(),
},
wgpu::BindGroupEntry {
binding: 1,
resource: overlay_quad_buffer.as_entire_binding(),
},
],
});
// Pipeline layout for quad rendering // Pipeline layout for quad rendering
let quad_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { let quad_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor {
label: Some("Quad Pipeline Layout"), label: Some("Quad Pipeline Layout"),
@@ -2890,6 +2918,8 @@ impl Renderer {
quad_pipeline, quad_pipeline,
quad_bind_group, quad_bind_group,
overlay_quads: Vec::with_capacity(32), overlay_quads: Vec::with_capacity(32),
overlay_quad_buffer,
overlay_quad_bind_group,
} }
} }
@@ -2992,6 +3022,120 @@ impl Renderer {
(available_height - used_height) / 2.0 (available_height - used_height) / 2.0
} }
/// Calculates screen-space bounds for edge glow given pane geometry.
/// Takes pane coordinates in grid-relative space and transforms them to screen coordinates,
/// extending to fill the terminal grid area (but not into tab bar or statusline).
/// Returns (screen_x, screen_y, width, height) for the glow mask area.
pub fn calculate_edge_glow_bounds(&self, pane_x: f32, pane_y: f32, pane_width: f32, pane_height: f32) -> (f32, f32, f32, f32) {
let grid_x_offset = self.grid_x_offset();
let grid_y_offset = self.grid_y_offset();
let terminal_y_offset = self.terminal_y_offset();
let (available_width, available_height) = self.available_grid_space();
// Calculate the terminal grid area boundaries in screen coordinates
// This is the area where content is rendered, excluding tab bar and statusline
let grid_top = terminal_y_offset;
let grid_bottom = terminal_y_offset + available_height;
let grid_left = 0.0_f32;
let grid_right = self.width as f32;
log::debug!("calculate_edge_glow_bounds: pane=({}, {}, {}, {})", pane_x, pane_y, pane_width, pane_height);
log::debug!(" grid area: top={}, bottom={}, left={}, right={}", grid_top, grid_bottom, grid_left, grid_right);
log::debug!(" offsets: grid_x={}, grid_y={}, terminal_y={}", grid_x_offset, grid_y_offset, terminal_y_offset);
// Transform pane coordinates to screen space (same as border rendering)
let mut screen_x = grid_x_offset + pane_x;
let mut screen_y = terminal_y_offset + grid_y_offset + pane_y;
let mut width = pane_width;
let mut height = pane_height;
log::debug!(" initial screen: ({}, {}, {}, {})", screen_x, screen_y, width, height);
// Use a larger epsilon to account for cell-alignment gaps in split panes
// With cell-aligned splits, gaps can be up to one cell height
let epsilon = self.cell_height.max(self.cell_width);
// Left edge at screen boundary - extend to screen left edge
if pane_x < epsilon {
width += screen_x - grid_left;
screen_x = grid_left;
}
// Right edge at screen boundary - extend to screen right edge
if (pane_x + pane_width) >= available_width - epsilon {
width = grid_right - screen_x;
}
// Top edge at grid boundary - extend to grid top (respects tab bar/statusline at top)
if pane_y < epsilon {
height += screen_y - grid_top;
screen_y = grid_top;
}
// Bottom edge at grid boundary - extend to grid bottom (respects tab bar/statusline at bottom)
if (pane_y + pane_height) >= available_height - epsilon {
height = grid_bottom - screen_y;
}
log::debug!(" final screen: ({}, {}, {}, {})", screen_x, screen_y, width, height);
(screen_x, screen_y, width, height)
}
/// Calculates screen-space bounds for dim overlay given pane geometry.
/// Takes pane coordinates in grid-relative space and transforms them to screen coordinates,
/// extending to fill the terminal grid area (but not into tab bar or statusline).
/// This is identical to calculate_edge_glow_bounds - ensures dim overlays cover the full
/// pane area including centering margins, matching edge glow behavior.
/// Returns (screen_x, screen_y, width, height) for the overlay area.
pub fn calculate_dim_overlay_bounds(&self, pane_x: f32, pane_y: f32, pane_width: f32, pane_height: f32) -> (f32, f32, f32, f32) {
let grid_x_offset = self.grid_x_offset();
let grid_y_offset = self.grid_y_offset();
let terminal_y_offset = self.terminal_y_offset();
let (available_width, available_height) = self.available_grid_space();
// Calculate the terminal grid area boundaries in screen coordinates
// This is the area where content is rendered, excluding tab bar and statusline
let grid_top = terminal_y_offset;
let grid_bottom = terminal_y_offset + available_height;
let grid_left = 0.0_f32;
let grid_right = self.width as f32;
// Transform pane coordinates to screen space (same as border rendering)
let mut screen_x = grid_x_offset + pane_x;
let mut screen_y = terminal_y_offset + grid_y_offset + pane_y;
let mut width = pane_width;
let mut height = pane_height;
// Use a larger epsilon to account for cell-alignment gaps in split panes
// With cell-aligned splits, gaps can be up to one cell height
let epsilon = self.cell_height.max(self.cell_width);
// Left edge at screen boundary - extend to screen left edge
if pane_x < epsilon {
width += screen_x - grid_left;
screen_x = grid_left;
}
// Right edge at screen boundary - extend to screen right edge
if (pane_x + pane_width) >= available_width - epsilon {
width = grid_right - screen_x;
}
// Top edge at grid boundary - extend to grid top (respects tab bar/statusline at top)
if pane_y < epsilon {
height += screen_y - grid_top;
screen_y = grid_top;
}
// Bottom edge at grid boundary - extend to grid bottom (respects tab bar/statusline at bottom)
if (pane_y + pane_height) >= available_height - epsilon {
height = grid_bottom - screen_y;
}
(screen_x, screen_y, width, height)
}
/// Converts a pixel position to a terminal cell position. /// Converts a pixel position to a terminal cell position.
/// Returns None if the position is outside the terminal area (e.g., in the tab bar or statusline). /// Returns None if the position is outside the terminal area (e.g., in the tab bar or statusline).
pub fn pixel_to_cell(&self, x: f64, y: f64) -> Option<(usize, usize)> { pub fn pixel_to_cell(&self, x: f64, y: f64) -> Option<(usize, usize)> {
@@ -4055,8 +4199,10 @@ impl Renderer {
self.statusline_gpu_cells.clear(); self.statusline_gpu_cells.clear();
// Calculate target columns based on window width // Calculate target columns based on window width
// Use ceil() to ensure we cover the entire window edge-to-edge
// (the rightmost cell may extend slightly past the window, which is fine)
let target_cols = if self.cell_width > 0.0 { let target_cols = if self.cell_width > 0.0 {
(target_width / self.cell_width).floor() as usize (target_width / self.cell_width).ceil() as usize
} else { } else {
self.statusline_max_cols self.statusline_max_cols
}; };
@@ -4224,9 +4370,16 @@ impl Renderer {
} }
} }
StatuslineContent::Sections(sections) => { StatuslineContent::Sections(sections) => {
for section in sections.iter() { for (section_idx, section) in sections.iter().enumerate() {
let section_bg = Self::pack_statusline_color(section.bg); let section_bg = Self::pack_statusline_color(section.bg);
// Get next section's background for powerline arrow transition
let next_section_bg = if section_idx + 1 < sections.len() {
Self::pack_statusline_color(sections[section_idx + 1].bg)
} else {
default_bg
};
for component in section.components.iter() { for component in section.components.iter() {
let component_fg = Self::pack_statusline_color(component.fg); let component_fg = Self::pack_statusline_color(component.fg);
let style = if component.bold { FontStyle::Bold } else { FontStyle::Regular }; let style = if component.bold { FontStyle::Bold } else { FontStyle::Regular };
@@ -4358,10 +4511,10 @@ impl Renderer {
let arrow_char = '\u{E0B0}'; let arrow_char = '\u{E0B0}';
let (sprite_idx, _) = self.get_or_create_sprite_for(arrow_char, FontStyle::Regular, SpriteTarget::Statusline); let (sprite_idx, _) = self.get_or_create_sprite_for(arrow_char, FontStyle::Regular, SpriteTarget::Statusline);
// Arrow foreground is section background, arrow background is next section bg or default // Arrow foreground is current section's bg, arrow background is next section's bg
self.statusline_gpu_cells.push(GPUCell { self.statusline_gpu_cells.push(GPUCell {
fg: section_bg, // Arrow takes section bg color as its foreground fg: section_bg, // Arrow takes section bg color as its foreground
bg: default_bg, // Will be overwritten if there's a next section bg: next_section_bg, // Background is the next section's background
decoration_fg: 0, decoration_fg: 0,
sprite_idx, sprite_idx,
attrs: 0, attrs: 0,
@@ -4371,6 +4524,19 @@ impl Renderer {
} }
} }
// Fill remaining width with default background cells
// This ensures the statusline covers the entire window width
let default_bg_packed = Self::pack_statusline_color(StatuslineColor::Default);
while self.statusline_gpu_cells.len() < target_cols && self.statusline_gpu_cells.len() < self.statusline_max_cols {
self.statusline_gpu_cells.push(GPUCell {
fg: 0,
bg: default_bg_packed,
decoration_fg: 0,
sprite_idx: 0,
attrs: 0,
});
}
self.statusline_gpu_cells.len() self.statusline_gpu_cells.len()
} }
@@ -6705,6 +6871,7 @@ impl Renderer {
Direction::Right => 3, Direction::Right => 3,
}; };
// Glow coordinates are already in screen space (transformed by calculate_edge_glow_bounds)
glow_instances[i] = GlowInstance { glow_instances[i] = GlowInstance {
direction, direction,
progress: glow.progress(), progress: glow.progress(),
@@ -6753,6 +6920,10 @@ impl Renderer {
// Sync palette from first terminal // Sync palette from first terminal
if let Some((terminal, _, _)) = panes.first() { if let Some((terminal, _, _)) = panes.first() {
self.palette = terminal.palette.clone(); self.palette = terminal.palette.clone();
log::debug!("render_panes: synced palette from first terminal, default_bg={:?}, default_fg={:?}",
self.palette.default_bg, self.palette.default_fg);
} else {
log::debug!("render_panes: no panes, using existing palette");
} }
let output = self.surface.get_current_texture()?; let output = self.surface.get_current_texture()?;
@@ -6794,19 +6965,20 @@ impl Renderer {
TabBarPosition::Hidden => unreachable!(), TabBarPosition::Hidden => unreachable!(),
}; };
let tab_bar_bg = { // Use same color as statusline: 0x1a1a1a (26, 26, 26) in sRGB
let [r, g, b] = self.palette.default_bg; // Linear RGB value: ~0.00972
let factor = 0.85_f32; let tab_bar_bg = [
[ Self::srgb_to_linear(26.0 / 255.0),
Self::srgb_to_linear((r as f32 / 255.0) * factor), Self::srgb_to_linear(26.0 / 255.0),
Self::srgb_to_linear((g as f32 / 255.0) * factor), Self::srgb_to_linear(26.0 / 255.0),
Self::srgb_to_linear((b as f32 / 255.0) * factor),
1.0, 1.0,
] ];
};
// Draw tab bar background // Draw tab bar background
log::debug!("render_panes: drawing tab bar at y={}, height={}, num_tabs={}, quads_before={}",
tab_bar_y, tab_bar_height, num_tabs, self.quads.len());
self.render_rect(0.0, tab_bar_y, width, tab_bar_height, tab_bar_bg); self.render_rect(0.0, tab_bar_y, width, tab_bar_height, tab_bar_bg);
log::debug!("render_panes: after tab bar rect, quads_count={}", self.quads.len());
// Render each tab // Render each tab
let mut tab_x = 4.0_f32; let mut tab_x = 4.0_f32;
@@ -6820,15 +6992,25 @@ impl Renderer {
let tab_width = title_width.max(min_tab_width); let tab_width = title_width.max(min_tab_width);
let tab_bg = if is_active { let tab_bg = if is_active {
// Active tab: brightest - significantly brighter than tab bar
let [r, g, b] = self.palette.default_bg; let [r, g, b] = self.palette.default_bg;
let boost = 50.0_f32; // More visible for active tab
[ [
Self::srgb_to_linear(r as f32 / 255.0), Self::srgb_to_linear((r as f32 + boost).min(255.0) / 255.0),
Self::srgb_to_linear(g as f32 / 255.0), Self::srgb_to_linear((g as f32 + boost).min(255.0) / 255.0),
Self::srgb_to_linear(b as f32 / 255.0), Self::srgb_to_linear((b as f32 + boost).min(255.0) / 255.0),
1.0, 1.0,
] ]
} else { } else {
tab_bar_bg // Inactive tab: slightly brighter than tab bar background
let [r, g, b] = self.palette.default_bg;
let boost = 30.0_f32;
[
Self::srgb_to_linear((r as f32 + boost).min(255.0) / 255.0),
Self::srgb_to_linear((g as f32 + boost).min(255.0) / 255.0),
Self::srgb_to_linear((b as f32 + boost).min(255.0) / 255.0),
1.0,
]
}; };
let tab_fg = { let tab_fg = {
@@ -6934,6 +7116,15 @@ impl Renderer {
// Tolerance for detecting adjacent panes (should be touching or very close) // Tolerance for detecting adjacent panes (should be touching or very close)
let adjacency_tolerance = 1.0; let adjacency_tolerance = 1.0;
// Calculate grid boundaries for extending borders to screen edges
// Same technique as edge glow and dim overlay
let (available_width, available_height) = self.available_grid_space();
let grid_top = terminal_y_offset;
let grid_bottom = terminal_y_offset + available_height;
let grid_left = 0.0_f32;
let grid_right = width;
let epsilon = self.cell_height.max(self.cell_width);
// Check each pair of panes to find adjacent ones // Check each pair of panes to find adjacent ones
for i in 0..panes.len() { for i in 0..panes.len() {
for j in (i + 1)..panes.len() { for j in (i + 1)..panes.len() {
@@ -6962,9 +7153,19 @@ impl Renderer {
// Pane A is to the left of pane B (A's right edge touches B's left edge) // Pane A is to the left of pane B (A's right edge touches B's left edge)
if (a_right - b_x).abs() < adjacency_tolerance { if (a_right - b_x).abs() < adjacency_tolerance {
// Check if they overlap vertically // Check if they overlap vertically
let top = a_y.max(b_y); let mut top = a_y.max(b_y);
let bottom = a_bottom.min(b_bottom); let mut bottom = a_bottom.min(b_bottom);
if bottom > top { if bottom > top {
// Extend to grid edges if both panes reach the edge
// Top edge: extend if both panes are at grid top
if info_a.y < epsilon && info_b.y < epsilon {
top = grid_top;
}
// Bottom edge: extend if both panes reach grid bottom
if (info_a.y + info_a.height) >= available_height - epsilon
&& (info_b.y + info_b.height) >= available_height - epsilon {
bottom = grid_bottom;
}
// Draw vertical border centered on their shared edge // Draw vertical border centered on their shared edge
let border_x = a_right - border_thickness / 2.0; let border_x = a_right - border_thickness / 2.0;
self.render_overlay_rect(border_x, top, border_thickness, bottom - top, border_color); self.render_overlay_rect(border_x, top, border_thickness, bottom - top, border_color);
@@ -6972,9 +7173,17 @@ impl Renderer {
} }
// Pane B is to the left of pane A // Pane B is to the left of pane A
if (b_right - a_x).abs() < adjacency_tolerance { if (b_right - a_x).abs() < adjacency_tolerance {
let top = a_y.max(b_y); let mut top = a_y.max(b_y);
let bottom = a_bottom.min(b_bottom); let mut bottom = a_bottom.min(b_bottom);
if bottom > top { if bottom > top {
// Extend to grid edges if both panes reach the edge
if info_a.y < epsilon && info_b.y < epsilon {
top = grid_top;
}
if (info_a.y + info_a.height) >= available_height - epsilon
&& (info_b.y + info_b.height) >= available_height - epsilon {
bottom = grid_bottom;
}
let border_x = b_right - border_thickness / 2.0; let border_x = b_right - border_thickness / 2.0;
self.render_overlay_rect(border_x, top, border_thickness, bottom - top, border_color); self.render_overlay_rect(border_x, top, border_thickness, bottom - top, border_color);
} }
@@ -6984,9 +7193,19 @@ impl Renderer {
// Pane A is above pane B (A's bottom edge touches B's top edge) // Pane A is above pane B (A's bottom edge touches B's top edge)
if (a_bottom - b_y).abs() < adjacency_tolerance { if (a_bottom - b_y).abs() < adjacency_tolerance {
// Check if they overlap horizontally // Check if they overlap horizontally
let left = a_x.max(b_x); let mut left = a_x.max(b_x);
let right = a_right.min(b_right); let mut right = a_right.min(b_right);
if right > left { if right > left {
// Extend to screen edges if both panes reach the edge
// Left edge: extend if both panes are at grid left
if info_a.x < epsilon && info_b.x < epsilon {
left = grid_left;
}
// Right edge: extend if both panes reach grid right
if (info_a.x + info_a.width) >= available_width - epsilon
&& (info_b.x + info_b.width) >= available_width - epsilon {
right = grid_right;
}
// Draw horizontal border centered on their shared edge // Draw horizontal border centered on their shared edge
let border_y = a_bottom - border_thickness / 2.0; let border_y = a_bottom - border_thickness / 2.0;
self.render_overlay_rect(left, border_y, right - left, border_thickness, border_color); self.render_overlay_rect(left, border_y, right - left, border_thickness, border_color);
@@ -6994,9 +7213,17 @@ impl Renderer {
} }
// Pane B is above pane A // Pane B is above pane A
if (b_bottom - a_y).abs() < adjacency_tolerance { if (b_bottom - a_y).abs() < adjacency_tolerance {
let left = a_x.max(b_x); let mut left = a_x.max(b_x);
let right = a_right.min(b_right); let mut right = a_right.min(b_right);
if right > left { if right > left {
// Extend to screen edges if both panes reach the edge
if info_a.x < epsilon && info_b.x < epsilon {
left = grid_left;
}
if (info_a.x + info_a.width) >= available_width - epsilon
&& (info_b.x + info_b.width) >= available_width - epsilon {
right = grid_right;
}
let border_y = b_bottom - border_thickness / 2.0; let border_y = b_bottom - border_thickness / 2.0;
self.render_overlay_rect(left, border_y, right - left, border_thickness, border_color); self.render_overlay_rect(left, border_y, right - left, border_thickness, border_color);
} }
@@ -7027,6 +7254,9 @@ impl Renderer {
let pane_width = info.width; let pane_width = info.width;
let pane_height = info.height; let pane_height = info.height;
log::debug!("render_panes: pane {} at ({}, {}), size {}x{}, bottom_edge={}",
info.pane_id, pane_x, pane_y, pane_width, pane_height, pane_y + pane_height);
// Update GPU cells for this terminal (populates self.gpu_cells) // Update GPU cells for this terminal (populates self.gpu_cells)
self.update_gpu_cells(terminal); self.update_gpu_cells(terminal);
@@ -7056,8 +7286,9 @@ impl Renderer {
screen_height: self.height as f32, screen_height: self.height as f32,
x_offset: pane_x, x_offset: pane_x,
y_offset: pane_y, y_offset: pane_y,
cursor_col: if terminal.cursor_visible { terminal.cursor_col as i32 } else { -1 }, // Hide cursor when scrolled into scrollback buffer or when cursor is explicitly hidden
cursor_row: if terminal.cursor_visible { terminal.cursor_row as i32 } else { -1 }, cursor_col: if terminal.cursor_visible && terminal.scroll_offset == 0 { terminal.cursor_col as i32 } else { -1 },
cursor_row: if terminal.cursor_visible && terminal.scroll_offset == 0 { terminal.cursor_row as i32 } else { -1 },
cursor_style: match terminal.cursor_shape { cursor_style: match terminal.cursor_shape {
CursorShape::BlinkingBlock | CursorShape::SteadyBlock => 0, CursorShape::BlinkingBlock | CursorShape::SteadyBlock => 0,
CursorShape::BlinkingUnderline | CursorShape::SteadyUnderline => 1, CursorShape::BlinkingUnderline | CursorShape::SteadyUnderline => 1,
@@ -7098,11 +7329,14 @@ impl Renderer {
); );
} }
// Build dim overlay if needed // Build dim overlay if needed - use calculate_dim_overlay_bounds to extend
// edge panes to fill the terminal grid area (matching edge glow behavior)
let dim_overlay = if info.dim_factor < 1.0 { let dim_overlay = if info.dim_factor < 1.0 {
let overlay_alpha = 1.0 - info.dim_factor; let overlay_alpha = 1.0 - info.dim_factor;
let overlay_color = [0.0, 0.0, 0.0, overlay_alpha]; let overlay_color = [0.0, 0.0, 0.0, overlay_alpha];
Some((pane_x, pane_y, pane_width, pane_height, overlay_color)) // Pass raw grid-relative coordinates, the helper transforms to screen space
let (ox, oy, ow, oh) = self.calculate_dim_overlay_bounds(info.x, info.y, info.width, info.height);
Some((ox, oy, ow, oh, overlay_color))
} else { } else {
None None
}; };
@@ -7447,12 +7681,9 @@ impl Renderer {
render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..)); render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
render_pass.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint32); render_pass.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
// Draw bg + glyph indices (tab bar text uses legacy vertex rendering)
render_pass.draw_indexed(0..total_index_count as u32, 0, 0..1);
// ═══════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════
// INSTANCED QUAD RENDERING (tab bar backgrounds, borders, etc.) // INSTANCED QUAD RENDERING (tab bar backgrounds, borders, etc.)
// Rendered before cell content so backgrounds appear behind cells // Rendered FIRST so backgrounds appear behind text
// ═══════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════
if !self.quads.is_empty() { if !self.quads.is_empty() {
render_pass.set_pipeline(&self.quad_pipeline); render_pass.set_pipeline(&self.quad_pipeline);
@@ -7460,6 +7691,14 @@ impl Renderer {
render_pass.draw(0..4, 0..self.quads.len() as u32); render_pass.draw(0..4, 0..self.quads.len() as u32);
} }
// Draw bg + glyph indices (tab bar text uses legacy vertex rendering)
// Rendered AFTER quads so text appears on top of backgrounds
render_pass.set_pipeline(&self.glyph_pipeline);
render_pass.set_bind_group(0, &self.glyph_bind_group, &[]);
render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
render_pass.set_index_buffer(self.index_buffer.slice(..), wgpu::IndexFormat::Uint32);
render_pass.draw_indexed(0..total_index_count as u32, 0, 0..1);
// ═══════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════
// INSTANCED CELL RENDERING (Like Kitty's per-window VAO approach) // INSTANCED CELL RENDERING (Like Kitty's per-window VAO approach)
// Each pane has its own bind group with its own buffers. // Each pane has its own bind group with its own buffers.
@@ -7518,10 +7757,10 @@ impl Renderer {
// Rendered last so overlays appear on top of everything // Rendered last so overlays appear on top of everything
// ═══════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════
if !self.overlay_quads.is_empty() { if !self.overlay_quads.is_empty() {
// Upload overlay quads to the buffer (reusing the same buffer) // Upload overlay quads to the SEPARATE overlay buffer to avoid overwriting tab bar quads
self.queue.write_buffer(&self.quad_buffer, 0, bytemuck::cast_slice(&self.overlay_quads)); self.queue.write_buffer(&self.overlay_quad_buffer, 0, bytemuck::cast_slice(&self.overlay_quads));
render_pass.set_pipeline(&self.quad_pipeline); render_pass.set_pipeline(&self.quad_pipeline);
render_pass.set_bind_group(0, &self.quad_bind_group, &[]); render_pass.set_bind_group(0, &self.overlay_quad_bind_group, &[]);
render_pass.draw(0..4, 0..self.overlay_quads.len() as u32); render_pass.draw(0..4, 0..self.overlay_quads.len() as u32);
} }
} }
+7 -7
View File
@@ -144,6 +144,9 @@ struct VertexOutput {
// HELPER FUNCTIONS // HELPER FUNCTIONS
// ═══════════════════════════════════════════════════════════════════════════════ // ═══════════════════════════════════════════════════════════════════════════════
// Statusline default background color (0x1a1a1a in linear RGB)
const STATUSLINE_DEFAULT_BG: vec3<f32> = vec3<f32>(0.00972, 0.00972, 0.00972);
// Resolve a packed color to RGBA // Resolve a packed color to RGBA
fn resolve_color(packed: u32, is_foreground: bool) -> vec4<f32> { fn resolve_color(packed: u32, is_foreground: bool) -> vec4<f32> {
let color_type = packed & 0xFFu; let color_type = packed & 0xFFu;
@@ -152,7 +155,8 @@ fn resolve_color(packed: u32, is_foreground: bool) -> vec4<f32> {
if is_foreground { if is_foreground {
return color_table.colors[256]; return color_table.colors[256];
} else { } else {
return color_table.colors[257]; // Statusline uses a solid default background, not the terminal's transparent one
return vec4<f32>(STATUSLINE_DEFAULT_BG, 1.0);
} }
} else if color_type == COLOR_TYPE_INDEXED { } else if color_type == COLOR_TYPE_INDEXED {
let index = (packed >> 8u) & 0xFFu; let index = (packed >> 8u) & 0xFFu;
@@ -207,13 +211,9 @@ fn vs_statusline_bg(
let ndc_pos = pixel_to_ndc(positions[vertex_index], screen_size); let ndc_pos = pixel_to_ndc(positions[vertex_index], screen_size);
let fg = resolve_color(cell.fg, true); let fg = resolve_color(cell.fg, true);
var bg = resolve_color(cell.bg, false); let bg = resolve_color(cell.bg, false);
// For default background, use transparent // Statusline always has solid background (no transparency for default bg)
let bg_type = cell.bg & 0xFFu;
if bg_type == COLOR_TYPE_DEFAULT {
bg.a = 0.0;
}
var out: VertexOutput; var out: VertexOutput;
out.clip_position = vec4<f32>(ndc_pos, 0.0, 1.0); out.clip_position = vec4<f32>(ndc_pos, 0.0, 1.0);
+13
View File
@@ -517,6 +517,7 @@ impl Terminal {
/// Creates a new terminal with the given dimensions and scrollback limit. /// Creates a new terminal with the given dimensions and scrollback limit.
pub fn new(cols: usize, rows: usize, scrollback_limit: usize) -> Self { pub fn new(cols: usize, rows: usize, scrollback_limit: usize) -> Self {
log::info!("Terminal::new: cols={}, rows={}, scroll_bottom={}", cols, rows, rows.saturating_sub(1));
let grid = vec![vec![Cell::default(); cols]; rows]; let grid = vec![vec![Cell::default(); cols]; rows];
let line_map: Vec<usize> = (0..rows).collect(); let line_map: Vec<usize> = (0..rows).collect();
@@ -704,6 +705,8 @@ impl Terminal {
return; return;
} }
log::info!("Terminal::resize: {}x{} -> {}x{}", self.cols, self.rows, cols, rows);
let old_cols = self.cols; let old_cols = self.cols;
let old_rows = self.rows; let old_rows = self.rows;
@@ -1260,10 +1263,12 @@ impl Handler for Terminal {
} }
// Line feed, Vertical tab, Form feed // Line feed, Vertical tab, Form feed
'\x0A' | '\x0B' | '\x0C' => { '\x0A' | '\x0B' | '\x0C' => {
let old_row = self.cursor_row;
self.cursor_row += 1; self.cursor_row += 1;
if self.cursor_row > self.scroll_bottom { if self.cursor_row > self.scroll_bottom {
self.scroll_up(1); self.scroll_up(1);
self.cursor_row = self.scroll_bottom; self.cursor_row = self.scroll_bottom;
log::trace!("LF: scrolled at row {}, now at scroll_bottom {}", old_row, self.cursor_row);
} }
// Update cache after line change // Update cache after line change
cached_row = self.cursor_row; cached_row = self.cursor_row;
@@ -1609,17 +1614,23 @@ impl Handler for Terminal {
// Cursor Up // Cursor Up
'A' => { 'A' => {
let n = params.get(0, 1).max(1) as usize; let n = params.get(0, 1).max(1) as usize;
let old_row = self.cursor_row;
self.cursor_row = self.cursor_row.saturating_sub(n); self.cursor_row = self.cursor_row.saturating_sub(n);
log::trace!("CSI A: cursor up {} from row {} to {}", n, old_row, self.cursor_row);
} }
// Cursor Down // Cursor Down
'B' => { 'B' => {
let n = params.get(0, 1).max(1) as usize; let n = params.get(0, 1).max(1) as usize;
let old_row = self.cursor_row;
self.cursor_row = (self.cursor_row + n).min(self.rows - 1); self.cursor_row = (self.cursor_row + n).min(self.rows - 1);
log::trace!("CSI B: cursor down {} from row {} to {}", n, old_row, self.cursor_row);
} }
// Cursor Forward // Cursor Forward
'C' => { 'C' => {
let n = params.get(0, 1).max(1) as usize; let n = params.get(0, 1).max(1) as usize;
let old_col = self.cursor_col;
self.cursor_col = (self.cursor_col + n).min(self.cols - 1); self.cursor_col = (self.cursor_col + n).min(self.cols - 1);
log::trace!("CSI C: cursor forward {} from col {} to {}", n, old_col, self.cursor_col);
} }
// Cursor Back // Cursor Back
'D' => { 'D' => {
@@ -1641,7 +1652,9 @@ impl Handler for Terminal {
// Cursor Horizontal Absolute (CHA) // Cursor Horizontal Absolute (CHA)
'G' => { 'G' => {
let col = params.get(0, 1).max(1) as usize; let col = params.get(0, 1).max(1) as usize;
let old_col = self.cursor_col;
self.cursor_col = (col - 1).min(self.cols - 1); self.cursor_col = (col - 1).min(self.cols - 1);
log::trace!("CSI G: cursor to col {} (was {})", self.cursor_col, old_col);
} }
// Cursor Position // Cursor Position
'H' | 'f' => { 'H' | 'f' => {