From ad055d03264ca9b1ae81eb3abc3f12b8ef2f8585 Mon Sep 17 00:00:00 2001 From: Zacharias-Brohn Date: Thu, 18 Dec 2025 23:43:31 +0100 Subject: [PATCH] fixed glow + dimming --- src/glyph_shader.wgsl | 60 ++++++- src/main.rs | 71 ++++++--- src/pty.rs | 18 ++- src/renderer.rs | 315 ++++++++++++++++++++++++++++++++----- src/statusline_shader.wgsl | 14 +- src/terminal.rs | 13 ++ 6 files changed, 414 insertions(+), 77 deletions(-) diff --git a/src/glyph_shader.wgsl b/src/glyph_shader.wgsl index eb4efb0..e5dfda6 100644 --- a/src/glyph_shader.wgsl +++ b/src/glyph_shader.wgsl @@ -219,6 +219,43 @@ const CURSOR_BLOCK: u32 = 0u; const CURSOR_UNDERLINE: u32 = 1u; 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 struct CellVertexOutput { @builtin(position) clip_position: vec4, @@ -327,8 +364,8 @@ fn vs_cell_bg( bg = tmp; } - // Check if this cell is selected (per-cell flag set by CPU, respects xlimit) - let is_selected = (attrs & ATTR_SELECTED_BIT) != 0u; + // Check if this cell is selected using GridParams selection range + let is_selected = is_cell_selected(col, row); if is_selected { fg = vec4(0.0, 0.0, 0.0, 1.0); // Black foreground bg = vec4(1.0, 1.0, 1.0, 1.0); // White background @@ -346,9 +383,18 @@ fn vs_cell_bg( bg.a = 0.0; } - // Calculate cursor color - use fg color (inverted from bg) for visibility - // For block cursor, we'll use fg as the cursor background - var cursor_color = fg; + // Calculate cursor color + // If the cell is empty (no glyph), use default foreground color for cursor + // Otherwise use the cell's foreground color + var cursor_color: vec4; + 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; var out: CellVertexOutput; @@ -444,8 +490,8 @@ fn vs_cell_glyph( bg = tmp; } - // Check if this cell is selected (per-cell flag set by CPU, respects xlimit) - let is_selected = (attrs & ATTR_SELECTED_BIT) != 0u; + // Check if this cell is selected using GridParams selection range + let is_selected = is_cell_selected(col, row); if is_selected { fg = vec4(0.0, 0.0, 0.0, 1.0); // Black foreground bg = vec4(1.0, 1.0, 1.0, 1.0); // White background diff --git a/src/main.rs b/src/main.rs index 3c95ff8..9ded89f 100644 --- a/src/main.rs +++ b/src/main.rs @@ -270,19 +270,23 @@ impl Pane { /// Create a new pane with its own terminal and PTY. fn new(cols: usize, rows: usize, scrollback_lines: usize) -> Result { 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_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, - rows as u16, - cols as u16 * default_cell_width, - rows as u16 * default_cell_height, - ) { - log::warn!("Failed to set initial PTY size: {}", e); - } + rows as u16, + width_px, + height_px + ).map_err(|e| format!("Failed to spawn PTY: {}", e))?; let pty_fd = pty.as_raw_fd(); @@ -302,10 +306,18 @@ impl Pane { } /// 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) { + // 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); - if let Err(e) = self.pty.resize(cols as u16, rows as u16, width_px, height_px) { - log::warn!("Failed to resize PTY: {}", e); + + if size_changed { + if let Err(e) = self.pty.resize(cols as u16, rows as u16, width_px, height_px) { + log::warn!("Failed to resize PTY: {}", e); + } } } @@ -439,17 +451,19 @@ impl SplitNode { // Calculate how many cells fit let cols = (width / cell_width).floor() as usize; let rows = (height / cell_height).floor() as usize; - // Store actual cell-aligned dimensions (not allocated space) - let actual_width = cols.max(1) as f32 * cell_width; - let actual_height = rows.max(1) as f32 * cell_height; + // Store the full allocated dimensions (not just cell-aligned) + // This ensures edge glow and pane dimming cover the full pane area *geometry = PaneGeometry { x, y, - width: actual_width, - height: actual_height, + width, // Full allocated width + height, // Full allocated height cols: cols.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) } SplitNode::Split { horizontal, ratio, first, second } => { @@ -1954,14 +1968,17 @@ impl App { if !navigated { // No neighbor in that direction - trigger edge glow animation - // Add to existing glows (don't replace) so multiple can be visible - if let Some(geom) = active_pane_geom { + // Use renderer's helper to calculate proper screen-space glow bounds + 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( direction, - geom.x, - geom.y, - geom.width, - geom.height, + glow_x, + glow_y, + glow_width, + glow_height, )); } } @@ -2380,6 +2397,7 @@ impl ApplicationHandler for App { let scroll_offset = self.get_scroll_offset(); let content_row = screen_row as isize - scroll_offset as isize; 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(pane) = tab.active_pane_mut() { pane.selection = Some(Selection { start: pos, end: pos }); @@ -2505,8 +2523,13 @@ impl ApplicationHandler for App { // 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)) + let sel = pane.selection.as_ref() + .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 { None }; diff --git a/src/pty.rs b/src/pty.rs index b277060..4e20b89 100644 --- a/src/pty.rs +++ b/src/pty.rs @@ -35,7 +35,8 @@ pub struct Pty { impl Pty { /// Creates a new PTY and spawns a shell process. - pub fn spawn(shell: Option<&str>) -> Result { + /// 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 { // Open the PTY master let master = openpt(OpenptFlags::RDWR | OpenptFlags::NOCTTY | OpenptFlags::CLOEXEC) .map_err(PtyError::OpenMaster)?; @@ -49,6 +50,21 @@ impl Pty { // Get the slave name 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 // SAFETY: We're careful to only use async-signal-safe functions in the child diff --git a/src/renderer.rs b/src/renderer.rs index 403cd90..70f50c9 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -786,6 +786,10 @@ pub struct Renderer { /// GPU quads for overlay rendering (rendered on top of everything). overlay_quads: Vec, + /// 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::()) 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 let quad_pipeline_layout = device.create_pipeline_layout(&wgpu::PipelineLayoutDescriptor { label: Some("Quad Pipeline Layout"), @@ -2890,6 +2918,8 @@ impl Renderer { quad_pipeline, quad_bind_group, 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 } + /// 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. /// 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)> { @@ -4055,8 +4199,10 @@ impl Renderer { self.statusline_gpu_cells.clear(); // 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 { - (target_width / self.cell_width).floor() as usize + (target_width / self.cell_width).ceil() as usize } else { self.statusline_max_cols }; @@ -4224,9 +4370,16 @@ impl Renderer { } } 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); + // 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() { let component_fg = Self::pack_statusline_color(component.fg); let style = if component.bold { FontStyle::Bold } else { FontStyle::Regular }; @@ -4358,10 +4511,10 @@ impl Renderer { let arrow_char = '\u{E0B0}'; 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 { - fg: section_bg, // Arrow takes section bg color as its foreground - bg: default_bg, // Will be overwritten if there's a next section + fg: section_bg, // Arrow takes section bg color as its foreground + bg: next_section_bg, // Background is the next section's background decoration_fg: 0, sprite_idx, 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() } @@ -6705,6 +6871,7 @@ impl Renderer { Direction::Right => 3, }; + // Glow coordinates are already in screen space (transformed by calculate_edge_glow_bounds) glow_instances[i] = GlowInstance { direction, progress: glow.progress(), @@ -6753,6 +6920,10 @@ impl Renderer { // Sync palette from first terminal if let Some((terminal, _, _)) = panes.first() { 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()?; @@ -6794,19 +6965,20 @@ impl Renderer { TabBarPosition::Hidden => unreachable!(), }; - let tab_bar_bg = { - let [r, g, b] = self.palette.default_bg; - let factor = 0.85_f32; - [ - Self::srgb_to_linear((r as f32 / 255.0) * factor), - Self::srgb_to_linear((g as f32 / 255.0) * factor), - Self::srgb_to_linear((b as f32 / 255.0) * factor), - 1.0, - ] - }; + // Use same color as statusline: 0x1a1a1a (26, 26, 26) in sRGB + // Linear RGB value: ~0.00972 + let tab_bar_bg = [ + Self::srgb_to_linear(26.0 / 255.0), + Self::srgb_to_linear(26.0 / 255.0), + Self::srgb_to_linear(26.0 / 255.0), + 1.0, + ]; // 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); + log::debug!("render_panes: after tab bar rect, quads_count={}", self.quads.len()); // Render each tab let mut tab_x = 4.0_f32; @@ -6820,15 +6992,25 @@ impl Renderer { let tab_width = title_width.max(min_tab_width); let tab_bg = if is_active { + // Active tab: brightest - significantly brighter than tab bar 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(g as f32 / 255.0), - Self::srgb_to_linear(b as f32 / 255.0), + 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, ] } 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 = { @@ -6933,6 +7115,15 @@ impl Renderer { if panes.len() > 1 { // Tolerance for detecting adjacent panes (should be touching or very close) 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 for i in 0..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) if (a_right - b_x).abs() < adjacency_tolerance { // Check if they overlap vertically - let top = a_y.max(b_y); - let bottom = a_bottom.min(b_bottom); + let mut top = a_y.max(b_y); + let mut bottom = a_bottom.min(b_bottom); 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 let border_x = a_right - border_thickness / 2.0; 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 if (b_right - a_x).abs() < adjacency_tolerance { - let top = a_y.max(b_y); - let bottom = a_bottom.min(b_bottom); + let mut top = a_y.max(b_y); + let mut bottom = a_bottom.min(b_bottom); 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; 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) if (a_bottom - b_y).abs() < adjacency_tolerance { // Check if they overlap horizontally - let left = a_x.max(b_x); - let right = a_right.min(b_right); + let mut left = a_x.max(b_x); + let mut right = a_right.min(b_right); 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 let border_y = a_bottom - border_thickness / 2.0; 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 if (b_bottom - a_y).abs() < adjacency_tolerance { - let left = a_x.max(b_x); - let right = a_right.min(b_right); + let mut left = a_x.max(b_x); + let mut right = a_right.min(b_right); 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; self.render_overlay_rect(left, border_y, right - left, border_thickness, border_color); } @@ -7026,6 +7253,9 @@ impl Renderer { let pane_y = terminal_y_offset + grid_y_offset + info.y; let pane_width = info.width; 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) self.update_gpu_cells(terminal); @@ -7056,8 +7286,9 @@ impl Renderer { screen_height: self.height as f32, x_offset: pane_x, y_offset: pane_y, - cursor_col: if terminal.cursor_visible { terminal.cursor_col as i32 } else { -1 }, - cursor_row: if terminal.cursor_visible { terminal.cursor_row as i32 } else { -1 }, + // Hide cursor when scrolled into scrollback buffer or when cursor is explicitly hidden + 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 { CursorShape::BlinkingBlock | CursorShape::SteadyBlock => 0, 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 overlay_alpha = 1.0 - info.dim_factor; 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 { None }; @@ -7447,18 +7681,23 @@ impl Renderer { render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..)); 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.) - // Rendered before cell content so backgrounds appear behind cells + // Rendered FIRST so backgrounds appear behind text // ═══════════════════════════════════════════════════════════════════ if !self.quads.is_empty() { render_pass.set_pipeline(&self.quad_pipeline); render_pass.set_bind_group(0, &self.quad_bind_group, &[]); 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) @@ -7518,10 +7757,10 @@ impl Renderer { // Rendered last so overlays appear on top of everything // ═══════════════════════════════════════════════════════════════════ if !self.overlay_quads.is_empty() { - // Upload overlay quads to the buffer (reusing the same buffer) - self.queue.write_buffer(&self.quad_buffer, 0, bytemuck::cast_slice(&self.overlay_quads)); + // Upload overlay quads to the SEPARATE overlay buffer to avoid overwriting tab bar 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_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); } } diff --git a/src/statusline_shader.wgsl b/src/statusline_shader.wgsl index 30a516e..3629fcd 100644 --- a/src/statusline_shader.wgsl +++ b/src/statusline_shader.wgsl @@ -144,6 +144,9 @@ struct VertexOutput { // HELPER FUNCTIONS // ═══════════════════════════════════════════════════════════════════════════════ +// Statusline default background color (0x1a1a1a in linear RGB) +const STATUSLINE_DEFAULT_BG: vec3 = vec3(0.00972, 0.00972, 0.00972); + // Resolve a packed color to RGBA fn resolve_color(packed: u32, is_foreground: bool) -> vec4 { let color_type = packed & 0xFFu; @@ -152,7 +155,8 @@ fn resolve_color(packed: u32, is_foreground: bool) -> vec4 { if is_foreground { return color_table.colors[256]; } else { - return color_table.colors[257]; + // Statusline uses a solid default background, not the terminal's transparent one + return vec4(STATUSLINE_DEFAULT_BG, 1.0); } } else if color_type == COLOR_TYPE_INDEXED { 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 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 - let bg_type = cell.bg & 0xFFu; - if bg_type == COLOR_TYPE_DEFAULT { - bg.a = 0.0; - } + // Statusline always has solid background (no transparency for default bg) var out: VertexOutput; out.clip_position = vec4(ndc_pos, 0.0, 1.0); diff --git a/src/terminal.rs b/src/terminal.rs index de0f56a..c4ff761 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -517,6 +517,7 @@ impl Terminal { /// Creates a new terminal with the given dimensions and scrollback limit. 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 line_map: Vec = (0..rows).collect(); @@ -703,6 +704,8 @@ impl Terminal { if cols == self.cols && rows == self.rows { return; } + + log::info!("Terminal::resize: {}x{} -> {}x{}", self.cols, self.rows, cols, rows); let old_cols = self.cols; let old_rows = self.rows; @@ -1260,10 +1263,12 @@ impl Handler for Terminal { } // Line feed, Vertical tab, Form feed '\x0A' | '\x0B' | '\x0C' => { + let old_row = self.cursor_row; self.cursor_row += 1; if self.cursor_row > self.scroll_bottom { self.scroll_up(1); 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 cached_row = self.cursor_row; @@ -1609,17 +1614,23 @@ impl Handler for Terminal { // Cursor Up 'A' => { 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); + log::trace!("CSI A: cursor up {} from row {} to {}", n, old_row, self.cursor_row); } // Cursor Down 'B' => { 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); + log::trace!("CSI B: cursor down {} from row {} to {}", n, old_row, self.cursor_row); } // Cursor Forward 'C' => { 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); + log::trace!("CSI C: cursor forward {} from col {} to {}", n, old_col, self.cursor_col); } // Cursor Back 'D' => { @@ -1641,7 +1652,9 @@ impl Handler for Terminal { // Cursor Horizontal Absolute (CHA) 'G' => { 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); + log::trace!("CSI G: cursor to col {} (was {})", self.cursor_col, old_col); } // Cursor Position 'H' | 'f' => {