use crate::cell_renderer::{Cell, CellRenderer, CellRendererConfig, PaneViewport};
use crate::custom_shader_renderer::CustomShaderRenderer;
use crate::graphics_renderer::GraphicsRenderer;
use anyhow::Result;
use winit::dpi::PhysicalSize;
mod egui_render;
pub mod graphics;
pub mod params;
mod render_passes;
mod rendering;
pub mod shaders;
mod state;
pub use par_term_config::SeparatorMark;
pub use params::RendererParams;
pub use rendering::SplitPanesRenderParams;
pub fn compute_visible_separator_marks(
marks: &[par_term_config::ScrollbackMark],
scrollback_len: usize,
scroll_offset: usize,
visible_lines: usize,
) -> Vec<SeparatorMark> {
let mut out = Vec::new();
fill_visible_separator_marks(
&mut out,
marks,
scrollback_len,
scroll_offset,
visible_lines,
);
out
}
pub(crate) fn fill_visible_separator_marks(
out: &mut Vec<SeparatorMark>,
marks: &[par_term_config::ScrollbackMark],
scrollback_len: usize,
scroll_offset: usize,
visible_lines: usize,
) {
out.clear();
let viewport_start = scrollback_len.saturating_sub(scroll_offset);
let viewport_end = viewport_start + visible_lines;
for mark in marks {
if mark.line >= viewport_start && mark.line < viewport_end {
let screen_row = mark.line - viewport_start;
out.push((screen_row, mark.exit_code, mark.color));
}
}
}
pub struct PaneRenderInfo<'a> {
pub viewport: PaneViewport,
pub cells: &'a [Cell],
pub grid_size: (usize, usize),
pub cursor_pos: Option<(usize, usize)>,
pub cursor_opacity: f32,
pub show_scrollbar: bool,
pub marks: Vec<par_term_config::ScrollbackMark>,
pub scrollback_len: usize,
pub scroll_offset: usize,
pub background: Option<par_term_config::PaneBackground>,
pub graphics: Vec<par_term_emu_core_rust::graphics::TerminalGraphic>,
}
#[derive(Clone, Copy, Debug)]
pub struct DividerRenderInfo {
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
pub hovered: bool,
}
impl DividerRenderInfo {
pub fn from_rect(rect: &par_term_config::DividerRect, hovered: bool) -> Self {
Self {
x: rect.x,
y: rect.y,
width: rect.width,
height: rect.height,
hovered,
}
}
}
#[derive(Clone, Debug)]
pub struct PaneTitleInfo {
pub x: f32,
pub y: f32,
pub width: f32,
pub height: f32,
pub title: String,
pub focused: bool,
pub text_color: [f32; 3],
pub bg_color: [f32; 3],
}
#[derive(Clone, Copy, Debug)]
pub struct PaneDividerSettings {
pub divider_color: [f32; 3],
pub hover_color: [f32; 3],
pub show_focus_indicator: bool,
pub focus_color: [f32; 3],
pub focus_width: f32,
pub divider_style: par_term_config::DividerStyle,
}
impl Default for PaneDividerSettings {
fn default() -> Self {
Self {
divider_color: [0.3, 0.3, 0.3],
hover_color: [0.5, 0.6, 0.8],
show_focus_indicator: true,
focus_color: [0.4, 0.6, 1.0],
focus_width: 1.0,
divider_style: par_term_config::DividerStyle::default(),
}
}
}
pub struct Renderer {
pub(crate) cell_renderer: CellRenderer,
pub(crate) graphics_renderer: GraphicsRenderer,
pub(crate) sixel_graphics: Vec<crate::graphics_renderer::GraphicRenderInfo>,
pub(crate) egui_renderer: egui_wgpu::Renderer,
pub(crate) custom_shader_renderer: Option<CustomShaderRenderer>,
pub(crate) custom_shader_path: Option<String>,
pub(crate) cursor_shader_renderer: Option<CustomShaderRenderer>,
pub(crate) cursor_shader_path: Option<String>,
pub(crate) size: PhysicalSize<u32>,
pub(crate) dirty: bool,
pub(crate) last_scrollbar_state: (usize, usize, usize, usize, u32, u32, u32, u32, u32, u32),
pub(crate) cursor_shader_disabled_for_alt_screen: bool,
pub(crate) debug_text: Option<String>,
pub(crate) scratch_divider_instances: Vec<crate::cell_renderer::BackgroundInstance>,
}
impl Renderer {
pub async fn new(params: RendererParams<'_>) -> Result<Self> {
let window = params.window;
let font_family = params.font_family;
let font_family_bold = params.font_family_bold;
let font_family_italic = params.font_family_italic;
let font_family_bold_italic = params.font_family_bold_italic;
let font_ranges = params.font_ranges;
let font_size = params.font_size;
let line_spacing = params.line_spacing;
let char_spacing = params.char_spacing;
let scrollbar_position = params.scrollbar_position;
let scrollbar_thumb_color = params.scrollbar_thumb_color;
let scrollbar_track_color = params.scrollbar_track_color;
let enable_text_shaping = params.enable_text_shaping;
let enable_ligatures = params.enable_ligatures;
let enable_kerning = params.enable_kerning;
let font_antialias = params.font_antialias;
let font_hinting = params.font_hinting;
let font_thin_strokes = params.font_thin_strokes;
let minimum_contrast = params.minimum_contrast;
let vsync_mode = params.vsync_mode;
let power_preference = params.power_preference;
let window_opacity = params.window_opacity;
let background_color = params.background_color;
let background_image_path = params.background_image_path;
let background_image_enabled = params.background_image_enabled;
let background_image_mode = params.background_image_mode;
let background_image_opacity = params.background_image_opacity;
let custom_shader_path = params.custom_shader_path;
let custom_shader_enabled = params.custom_shader_enabled;
let custom_shader_animation = params.custom_shader_animation;
let custom_shader_animation_speed = params.custom_shader_animation_speed;
let custom_shader_full_content = params.custom_shader_full_content;
let custom_shader_brightness = params.custom_shader_brightness;
let custom_shader_channel_paths = params.custom_shader_channel_paths;
let custom_shader_cubemap_path = params.custom_shader_cubemap_path;
let use_background_as_channel0 = params.use_background_as_channel0;
let image_scaling_mode = params.image_scaling_mode;
let image_preserve_aspect_ratio = params.image_preserve_aspect_ratio;
let cursor_shader_path = params.cursor_shader_path;
let cursor_shader_enabled = params.cursor_shader_enabled;
let cursor_shader_animation = params.cursor_shader_animation;
let cursor_shader_animation_speed = params.cursor_shader_animation_speed;
let size = window.inner_size();
let scale_factor = window.scale_factor();
let platform_dpi = if cfg!(target_os = "macos") {
72.0
} else {
96.0
};
let base_font_pixels = font_size * platform_dpi / 72.0;
let font_size_pixels = (base_font_pixels * scale_factor as f32).max(1.0);
let font_manager = par_term_fonts::font_manager::FontManager::new(
font_family,
font_family_bold,
font_family_italic,
font_family_bold_italic,
font_ranges,
)?;
let (font_ascent, font_descent, font_leading, char_advance) = {
let primary_font = font_manager
.get_font(0)
.expect("Primary font at index 0 must exist after FontManager initialization");
let metrics = primary_font.metrics(&[]);
let scale = font_size_pixels / metrics.units_per_em as f32;
let glyph_id = primary_font.charmap().map('m');
let advance = primary_font.glyph_metrics(&[]).advance_width(glyph_id) * scale;
(
metrics.ascent * scale,
metrics.descent * scale,
metrics.leading * scale,
advance,
)
};
let natural_line_height = font_ascent + font_descent + font_leading;
let char_height = (natural_line_height * line_spacing).max(1.0).round();
let scale = scale_factor as f32;
let window_padding = params.window_padding * scale;
let scrollbar_width = params.scrollbar_width * scale;
let available_width = (size.width as f32 - window_padding * 2.0 - scrollbar_width).max(0.0);
let available_height = (size.height as f32 - window_padding * 2.0).max(0.0);
let char_width = (char_advance * char_spacing).max(1.0).round(); let cols = (available_width / char_width).max(1.0) as usize;
let rows = (available_height / char_height).max(1.0) as usize;
let bg_path = if background_image_enabled {
background_image_path
} else {
None
};
log::info!(
"Renderer::new: background_image_enabled={}, path={:?}",
background_image_enabled,
bg_path
);
let cell_renderer = CellRenderer::new(
window.clone(),
CellRendererConfig {
font_family,
font_family_bold,
font_family_italic,
font_family_bold_italic,
font_ranges,
font_size,
cols,
rows,
window_padding,
line_spacing,
char_spacing,
scrollbar_position,
scrollbar_width,
scrollbar_thumb_color,
scrollbar_track_color,
enable_text_shaping,
enable_ligatures,
enable_kerning,
font_antialias,
font_hinting,
font_thin_strokes,
minimum_contrast,
vsync_mode,
power_preference,
window_opacity,
background_color,
background_image_path: bg_path,
background_image_mode,
background_image_opacity,
},
)
.await?;
let egui_renderer = egui_wgpu::Renderer::new(
cell_renderer.device(),
cell_renderer.surface_format(),
egui_wgpu::RendererOptions {
msaa_samples: 1,
depth_stencil_format: None,
dithering: false,
predictable_texture_filtering: false,
},
);
let graphics_renderer = GraphicsRenderer::new(
cell_renderer.device(),
cell_renderer.surface_format(),
cell_renderer.cell_width(),
cell_renderer.cell_height(),
cell_renderer.window_padding(),
image_scaling_mode,
image_preserve_aspect_ratio,
)?;
let (mut custom_shader_renderer, initial_shader_path) = shaders::init_custom_shader(
&cell_renderer,
shaders::CustomShaderInitParams {
size_width: size.width,
size_height: size.height,
window_padding,
path: custom_shader_path,
enabled: custom_shader_enabled,
animation: custom_shader_animation,
animation_speed: custom_shader_animation_speed,
window_opacity,
full_content: custom_shader_full_content,
brightness: custom_shader_brightness,
channel_paths: custom_shader_channel_paths,
cubemap_path: custom_shader_cubemap_path,
use_background_as_channel0,
},
);
let (mut cursor_shader_renderer, initial_cursor_shader_path) = shaders::init_cursor_shader(
&cell_renderer,
shaders::CursorShaderInitParams {
size_width: size.width,
size_height: size.height,
window_padding,
path: cursor_shader_path,
enabled: cursor_shader_enabled,
animation: cursor_shader_animation,
animation_speed: cursor_shader_animation_speed,
window_opacity,
},
);
if let Some(ref mut cs) = custom_shader_renderer {
cs.set_scale_factor(scale);
}
if let Some(ref mut cs) = cursor_shader_renderer {
cs.set_scale_factor(scale);
}
log::info!(
"[renderer] Renderer created: custom_shader_loaded={}, cursor_shader_loaded={}",
initial_shader_path.is_some(),
initial_cursor_shader_path.is_some()
);
Ok(Self {
cell_renderer,
graphics_renderer,
sixel_graphics: Vec::new(),
egui_renderer,
custom_shader_renderer,
custom_shader_path: initial_shader_path,
cursor_shader_renderer,
cursor_shader_path: initial_cursor_shader_path,
size,
dirty: true, last_scrollbar_state: (usize::MAX, 0, 0, 0, 0, 0, 0, 0, 0, 0), cursor_shader_disabled_for_alt_screen: false,
debug_text: None,
scratch_divider_instances: Vec::new(),
})
}
pub fn resize(&mut self, new_size: PhysicalSize<u32>) -> (usize, usize) {
if new_size.width > 0 && new_size.height > 0 {
self.size = new_size;
self.dirty = true; let result = self.cell_renderer.resize(new_size.width, new_size.height);
self.graphics_renderer.update_cell_dimensions(
self.cell_renderer.cell_width(),
self.cell_renderer.cell_height(),
self.cell_renderer.window_padding(),
);
if let Some(ref mut custom_shader) = self.custom_shader_renderer {
custom_shader.resize(self.cell_renderer.device(), new_size.width, new_size.height);
custom_shader.update_cell_dimensions(
self.cell_renderer.cell_width(),
self.cell_renderer.cell_height(),
self.cell_renderer.window_padding(),
);
}
if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
cursor_shader.resize(self.cell_renderer.device(), new_size.width, new_size.height);
cursor_shader.update_cell_dimensions(
self.cell_renderer.cell_width(),
self.cell_renderer.cell_height(),
self.cell_renderer.window_padding(),
);
}
return result;
}
self.cell_renderer.grid_size()
}
pub fn handle_scale_factor_change(
&mut self,
scale_factor: f64,
new_size: PhysicalSize<u32>,
) -> (usize, usize) {
let old_scale = self.cell_renderer.scale_factor;
self.cell_renderer.update_scale_factor(scale_factor);
let new_scale = self.cell_renderer.scale_factor;
if old_scale > 0.0 && (old_scale - new_scale).abs() > f32::EPSILON {
let logical_offset_y = self.cell_renderer.content_offset_y() / old_scale;
let new_physical_offset_y = logical_offset_y * new_scale;
self.cell_renderer
.set_content_offset_y(new_physical_offset_y);
self.graphics_renderer
.set_content_offset_y(new_physical_offset_y);
if let Some(ref mut cs) = self.custom_shader_renderer {
cs.set_content_offset_y(new_physical_offset_y);
}
if let Some(ref mut cs) = self.cursor_shader_renderer {
cs.set_content_offset_y(new_physical_offset_y);
}
let logical_offset_x = self.cell_renderer.content_offset_x() / old_scale;
let new_physical_offset_x = logical_offset_x * new_scale;
self.cell_renderer
.set_content_offset_x(new_physical_offset_x);
self.graphics_renderer
.set_content_offset_x(new_physical_offset_x);
if let Some(ref mut cs) = self.custom_shader_renderer {
cs.set_content_offset_x(new_physical_offset_x);
}
if let Some(ref mut cs) = self.cursor_shader_renderer {
cs.set_content_offset_x(new_physical_offset_x);
}
let logical_inset_bottom = self.cell_renderer.content_inset_bottom() / old_scale;
let new_physical_inset_bottom = logical_inset_bottom * new_scale;
self.cell_renderer
.set_content_inset_bottom(new_physical_inset_bottom);
if self.cell_renderer.grid.egui_bottom_inset > 0.0 {
let logical_egui_bottom = self.cell_renderer.grid.egui_bottom_inset / old_scale;
self.cell_renderer.grid.egui_bottom_inset = logical_egui_bottom * new_scale;
}
if self.cell_renderer.grid.content_inset_right > 0.0 {
let logical_inset_right = self.cell_renderer.grid.content_inset_right / old_scale;
self.cell_renderer.grid.content_inset_right = logical_inset_right * new_scale;
}
if self.cell_renderer.grid.egui_right_inset > 0.0 {
let logical_egui_right = self.cell_renderer.grid.egui_right_inset / old_scale;
self.cell_renderer.grid.egui_right_inset = logical_egui_right * new_scale;
}
let logical_padding = self.cell_renderer.window_padding() / old_scale;
let new_physical_padding = logical_padding * new_scale;
self.cell_renderer
.update_window_padding(new_physical_padding);
let logical_scrollbar = self.cell_renderer.scrollbar.width() / old_scale;
let new_physical_scrollbar = logical_scrollbar * new_scale;
self.cell_renderer.scrollbar.update_appearance(
new_physical_scrollbar,
self.cell_renderer.scrollbar.thumb_color(),
self.cell_renderer.scrollbar.track_color(),
);
if let Some(ref mut cs) = self.custom_shader_renderer {
cs.set_scale_factor(new_scale);
}
if let Some(ref mut cs) = self.cursor_shader_renderer {
cs.set_scale_factor(new_scale);
}
}
self.resize(new_size)
}
}
impl Renderer {
pub fn size(&self) -> PhysicalSize<u32> {
self.size
}
pub fn grid_size(&self) -> (usize, usize) {
self.cell_renderer.grid_size()
}
pub fn cell_width(&self) -> f32 {
self.cell_renderer.cell_width()
}
pub fn cell_height(&self) -> f32 {
self.cell_renderer.cell_height()
}
pub fn window_padding(&self) -> f32 {
self.cell_renderer.window_padding()
}
pub fn scrollbar_width(&self) -> f32 {
self.cell_renderer.scrollbar.width()
}
pub fn chrome_overhead(&self) -> (f32, f32) {
self.cell_renderer.chrome_overhead()
}
pub fn content_offset_y(&self) -> f32 {
self.cell_renderer.content_offset_y()
}
pub fn scale_factor(&self) -> f32 {
self.cell_renderer.scale_factor
}
pub fn set_content_offset_y(&mut self, logical_offset: f32) -> Option<(usize, usize)> {
let physical_offset = logical_offset * self.cell_renderer.scale_factor;
let result = self.cell_renderer.set_content_offset_y(physical_offset);
self.graphics_renderer.set_content_offset_y(physical_offset);
if let Some(ref mut custom_shader) = self.custom_shader_renderer {
custom_shader.set_content_offset_y(physical_offset);
}
if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
cursor_shader.set_content_offset_y(physical_offset);
}
if result.is_some() {
self.dirty = true;
}
result
}
pub fn content_offset_x(&self) -> f32 {
self.cell_renderer.content_offset_x()
}
pub fn set_content_offset_x(&mut self, logical_offset: f32) -> Option<(usize, usize)> {
let physical_offset = logical_offset * self.cell_renderer.scale_factor;
let result = self.cell_renderer.set_content_offset_x(physical_offset);
self.graphics_renderer.set_content_offset_x(physical_offset);
if let Some(ref mut custom_shader) = self.custom_shader_renderer {
custom_shader.set_content_offset_x(physical_offset);
}
if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
cursor_shader.set_content_offset_x(physical_offset);
}
if result.is_some() {
self.dirty = true;
}
result
}
pub fn content_inset_bottom(&self) -> f32 {
self.cell_renderer.content_inset_bottom()
}
pub fn content_inset_right(&self) -> f32 {
self.cell_renderer.content_inset_right()
}
pub fn set_content_inset_bottom(&mut self, logical_inset: f32) -> Option<(usize, usize)> {
let physical_inset = logical_inset * self.cell_renderer.scale_factor;
let result = self.cell_renderer.set_content_inset_bottom(physical_inset);
if result.is_some() {
self.dirty = true;
self.last_scrollbar_state = (usize::MAX, 0, 0, 0, 0, 0, 0, 0, 0, 0);
}
result
}
pub fn set_content_inset_right(&mut self, logical_inset: f32) -> Option<(usize, usize)> {
let physical_inset = logical_inset * self.cell_renderer.scale_factor;
let result = self.cell_renderer.set_content_inset_right(physical_inset);
if let Some(ref mut custom_shader) = self.custom_shader_renderer {
custom_shader.set_content_inset_right(physical_inset);
}
if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
cursor_shader.set_content_inset_right(physical_inset);
}
if result.is_some() {
self.dirty = true;
self.last_scrollbar_state = (usize::MAX, 0, 0, 0, 0, 0, 0, 0, 0, 0);
}
result
}
pub fn set_egui_bottom_inset(&mut self, logical_inset: f32) -> Option<(usize, usize)> {
let physical_inset = logical_inset * self.cell_renderer.scale_factor;
if (self.cell_renderer.grid.egui_bottom_inset - physical_inset).abs() > f32::EPSILON {
self.cell_renderer.grid.egui_bottom_inset = physical_inset;
let (w, h) = (
self.cell_renderer.config.width,
self.cell_renderer.config.height,
);
return Some(self.cell_renderer.resize(w, h));
}
None
}
pub fn set_egui_right_inset(&mut self, logical_inset: f32) {
let physical_inset = logical_inset * self.cell_renderer.scale_factor;
self.cell_renderer.grid.egui_right_inset = physical_inset;
}
}