use anyhow::{Context, Result};
use std::collections::HashMap;
use std::sync::Arc;
use winit::window::Window;
use crate::scrollbar::Scrollbar;
use par_term_config::{SeparatorMark, color_u8_to_f32_a};
use par_term_fonts::font_manager::FontManager;
pub mod atlas;
pub mod background;
mod bg_instance_builder;
pub mod block_chars;
mod cursor;
mod font;
mod instance_buffers;
mod layout;
pub(crate) mod pane_render;
pub mod pipeline;
pub mod render;
mod settings;
mod surface;
mod text_instance_builder;
pub mod types;
pub(crate) use pane_render::PaneRenderViewParams;
pub use types::{Cell, PaneViewport};
pub(crate) use types::{BackgroundInstance, GlyphInfo, RowCacheEntry, TextInstance};
pub(crate) use instance_buffers::{CURSOR_OVERLAY_SLOTS, TEXT_INSTANCES_PER_CELL};
pub(crate) use cursor::CursorState;
pub(crate) use font::FontState;
pub(crate) use layout::GridLayout;
pub(crate) const MACOS_PLATFORM_DPI: f32 = 72.0;
pub(crate) const DEFAULT_PLATFORM_DPI: f32 = 96.0;
pub(crate) const FONT_REFERENCE_DPI: f32 = 72.0;
const SOLID_PIXEL_SIZE: u32 = 2;
pub(crate) const ATLAS_GLYPH_PADDING: u32 = 2;
const SURFACE_FRAME_LATENCY: u32 = 2;
const DEFAULT_GUIDE_OPACITY: f32 = 0.08;
const DEFAULT_SHADOW_ALPHA: f32 = 0.5;
const DEFAULT_SHADOW_OFFSET_PX: f32 = 2.0;
const DEFAULT_SHADOW_BLUR_PX: f32 = 3.0;
pub(crate) struct GpuPipelines {
pub(crate) bg_pipeline: wgpu::RenderPipeline,
pub(crate) text_pipeline: wgpu::RenderPipeline,
pub(crate) bg_image_pipeline: wgpu::RenderPipeline,
pub(crate) visual_bell_pipeline: wgpu::RenderPipeline,
pub(crate) text_bind_group: wgpu::BindGroup,
#[allow(dead_code)] pub(crate) text_bind_group_layout: wgpu::BindGroupLayout,
pub(crate) bg_image_bind_group: Option<wgpu::BindGroup>,
pub(crate) bg_image_bind_group_layout: wgpu::BindGroupLayout,
pub(crate) visual_bell_bind_group: wgpu::BindGroup,
pub(crate) opaque_alpha_pipeline: wgpu::RenderPipeline,
}
pub(crate) struct GpuBuffers {
pub(crate) vertex_buffer: wgpu::Buffer,
pub(crate) bg_instance_buffer: wgpu::Buffer,
pub(crate) text_instance_buffer: wgpu::Buffer,
pub(crate) bg_image_uniform_buffer: wgpu::Buffer,
pub(crate) visual_bell_uniform_buffer: wgpu::Buffer,
pub(crate) max_bg_instances: usize,
pub(crate) max_text_instances: usize,
pub(crate) actual_bg_instances: usize,
pub(crate) actual_text_instances: usize,
}
pub(crate) struct GlyphAtlas {
pub(crate) atlas_texture: wgpu::Texture,
#[allow(dead_code)] pub(crate) atlas_view: wgpu::TextureView,
pub(crate) glyph_cache: HashMap<u64, GlyphInfo>,
pub(crate) lru_head: Option<u64>,
pub(crate) lru_tail: Option<u64>,
pub(crate) atlas_next_x: u32,
pub(crate) atlas_next_y: u32,
pub(crate) atlas_row_height: u32,
pub(crate) atlas_size: u32,
pub(crate) solid_pixel_offset: (u32, u32),
}
pub(crate) struct BackgroundImageState {
pub(crate) bg_image_texture: Option<wgpu::Texture>,
pub(crate) bg_image_mode: par_term_config::BackgroundImageMode,
pub(crate) bg_image_opacity: f32,
pub(crate) bg_image_width: u32,
pub(crate) bg_image_height: u32,
pub(crate) bg_is_solid_color: bool,
pub(crate) solid_bg_color: [f32; 3],
pub(crate) pane_bg_cache: HashMap<String, background::PaneBackgroundEntry>,
pub(crate) pane_bg_uniform_cache: HashMap<String, background::PaneBgUniformEntry>,
}
pub(crate) struct SeparatorConfig {
pub(crate) enabled: bool,
pub(crate) thickness: f32,
pub(crate) opacity: f32,
pub(crate) exit_color: bool,
pub(crate) color: [f32; 3],
pub(crate) visible_marks: Vec<SeparatorMark>,
}
pub struct CellRenderer {
pub(crate) device: Arc<wgpu::Device>,
pub(crate) queue: Arc<wgpu::Queue>,
pub(crate) surface: wgpu::Surface<'static>,
pub(crate) config: wgpu::SurfaceConfiguration,
pub(crate) supported_present_modes: Vec<wgpu::PresentMode>,
pub(crate) pipelines: GpuPipelines,
pub(crate) buffers: GpuBuffers,
pub(crate) atlas: GlyphAtlas,
pub(crate) grid: GridLayout,
pub(crate) cursor: CursorState,
pub(crate) font: FontState,
pub(crate) bg_state: BackgroundImageState,
pub(crate) separator: SeparatorConfig,
pub(crate) scale_factor: f32,
pub(crate) font_manager: FontManager,
pub(crate) scrollbar: Scrollbar,
pub(crate) cells: Vec<Cell>,
pub(crate) dirty_rows: Vec<bool>,
pub(crate) row_cache: Vec<Option<RowCacheEntry>>,
pub(crate) visual_bell_intensity: f32,
pub(crate) visual_bell_color: [f32; 3],
pub(crate) window_opacity: f32,
pub(crate) background_color: [f32; 4],
pub(crate) is_focused: bool,
pub(crate) bg_instances: Vec<BackgroundInstance>,
pub(crate) text_instances: Vec<TextInstance>,
pub(crate) scratch_row_bg: Vec<BackgroundInstance>,
pub(crate) scratch_row_text: Vec<TextInstance>,
pub(crate) scratch_row_cells: Vec<Cell>,
pub(crate) scale_context: swash::scale::ScaleContext,
pub(crate) transparency_affects_only_default_background: bool,
pub(crate) keep_text_opaque: bool,
pub(crate) link_underline_style: par_term_config::LinkUnderlineStyle,
pub(crate) gutter_indicators: Vec<(usize, [f32; 4])>,
}
pub struct CellRendererConfig<'a> {
pub font_family: Option<&'a str>,
pub font_family_bold: Option<&'a str>,
pub font_family_italic: Option<&'a str>,
pub font_family_bold_italic: Option<&'a str>,
pub font_ranges: &'a [par_term_config::FontRange],
pub font_size: f32,
pub cols: usize,
pub rows: usize,
pub window_padding: f32,
pub line_spacing: f32,
pub char_spacing: f32,
pub scrollbar_position: &'a str,
pub scrollbar_width: f32,
pub scrollbar_thumb_color: [f32; 4],
pub scrollbar_track_color: [f32; 4],
pub enable_text_shaping: bool,
pub enable_ligatures: bool,
pub enable_kerning: bool,
pub font_antialias: bool,
pub font_hinting: bool,
pub font_thin_strokes: par_term_config::ThinStrokesMode,
pub minimum_contrast: f32,
pub vsync_mode: par_term_config::VsyncMode,
pub power_preference: par_term_config::PowerPreference,
pub window_opacity: f32,
pub background_color: [u8; 3],
pub background_image_path: Option<&'a str>,
pub background_image_mode: par_term_config::BackgroundImageMode,
pub background_image_opacity: f32,
}
impl CellRenderer {
pub async fn new(window: Arc<Window>, config: CellRendererConfig<'_>) -> Result<Self> {
let 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,
background_image_mode,
background_image_opacity,
} = config;
#[cfg(target_os = "windows")]
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
backends: wgpu::Backends::DX12,
..Default::default()
});
#[cfg(target_os = "macos")]
let instance = wgpu::Instance::default();
#[cfg(target_os = "linux")]
let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
backends: wgpu::Backends::VULKAN | wgpu::Backends::GL,
..Default::default()
});
let surface = instance.create_surface(window.clone())?;
let adapter = instance
.request_adapter(&wgpu::RequestAdapterOptions {
power_preference: power_preference.to_wgpu(),
compatible_surface: Some(&surface),
force_fallback_adapter: false,
})
.await
.context("Failed to find wgpu adapter")?;
let (device, queue) = adapter
.request_device(&wgpu::DeviceDescriptor {
label: Some("device"),
required_features: wgpu::Features::empty(),
required_limits: wgpu::Limits::default(),
memory_hints: wgpu::MemoryHints::default(),
..Default::default()
})
.await?;
let device = Arc::new(device);
let queue = Arc::new(queue);
let size = window.inner_size();
let surface_caps = surface.get_capabilities(&adapter);
let surface_format = surface_caps
.formats
.iter()
.copied()
.find(|f| !f.is_srgb())
.unwrap_or(surface_caps.formats[0]);
let supported_present_modes = surface_caps.present_modes.clone();
let requested_mode = vsync_mode.to_present_mode();
let present_mode = if supported_present_modes.contains(&requested_mode) {
requested_mode
} else {
log::warn!(
"Requested present mode {:?} not supported (available: {:?}), falling back",
requested_mode,
supported_present_modes
);
if supported_present_modes.contains(&wgpu::PresentMode::Fifo) {
wgpu::PresentMode::Fifo
} else {
supported_present_modes[0]
}
};
let alpha_mode = if surface_caps
.alpha_modes
.contains(&wgpu::CompositeAlphaMode::PreMultiplied)
{
wgpu::CompositeAlphaMode::PreMultiplied
} else if surface_caps
.alpha_modes
.contains(&wgpu::CompositeAlphaMode::PostMultiplied)
{
wgpu::CompositeAlphaMode::PostMultiplied
} else if surface_caps
.alpha_modes
.contains(&wgpu::CompositeAlphaMode::Auto)
{
wgpu::CompositeAlphaMode::Auto
} else {
surface_caps.alpha_modes[0]
};
log::info!(
"Selected alpha mode: {:?} (available: {:?})",
alpha_mode,
surface_caps.alpha_modes
);
let config = wgpu::SurfaceConfiguration {
usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
format: surface_format,
width: size.width.max(1),
height: size.height.max(1),
present_mode,
alpha_mode,
view_formats: vec![],
desired_maximum_frame_latency: SURFACE_FRAME_LATENCY,
};
surface.configure(&device, &config);
let scale_factor = window.scale_factor() as f32;
let platform_dpi = if cfg!(target_os = "macos") {
MACOS_PLATFORM_DPI
} else {
DEFAULT_PLATFORM_DPI
};
let base_font_pixels = font_size * platform_dpi / FONT_REFERENCE_DPI;
let font_size_pixels = (base_font_pixels * scale_factor).max(1.0);
let 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 cell_height = (natural_line_height * line_spacing).max(1.0).round();
let cell_width = (char_advance * char_spacing).max(1.0).round();
let scrollbar = Scrollbar::new(
Arc::clone(&device),
surface_format,
scrollbar_width,
scrollbar_position,
scrollbar_thumb_color,
scrollbar_track_color,
);
let bg_pipeline = pipeline::create_bg_pipeline(&device, surface_format);
let (atlas_texture, atlas_view, atlas_sampler, atlas_size) =
pipeline::create_atlas(&device);
let text_bind_group_layout = pipeline::create_text_bind_group_layout(&device);
let text_bind_group = pipeline::create_text_bind_group(
&device,
&text_bind_group_layout,
&atlas_view,
&atlas_sampler,
);
let text_pipeline =
pipeline::create_text_pipeline(&device, surface_format, &text_bind_group_layout);
let bg_image_bind_group_layout = pipeline::create_bg_image_bind_group_layout(&device);
let bg_image_pipeline = pipeline::create_bg_image_pipeline(
&device,
surface_format,
&bg_image_bind_group_layout,
);
let bg_image_uniform_buffer = pipeline::create_bg_image_uniform_buffer(&device);
let (visual_bell_pipeline, visual_bell_bind_group, _, visual_bell_uniform_buffer) =
pipeline::create_visual_bell_pipeline(&device, surface_format);
let opaque_alpha_pipeline = pipeline::create_opaque_alpha_pipeline(&device, surface_format);
let vertex_buffer = pipeline::create_vertex_buffer(&device);
let max_bg_instances = cols * rows + CURSOR_OVERLAY_SLOTS + rows + rows;
let max_text_instances = cols * rows * TEXT_INSTANCES_PER_CELL;
let (bg_instance_buffer, text_instance_buffer) =
pipeline::create_instance_buffers(&device, max_bg_instances, max_text_instances);
let mut renderer = Self {
device,
queue,
surface,
config,
supported_present_modes,
pipelines: GpuPipelines {
bg_pipeline,
text_pipeline,
bg_image_pipeline,
visual_bell_pipeline,
text_bind_group,
text_bind_group_layout,
bg_image_bind_group: None,
bg_image_bind_group_layout,
visual_bell_bind_group,
opaque_alpha_pipeline,
},
buffers: GpuBuffers {
vertex_buffer,
bg_instance_buffer,
text_instance_buffer,
bg_image_uniform_buffer,
visual_bell_uniform_buffer,
max_bg_instances,
max_text_instances,
actual_bg_instances: 0,
actual_text_instances: 0,
},
atlas: GlyphAtlas {
atlas_texture,
atlas_view,
glyph_cache: HashMap::new(),
lru_head: None,
lru_tail: None,
atlas_next_x: 0,
atlas_next_y: 0,
atlas_row_height: 0,
atlas_size,
solid_pixel_offset: (0, 0),
},
grid: GridLayout {
cols,
rows,
cell_width,
cell_height,
window_padding,
content_offset_y: 0.0,
content_offset_x: 0.0,
content_inset_bottom: 0.0,
content_inset_right: 0.0,
egui_bottom_inset: 0.0,
egui_right_inset: 0.0,
},
cursor: CursorState {
pos: (0, 0),
opacity: 0.0,
style: par_term_emu_core_rust::cursor::CursorStyle::SteadyBlock,
color: [1.0, 1.0, 1.0],
text_color: None,
hidden_for_shader: false,
guide_enabled: false,
guide_color: [1.0, 1.0, 1.0, DEFAULT_GUIDE_OPACITY],
shadow_enabled: false,
shadow_color: [0.0, 0.0, 0.0, DEFAULT_SHADOW_ALPHA],
shadow_offset: [DEFAULT_SHADOW_OFFSET_PX, DEFAULT_SHADOW_OFFSET_PX],
shadow_blur: DEFAULT_SHADOW_BLUR_PX,
boost: 0.0,
boost_color: [1.0, 1.0, 1.0],
unfocused_style: par_term_config::UnfocusedCursorStyle::default(),
},
font: FontState {
base_font_size: font_size,
line_spacing,
char_spacing,
font_ascent,
font_descent,
font_leading,
font_size_pixels,
char_advance,
enable_text_shaping,
enable_ligatures,
enable_kerning,
font_antialias,
font_hinting,
font_thin_strokes,
minimum_contrast: minimum_contrast.clamp(0.0, 1.0),
},
bg_state: BackgroundImageState {
bg_image_texture: None,
bg_image_mode: background_image_mode,
bg_image_opacity: background_image_opacity,
bg_image_width: 0,
bg_image_height: 0,
bg_is_solid_color: false,
solid_bg_color: [0.0, 0.0, 0.0],
pane_bg_cache: HashMap::new(),
pane_bg_uniform_cache: HashMap::new(),
},
separator: SeparatorConfig {
enabled: false,
thickness: 1.0,
opacity: 0.4,
exit_color: true,
color: [0.5, 0.5, 0.5],
visible_marks: Vec::new(),
},
scale_factor,
font_manager,
scrollbar,
cells: vec![Cell::default(); cols * rows],
dirty_rows: vec![true; rows],
row_cache: (0..rows).map(|_| None).collect(),
is_focused: true,
visual_bell_intensity: 0.0,
visual_bell_color: [1.0, 1.0, 1.0], window_opacity,
background_color: color_u8_to_f32_a(background_color, 1.0),
bg_instances: vec![
BackgroundInstance {
position: [0.0, 0.0],
size: [0.0, 0.0],
color: [0.0, 0.0, 0.0, 0.0],
};
max_bg_instances
],
text_instances: vec![
TextInstance {
position: [0.0, 0.0],
size: [0.0, 0.0],
tex_offset: [0.0, 0.0],
tex_size: [0.0, 0.0],
color: [0.0, 0.0, 0.0, 0.0],
is_colored: 0,
};
max_text_instances
],
transparency_affects_only_default_background: false,
keep_text_opaque: true,
link_underline_style: par_term_config::LinkUnderlineStyle::default(),
gutter_indicators: Vec::new(),
scratch_row_bg: Vec::with_capacity(cols),
scratch_row_text: Vec::with_capacity(cols * 2),
scratch_row_cells: Vec::with_capacity(cols),
scale_context: swash::scale::ScaleContext::new(),
};
renderer.upload_solid_pixel();
log::info!(
"CellRenderer::new: background_image_path={:?}",
background_image_path
);
if let Some(path) = background_image_path {
if let Err(e) = renderer.load_background_image(path) {
log::warn!(
"Could not load background image '{}': {} - continuing without background image",
path,
e
);
}
}
Ok(renderer)
}
pub(crate) fn upload_solid_pixel(&mut self) {
let size = SOLID_PIXEL_SIZE;
let white_pixels: Vec<u8> = vec![255; (size * size * 4) as usize];
self.queue.write_texture(
wgpu::TexelCopyTextureInfo {
texture: &self.atlas.atlas_texture,
mip_level: 0,
origin: wgpu::Origin3d {
x: self.atlas.atlas_next_x,
y: self.atlas.atlas_next_y,
z: 0,
},
aspect: wgpu::TextureAspect::All,
},
&white_pixels,
wgpu::TexelCopyBufferLayout {
offset: 0,
bytes_per_row: Some(4 * size),
rows_per_image: Some(size),
},
wgpu::Extent3d {
width: size,
height: size,
depth_or_array_layers: 1,
},
);
self.atlas.solid_pixel_offset = (self.atlas.atlas_next_x, self.atlas.atlas_next_y);
self.atlas.atlas_next_x += size + ATLAS_GLYPH_PADDING;
self.atlas.atlas_row_height = self.atlas.atlas_row_height.max(size);
}
pub fn device(&self) -> &wgpu::Device {
&self.device
}
pub fn queue(&self) -> &wgpu::Queue {
&self.queue
}
pub fn surface_format(&self) -> wgpu::TextureFormat {
self.config.format
}
pub fn keep_text_opaque(&self) -> bool {
self.keep_text_opaque
}
pub fn update_cells(&mut self, new_cells: &[Cell]) -> bool {
let mut changed = false;
for row in 0..self.grid.rows {
let start = row * self.grid.cols;
let end = (row + 1) * self.grid.cols;
if start < new_cells.len() && end <= new_cells.len() {
let row_slice = &new_cells[start..end];
if row_slice != &self.cells[start..end] {
self.cells[start..end].clone_from_slice(row_slice);
self.dirty_rows[row] = true;
changed = true;
}
}
}
changed
}
pub fn clear_all_cells(&mut self) {
for cell in &mut self.cells {
*cell = Cell::default();
}
for dirty in &mut self.dirty_rows {
*dirty = true;
}
}
pub fn update_graphics(
&mut self,
_graphics: &[par_term_emu_core_rust::graphics::TerminalGraphic],
_scroll_offset: usize,
_scrollback_len: usize,
_visible_lines: usize,
) -> Result<()> {
Ok(())
}
}