use super::types::RendererSizing;
use crate::config::{Config, PaneTitlePosition, ScrollbackMark, color_u8_to_f32};
use crate::selection::SelectionMode;
use anyhow::Result;
use par_term_render::cell_renderer::PaneViewport;
use par_term_render::renderer::{
DividerRenderInfo, PaneDividerSettings, PaneRenderInfo, PaneTitleInfo, Renderer,
};
use std::sync::Arc;
pub(super) struct PaneRenderData {
pub(super) viewport: PaneViewport,
pub(super) cells: Arc<Vec<crate::config::Cell>>,
pub(super) grid_size: (usize, usize),
pub(super) cursor_pos: Option<(usize, usize)>,
pub(super) cursor_opacity: f32,
pub(super) marks: Vec<ScrollbackMark>,
pub(super) scrollback_len: usize,
pub(super) scroll_offset: usize,
pub(super) background: Option<crate::pane::PaneBackground>,
pub(super) graphics: Vec<par_term_emu_core_rust::graphics::TerminalGraphic>,
pub(super) virtual_placements: Vec<par_term_emu_core_rust::graphics::TerminalGraphic>,
}
pub(super) type PaneRenderDataResult = Option<(
Vec<PaneRenderData>,
Vec<crate::pane::DividerRect>,
Vec<PaneTitleInfo>,
Option<PaneViewport>,
usize, // focused pane scrollback_len (for tab.cache update)
)>;
pub(super) fn gather_pane_render_data(
tab: &mut crate::tab::Tab,
config: &Config,
sizing: &RendererSizing,
effective_pane_padding: f32,
cursor_opacity: f32,
pane_count: usize,
scrollbar_inset: f32,
) -> PaneRenderDataResult {
let effective_padding = if pane_count > 1 && config.window.hide_window_padding_on_split {
0.0
} else {
sizing.padding
};
let content_width = sizing.size.width as f32
- effective_padding * 2.0
- sizing.content_offset_x
- sizing.content_inset_right;
let content_height = sizing.size.height as f32
- sizing.content_offset_y
- sizing.content_inset_bottom
- effective_padding
- sizing.status_bar_height;
let tab_scroll_offset = tab.active_scroll_state().offset;
let pm = tab.pane_manager.as_mut()?;
let bounds = crate::pane::PaneBounds::new(
effective_padding + sizing.content_offset_x,
sizing.content_offset_y,
content_width,
content_height,
);
pm.set_bounds(bounds);
let focused_pane_id = pm.focused_pane_id();
let all_pane_ids: Vec<_> = pm.all_panes().iter().map(|p| p.id).collect();
let dividers = pm.get_dividers();
let pane_bg_opacity = config.panes.pane_background_opacity;
let inactive_opacity = if config.panes.dim_inactive_panes {
config.panes.inactive_pane_opacity
} else {
1.0
};
let show_titles = config.panes.show_pane_titles;
let title_height = config.panes.pane_title_height * sizing.scale_factor;
let title_position = config.panes.pane_title_position;
let title_text_color = color_u8_to_f32(config.panes.pane_title_color);
let title_bg_color = color_u8_to_f32(config.panes.pane_title_bg_color);
let need_marks = config.scrollbar.scrollbar_command_marks
|| config.command_separator.command_separator_enabled;
let mut pane_data: Vec<PaneRenderData> = Vec::new();
let mut pane_titles: Vec<PaneTitleInfo> = Vec::new();
let mut focused_pane_scrollback_len: usize = 0;
let mut focused_viewport: Option<PaneViewport> = None;
for pane_id in &all_pane_ids {
let Some(pane) = pm.get_pane_mut(*pane_id) else {
continue;
};
let is_focused = Some(*pane_id) == focused_pane_id;
let bounds = pane.bounds;
let (viewport_y, viewport_height) = if show_titles {
match title_position {
PaneTitlePosition::Top => (
bounds.y + title_height,
(bounds.height - title_height).max(0.0),
),
PaneTitlePosition::Bottom => (bounds.y, (bounds.height - title_height).max(0.0)),
}
} else {
(bounds.y, bounds.height)
};
let physical_pane_padding = effective_pane_padding * sizing.scale_factor;
let sb_inset = scrollbar_inset;
let content_w =
(bounds.width - physical_pane_padding * 2.0 - sb_inset).max(sizing.cell_width);
let content_h = (viewport_height - physical_pane_padding * 2.0).max(sizing.cell_height);
let cols = ((content_w / sizing.cell_width).floor() as usize).max(1);
let rows = ((content_h / sizing.cell_height).floor() as usize).max(1);
let actual_content_w = cols as f32 * sizing.cell_width;
let actual_content_h = rows as f32 * sizing.cell_height;
let center_offset_x = ((content_w - actual_content_w) / 2.0).floor();
let center_offset_y = ((content_h - actual_content_h) / 2.0).floor();
pane.resize_terminal_with_cell_dims(
cols,
rows,
sizing.cell_width as u32,
sizing.cell_height as u32,
);
let mut viewport = PaneViewport::with_padding(
bounds.x,
viewport_y,
bounds.width,
viewport_height,
is_focused,
if is_focused {
pane_bg_opacity
} else {
pane_bg_opacity * inactive_opacity
},
physical_pane_padding,
);
viewport.content_offset_x = center_offset_x;
viewport.content_offset_y = center_offset_y;
if is_focused {
focused_viewport = Some(viewport);
}
if show_titles {
let title_y = match title_position {
PaneTitlePosition::Top => bounds.y,
PaneTitlePosition::Bottom => bounds.y + bounds.height - title_height,
};
pane_titles.push(PaneTitleInfo {
x: bounds.x,
y: title_y,
width: bounds.width,
height: title_height,
title: pane.get_title(),
focused: is_focused,
text_color: title_text_color,
bg_color: title_bg_color,
});
}
let grid_size = (cols, rows);
let expected_cell_count = cols * rows;
let scroll_offset = if is_focused { tab_scroll_offset } else { 0 };
let current_selection = pane.mouse.selection;
let cache_dims_match = is_focused
&& pane.cache.pane_cells_generation > 0
&& pane.cache.pane_cells_scroll_offset == scroll_offset
&& pane.cache.pane_cells_selection == current_selection
&& pane.cache.pane_cells_grid_dims == grid_size
&& pane
.cache
.pane_cells
.as_ref()
.is_some_and(|c| c.len() == expected_cell_count);
let cells = if cache_dims_match {
Arc::clone(
pane.cache
.pane_cells
.as_ref()
.expect("pane_cells must exist when pane cache dimensions match"),
)
} else if let Ok(term) = pane.terminal.try_read() {
let current_gen = term.update_generation();
let selection =
current_selection.map(|sel| sel.viewport_adjusted(scroll_offset).normalized());
let rectangular = current_selection
.map(|sel| sel.mode == SelectionMode::Rectangular)
.unwrap_or(false);
if current_gen == pane.cache.pane_cells_generation
&& pane.cache.pane_cells_scroll_offset == scroll_offset
&& pane.cache.pane_cells_selection == current_selection
&& pane.cache.pane_cells_grid_dims == grid_size
&& let Some(ref cached) = pane.cache.pane_cells
&& cached.len() == expected_cell_count
{
Arc::clone(cached)
} else if let Some(fresh) =
term.try_get_cells_with_scrollback(scroll_offset, selection, rectangular)
{
let fresh = Arc::new(fresh);
pane.cache.pane_cells = Some(Arc::clone(&fresh));
pane.cache.pane_cells_generation = current_gen;
pane.cache.pane_cells_scroll_offset = scroll_offset;
pane.cache.pane_cells_selection = current_selection;
pane.cache.pane_cells_grid_dims = grid_size;
fresh
} else if pane.cache.pane_cells_grid_dims == grid_size
&& let Some(ref cached) = pane.cache.pane_cells
{
Arc::clone(cached)
} else {
Arc::new(Vec::new())
}
} else if pane.cache.pane_cells_grid_dims == grid_size
&& let Some(ref cached) = pane.cache.pane_cells
{
Arc::clone(cached)
} else {
Arc::new(Vec::new())
};
let (marks, pane_scrollback_len) = if need_marks {
if let Ok(mut term) = pane.terminal.try_write() {
let sb_len = term.scrollback_len();
term.update_scrollback_metadata(sb_len, 0);
pane.cache.pane_scrollback_len = sb_len;
(term.scrollback_marks(), sb_len)
} else {
(Vec::new(), pane.cache.pane_scrollback_len)
}
} else {
let sb_len = if let Ok(term) = pane.terminal.try_read() {
pane.cache.pane_scrollback_len = term.scrollback_len();
pane.cache.pane_scrollback_len
} else {
pane.cache.pane_scrollback_len
};
(Vec::new(), sb_len)
};
let pane_scroll_offset = if is_focused { tab_scroll_offset } else { 0 };
if is_focused && pane_scrollback_len > 0 {
focused_pane_scrollback_len = pane_scrollback_len;
}
let pane_background = if all_pane_ids.len() > 1 && pane.background().has_image() {
Some(pane.background().clone())
} else {
None
};
let cursor_pos = if scroll_offset == 0 {
if let Ok(term) = pane.terminal.try_read() {
if term.is_cursor_visible() {
Some(term.cursor_position())
} else {
None
}
} else {
None
}
} else {
None
};
let pane_virtual_placements = if let Ok(term) = pane.terminal.try_read() {
term.get_virtual_placements()
} else {
Vec::new()
};
let pane_graphics = if let Ok(term) = pane.terminal.try_read() {
term.invalidate_overwritten_graphics();
let mut g = term.get_graphics_with_animations();
let sb = term.get_scrollback_graphics();
crate::debug_log!(
"GRAPHICS",
"pane {:?}: active_graphics={}, scrollback_graphics={}, scrollback_len={}, scroll_offset={}, visible_rows={}, viewport=({},{},{}x{})",
pane_id,
g.len(),
sb.len(),
pane_scrollback_len,
pane_scroll_offset,
rows,
viewport.x,
viewport.y,
viewport.width,
viewport.height
);
for (i, gfx) in g.iter().chain(sb.iter()).enumerate() {
crate::debug_log!(
"GRAPHICS",
" graphic[{}]: id={}, pos=({},{}), scroll_offset_rows={}, scrollback_row={:?}, size={}x{}",
i,
gfx.id,
gfx.position.0,
gfx.position.1,
gfx.scroll_offset_rows,
gfx.scrollback_row,
gfx.width,
gfx.height
);
}
g.extend(sb);
g
} else {
crate::debug_log!(
"GRAPHICS",
"pane {:?}: try_lock() failed, no graphics",
pane_id
);
Vec::new()
};
pane_data.push(PaneRenderData {
viewport,
cells,
grid_size: (cols, rows),
cursor_pos,
cursor_opacity: if is_focused { cursor_opacity } else { 0.0 },
marks,
scrollback_len: pane_scrollback_len,
scroll_offset: pane_scroll_offset,
background: pane_background,
graphics: pane_graphics,
virtual_placements: pane_virtual_placements,
});
}
Some((
pane_data,
dividers,
pane_titles,
focused_viewport,
focused_pane_scrollback_len,
))
}
pub(super) struct SplitPaneRenderParams<'a> {
pub pane_data: Vec<PaneRenderData>,
pub dividers: Vec<crate::pane::DividerRect>,
pub pane_titles: Vec<PaneTitleInfo>,
pub focused_viewport: Option<PaneViewport>,
pub config: &'a Config,
pub egui_data: Option<(egui::FullOutput, &'a egui::Context)>,
pub hovered_divider_index: Option<usize>,
pub show_scrollbar: bool,
}
pub(super) struct PaneCaptureInput<'a> {
pub pane_data: Vec<PaneRenderData>,
pub dividers: Vec<crate::pane::DividerRect>,
pub pane_titles: Vec<PaneTitleInfo>,
pub focused_viewport: Option<PaneViewport>,
pub config: &'a Config,
pub hovered_divider_index: Option<usize>,
pub show_scrollbar: bool,
}
fn with_pane_capture_params<R>(
renderer: &mut Renderer,
input: PaneCaptureInput<'_>,
f: impl FnOnce(&mut Renderer, par_term_render::renderer::PaneCaptureParams<'_>) -> R,
) -> R {
let PaneCaptureInput {
pane_data,
dividers,
pane_titles,
focused_viewport,
config,
hovered_divider_index,
show_scrollbar,
} = input;
let mut owned_cells: Vec<Arc<Vec<crate::config::Cell>>> = Vec::with_capacity(pane_data.len());
let mut partial_infos: Vec<PaneRenderInfo> = Vec::with_capacity(pane_data.len());
for pane in pane_data {
let focused = pane.viewport.focused;
owned_cells.push(pane.cells);
partial_infos.push(PaneRenderInfo {
viewport: pane.viewport,
cells: &[],
grid_size: pane.grid_size,
cursor_pos: pane.cursor_pos,
cursor_opacity: pane.cursor_opacity,
show_scrollbar: if focused {
show_scrollbar && pane.scrollback_len > 0
} else {
pane.scrollback_len > 0
},
marks: pane.marks,
scrollback_len: pane.scrollback_len,
scroll_offset: pane.scroll_offset,
background: pane.background,
graphics: pane.graphics,
virtual_placements: pane.virtual_placements,
});
}
for (info, cells) in partial_infos.iter_mut().zip(owned_cells.iter()) {
info.cells = cells.as_slice();
}
let pane_render_infos = partial_infos;
let divider_render_infos: Vec<DividerRenderInfo> = dividers
.iter()
.enumerate()
.map(|(i, d)| DividerRenderInfo::from_rect(d, hovered_divider_index == Some(i)))
.collect();
let divider_settings = PaneDividerSettings {
divider_color: color_u8_to_f32(config.panes.pane_divider_color),
hover_color: color_u8_to_f32(config.panes.pane_divider_hover_color),
show_focus_indicator: config.panes.pane_focus_indicator,
focus_color: color_u8_to_f32(config.panes.pane_focus_color),
focus_width: config.panes.pane_focus_width * renderer.scale_factor(),
divider_style: config.panes.pane_divider_style,
};
renderer.update_shader_focused_pane(focused_viewport.as_ref());
f(
renderer,
par_term_render::renderer::PaneCaptureParams {
panes: &pane_render_infos,
dividers: ÷r_render_infos,
pane_titles: &pane_titles,
focused_viewport: focused_viewport.as_ref(),
divider_settings: ÷r_settings,
},
)
}
impl crate::app::window_state::WindowState {
pub(super) fn render_split_panes_with_data(
renderer: &mut Renderer,
p: SplitPaneRenderParams<'_>,
) -> Result<bool> {
let SplitPaneRenderParams {
pane_data,
dividers,
pane_titles,
focused_viewport,
config,
egui_data,
hovered_divider_index,
show_scrollbar,
} = p;
with_pane_capture_params(
renderer,
PaneCaptureInput {
pane_data,
dividers,
pane_titles,
focused_viewport,
config,
hovered_divider_index,
show_scrollbar,
},
move |renderer, cap| {
renderer.render_split_panes(par_term_render::renderer::SplitPanesRenderParams {
panes: cap.panes,
dividers: cap.dividers,
pane_titles: cap.pane_titles,
focused_viewport: cap.focused_viewport,
divider_settings: cap.divider_settings,
egui_data,
force_egui_opaque: false,
})
},
)
}
pub(crate) fn capture_frame_image(&mut self) -> Result<image::RgbaImage, String> {
let config = self.config.load_full();
let is_tmux_gateway = self.is_gateway_active();
let is_tmux_connected = self.is_tmux_connected();
let show_scrollbar = self.should_show_scrollbar();
let cursor_opacity = self.cursor_anim.cursor_opacity;
let status_bar_height =
crate::tmux_status_bar_ui::TmuxStatusBarUI::height(&config, is_tmux_connected);
let custom_status_bar_height = self.status_bar_ui.height(&config, self.is_fullscreen);
let pane_count = self
.tab_manager
.active_tab()
.and_then(|t| t.pane_manager.as_ref())
.map(|pm| pm.pane_count())
.unwrap_or(0);
let hovered_divider_index = self
.tab_manager
.active_tab()
.and_then(|t| t.active_mouse().hovered_divider_index);
let effective_pane_padding = if is_tmux_gateway || pane_count <= 1 {
0.0
} else {
config.panes.pane_divider_width.unwrap_or(2.0) / 2.0 + config.panes.pane_padding
};
let Some(renderer) = self.renderer.as_mut() else {
return Err("No renderer available for screenshot".to_string());
};
let sizing = RendererSizing {
size: renderer.size(),
content_offset_y: renderer.content_offset_y(),
content_offset_x: renderer.content_offset_x(),
content_inset_bottom: renderer.content_inset_bottom(),
content_inset_right: renderer.content_inset_right(),
cell_width: renderer.cell_width(),
cell_height: renderer.cell_height(),
padding: renderer.window_padding(),
status_bar_height: (status_bar_height + custom_status_bar_height)
* renderer.scale_factor(),
scale_factor: renderer.scale_factor(),
scrollbar_width: renderer.scrollbar_width(),
};
let Some((pane_data, dividers, pane_titles, focused_viewport, _)) =
self.tab_manager.active_tab_mut().and_then(|tab| {
gather_pane_render_data(
tab,
&config,
&sizing,
effective_pane_padding,
cursor_opacity,
pane_count,
sizing.scrollbar_width,
)
})
else {
return Err("No pane data available for screenshot".to_string());
};
with_pane_capture_params(
renderer,
PaneCaptureInput {
pane_data,
dividers,
pane_titles,
focused_viewport,
config: &config,
hovered_divider_index,
show_scrollbar,
},
|renderer, cap| renderer.take_screenshot(cap),
)
.map_err(|e| format!("Renderer screenshot failed: {e}"))
}
}