diff --git a/src/config.rs b/src/config.rs index 23fdbe1..f641829 100644 --- a/src/config.rs +++ b/src/config.rs @@ -387,7 +387,6 @@ impl Config { }; if !config_path.exists() { - log::info!("No config file found at {:?}, creating with defaults", config_path); let default_config = Self::default(); if let Err(e) = default_config.save() { log::warn!("Failed to write default config: {}", e); @@ -398,7 +397,6 @@ impl Config { match fs::read_to_string(&config_path) { Ok(contents) => match serde_json::from_str(&contents) { Ok(config) => { - log::info!("Loaded config from {:?}", config_path); config } Err(e) => { @@ -431,7 +429,6 @@ impl Config { .map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?; fs::write(&config_path, json)?; - log::info!("Saved config to {:?}", config_path); Ok(()) } } diff --git a/src/font_loader.rs b/src/font_loader.rs index eddad86..091e0f6 100644 --- a/src/font_loader.rs +++ b/src/font_loader.rs @@ -214,19 +214,6 @@ pub fn load_font_family(font_family: Option<&str>) -> (Box<[u8]>, FontRef<'stati // Try to use fontconfig to find the font family if let Some(family) = font_family { let paths = find_font_family_variants(family); - log::info!("Font family '{}' resolved to:", family); - for (i, path) in paths.iter().enumerate() { - let style = match i { - 0 => "Regular", - 1 => "Bold", - 2 => "Italic", - 3 => "BoldItalic", - _ => "Unknown", - }; - if let Some(p) = path { - log::info!(" {}: {:?}", style, p); - } - } // Load the regular font (required) if let Some(regular_path) = &paths[0] { @@ -277,11 +264,7 @@ pub fn load_font_family(font_family: Option<&str>) -> (Box<[u8]>, FontRef<'stati load_font_variant(std::path::Path::new(bold_italic)), ]; - log::info!("Loaded font from fallback paths:"); - log::info!(" Regular: {}", regular); - if variants[1].is_some() { log::info!(" Bold: {}", bold); } - if variants[2].is_some() { log::info!(" Italic: {}", italic); } - if variants[3].is_some() { log::info!(" BoldItalic: {}", bold_italic); } + return (font_data, primary_font, variants); } @@ -293,7 +276,7 @@ pub fn load_font_family(font_family: Option<&str>) -> (Box<[u8]>, FontRef<'stati let primary_font = regular_variant.clone_font(); let font_data = regular_variant.clone_data(); let variants: [Option; 4] = [Some(regular_variant), None, None, None]; - log::info!("Loaded NotoSansMono as fallback"); + return (font_data, primary_font, variants); } diff --git a/src/main.rs b/src/main.rs index 5cbd87d..69fbe30 100644 --- a/src/main.rs +++ b/src/main.rs @@ -219,6 +219,8 @@ impl Pane { /// Geometry of a pane in pixels. #[derive(Debug, Clone, Copy)] struct PaneGeometry { + /// Pane ID. + pane_id: PaneId, /// Left edge in pixels. x: f32, /// Top edge in pixels. @@ -260,6 +262,7 @@ impl SplitNode { SplitNode::Leaf { pane_id, geometry: PaneGeometry { + pane_id, x: 0.0, y: 0.0, width: 0.0, @@ -295,13 +298,14 @@ impl SplitNode { _border_width: f32, ) -> (f32, f32) { match self { - SplitNode::Leaf { geometry, .. } => { + SplitNode::Leaf { pane_id, geometry } => { // Calculate how many cells fit let cols = (width / cell_width).floor() as usize; let rows = (height / cell_height).floor() as usize; // Store the full allocated dimensions (not just cell-aligned) // This ensures edge glow and pane dimming cover the full pane area *geometry = PaneGeometry { + pane_id: *pane_id, x, y, width, // Full allocated width @@ -436,6 +440,30 @@ impl SplitNode { } } + /// Find the geometry of the pane that contains the given pixel position. + fn find_pane_at_pixel(&self, x: f64, y: f64) -> Option { + match self { + SplitNode::Leaf { geometry, .. } => { + if x >= geometry.x as f64 + && x < (geometry.x + geometry.width) as f64 + && y >= geometry.y as f64 + && y < (geometry.y + geometry.height) as f64 + { + Some(*geometry) + } else { + None + } + } + SplitNode::Split { + first, second, .. + } => { + first + .find_pane_at_pixel(x, y) + .or_else(|| second.find_pane_at_pixel(x, y)) + } + } + } + /// Find a neighbor pane in the given direction. /// Returns the pane ID of the neighbor, if any. fn find_neighbor( @@ -833,6 +861,17 @@ impl Tab { fn child_exited(&mut self) -> bool { self.check_exited_panes() } + + /// Focus a specific pane by ID. Returns true if focus changed. + fn focus_pane_by_id(&mut self, pane_id: PaneId) -> bool { + if self.panes.contains_key(&pane_id) { + let old = self.active_pane; + self.active_pane = pane_id; + old != pane_id + } else { + false + } + } } /// PID file location for single-instance support. @@ -854,7 +893,6 @@ fn signal_existing_instance() -> bool { if alive { // Send SIGUSR1 to show window - log::info!("Signaling existing instance (PID {})", pid); unsafe { libc::kill(pid, libc::SIGUSR1) }; return true; } else { @@ -1416,13 +1454,8 @@ struct App { impl App { fn new() -> Self { let config = Config::load(); - log::info!("Config: font_size={}", config.font_size); let action_map = config.keybindings.build_action_map(); - log::info!("Action map built with {} bindings:", action_map.len()); - for (key, action) in &action_map { - log::info!(" {:?} => {:?}", key, action); - } Self { window: None, @@ -1463,7 +1496,6 @@ impl App { /// Reload configuration from disk and apply changes. fn reload_config(&mut self) { - log::info!("Reloading configuration..."); let new_config = Config::load(); // Check what changed and apply updates @@ -1475,6 +1507,7 @@ impl App { > 0.01; let tab_bar_changed = new_config.tab_bar_position != self.config.tab_bar_position; + let new_font_size = new_config.font_size; // Update the config self.config = new_config; @@ -1486,23 +1519,14 @@ impl App { if let Some(renderer) = &mut self.renderer { if opacity_changed { renderer.set_background_opacity(self.config.background_opacity); - log::info!( - "Updated background opacity to {}", - self.config.background_opacity - ); } if tab_bar_changed { renderer.set_tab_bar_position(self.config.tab_bar_position); - log::info!( - "Updated tab bar position to {:?}", - self.config.tab_bar_position - ); } - if font_size_changed { - renderer.set_font_size(self.config.font_size); - log::info!("Updated font size to {}", self.config.font_size); + if font_size_changed { + self.config.font_size = new_font_size; // Font size change requires resize to recalculate cell dimensions self.resize_all_panes(); } @@ -1510,8 +1534,6 @@ impl App { // Request redraw to apply visual changes self.request_redraw(); - - log::info!("Configuration reloaded successfully"); } /// Request a window redraw if window is available. @@ -1525,8 +1547,6 @@ impl App { /// Create a new tab and start its I/O thread. /// Returns the index of the new tab. fn create_tab(&mut self, cols: usize, rows: usize) -> Option { - log::info!("Creating new tab with {}x{} terminal", cols, rows); - match Tab::new(cols, rows, self.config.scrollback_lines) { Ok(tab) => { let tab_idx = self.tabs.len(); @@ -1539,11 +1559,6 @@ impl App { self.tabs.push(tab); self.active_tab = tab_idx; - log::info!( - "Tab {} created (total: {})", - tab_idx, - self.tabs.len() - ); Some(tab_idx) } Err(e) => { @@ -1749,17 +1764,6 @@ impl App { } } - #[cfg(feature = "render_timing")] - { - let elapsed = io_start.elapsed(); - log::info!("[IO-{}] Thread exiting: loops={} total_bytes={} elapsed={:?} throughput={:.1} MB/s", - pane_id.0, loop_count, total_bytes_read, elapsed, - total_bytes_read as f64 / elapsed.as_secs_f64() / 1_000_000.0); - } - - #[cfg(not(feature = "render_timing"))] - let _ = (io_start, loop_count, total_bytes_read); // silence unused warnings - log::debug!("PTY I/O thread for pane {} exiting", pane_id.0); }) .expect("Failed to spawn PTY I/O thread"); @@ -1771,8 +1775,6 @@ impl App { return; // Window already exists } - log::info!("Creating window"); - let mut window_attributes = Window::default_attributes() .with_title("ZTerm") .with_inner_size(PhysicalSize::new(800, 600)); @@ -1802,13 +1804,10 @@ impl App { self.window = Some(window); self.renderer = Some(renderer); self.should_create_window = false; - - log::info!("Window created: {}x{} cells", cols, rows); } /// Destroy the window but keep terminal state. fn destroy_window(&mut self) { - log::info!("Destroying window (keeping terminal alive)"); self.renderer = None; self.window = None; } @@ -1977,8 +1976,8 @@ impl App { .saturating_sub(pane.last_scrollback_len) as isize; if let Some(ref mut selection) = pane.selection { - selection.start.row -= lines_added; - selection.end.row -= lines_added; + selection.start.row = selection.start.row.saturating_sub(lines_added); + selection.end.row = selection.end.row.saturating_sub(lines_added); } pane.last_scrollback_len = scrollback_len; } @@ -1990,6 +1989,7 @@ impl App { let active_tab_idx = self.active_tab; let fade_duration_ms = self.config.inactive_pane_fade_ms; let inactive_dim = self.config.inactive_pane_dim; + let has_pending_redraw = self.renderer.as_ref().map(|r| r.has_pending_redraw()).unwrap_or(false); if let Some(renderer) = &mut self.renderer { if let Some(tab) = self.tabs.get_mut(active_tab_idx) { @@ -2019,7 +2019,8 @@ impl App { .unwrap_or(false) }); - if !has_dirty_content && !self.needs_redraw && self.edge_glows.is_empty() && !fade_in_progress { + let has_selection = tab.panes.values().any(|p| p.selection.is_some()); + if !has_dirty_content && !self.needs_redraw && self.edge_glows.is_empty() && !fade_in_progress && !has_selection && !has_pending_redraw { let image_animation_in_progress = tab.panes.values().any(|p| { p.terminal.image_storage.has_animations() }); @@ -2083,14 +2084,9 @@ impl App { inactive_dim }); - // Convert selection to screen coords for this pane - let selection = if is_active { - pane.selection.as_ref().and_then(|sel| { - sel.to_screen_coords(scroll_offset, geom.rows) - }) - } else { - None - }; + let selection = pane.selection.as_ref().and_then(|sel| { + sel.to_screen_coords(scroll_offset, geom.rows) + }); let render_info = PaneRenderInfo { pane_id: pane_id.0, @@ -2155,9 +2151,9 @@ impl App { StatuslineContent::Sections(Vec::new()) } }) - .unwrap_or_default(); + .unwrap_or_default(); - match renderer.render_panes( + match renderer.render_panes( &pane_render_data, num_tabs, active_tab_idx, @@ -2165,13 +2161,17 @@ impl App { self.config.edge_glow_intensity, &statusline_content, ) { - Ok(_) => { + Ok(_) => { // Clear dirty lines after successful render (like Kitty's linebuf_mark_line_clean) for (pane_id, _) in &geometries { if let Some(pane) = tab.panes.get_mut(pane_id) { pane.terminal.clear_dirty_lines(); } } + // Clear pending redraw and needs_redraw + if let Some(renderer) = &mut self.renderer { + renderer.clear_pending_redraw(); + } // Only clear needs_redraw if no pane fade is still in progress let any_fade_in_progress = geometries.iter().any(|(pane_id, _)| { tab.panes @@ -2182,12 +2182,12 @@ impl App { }) .unwrap_or(false) }); - if !any_fade_in_progress { + let any_selection = tab.panes.values().any(|p| p.selection.is_some()); + if !any_fade_in_progress && !any_selection { self.needs_redraw = false; } } Err(wgpu::SurfaceError::Lost) => { - log::info!("RENDER LOST: resizing"); renderer.resize(renderer.width, renderer.height); } Err(wgpu::SurfaceError::OutOfMemory) => { @@ -2273,6 +2273,19 @@ impl App { .unwrap_or(false) } + fn has_mouse_tracking_for_pane(&self, pane_id: PaneId) -> bool { + self.active_tab() + .and_then(|t| t.panes.get(&pane_id)) + .map(|p| p.terminal.mouse_tracking != MouseTrackingMode::None) + .unwrap_or(false) + } + + /// Find the pane geometry at a given pixel position in the active tab. + fn pane_at_pixel(&self, x: f64, y: f64) -> Option { + self.active_tab() + .and_then(|t| t.split_root.find_pane_at_pixel(x, y)) + } + fn get_mouse_modifiers(&self) -> u8 { let mod_state = self.modifiers.state(); let mut mods = 0u8; @@ -2290,27 +2303,30 @@ impl App { fn send_mouse_event( &mut self, + pane_id: PaneId, button: u8, col: u16, row: u16, pressed: bool, is_motion: bool, ) { - let seq = { - let Some(pane) = self.active_pane() else { - return; - }; - pane.terminal.encode_mouse( - button, - col, - row, - pressed, - is_motion, - self.get_mouse_modifiers(), - ) + let modifiers = self.get_mouse_modifiers(); + let Some(tab) = self.active_tab_mut() else { + return; }; + let Some(pane) = tab.panes.get_mut(&pane_id) else { + return; + }; + let seq = pane.terminal.encode_mouse( + button, + col, + row, + pressed, + is_motion, + modifiers, + ); if !seq.is_empty() { - self.write_to_pty(&seq); + pane.pty.write(&seq); } } @@ -2369,8 +2385,6 @@ impl App { return false; }; - log::info!("Executing action: {:?}", action); - self.execute_action(action); true } @@ -2384,13 +2398,19 @@ impl App { self.paste_from_clipboard(); } Action::NewTab => { - if let Some(renderer) = &self.renderer { - let (cols, rows) = renderer.terminal_size(); - self.create_tab(cols, rows); - // Resize the new tab to calculate pane geometries - self.resize_all_panes(); - self.request_redraw(); + let (cols, rows) = if let Some(renderer) = &self.renderer { + renderer.terminal_size() + } else { + return; + }; + self.create_tab(cols, rows); + // Resize the new tab to calculate pane geometries + self.resize_all_panes(); + self.needs_redraw = true; + if let Some(renderer) = &mut self.renderer { + renderer.force_full_redraw(); } + self.request_redraw(); } Action::ClosePane => { self.close_active_pane(); @@ -2477,11 +2497,6 @@ impl App { self.resize_all_panes(); self.needs_redraw = true; self.request_redraw(); - log::info!( - "Split pane (horizontal={}), new pane {}", - horizontal, - pane_id.0 - ); } } @@ -2855,23 +2870,21 @@ impl ApplicationHandler for App { fn resumed(&mut self, event_loop: &ActiveEventLoop) { #[cfg(feature = "render_timing")] let start = std::time::Instant::now(); - if self.window.is_none() { + if self.window.is_none() { self.create_window(event_loop); } - #[cfg(feature = "render_timing")] - log::info!("App resumed (window creation): {:?}", start.elapsed()); } fn user_event(&mut self, event_loop: &ActiveEventLoop, event: UserEvent) { match event { UserEvent::ShowWindow => { - log::info!("Received signal to show window"); + if self.window.is_none() { self.create_window(event_loop); } } UserEvent::Tick => { - log::info!("[MAIN] Tick received"); + // Check for fatal render errors from previous frames if self.render_fatal_error { log::error!("Fatal render error occurred, exiting"); @@ -3011,16 +3024,6 @@ impl ApplicationHandler for App { let _ = proxy.send_event(UserEvent::Tick); } } - - // Log every tick during benchmark for analysis (only with render_timing feature) - #[cfg(feature = "render_timing")] - { - let tick_time = tick_start.elapsed(); - if tick_time.as_millis() > 5 { - log::info!("[TICK] render={:?} total={:?} has_more={} rendered={}", - render_time, tick_time, any_has_more, should_render); - } - } } UserEvent::ConfigReloaded => { self.reload_config(); @@ -3036,7 +3039,7 @@ impl ApplicationHandler for App { ) { match event { WindowEvent::CloseRequested => { - log::info!("Window close requested - hiding window"); + self.destroy_window(); // Don't exit - keep running headless } @@ -3046,7 +3049,7 @@ impl ApplicationHandler for App { } WindowEvent::ScaleFactorChanged { scale_factor, .. } => { - log::info!("Scale factor changed to {}", scale_factor); + let should_resize = if let Some(renderer) = &mut self.renderer { renderer.set_scale_factor(scale_factor) } else { @@ -3085,23 +3088,45 @@ impl ApplicationHandler for App { }; if lines != 0 { - if self.has_mouse_tracking() { - if let Some(renderer) = &self.renderer { - if let Some((col, row)) = renderer.pixel_to_cell( - self.cursor_position.x, - self.cursor_position.y, - ) { - let button = if lines > 0 { 64 } else { 65 }; - let count = lines.abs().min(3); - for _ in 0..count { - self.send_mouse_event( - button, col as u16, row as u16, true, - false, - ); + if let Some(tab) = self.active_tab() { + if let Some(pane_geom) = tab.split_root.find_pane_at_pixel( + self.cursor_position.x, + self.cursor_position.y, + ) { + if let Some(renderer) = &self.renderer { + if let Some((col, row)) = renderer.pane_pixel_to_cell( + self.cursor_position.x, + self.cursor_position.y, + pane_geom.x, + pane_geom.y, + pane_geom.width, + pane_geom.height, + pane_geom.cols, + pane_geom.rows, + ) { + if self.has_mouse_tracking_for_pane(pane_geom.pane_id) { + let button = if lines > 0 { 64 } else { 65 }; + let count = lines.abs().min(3); + let modifiers = self.get_mouse_modifiers(); + for _ in 0..count { + if let Some(active_tab) = self.active_tab_mut() { + if let Some(pane) = active_tab.panes.get_mut(&pane_geom.pane_id) { + let seq = pane.terminal.encode_mouse( + button, col as u16, row as u16, true, false, + modifiers, + ); + if !seq.is_empty() { + pane.pty.write(&seq); + } + } + } + } + } } } } - } else if let Some(tab) = self.active_tab_mut() { + } + if let Some(tab) = self.active_tab_mut() { // Positive lines = scroll wheel up = go into history (increase offset) if let Some(pane) = tab.active_pane_mut() { pane.terminal.scroll(lines); @@ -3114,48 +3139,146 @@ impl ApplicationHandler for App { WindowEvent::CursorMoved { position, .. } => { self.cursor_position = position; - let is_selecting = - self.active_pane().map(|p| p.is_selecting).unwrap_or(false); + // Find the pane under the mouse for pane-aware coordinate conversion + let pane_geom = if let Some(tab) = self.active_tab() { + tab.split_root.find_pane_at_pixel(position.x, position.y) + } else { + None + }; - if is_selecting && self.has_mouse_tracking() { - // Send mouse drag/motion events to PTY for apps like Neovim - if let Some(renderer) = &self.renderer { - if let Some((col, row)) = - renderer.pixel_to_cell(position.x, position.y) - { - // Button 0 (left) with motion flag - self.send_mouse_event( - 0, col as u16, row as u16, true, true, - ); + // Switch focus if dragging on a different pane + if self.mouse_down_pos.is_some() { + if let Some(geom) = &pane_geom { + if let Some(tab) = self.active_tab_mut() { + if tab.active_pane != geom.pane_id { + if tab.focus_pane_by_id(geom.pane_id) { + self.needs_redraw = true; + } + } } } - } else if is_selecting && !self.has_mouse_tracking() { + } + + // Check is_selecting on both the active pane and the pane under the mouse + let active_is_selecting = + self.active_pane().map(|p| p.is_selecting).unwrap_or(false); + let mouse_pane_is_selecting = pane_geom + .as_ref() + .and_then(|g| self.active_tab().and_then(|t| t.panes.get(&g.pane_id))) + .map(|p| p.is_selecting) + .unwrap_or(false); + let is_selecting = active_is_selecting || mouse_pane_is_selecting; + + let mouse_tracking = pane_geom + .as_ref() + .map(|g| self.has_mouse_tracking_for_pane(g.pane_id)) + .unwrap_or(false); + + if is_selecting && mouse_tracking { + let modifiers = self.get_mouse_modifiers(); + // Send mouse drag/motion events to PTY for apps like Neovim + if let Some(renderer) = &self.renderer { + if let Some(geom) = pane_geom { + if let Some((col, row)) = renderer.pane_pixel_to_cell( + position.x, position.y, + geom.x, geom.y, + geom.width, geom.height, + geom.cols, geom.rows, + ) { + // Button 0 (left) with motion flag + if let Some(active_tab) = self.active_tab_mut() { + if let Some(pane) = active_tab.panes.get_mut(&geom.pane_id) { + let seq = pane.terminal.encode_mouse( + 0, col as u16, row as u16, true, true, + modifiers, + ); + if !seq.is_empty() { + pane.pty.write(&seq); + } + } + } + } + } + } + // Also update terminal-native selection for rendering + if let Some(renderer) = &self.renderer { + if let Some(geom) = pane_geom { + if let Some((col, screen_row)) = renderer.pane_pixel_to_cell( + position.x, position.y, + geom.x, geom.y, + geom.width, geom.height, + geom.cols, geom.rows, + ) { + let scroll_offset = if let Some(tab) = self.active_tab() { + tab.panes.get(&geom.pane_id) + .map(|p| p.terminal.scroll_offset) + .unwrap_or(0) + } else { 0 }; + let content_row = + screen_row as isize - scroll_offset as isize; + + if let Some(active_tab) = self.active_tab_mut() { + if let Some(pane) = active_tab.panes.get_mut(&geom.pane_id) { + if let Some(ref mut selection) = + pane.selection + { + selection.end = CellPosition { + col, + row: content_row, + }; + // Force GPU buffer upload so selection renders correctly + if let Some(renderer) = &mut self.renderer { + renderer.force_full_redraw(); + } + } + } + } + } + } + } + } else if is_selecting && !mouse_tracking { // Terminal-native selection if let Some(renderer) = &self.renderer { - if let Some((col, screen_row)) = - renderer.pixel_to_cell(position.x, position.y) - { - let scroll_offset = self.get_scroll_offset(); - let content_row = - screen_row as isize - scroll_offset as isize; + if let Some(geom) = pane_geom { + if let Some((col, screen_row)) = renderer.pane_pixel_to_cell( + position.x, position.y, + geom.x, geom.y, + geom.width, geom.height, + geom.cols, geom.rows, + ) { + let scroll_offset = if let Some(tab) = self.active_tab() { + tab.panes.get(&geom.pane_id) + .map(|p| p.terminal.scroll_offset) + .unwrap_or(0) + } else { 0 }; + let content_row = + screen_row as isize - scroll_offset as isize; - if let Some(tab) = self.active_tab_mut() { - if let Some(pane) = tab.active_pane_mut() { - if let Some(ref mut selection) = - pane.selection - { - selection.end = CellPosition { - col, - row: content_row, - }; - self.request_redraw(); + if let Some(active_tab) = self.active_tab_mut() { + if let Some(pane) = active_tab.panes.get_mut(&geom.pane_id) { + if let Some(ref mut selection) = + pane.selection + { + selection.end = CellPosition { + col, + row: content_row, + }; + if let Some(renderer) = &mut self.renderer { + renderer.force_full_redraw(); + } + self.request_redraw(); + } } } } } } } else if let Some(down_pos) = self.mouse_down_pos { - if !self.has_mouse_tracking() { + let mouse_pane_tracking = pane_geom + .as_ref() + .map(|g| self.has_mouse_tracking_for_pane(g.pane_id)) + .unwrap_or(false); + if !mouse_pane_tracking { let dx = position.x - down_pos.x; let dy = position.y - down_pos.y; let distance_sq = dx * dx + dy * dy; @@ -3170,40 +3293,56 @@ impl ApplicationHandler for App { if distance_sq > threshold * threshold { // Dragged far enough, start selection if let Some(renderer) = &self.renderer { - if let Some((start_col, start_screen_row)) = - renderer - .pixel_to_cell(down_pos.x, down_pos.y) - { - if let Some((end_col, end_screen_row)) = - renderer.pixel_to_cell( - position.x, position.y, + if let Some(geom) = pane_geom { + if let Some((start_col, start_screen_row)) = + renderer.pane_pixel_to_cell( + down_pos.x, down_pos.y, + geom.x, geom.y, + geom.width, geom.height, + geom.cols, geom.rows, ) { - let scroll_offset = - self.get_scroll_offset() as isize; - let start_pos = CellPosition { - col: start_col, - row: start_screen_row as isize - - scroll_offset, - }; - let end_pos = CellPosition { - col: end_col, - row: end_screen_row as isize - - scroll_offset, - }; - - if let Some(tab) = self.active_tab_mut() + if let Some((end_col, end_screen_row)) = + renderer.pane_pixel_to_cell( + position.x, position.y, + geom.x, geom.y, + geom.width, geom.height, + geom.cols, geom.rows, + ) { - if let Some(pane) = - tab.active_pane_mut() + let scroll_offset = if let Some(tab) = self.active_tab() { + tab.panes.get(&geom.pane_id) + .map(|p| p.terminal.scroll_offset) + .unwrap_or(0) as isize + } else { 0 }; + let start_pos = CellPosition { + col: start_col, + row: start_screen_row as isize + - scroll_offset, + }; + let end_pos = CellPosition { + col: end_col, + row: end_screen_row as isize + - scroll_offset, + }; + + if let Some(active_tab) = self.active_tab_mut() { - pane.selection = - Some(Selection { - start: start_pos, - end: end_pos, - }); - pane.is_selecting = true; - self.request_redraw(); + if let Some(pane) = + active_tab.panes.get_mut(&geom.pane_id) + { + pane.selection = + Some(Selection { + start: start_pos, + end: end_pos, + }); + pane.is_selecting = true; + // Force GPU buffer upload so selection renders correctly + if let Some(renderer) = &mut self.renderer { + renderer.force_full_redraw(); + } + self.request_redraw(); + } } } } @@ -3222,34 +3361,74 @@ impl ApplicationHandler for App { _ => return, }; - if self.has_mouse_tracking() { + // Find the pane under the mouse for pane-aware coordinate conversion + let pane_geom = if let Some(tab) = self.active_tab() { + tab.split_root.find_pane_at_pixel( + self.cursor_position.x, + self.cursor_position.y, + ) + } else { + None + }; + + // Switch focus if clicking on a different pane + if let Some(geom) = &pane_geom { + if let Some(tab) = self.active_tab_mut() { + if tab.active_pane != geom.pane_id { + if tab.focus_pane_by_id(geom.pane_id) { + self.needs_redraw = true; + } + } + } + } + + let mouse_tracking = pane_geom + .as_ref() + .map(|g| self.has_mouse_tracking_for_pane(g.pane_id)) + .unwrap_or(false); + if mouse_tracking { if let Some(renderer) = &self.renderer { - if let Some((col, row)) = renderer.pixel_to_cell( - self.cursor_position.x, - self.cursor_position.y, - ) { - let pressed = state == ElementState::Pressed; - self.send_mouse_event( - button_code, - col as u16, - row as u16, - pressed, - false, - ); - if button == MouseButton::Left { - if let Some(tab) = self.active_tab_mut() { - if let Some(pane) = tab.active_pane_mut() { - pane.is_selecting = pressed; + if let Some(geom) = pane_geom { + if let Some((col, row)) = renderer.pane_pixel_to_cell( + self.cursor_position.x, + self.cursor_position.y, + geom.x, geom.y, + geom.width, geom.height, + geom.cols, geom.rows, + ) { + let pressed = state == ElementState::Pressed; + let modifiers = self.get_mouse_modifiers(); + if let Some(active_tab) = self.active_tab_mut() { + if let Some(pane) = active_tab.panes.get_mut(&geom.pane_id) { + let seq = pane.terminal.encode_mouse( + button_code, + col as u16, + row as u16, + pressed, + false, + modifiers, + ); + if !seq.is_empty() { + pane.pty.write(&seq); + } + if button == MouseButton::Left { + pane.is_selecting = pressed; + } } } } } } - if let Some(tab) = self.active_tab_mut() { - if let Some(pane) = tab.active_pane_mut() { - pane.selection = None; + if mouse_tracking { + if let Some(tab) = self.active_tab_mut() { + if let Some(pane) = tab.active_pane_mut() { + pane.selection = None; + } } } + if let Some(renderer) = &mut self.renderer { + renderer.force_full_redraw(); + } } else if button == MouseButton::Left { match state { ElementState::Pressed => { @@ -3260,6 +3439,9 @@ impl ApplicationHandler for App { pane.is_selecting = false; } } + if let Some(renderer) = &mut self.renderer { + renderer.force_full_redraw(); + } } ElementState::Released => { self.mouse_down_pos = None; @@ -3313,12 +3495,21 @@ impl ApplicationHandler for App { dl[0] != 0 || dl[1] != 0 || dl[2] != 0 || dl[3] != 0 }) }); - let need_render = any_dirty + let need_render = any_dirty || self.needs_redraw || !self.edge_glows.is_empty() || self.tabs.iter().any(|tab| { tab.panes.values().any(|pane| pane.terminal.image_storage.has_animations()) - }); + }) + || self.tabs.iter().any(|tab| { + tab.panes.values().any(|pane| { + pane.selection.is_some() || pane.is_selecting + }) + }) + || self.renderer.as_ref().map(|r| r.has_pending_redraw()).unwrap_or(false); + + if need_render { + } if !need_render { return; @@ -3335,33 +3526,11 @@ impl ApplicationHandler for App { // Log cumulative stats every second (only with render_timing feature) #[cfg(feature = "render_timing")] if self.last_stats_log.elapsed() >= Duration::from_secs(1) { - let parse_ms = self.total_parse_ns as f64 / 1_000_000.0; - let render_ms = self.total_render_ns as f64 / 1_000_000.0; - log::info!( - "STATS: parse={:.1}ms/{} render={:.1}ms/{} ratio={:.2}", - parse_ms, - self.parse_count, - render_ms, - self.render_count, - if parse_ms > 0.0 { - render_ms / parse_ms - } else { - 0.0 - } - ); self.total_parse_ns = 0; self.total_render_ns = 0; self.parse_count = 0; self.render_count = 0; - self.last_stats_log = std::time::Instant::now(); - } - - #[cfg(feature = "render_timing")] - { - let frame_time = frame_start.elapsed(); - if frame_time.as_millis() > 16 { - log::info!("Slow frame: {:?}", frame_time); - } + self.last_stats_log = std::time::Instant::now(); } } @@ -3369,10 +3538,9 @@ impl ApplicationHandler for App { } } - fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { + fn about_to_wait(&mut self, event_loop: &ActiveEventLoop) { // Check if all tabs have exited if self.tabs.is_empty() { - log::info!("All tabs closed, exiting"); event_loop.exit(); return; } @@ -3382,7 +3550,6 @@ impl ApplicationHandler for App { let mut tabs_removed = false; while i < self.tabs.len() { if self.tabs[i].child_exited() { - log::info!("Tab {} shell exited", i); self.tabs.remove(i); tabs_removed = true; if self.active_tab >= self.tabs.len() && !self.tabs.is_empty() { @@ -3403,7 +3570,7 @@ impl ApplicationHandler for App { } if self.tabs.is_empty() { - log::info!("All tabs closed, exiting"); + event_loop.exit(); return; } @@ -3494,7 +3661,7 @@ fn setup_config_watcher( return None; } - log::info!("Config hot-reload enabled, watching {:?}", watch_path); + Some(watcher) } @@ -3504,11 +3671,11 @@ fn main() { ) .init(); - log::info!("Starting ZTerm"); + // Check for existing instance if signal_existing_instance() { - log::info!("Signaled existing instance, exiting"); + return; } diff --git a/src/renderer.rs b/src/renderer.rs index e445d50..d65a1d3 100644 --- a/src/renderer.rs +++ b/src/renderer.rs @@ -1552,6 +1552,53 @@ impl Renderer { Some((col, row)) } + /// Converts a pixel position to a terminal cell position relative to a specific pane. + /// `pane_x` and `pane_y` are the pane's pixel offset within the grid area. + /// `pane_width` and `pane_height` are the pane's dimensions in pixels. + /// `pane_cols` and `pane_rows` are the pane's dimensions in cells. + /// Returns None if the position is outside this pane's area. + pub fn pane_pixel_to_cell( + &self, + x: f64, + y: f64, + pane_x: f32, + pane_y: f32, + pane_width: f32, + pane_height: f32, + pane_cols: usize, + pane_rows: usize, + ) -> Option<(usize, usize)> { + let terminal_y_offset = self.terminal_y_offset(); + let grid_x_offset = self.grid_x_offset(); + let grid_y_offset = self.grid_y_offset(); + + // Convert pane pixel offset to screen pixel offset + let pane_screen_x = grid_x_offset + pane_x; + let pane_screen_y = terminal_y_offset + grid_y_offset + pane_y; + + // Check if position is within this pane's screen bounds + if (x as f32) < pane_screen_x + || (x as f32) >= pane_screen_x + pane_width + || (y as f32) < pane_screen_y + || (y as f32) >= pane_screen_y + pane_height + { + return None; + } + + // Calculate cell position relative to pane origin + let local_x = (x as f32) - pane_screen_x; + let local_y = (y as f32) - pane_screen_y; + + let col = (local_x / self.cell_metrics.cell_width as f32).floor() as usize; + let row = (local_y / self.cell_metrics.cell_height as f32).floor() as usize; + + if col >= pane_cols || row >= pane_rows { + return None; + } + + Some((col, row)) + } + /// Updates the scale factor and recalculates font/cell dimensions. /// Returns true if the cell dimensions changed (terminal needs resize). pub fn set_scale_factor(&mut self, new_scale: f64) -> bool { @@ -1589,11 +1636,6 @@ impl Renderer { // Update the font units to pixels scale factor self.font_units_to_px = self.font_size / self.primary_font.height_unscaled(); - log::info!( - "Scale factor changed to {}: font {}px -> {}px, cell: {}x{}, baseline: {}", - new_scale, self.base_font_size, self.font_size, self.cell_metrics.cell_width, self.cell_metrics.cell_height, self.cell_metrics.baseline - ); - // Reset atlas and all sprite/glyph caches (includes cursor sprite creation) self.reset_atlas(); @@ -1648,11 +1690,6 @@ impl Renderer { // Update the font units to pixels scale factor self.font_units_to_px = self.font_size / self.primary_font.height_unscaled(); - log::info!( - "Font size changed to {}px -> {}px, cell: {}x{}, baseline: {}", - size, self.font_size, self.cell_metrics.cell_width, self.cell_metrics.cell_height, self.cell_metrics.baseline - ); - // Reset atlas and all sprite/glyph caches (includes cursor sprite creation) self.reset_atlas(); @@ -1666,7 +1703,7 @@ impl Renderer { /// NOTE: This should ONLY be called for font/scale changes, NOT when atlas is full /// (for that case, we add a new layer via add_atlas_layer()). fn reset_atlas(&mut self) { - log::info!("Resetting glyph atlas (font/scale changed)"); + // Clear all glyph caches - they need to be re-rasterized at new size self.char_cache.clear(); @@ -2146,6 +2183,16 @@ impl Renderer { self.cells_dirty = true; } + /// Check if a full redraw is pending. + pub fn has_pending_redraw(&self) -> bool { + self.cells_dirty + } + + /// Called after a render to clear the pending redraw flag. + pub fn clear_pending_redraw(&mut self) { + self.cells_dirty = false; + } + /// Update GPU cell buffer from terminal content. /// Like Kitty, this only processes dirty lines to minimize work. /// @@ -2163,14 +2210,17 @@ impl Renderer { self.cells_dirty = true; } + // Check if this terminal has any dirty lines + let has_dirty = terminal.has_any_dirty_line(); + // First pass: ensure all characters have sprites // This needs mutable access to self for sprite creation // Like Kitty's render_line(), detect PUA+space patterns for multi-cell rendering // OPTIMIZATION: Only process dirty lines or when full rebuild is needed // OPTIMIZATION: Use get_visible_row() to avoid Vec allocation for row_idx in 0..rows { - // Skip clean lines (unless size changed, which sets cells_dirty) - if !self.cells_dirty && !terminal.is_line_dirty(row_idx) { + // Skip clean lines (unless size changed or terminal has dirty lines) + if !self.cells_dirty && !has_dirty && !terminal.is_line_dirty(row_idx) { continue; } @@ -2344,61 +2394,21 @@ impl Renderer { } // Second pass: convert cells to GPU format - // OPTIMIZATION: Use get_visible_row() to avoid Vec allocation + // Always update self.gpu_cells from the current terminal to avoid + // stale data from a previous pane being written to the wrong GPU buffer. let mut any_updated = false; - // DEBUG: Log grid dimensions and buffer state - static DEBUG_COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); - let frame_num = DEBUG_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed); - if frame_num % 60 == 0 { // Log every 60 frames (~1 second at 60fps) - log::info!("DEBUG update_gpu_cells: cols={} rows={} total={} gpu_cells.len={} cells_dirty={}", - cols, rows, total_cells, self.gpu_cells.len(), self.cells_dirty); - } - - // If we did a full reset or size changed, update all lines - if self.cells_dirty { - static ROW_DEBUG_COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); - let row_frame = ROW_DEBUG_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed); - - if row_frame % 60 == 0 { - let first_col: String = (0..rows).filter_map(|r| { - terminal.get_visible_row(r).and_then(|row| { - row.first().map(|cell| { - let c = cell.character; - if c == '\0' { ' ' } else { c } - }) - }) - }).collect(); - log::info!("DEBUG col0: \"{}\"", first_col); - } - - for row_idx in 0..rows { - if let Some(row) = terminal.get_visible_row(row_idx) { - let start = row_idx * cols; - let end = start + cols; - - if end > self.gpu_cells.len() { - log::error!("DEBUG BUG: row_idx={} start={} end={} but gpu_cells.len={}", - row_idx, start, end, self.gpu_cells.len()); - continue; - } - - Self::cells_to_gpu_row_static(row, &mut self.gpu_cells[start..end], cols, &self.sprite_map); - } - } - self.cells_dirty = false; - any_updated = true; - } else { - // Only update dirty lines - use is_line_dirty() which handles all 256 lines - for row_idx in 0..rows { - if terminal.is_line_dirty(row_idx) { - if let Some(row) = terminal.get_visible_row(row_idx) { - let start = row_idx * cols; - let end = start + cols; - Self::cells_to_gpu_row_static(row, &mut self.gpu_cells[start..end], cols, &self.sprite_map); - any_updated = true; - } + for row_idx in 0..rows { + if let Some(row) = terminal.get_visible_row(row_idx) { + let start = row_idx * cols; + let end = start + cols; + + if end > self.gpu_cells.len() { + continue; } + + Self::cells_to_gpu_row_static(row, &mut self.gpu_cells[start..end], cols, &self.sprite_map); + any_updated = true; } } @@ -3681,7 +3691,7 @@ impl Renderer { return; } - log::info!("Adding atlas layer {} (was on layer {})", new_layer, self.atlas_current_layer); + // Create real texture for the new layer (replacing the dummy) self.ensure_atlas_layer_capacity(new_layer); @@ -3708,7 +3718,7 @@ impl Renderer { return; } - log::info!("Adding atlas layer {} (replacing dummy texture)", target_layer); + // Create new real texture (8192x8192) let texture = self.device.create_texture(&wgpu::TextureDescriptor { @@ -4636,7 +4646,7 @@ impl Renderer { { let update_time = t0.elapsed(); if update_time.as_micros() > 500 { - log::info!("update_gpu_cells took {:?}", update_time); + } } @@ -4679,36 +4689,7 @@ impl Renderer { selection_end_col: sel_end_col, selection_end_row: sel_end_row, }; - - // DEBUG: Log grid params every 60 frames - static PANE_DEBUG_COUNTER: std::sync::atomic::AtomicU64 = std::sync::atomic::AtomicU64::new(0); - let pane_frame = PANE_DEBUG_COUNTER.fetch_add(1, std::sync::atomic::Ordering::Relaxed); - if pane_frame % 60 == 0 { - log::info!("DEBUG pane {}: grid_params cols={} rows={} gpu_cells.len={} expected={}", - info.pane_id, grid_params.cols, grid_params.rows, - self.gpu_cells.len(), (grid_params.cols * grid_params.rows) as usize); - - // Sample a few cells to see if sprite indices look reasonable - if !self.gpu_cells.is_empty() { - let sample_indices = [0, 1, 2, cols as usize, cols as usize + 1]; - for &idx in &sample_indices { - if idx < self.gpu_cells.len() { - let cell = &self.gpu_cells[idx]; - let sprite_idx = cell.sprite_idx & !0x80000000; - log::info!("DEBUG cell[{}]: sprite_idx={} fg={:#x} bg={:#x}", - idx, sprite_idx, cell.fg, cell.bg); - - if sprite_idx > 0 && (sprite_idx as usize) < self.sprite_info.len() { - let sprite = &self.sprite_info[sprite_idx as usize]; - log::info!("DEBUG sprite[{}]: uv=({:.3},{:.3},{:.3},{:.3}) layer={} size=({:.1},{:.1})", - sprite_idx, sprite.uv[0], sprite.uv[1], sprite.uv[2], sprite.uv[3], - sprite.layer, sprite.size[0], sprite.size[1]); - } - } - } - } - } - + // Upload this pane's cell data to its own buffer (like Kitty's send_cell_data_to_gpu) // This happens BEFORE the render pass, so each pane has its own data if let Some(pane_res) = self.pane_resources.get(&info.pane_id) { @@ -4770,7 +4751,7 @@ impl Renderer { { let pane_loop_time = pane_loop_start.elapsed(); if pane_loop_time.as_micros() > 500 { - log::info!("pane_loop took {:?}", pane_loop_time); + } } @@ -5258,20 +5239,8 @@ impl Renderer { #[cfg(feature = "render_timing")] let after_submit = frame_start.elapsed(); output.present(); - - // Log timing if frame took more than 1ms (only with render_timing feature) - #[cfg(feature = "render_timing")] - { - let after_present = frame_start.elapsed(); - if after_present.as_micros() > 1000 { - log::info!("render_panes: before_submit={:?} submit={:?} present={:?} total={:?}", - before_submit, - after_submit - before_submit, - after_present - after_submit, - after_present); - } - } + self.clear_pending_redraw(); Ok(()) } diff --git a/src/terminal.rs b/src/terminal.rs index 050513c..83c346a 100644 --- a/src/terminal.rs +++ b/src/terminal.rs @@ -298,87 +298,6 @@ struct AlternateScreen { scroll_bottom: usize, } -/// Timing stats for performance debugging. -/// Only populated when the `render_timing` feature is enabled. -#[derive(Debug, Default)] -pub struct ProcessingStats { - #[cfg(feature = "render_timing")] - /// Total time spent in scroll_up operations (nanoseconds). - pub scroll_up_ns: u64, - #[cfg(feature = "render_timing")] - /// Number of scroll_up calls. - pub scroll_up_count: u32, - #[cfg(feature = "render_timing")] - /// Total time spent in scrollback operations (nanoseconds). - pub scrollback_ns: u64, - #[cfg(feature = "render_timing")] - /// Time in VecDeque pop_front. - pub pop_front_ns: u64, - #[cfg(feature = "render_timing")] - /// Time in VecDeque push_back. - pub push_back_ns: u64, - #[cfg(feature = "render_timing")] - /// Time in mem::swap. - pub swap_ns: u64, - #[cfg(feature = "render_timing")] - /// Total time spent in line clearing (nanoseconds). - pub clear_line_ns: u64, - #[cfg(feature = "render_timing")] - /// Total time spent in text handler (nanoseconds). - pub text_handler_ns: u64, - #[cfg(feature = "render_timing")] - /// Total time spent in CSI handler (nanoseconds). - pub csi_handler_ns: u64, - #[cfg(feature = "render_timing")] - /// Number of CSI sequences processed. - pub csi_count: u32, - #[cfg(feature = "render_timing")] - /// Number of characters processed. - pub chars_processed: u32, - #[cfg(feature = "render_timing")] - /// Total time spent in VT parser (consume_input) - nanoseconds. - pub vt_parser_ns: u64, - #[cfg(feature = "render_timing")] - /// Number of consume_input calls. - pub consume_input_count: u32, -} - -impl ProcessingStats { - #[cfg(feature = "render_timing")] - pub fn reset(&mut self) { - *self = Self::default(); - } - - #[cfg(not(feature = "render_timing"))] - pub fn reset(&mut self) {} - - #[cfg(feature = "render_timing")] - pub fn log_if_slow(&self, threshold_ms: u64) { - let total_ms = - (self.scroll_up_ns + self.text_handler_ns + self.csi_handler_ns) - / 1_000_000; - if total_ms >= threshold_ms { - let vt_only_ns = self - .vt_parser_ns - .saturating_sub(self.text_handler_ns + self.csi_handler_ns); - log::info!( - "[PARSE_DETAIL] text={:.2}ms ({}chars) csi={:.2}ms ({}x) vt_only={:.2}ms ({}calls) scroll={:.2}ms ({}x)", - self.text_handler_ns as f64 / 1_000_000.0, - self.chars_processed, - self.csi_handler_ns as f64 / 1_000_000.0, - self.csi_count, - vt_only_ns as f64 / 1_000_000.0, - self.consume_input_count, - self.scroll_up_ns as f64 / 1_000_000.0, - self.scroll_up_count, - ); - } - } - - #[cfg(not(feature = "render_timing"))] - pub fn log_if_slow(&self, _threshold_ms: u64) {} -} - /// Kitty-style ring buffer for scrollback history. /// /// Pre-allocates all lines upfront to avoid allocation during scrolling. @@ -569,7 +488,7 @@ pub struct Terminal { /// Synchronized output mode (for reducing flicker). synchronized_output: bool, /// Performance timing stats (for debugging). - pub stats: ProcessingStats, + /// Command queue for terminal-to-application communication. /// Commands are added by OSC handlers and consumed by the application. command_queue: Vec, @@ -587,12 +506,6 @@ 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(); @@ -632,7 +545,7 @@ impl Terminal { bracketed_paste: false, focus_reporting: false, synchronized_output: false, - stats: ProcessingStats::default(), + command_queue: Vec::new(), image_storage: ImageStorage::new(), cell_width: 10.0, // Default, will be set by renderer @@ -668,6 +581,12 @@ impl Terminal { } } + /// Check if any line is dirty. + #[inline] + pub fn has_any_dirty_line(&self) -> bool { + self.dirty_lines[0] != 0 || self.dirty_lines[1] != 0 || self.dirty_lines[2] != 0 || self.dirty_lines[3] != 0 + } + /// Clear all dirty line flags. #[inline] pub fn clear_dirty_lines(&mut self) { @@ -789,14 +708,6 @@ 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; @@ -1692,12 +1603,6 @@ impl Handler for Terminal { let statusline = content.filter(|s| !s.is_empty()); - log::info!( - "OSC 51: Set statusline: {:?}", - statusline - .as_ref() - .map(|s| format!("{} bytes", s.len())) - ); self.command_queue.push( TerminalCommand::SetStatusline(statusline), );