font rendering

This commit is contained in:
Zacharias-Brohn
2025-12-16 16:55:10 +01:00
parent f304fd18a8
commit 5c3eee3448
14 changed files with 4060 additions and 874 deletions
+186 -22
View File
@@ -17,6 +17,7 @@ use std::sync::atomic::{AtomicBool, Ordering};
use std::sync::{Arc, Mutex};
use std::time::Duration;
use notify::{RecommendedWatcher, RecursiveMode, Watcher};
use polling::{Event, Events, Poller};
use winit::application::ApplicationHandler;
use winit::dpi::{PhysicalPosition, PhysicalSize};
@@ -54,9 +55,18 @@ struct DoubleBuffer {
impl SharedPtyBuffer {
fn new() -> Self {
// Use with_capacity to avoid zeroing memory - we only need the allocation
let mut buf1 = Vec::with_capacity(PTY_BUF_SIZE);
let mut buf2 = Vec::with_capacity(PTY_BUF_SIZE);
// SAFETY: We're setting length to capacity. The data is uninitialized but
// we only read from portions that have been written to (tracked by write_len).
unsafe {
buf1.set_len(PTY_BUF_SIZE);
buf2.set_len(PTY_BUF_SIZE);
}
Self {
inner: Mutex::new(DoubleBuffer {
bufs: [vec![0u8; PTY_BUF_SIZE], vec![0u8; PTY_BUF_SIZE]],
bufs: [buf1, buf2],
write_idx: 0,
write_len: 0,
}),
@@ -158,8 +168,15 @@ impl Pane {
let terminal = Terminal::new(cols, rows, scrollback_lines);
let pty = Pty::spawn(None).map_err(|e| format!("Failed to spawn PTY: {}", e))?;
// Set terminal size
if let Err(e) = pty.resize(cols as u16, rows as u16) {
// Set terminal size (use default cell size estimate for initial pixel dimensions)
let default_cell_width = 10u16;
let default_cell_height = 20u16;
if let Err(e) = pty.resize(
cols as u16,
rows as u16,
cols as u16 * default_cell_width,
rows as u16 * default_cell_height,
) {
log::warn!("Failed to set initial PTY size: {}", e);
}
@@ -180,9 +197,9 @@ impl Pane {
}
/// Resize the terminal and PTY.
fn resize(&mut self, cols: usize, rows: usize) {
fn resize(&mut self, cols: usize, rows: usize, width_px: u16, height_px: u16) {
self.terminal.resize(cols, rows);
if let Err(e) = self.pty.resize(cols as u16, rows as u16) {
if let Err(e) = self.pty.resize(cols as u16, rows as u16, width_px, height_px) {
log::warn!("Failed to resize PTY: {}", e);
}
}
@@ -588,7 +605,11 @@ impl Tab {
for (pane_id, geom) in geometries {
if let Some(pane) = self.panes.get_mut(&pane_id) {
pane.resize(geom.cols, geom.rows);
// Report pixel dimensions as exact cell grid size (cols * cell_width, rows * cell_height)
// This ensures applications like kitten icat calculate image placement correctly
let pixel_width = (geom.cols as f32 * cell_width) as u16;
let pixel_height = (geom.rows as f32 * cell_height) as u16;
pane.resize(geom.cols, geom.rows, pixel_width, pixel_height);
}
}
}
@@ -784,6 +805,8 @@ enum UserEvent {
ShowWindow,
/// PTY has data available for a specific pane.
PtyReadable(PaneId),
/// Config file was modified and should be reloaded.
ConfigReloaded,
}
/// Main application state.
@@ -816,8 +839,8 @@ struct App {
last_frame_log: std::time::Instant,
/// Whether window should be created on next opportunity.
should_create_window: bool,
/// Edge glow animation state (for when navigation fails).
edge_glow: Option<EdgeGlow>,
/// Edge glow animations (for when navigation fails). Multiple can be active simultaneously.
edge_glows: Vec<EdgeGlow>,
}
const PTY_KEY: usize = 1;
@@ -848,7 +871,7 @@ impl App {
frame_count: 0,
last_frame_log: std::time::Instant::now(),
should_create_window: false,
edge_glow: None,
edge_glows: Vec::new(),
}
}
@@ -856,6 +879,50 @@ impl App {
self.event_loop_proxy = Some(proxy);
}
/// 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
let font_size_changed = (new_config.font_size - self.config.font_size).abs() > 0.01;
let opacity_changed = (new_config.background_opacity - self.config.background_opacity).abs() > 0.01;
let tab_bar_changed = new_config.tab_bar_position != self.config.tab_bar_position;
// Update the config
self.config = new_config;
// Rebuild action map for keybindings
self.action_map = self.config.keybindings.build_action_map();
// Apply renderer changes if we have a renderer
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);
// Font size change requires resize to recalculate cell dimensions
self.resize_all_panes();
}
}
// Request redraw to apply visual changes
if let Some(window) = &self.window {
window.request_redraw();
}
log::info!("Configuration reloaded successfully");
}
/// 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> {
@@ -1046,6 +1113,11 @@ impl App {
for tab in &mut self.tabs {
tab.resize(width, height, cell_width, cell_height, border_width);
// Update cell size on all terminals (needed for Kitty graphics protocol)
for pane in tab.panes.values_mut() {
pane.terminal.set_cell_size(cell_width, cell_height);
}
}
}
@@ -1368,6 +1440,13 @@ impl App {
}
fn focus_pane(&mut self, direction: Direction) {
// Get current active pane geometry before attempting navigation
let active_pane_geom = if let Some(tab) = self.tabs.get(self.active_tab) {
tab.split_root.find_geometry(tab.active_pane)
} else {
None
};
let navigated = if let Some(tab) = self.tabs.get_mut(self.active_tab) {
let old_pane = tab.active_pane;
tab.focus_neighbor(direction);
@@ -1378,7 +1457,16 @@ impl App {
if !navigated {
// No neighbor in that direction - trigger edge glow animation
self.edge_glow = Some(EdgeGlow::new(direction));
// Add to existing glows (don't replace) so multiple can be visible
if let Some(geom) = active_pane_geom {
self.edge_glows.push(EdgeGlow::new(
direction,
geom.x,
geom.y,
geom.width,
geom.height,
));
}
}
if let Some(window) = &self.window {
@@ -1605,9 +1693,11 @@ impl App {
impl ApplicationHandler<UserEvent> for App {
fn resumed(&mut self, event_loop: &ActiveEventLoop) {
let start = std::time::Instant::now();
if self.window.is_none() {
self.create_window(event_loop);
}
log::info!("App resumed (window creation): {:?}", start.elapsed());
}
fn user_event(&mut self, event_loop: &ActiveEventLoop, event: UserEvent) {
@@ -1633,6 +1723,9 @@ impl ApplicationHandler<UserEvent> for App {
log::info!("PTY process took {:?}", process_time);
}
}
UserEvent::ConfigReloaded => {
self.reload_config();
}
}
}
@@ -1851,13 +1944,16 @@ impl ApplicationHandler<UserEvent> for App {
let geometries = tab.collect_pane_geometries();
let active_pane_id = tab.active_pane;
// First pass: calculate dim factors (needs mutable access)
// First pass: sync images and calculate dim factors (needs mutable access)
let mut dim_factors: Vec<(PaneId, f32)> = Vec::new();
for (pane_id, _) in &geometries {
if let Some(pane) = tab.panes.get_mut(pane_id) {
let is_active = *pane_id == active_pane_id;
let dim_factor = pane.calculate_dim_factor(is_active, fade_duration_ms, inactive_dim);
dim_factors.push((*pane_id, dim_factor));
// Sync terminal images to GPU (Kitty graphics protocol)
renderer.sync_images(&mut pane.terminal.image_storage);
}
}
@@ -1914,11 +2010,15 @@ impl ApplicationHandler<UserEvent> for App {
}
}
// Handle edge glow animation
let edge_glow_ref = self.edge_glow.as_ref();
let glow_in_progress = edge_glow_ref.map(|g| !g.is_finished()).unwrap_or(false);
// Handle edge glow animations
let glow_in_progress = !self.edge_glows.is_empty();
match renderer.render_panes(&pane_render_data, num_tabs, active_tab_idx, edge_glow_ref) {
// Check if any pane has animated images
let image_animation_in_progress = tab.panes.values().any(|pane| {
pane.terminal.image_storage.has_animations()
});
match renderer.render_panes(&pane_render_data, num_tabs, active_tab_idx, &self.edge_glows, self.config.edge_glow_intensity) {
Ok(_) => {}
Err(wgpu::SurfaceError::Lost) => {
renderer.resize(renderer.width, renderer.height);
@@ -1932,8 +2032,8 @@ impl ApplicationHandler<UserEvent> for App {
}
}
// Request redraw if edge glow is animating
if glow_in_progress {
// Request redraw if edge glow or image animation is in progress
if glow_in_progress || image_animation_in_progress {
if let Some(window) = &self.window {
window.request_redraw();
}
@@ -1941,10 +2041,8 @@ impl ApplicationHandler<UserEvent> for App {
}
}
// Clean up finished edge glow animation
if self.edge_glow.as_ref().map(|g| g.is_finished()).unwrap_or(false) {
self.edge_glow = None;
}
// Clean up finished edge glow animations
self.edge_glows.retain(|g| !g.is_finished());
let render_time = render_start.elapsed();
let frame_time = frame_start.elapsed();
@@ -2000,6 +2098,69 @@ impl Drop for App {
}
}
/// Set up a file watcher to monitor the config file for changes.
/// Returns the watcher (must be kept alive for watching to continue).
fn setup_config_watcher(proxy: EventLoopProxy<UserEvent>) -> Option<RecommendedWatcher> {
let config_path = match Config::config_path() {
Some(path) => path,
None => {
log::warn!("Could not determine config path, config hot-reload disabled");
return None;
}
};
// Watch the parent directory since the file might be replaced atomically
let watch_path = match config_path.parent() {
Some(parent) => parent.to_path_buf(),
None => {
log::warn!("Could not determine config directory, config hot-reload disabled");
return None;
}
};
let config_filename = config_path.file_name().map(|s| s.to_os_string());
let mut watcher = match notify::recommended_watcher(move |res: Result<notify::Event, notify::Error>| {
match res {
Ok(event) => {
// Only trigger on modify/create events for the config file
use notify::EventKind;
match event.kind {
EventKind::Modify(_) | EventKind::Create(_) => {
// Check if the event is for our config file
let is_config_file = event.paths.iter().any(|p| {
p.file_name().map(|s| s.to_os_string()) == config_filename
});
if is_config_file {
log::debug!("Config file changed, triggering reload");
let _ = proxy.send_event(UserEvent::ConfigReloaded);
}
}
_ => {}
}
}
Err(e) => {
log::warn!("Config watcher error: {:?}", e);
}
}
}) {
Ok(w) => w,
Err(e) => {
log::warn!("Failed to create config watcher: {:?}", e);
return None;
}
};
if let Err(e) = watcher.watch(&watch_path, RecursiveMode::NonRecursive) {
log::warn!("Failed to watch config directory {:?}: {:?}", watch_path, e);
return None;
}
log::info!("Config hot-reload enabled, watching {:?}", watch_path);
Some(watcher)
}
fn main() {
env_logger::Builder::from_env(env_logger::Env::default().default_filter_or("info")).init();
@@ -2035,9 +2196,12 @@ fn main() {
// Store proxy for signal handler (uses the global static defined below)
unsafe {
EVENT_PROXY = Some(proxy);
EVENT_PROXY = Some(proxy.clone());
}
// Set up config file watcher for hot-reloading
let _config_watcher = setup_config_watcher(proxy);
event_loop.run_app(&mut app).expect("Event loop error");
}