fixed glow + dimming
This commit is contained in:
+53
-7
@@ -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<f32>,
|
||||
@@ -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<f32>(0.0, 0.0, 0.0, 1.0); // Black foreground
|
||||
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;
|
||||
}
|
||||
|
||||
// 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<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;
|
||||
|
||||
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<f32>(0.0, 0.0, 0.0, 1.0); // Black foreground
|
||||
bg = vec4<f32>(1.0, 1.0, 1.0, 1.0); // White background
|
||||
|
||||
+44
-21
@@ -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<Self, String> {
|
||||
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);
|
||||
}
|
||||
width_px,
|
||||
height_px
|
||||
).map_err(|e| format!("Failed to spawn PTY: {}", e))?;
|
||||
|
||||
let pty_fd = pty.as_raw_fd();
|
||||
|
||||
@@ -302,12 +306,20 @@ 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 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Write data to the PTY.
|
||||
fn write_to_pty(&mut self, data: &[u8]) {
|
||||
@@ -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<UserEvent> 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<UserEvent> 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
|
||||
};
|
||||
|
||||
+17
-1
@@ -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<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
|
||||
let master = openpt(OpenptFlags::RDWR | OpenptFlags::NOCTTY | OpenptFlags::CLOEXEC)
|
||||
.map_err(PtyError::OpenMaster)?;
|
||||
@@ -50,6 +51,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
|
||||
let fork_result = unsafe { libc::fork() };
|
||||
|
||||
+275
-36
@@ -786,6 +786,10 @@ pub struct Renderer {
|
||||
|
||||
/// GPU quads for overlay rendering (rendered on top of everything).
|
||||
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
|
||||
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
|
||||
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),
|
||||
// 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 = {
|
||||
@@ -6934,6 +7116,15 @@ impl Renderer {
|
||||
// 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() {
|
||||
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)
|
||||
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);
|
||||
}
|
||||
@@ -7027,6 +7254,9 @@ impl Renderer {
|
||||
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,12 +7681,9 @@ 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);
|
||||
@@ -7460,6 +7691,14 @@ impl Renderer {
|
||||
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)
|
||||
// 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
|
||||
// ═══════════════════════════════════════════════════════════════════
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -144,6 +144,9 @@ struct VertexOutput {
|
||||
// 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
|
||||
fn resolve_color(packed: u32, is_foreground: bool) -> vec4<f32> {
|
||||
let color_type = packed & 0xFFu;
|
||||
@@ -152,7 +155,8 @@ fn resolve_color(packed: u32, is_foreground: bool) -> vec4<f32> {
|
||||
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<f32>(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<f32>(ndc_pos, 0.0, 1.0);
|
||||
|
||||
@@ -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<usize> = (0..rows).collect();
|
||||
|
||||
@@ -704,6 +705,8 @@ impl Terminal {
|
||||
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' => {
|
||||
|
||||
Reference in New Issue
Block a user