use crate::cell_renderer::{PaneViewport, pane_instance_capacity};
use anyhow::Result;
use super::{
DividerRenderInfo, PaneDividerSettings, PaneRenderInfo, PaneTitleInfo, Renderer, SeparatorMark,
fill_visible_separator_marks,
};
fn should_populate_terminal_intermediate_texture(
full_content_mode: bool,
auto_dim_under_text: bool,
auto_dim_strength: f32,
) -> bool {
full_content_mode || (auto_dim_under_text && auto_dim_strength > 0.0)
}
struct PaneBatchSettings {
bg_capacity: usize,
text_capacity: usize,
skip_background_image: bool,
fill_default_bg_cells: bool,
}
pub struct PaneCaptureParams<'a> {
pub panes: &'a [PaneRenderInfo<'a>],
pub dividers: &'a [DividerRenderInfo],
pub pane_titles: &'a [PaneTitleInfo],
pub focused_viewport: Option<&'a PaneViewport>,
pub divider_settings: &'a PaneDividerSettings,
}
pub struct SplitPanesRenderParams<'a> {
pub panes: &'a [PaneRenderInfo<'a>],
pub dividers: &'a [DividerRenderInfo],
pub pane_titles: &'a [PaneTitleInfo],
pub focused_viewport: Option<&'a PaneViewport>,
pub divider_settings: &'a PaneDividerSettings,
pub egui_data: Option<(egui::FullOutput, &'a egui::Context)>,
pub force_egui_opaque: bool,
}
impl Renderer {
fn update_pane_scrollbar(&mut self, index: usize, pane: &PaneRenderInfo<'_>) {
if pane.show_scrollbar {
let total_lines = pane.scrollback_len + pane.grid_size.1;
self.cell_renderer.update_scrollbar_for_pane(
index,
pane.scroll_offset,
pane.grid_size.1,
total_lines,
&pane.marks,
&pane.viewport,
);
} else if pane.viewport.focused {
self.cell_renderer.scrollbar.clear_hit_state();
}
}
fn prepare_and_draw_panes(
&mut self,
target_view: &wgpu::TextureView,
panes: &[PaneRenderInfo<'_>],
settings: PaneBatchSettings,
) -> Result<()> {
self.cell_renderer
.begin_pane_batch(settings.bg_capacity, settings.text_capacity);
let mut scratch: Vec<SeparatorMark> = Vec::new();
let mut prepared = Vec::with_capacity(panes.len());
for (index, pane) in panes.iter().enumerate() {
self.update_pane_scrollbar(index, pane);
fill_visible_separator_marks(
&mut scratch,
&pane.marks,
pane.scrollback_len,
pane.scroll_offset,
pane.grid_size.1,
);
prepared.push(self.cell_renderer.prepare_pane(
index,
crate::cell_renderer::PaneRenderViewParams {
viewport: &pane.viewport,
cells: pane.cells,
cols: pane.grid_size.0,
rows: pane.grid_size.1,
cursor_pos: pane.cursor_pos,
cursor_opacity: pane.cursor_opacity,
show_scrollbar: pane.show_scrollbar,
clear_first: false, skip_background_image: settings.skip_background_image,
fill_default_bg_cells: settings.fill_default_bg_cells,
separator_marks: &scratch,
pane_background: pane.background.as_ref(),
},
)?);
}
self.cell_renderer
.draw_prepared_panes(target_view, &prepared);
Ok(())
}
fn preload_pane_backgrounds(&mut self, panes: &[PaneRenderInfo<'_>]) {
for pane in panes.iter() {
if let Some(ref bg) = pane.background
&& let Some(ref path) = bg.image_path
&& let Err(e) = self.cell_renderer.load_pane_background(path)
{
log::error!("Failed to load pane background '{}': {}", path, e);
}
}
}
fn composite_panes(
&mut self,
p: PaneCaptureParams<'_>,
final_view: &wgpu::TextureView,
) -> Result<()> {
let PaneCaptureParams {
panes,
dividers,
pane_titles,
focused_viewport,
divider_settings,
} = p;
let has_custom_shader = self.custom_shader_renderer.is_some();
let use_cursor_shader =
self.cursor_shader_renderer.is_some() && !self.cursor_shader_disabled_for_alt_screen;
let (batch_bg_capacity, batch_text_capacity) =
panes.iter().fold((0usize, 0usize), |(bg, text), pane| {
let (pane_bg, pane_text) =
pane_instance_capacity(pane.grid_size.0, pane.grid_size.1);
(bg + pane_bg, text + pane_text)
});
let cursor_intermediate: Option<wgpu::TextureView> = if use_cursor_shader {
Some(
self.cursor_shader_renderer
.as_ref()
.ok_or_else(|| {
crate::error::RenderError::ShaderUnavailable(
"cursor_shader_renderer unavailable (GPU device loss?)".into(),
)
})?
.intermediate_texture_view()
.clone(),
)
} else {
None
};
let content_view = cursor_intermediate.as_ref().unwrap_or(final_view);
let opacity = self.cell_renderer.window_opacity as f64;
let clear_color = if self.cell_renderer.pipelines.bg_image_bind_group.is_some() {
wgpu::Color::TRANSPARENT
} else if use_cursor_shader {
wgpu::Color {
r: self.cell_renderer.background_color[0] as f64,
g: self.cell_renderer.background_color[1] as f64,
b: self.cell_renderer.background_color[2] as f64,
a: 1.0,
}
} else {
wgpu::Color {
r: self.cell_renderer.background_color[0] as f64 * opacity,
g: self.cell_renderer.background_color[1] as f64 * opacity,
b: self.cell_renderer.background_color[2] as f64 * opacity,
a: opacity,
}
};
let (full_content_mode, populate_terminal_intermediate_texture) = self
.custom_shader_renderer
.as_ref()
.map(|s| {
let full_content_mode = s.full_content_mode();
(
full_content_mode,
should_populate_terminal_intermediate_texture(
full_content_mode,
s.auto_dim_under_text,
s.auto_dim_strength,
),
)
})
.unwrap_or((false, false));
if populate_terminal_intermediate_texture {
let custom_shader = self.custom_shader_renderer.as_mut().ok_or_else(|| {
crate::error::RenderError::ShaderUnavailable(
"custom_shader_renderer unavailable for iChannel4 content (GPU device loss?)"
.into(),
)
})?;
custom_shader.clear_intermediate_texture(
self.cell_renderer.device(),
self.cell_renderer.queue(),
);
let intermediate_view = custom_shader.intermediate_texture_view().clone();
self.prepare_and_draw_panes(
&intermediate_view,
panes,
PaneBatchSettings {
bg_capacity: batch_bg_capacity,
text_capacity: batch_text_capacity,
skip_background_image: true, fill_default_bg_cells: false, },
)?;
for pane in panes.iter() {
if !pane.graphics.is_empty() || !pane.virtual_placements.is_empty() {
self.render_pane_sixel_graphics(
&intermediate_view,
&pane.viewport,
&pane.graphics,
pane.scroll_offset,
pane.scrollback_len,
pane.grid_size.1,
pane.cells,
pane.grid_size.0,
&pane.virtual_placements,
)?;
}
}
}
if let Some(ref mut custom_shader) = self.custom_shader_renderer {
if !populate_terminal_intermediate_texture {
custom_shader.clear_intermediate_texture(
self.cell_renderer.device(),
self.cell_renderer.queue(),
);
}
custom_shader.render_with_clear_color(
self.cell_renderer.device(),
self.cell_renderer.queue(),
content_view,
!use_cursor_shader, clear_color,
)?;
} else {
let mut encoder = self.cell_renderer.device().create_command_encoder(
&wgpu::CommandEncoderDescriptor {
label: Some("split pane clear encoder"),
},
);
{
let _clear_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("surface clear pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: content_view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Clear(clear_color),
store: wgpu::StoreOp::Store,
},
depth_slice: None,
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
}
self.cell_renderer
.queue()
.submit(std::iter::once(encoder.finish()));
}
let any_pane_has_background = panes.iter().any(|p| p.background.is_some());
let has_background_image = if !has_custom_shader && !any_pane_has_background {
self.cell_renderer
.render_background_only(content_view, false)?
} else {
false
};
if !full_content_mode {
self.prepare_and_draw_panes(
content_view,
panes,
PaneBatchSettings {
bg_capacity: batch_bg_capacity,
text_capacity: batch_text_capacity,
skip_background_image: has_background_image || has_custom_shader,
fill_default_bg_cells: has_background_image,
},
)?;
for pane in panes {
if !pane.graphics.is_empty() || !pane.virtual_placements.is_empty() {
self.render_pane_sixel_graphics(
content_view,
&pane.viewport,
&pane.graphics,
pane.scroll_offset,
pane.scrollback_len,
pane.grid_size.1,
pane.cells,
pane.grid_size.0,
&pane.virtual_placements,
)?;
}
}
}
if !dividers.is_empty() {
self.render_dividers(content_view, dividers, divider_settings)?;
}
if !pane_titles.is_empty() {
self.render_pane_titles(content_view, pane_titles)?;
}
if self.cell_renderer.visual_bell_intensity > 0.0 {
let uniforms: [f32; 8] = [
-1.0, -1.0, 2.0, 2.0, self.cell_renderer.visual_bell_color[0], self.cell_renderer.visual_bell_color[1], self.cell_renderer.visual_bell_color[2], self.cell_renderer.visual_bell_intensity, ];
self.cell_renderer.queue().write_buffer(
&self.cell_renderer.buffers.visual_bell_uniform_buffer,
0,
bytemuck::cast_slice(&uniforms),
);
let mut encoder = self.cell_renderer.device().create_command_encoder(
&wgpu::CommandEncoderDescriptor {
label: Some("visual bell encoder"),
},
);
{
let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
label: Some("visual bell pass"),
color_attachments: &[Some(wgpu::RenderPassColorAttachment {
view: content_view,
resolve_target: None,
ops: wgpu::Operations {
load: wgpu::LoadOp::Load,
store: wgpu::StoreOp::Store,
},
depth_slice: None,
})],
depth_stencil_attachment: None,
timestamp_writes: None,
occlusion_query_set: None,
multiview_mask: None,
});
render_pass.set_pipeline(&self.cell_renderer.pipelines.visual_bell_pipeline);
render_pass.set_bind_group(
0,
&self.cell_renderer.pipelines.visual_bell_bind_group,
&[],
);
render_pass.draw(0..4, 0..1); }
self.cell_renderer
.queue()
.submit(std::iter::once(encoder.finish()));
}
if panes.len() > 1
&& let Some(viewport) = focused_viewport
{
self.render_focus_indicator(content_view, viewport, divider_settings)?;
}
if use_cursor_shader {
self.cursor_shader_renderer
.as_mut()
.ok_or_else(|| crate::error::RenderError::ShaderUnavailable(
"cursor_shader_renderer unavailable during final composite (GPU device loss?)".into(),
))?
.render(
self.cell_renderer.device(),
self.cell_renderer.queue(),
final_view,
true, )?;
}
Ok(())
}
pub fn render_split_panes(&mut self, params: SplitPanesRenderParams<'_>) -> Result<bool> {
let SplitPanesRenderParams {
panes,
dividers,
pane_titles,
focused_viewport,
divider_settings,
egui_data,
force_egui_opaque,
} = params;
let force_render = self.needs_continuous_render();
if !self.dirty && !force_render && egui_data.is_none() {
return Ok(false);
}
self.preload_pane_backgrounds(panes);
let surface_texture = match self.cell_renderer.surface.get_current_texture() {
wgpu::CurrentSurfaceTexture::Success(t)
| wgpu::CurrentSurfaceTexture::Suboptimal(t) => t,
other => return Err(crate::error::RenderError::Surface(format!("{other:?}")).into()),
};
let surface_view = surface_texture
.texture
.create_view(&wgpu::TextureViewDescriptor::default());
self.composite_panes(
PaneCaptureParams {
panes,
dividers,
pane_titles,
focused_viewport,
divider_settings,
},
&surface_view,
)?;
if let Some((egui_output, egui_ctx)) = egui_data {
self.render_egui(&surface_texture, egui_output, egui_ctx, force_egui_opaque)?;
}
self.cell_renderer.render_opaque_alpha(&surface_texture)?;
surface_texture.present();
self.dirty = false;
Ok(true)
}
pub(super) fn composite_panes_offscreen(
&mut self,
params: PaneCaptureParams<'_>,
target_view: &wgpu::TextureView,
) -> Result<()> {
self.preload_pane_backgrounds(params.panes);
self.composite_panes(params, target_view)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn background_shader_auto_dim_requires_terminal_intermediate_texture() {
assert!(should_populate_terminal_intermediate_texture(
false, true, 0.35
));
}
#[test]
fn full_content_mode_requires_terminal_intermediate_texture() {
assert!(should_populate_terminal_intermediate_texture(
true, false, 0.0
));
}
#[test]
fn background_only_shader_without_auto_dim_skips_terminal_intermediate_texture() {
assert!(!should_populate_terminal_intermediate_texture(
false, false, 0.35
));
assert!(!should_populate_terminal_intermediate_texture(
false, true, 0.0
));
}
}