fix rendering bug
This commit is contained in:
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
+2
-19
@@ -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<FontVariant>; 4] = [Some(regular_variant), None, None, None];
|
||||
log::info!("Loaded NotoSansMono as fallback");
|
||||
|
||||
return (font_data, primary_font, variants);
|
||||
}
|
||||
|
||||
|
||||
+323
-156
@@ -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<PaneGeometry> {
|
||||
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);
|
||||
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<usize> {
|
||||
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| {
|
||||
let selection = pane.selection.as_ref().and_then(|sel| {
|
||||
sel.to_screen_coords(scroll_offset, geom.rows)
|
||||
})
|
||||
} else {
|
||||
None
|
||||
};
|
||||
});
|
||||
|
||||
let render_info = PaneRenderInfo {
|
||||
pane_id: pane_id.0,
|
||||
@@ -2172,6 +2168,10 @@ impl App {
|
||||
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<PaneGeometry> {
|
||||
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 {
|
||||
let modifiers = self.get_mouse_modifiers();
|
||||
let Some(tab) = self.active_tab_mut() else {
|
||||
return;
|
||||
};
|
||||
pane.terminal.encode_mouse(
|
||||
let Some(pane) = tab.panes.get_mut(&pane_id) else {
|
||||
return;
|
||||
};
|
||||
let seq = pane.terminal.encode_mouse(
|
||||
button,
|
||||
col,
|
||||
row,
|
||||
pressed,
|
||||
is_motion,
|
||||
self.get_mouse_modifiers(),
|
||||
)
|
||||
};
|
||||
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();
|
||||
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.request_redraw();
|
||||
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
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2858,20 +2873,18 @@ impl ApplicationHandler<UserEvent> for App {
|
||||
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<UserEvent> 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<UserEvent> 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<UserEvent> 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<UserEvent> 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(
|
||||
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 {
|
||||
self.send_mouse_event(
|
||||
button, col as u16, row as u16, true,
|
||||
false,
|
||||
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,33 +3139,86 @@ impl ApplicationHandler<UserEvent> 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() {
|
||||
// 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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 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((col, row)) =
|
||||
renderer.pixel_to_cell(position.x, position.y)
|
||||
{
|
||||
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
|
||||
self.send_mouse_event(
|
||||
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);
|
||||
}
|
||||
}
|
||||
} else if is_selecting && !self.has_mouse_tracking() {
|
||||
// Terminal-native selection
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Also update terminal-native selection for rendering
|
||||
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();
|
||||
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(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
|
||||
{
|
||||
@@ -3148,14 +3226,59 @@ impl ApplicationHandler<UserEvent> for App {
|
||||
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(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,
|
||||
};
|
||||
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,17 +3293,28 @@ impl ApplicationHandler<UserEvent> for App {
|
||||
if distance_sq > threshold * threshold {
|
||||
// Dragged far enough, start selection
|
||||
if let Some(renderer) = &self.renderer {
|
||||
if let Some(geom) = pane_geom {
|
||||
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,
|
||||
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;
|
||||
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,
|
||||
)
|
||||
{
|
||||
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
|
||||
@@ -3192,10 +3326,10 @@ impl ApplicationHandler<UserEvent> for App {
|
||||
- scroll_offset,
|
||||
};
|
||||
|
||||
if let Some(tab) = self.active_tab_mut()
|
||||
if let Some(active_tab) = self.active_tab_mut()
|
||||
{
|
||||
if let Some(pane) =
|
||||
tab.active_pane_mut()
|
||||
active_tab.panes.get_mut(&geom.pane_id)
|
||||
{
|
||||
pane.selection =
|
||||
Some(Selection {
|
||||
@@ -3203,6 +3337,10 @@ impl ApplicationHandler<UserEvent> for App {
|
||||
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();
|
||||
}
|
||||
}
|
||||
@@ -3213,6 +3351,7 @@ impl ApplicationHandler<UserEvent> for App {
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
WindowEvent::MouseInput { state, button, .. } => {
|
||||
let button_code = match button {
|
||||
@@ -3222,34 +3361,74 @@ impl ApplicationHandler<UserEvent> for App {
|
||||
_ => return,
|
||||
};
|
||||
|
||||
if self.has_mouse_tracking() {
|
||||
if let Some(renderer) = &self.renderer {
|
||||
if let Some((col, row)) = renderer.pixel_to_cell(
|
||||
// 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(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;
|
||||
self.send_mouse_event(
|
||||
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 {
|
||||
if let Some(tab) = self.active_tab_mut() {
|
||||
if let Some(pane) = tab.active_pane_mut() {
|
||||
pane.is_selecting = pressed;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
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<UserEvent> for App {
|
||||
pane.is_selecting = false;
|
||||
}
|
||||
}
|
||||
if let Some(renderer) = &mut self.renderer {
|
||||
renderer.force_full_redraw();
|
||||
}
|
||||
}
|
||||
ElementState::Released => {
|
||||
self.mouse_down_pos = None;
|
||||
@@ -3318,7 +3500,16 @@ impl ApplicationHandler<UserEvent> for App {
|
||||
|| !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,34 +3526,12 @@ impl ApplicationHandler<UserEvent> 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);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
_ => {}
|
||||
@@ -3372,7 +3541,6 @@ impl ApplicationHandler<UserEvent> for App {
|
||||
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<UserEvent> 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<UserEvent> 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;
|
||||
}
|
||||
|
||||
|
||||
+70
-101
@@ -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;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4680,35 +4690,6 @@ impl Renderer {
|
||||
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);
|
||||
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5259,19 +5240,7 @@ impl Renderer {
|
||||
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(())
|
||||
}
|
||||
|
||||
|
||||
+8
-103
@@ -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<TerminalCommand>,
|
||||
@@ -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<usize> = (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),
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user