1use crate::cell_renderer::PaneViewport;
11use anyhow::Result;
12
13use super::{
14 DividerRenderInfo, PaneDividerSettings, PaneRenderInfo, PaneTitleInfo, Renderer, SeparatorMark,
15 fill_visible_separator_marks,
16};
17
18fn should_populate_terminal_intermediate_texture(
21 full_content_mode: bool,
22 auto_dim_under_text: bool,
23 auto_dim_strength: f32,
24) -> bool {
25 full_content_mode || (auto_dim_under_text && auto_dim_strength > 0.0)
26}
27
28pub struct SplitPanesRenderParams<'a> {
30 pub panes: &'a [PaneRenderInfo<'a>],
31 pub dividers: &'a [DividerRenderInfo],
32 pub pane_titles: &'a [PaneTitleInfo],
33 pub focused_viewport: Option<&'a PaneViewport>,
34 pub divider_settings: &'a PaneDividerSettings,
35 pub egui_data: Option<(egui::FullOutput, &'a egui::Context)>,
36 pub force_egui_opaque: bool,
37}
38
39impl Renderer {
40 pub fn render_split_panes(&mut self, params: SplitPanesRenderParams<'_>) -> Result<bool> {
62 let SplitPanesRenderParams {
63 panes,
64 dividers,
65 pane_titles,
66 focused_viewport,
67 divider_settings,
68 egui_data,
69 force_egui_opaque,
70 } = params;
71 let force_render = self.needs_continuous_render();
73 if !self.dirty && !force_render && egui_data.is_none() {
74 return Ok(false);
75 }
76
77 let has_custom_shader = self.custom_shader_renderer.is_some();
78 let use_cursor_shader =
80 self.cursor_shader_renderer.is_some() && !self.cursor_shader_disabled_for_alt_screen;
81
82 for pane in panes.iter() {
84 if let Some(ref bg) = pane.background
85 && let Some(ref path) = bg.image_path
86 && let Err(e) = self.cell_renderer.load_pane_background(path)
87 {
88 log::error!("Failed to load pane background '{}': {}", path, e);
89 }
90 }
91
92 let surface_texture = self.cell_renderer.surface.get_current_texture()?;
94 let surface_view = surface_texture
95 .texture
96 .create_view(&wgpu::TextureViewDescriptor::default());
97
98 let cursor_intermediate: Option<wgpu::TextureView> = if use_cursor_shader {
101 Some(
102 self.cursor_shader_renderer
103 .as_ref()
104 .ok_or_else(|| {
105 crate::error::RenderError::ShaderUnavailable(
106 "cursor_shader_renderer unavailable (GPU device loss?)".into(),
107 )
108 })?
109 .intermediate_texture_view()
110 .clone(),
111 )
112 } else {
113 None
114 };
115 let content_view = cursor_intermediate.as_ref().unwrap_or(&surface_view);
117
118 let opacity = self.cell_renderer.window_opacity as f64;
121 let clear_color = if self.cell_renderer.pipelines.bg_image_bind_group.is_some() {
122 wgpu::Color::TRANSPARENT
123 } else if use_cursor_shader {
124 wgpu::Color {
126 r: self.cell_renderer.background_color[0] as f64,
127 g: self.cell_renderer.background_color[1] as f64,
128 b: self.cell_renderer.background_color[2] as f64,
129 a: 1.0,
130 }
131 } else {
132 wgpu::Color {
133 r: self.cell_renderer.background_color[0] as f64 * opacity,
134 g: self.cell_renderer.background_color[1] as f64 * opacity,
135 b: self.cell_renderer.background_color[2] as f64 * opacity,
136 a: opacity,
137 }
138 };
139
140 let (full_content_mode, populate_terminal_intermediate_texture) = self
144 .custom_shader_renderer
145 .as_ref()
146 .map(|s| {
147 let full_content_mode = s.full_content_mode();
148 (
149 full_content_mode,
150 should_populate_terminal_intermediate_texture(
151 full_content_mode,
152 s.auto_dim_under_text,
153 s.auto_dim_strength,
154 ),
155 )
156 })
157 .unwrap_or((false, false));
158
159 if populate_terminal_intermediate_texture {
164 let custom_shader = self.custom_shader_renderer.as_mut().ok_or_else(|| {
165 crate::error::RenderError::ShaderUnavailable(
166 "custom_shader_renderer unavailable for iChannel4 content (GPU device loss?)"
167 .into(),
168 )
169 })?;
170 custom_shader.clear_intermediate_texture(
171 self.cell_renderer.device(),
172 self.cell_renderer.queue(),
173 );
174 let intermediate_view = custom_shader.intermediate_texture_view().clone();
175
176 let mut scratch: Vec<SeparatorMark> = Vec::new();
182 for pane in panes.iter() {
183 if pane.show_scrollbar {
184 let total_lines = pane.scrollback_len + pane.grid_size.1;
185 self.cell_renderer.update_scrollbar_for_pane(
186 pane.scroll_offset,
187 pane.grid_size.1,
188 total_lines,
189 &pane.marks,
190 &pane.viewport,
191 );
192 }
193 fill_visible_separator_marks(
194 &mut scratch,
195 &pane.marks,
196 pane.scrollback_len,
197 pane.scroll_offset,
198 pane.grid_size.1,
199 );
200 self.cell_renderer.render_pane_to_view(
201 &intermediate_view,
202 crate::cell_renderer::PaneRenderViewParams {
203 viewport: &pane.viewport,
204 cells: pane.cells,
205 cols: pane.grid_size.0,
206 rows: pane.grid_size.1,
207 cursor_pos: pane.cursor_pos,
208 cursor_opacity: pane.cursor_opacity,
209 show_scrollbar: pane.show_scrollbar,
210 clear_first: false,
211 skip_background_image: true, fill_default_bg_cells: false, separator_marks: &scratch,
214 pane_background: pane.background.as_ref(),
215 },
216 )?;
217 }
218
219 for pane in panes.iter() {
221 if !pane.graphics.is_empty() || !pane.virtual_placements.is_empty() {
222 self.render_pane_sixel_graphics(
223 &intermediate_view,
224 &pane.viewport,
225 &pane.graphics,
226 pane.scroll_offset,
227 pane.scrollback_len,
228 pane.grid_size.1,
229 pane.cells,
230 pane.grid_size.0,
231 &pane.virtual_placements,
232 )?;
233 }
234 }
235 }
236
237 if let Some(ref mut custom_shader) = self.custom_shader_renderer {
240 if !populate_terminal_intermediate_texture {
241 custom_shader.clear_intermediate_texture(
244 self.cell_renderer.device(),
245 self.cell_renderer.queue(),
246 );
247 }
248
249 custom_shader.render_with_clear_color(
253 self.cell_renderer.device(),
254 self.cell_renderer.queue(),
255 content_view,
256 !use_cursor_shader, clear_color,
258 )?;
259 } else {
260 let mut encoder = self.cell_renderer.device().create_command_encoder(
262 &wgpu::CommandEncoderDescriptor {
263 label: Some("split pane clear encoder"),
264 },
265 );
266
267 {
268 let _clear_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
269 label: Some("surface clear pass"),
270 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
271 view: content_view,
272 resolve_target: None,
273 ops: wgpu::Operations {
274 load: wgpu::LoadOp::Clear(clear_color),
275 store: wgpu::StoreOp::Store,
276 },
277 depth_slice: None,
278 })],
279 depth_stencil_attachment: None,
280 timestamp_writes: None,
281 occlusion_query_set: None,
282 });
283 }
284
285 self.cell_renderer
286 .queue()
287 .submit(std::iter::once(encoder.finish()));
288 }
289
290 let any_pane_has_background = panes.iter().any(|p| p.background.is_some());
295 let has_background_image = if !has_custom_shader && !any_pane_has_background {
296 self.cell_renderer
297 .render_background_only(content_view, false)?
298 } else {
299 false
300 };
301
302 if !full_content_mode {
306 let mut scratch: Vec<SeparatorMark> = Vec::new();
312 for pane in panes {
313 if pane.show_scrollbar {
314 let total_lines = pane.scrollback_len + pane.grid_size.1;
315 self.cell_renderer.update_scrollbar_for_pane(
316 pane.scroll_offset,
317 pane.grid_size.1,
318 total_lines,
319 &pane.marks,
320 &pane.viewport,
321 );
322 }
323 fill_visible_separator_marks(
324 &mut scratch,
325 &pane.marks,
326 pane.scrollback_len,
327 pane.scroll_offset,
328 pane.grid_size.1,
329 );
330 self.cell_renderer.render_pane_to_view(
331 content_view,
332 crate::cell_renderer::PaneRenderViewParams {
333 viewport: &pane.viewport,
334 cells: pane.cells,
335 cols: pane.grid_size.0,
336 rows: pane.grid_size.1,
337 cursor_pos: pane.cursor_pos,
338 cursor_opacity: pane.cursor_opacity,
339 show_scrollbar: pane.show_scrollbar,
340 clear_first: false, skip_background_image: has_background_image || has_custom_shader,
342 fill_default_bg_cells: has_background_image, separator_marks: &scratch,
344 pane_background: pane.background.as_ref(),
345 },
346 )?;
347 }
348
349 for pane in panes {
351 if !pane.graphics.is_empty() || !pane.virtual_placements.is_empty() {
352 self.render_pane_sixel_graphics(
353 content_view,
354 &pane.viewport,
355 &pane.graphics,
356 pane.scroll_offset,
357 pane.scrollback_len,
358 pane.grid_size.1,
359 pane.cells,
360 pane.grid_size.0,
361 &pane.virtual_placements,
362 )?;
363 }
364 }
365 }
366
367 if !dividers.is_empty() {
369 self.render_dividers(content_view, dividers, divider_settings)?;
370 }
371
372 if !pane_titles.is_empty() {
374 self.render_pane_titles(content_view, pane_titles)?;
375 }
376
377 if self.cell_renderer.visual_bell_intensity > 0.0 {
379 let uniforms: [f32; 8] = [
380 -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, ];
389 self.cell_renderer.queue().write_buffer(
390 &self.cell_renderer.buffers.visual_bell_uniform_buffer,
391 0,
392 bytemuck::cast_slice(&uniforms),
393 );
394
395 let mut encoder = self.cell_renderer.device().create_command_encoder(
396 &wgpu::CommandEncoderDescriptor {
397 label: Some("visual bell encoder"),
398 },
399 );
400 {
401 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
402 label: Some("visual bell pass"),
403 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
404 view: content_view,
405 resolve_target: None,
406 ops: wgpu::Operations {
407 load: wgpu::LoadOp::Load,
408 store: wgpu::StoreOp::Store,
409 },
410 depth_slice: None,
411 })],
412 depth_stencil_attachment: None,
413 timestamp_writes: None,
414 occlusion_query_set: None,
415 });
416 render_pass.set_pipeline(&self.cell_renderer.pipelines.visual_bell_pipeline);
417 render_pass.set_bind_group(
418 0,
419 &self.cell_renderer.pipelines.visual_bell_bind_group,
420 &[],
421 );
422 render_pass.draw(0..4, 0..1); }
424 self.cell_renderer
425 .queue()
426 .submit(std::iter::once(encoder.finish()));
427 }
428
429 if panes.len() > 1
431 && let Some(viewport) = focused_viewport
432 {
433 self.render_focus_indicator(content_view, viewport, divider_settings)?;
434 }
435
436 if use_cursor_shader {
438 self.cursor_shader_renderer
439 .as_mut()
440 .ok_or_else(|| crate::error::RenderError::ShaderUnavailable(
441 "cursor_shader_renderer unavailable during final composite (GPU device loss?)".into(),
442 ))?
443 .render(
444 self.cell_renderer.device(),
445 self.cell_renderer.queue(),
446 &surface_view,
447 true, )?;
449 }
450
451 if let Some((egui_output, egui_ctx)) = egui_data {
453 self.render_egui(&surface_texture, egui_output, egui_ctx, force_egui_opaque)?;
454 }
455
456 self.cell_renderer.render_opaque_alpha(&surface_texture)?;
458
459 surface_texture.present();
461
462 self.dirty = false;
463 Ok(true)
464 }
465
466 fn render_cells_to_target(
479 &mut self,
480 target_view: &wgpu::TextureView,
481 ) -> Result<(), crate::error::RenderError> {
482 let has_custom_shader = self.custom_shader_renderer.is_some();
483 let use_cursor_shader =
484 self.cursor_shader_renderer.is_some() && !self.cursor_shader_disabled_for_alt_screen;
485
486 let map_err = |e: anyhow::Error| {
487 crate::error::RenderError::ScreenshotMap(format!("Render failed: {:#}", e))
488 };
489
490 if has_custom_shader {
491 let intermediate_view = self
493 .custom_shader_renderer
494 .as_ref()
495 .ok_or_else(|| {
496 crate::error::RenderError::ShaderUnavailable(
497 "custom_shader_renderer unavailable (GPU device loss?)".into(),
498 )
499 })?
500 .intermediate_texture_view()
501 .clone();
502 self.cell_renderer
503 .render_to_texture(&intermediate_view, true)
504 .map_err(map_err)?;
505
506 if use_cursor_shader {
507 let cursor_intermediate = self
509 .cursor_shader_renderer
510 .as_ref()
511 .ok_or_else(|| crate::error::RenderError::ShaderUnavailable(
512 "cursor_shader_renderer unavailable during shader chain (GPU device loss?)".into(),
513 ))?
514 .intermediate_texture_view()
515 .clone();
516 self.custom_shader_renderer
517 .as_mut()
518 .ok_or_else(|| crate::error::RenderError::ShaderUnavailable(
519 "custom_shader_renderer unavailable during shader chain (GPU device loss?)".into(),
520 ))?
521 .render(
522 self.cell_renderer.device(),
523 self.cell_renderer.queue(),
524 &cursor_intermediate,
525 false,
526 )
527 .map_err(map_err)?;
528 self.cursor_shader_renderer
529 .as_mut()
530 .ok_or_else(|| crate::error::RenderError::ShaderUnavailable(
531 "cursor_shader_renderer unavailable during shader chain (GPU device loss?)".into(),
532 ))?
533 .render(
534 self.cell_renderer.device(),
535 self.cell_renderer.queue(),
536 target_view,
537 true,
538 )
539 .map_err(map_err)?;
540 } else {
541 self.custom_shader_renderer
543 .as_mut()
544 .ok_or_else(|| {
545 crate::error::RenderError::ShaderUnavailable(
546 "custom_shader_renderer unavailable during render (GPU device loss?)"
547 .into(),
548 )
549 })?
550 .render(
551 self.cell_renderer.device(),
552 self.cell_renderer.queue(),
553 target_view,
554 true,
555 )
556 .map_err(map_err)?;
557 }
558 } else if use_cursor_shader {
559 let cursor_intermediate = self
561 .cursor_shader_renderer
562 .as_ref()
563 .ok_or_else(|| {
564 crate::error::RenderError::ShaderUnavailable(
565 "cursor_shader_renderer unavailable (GPU device loss?)".into(),
566 )
567 })?
568 .intermediate_texture_view()
569 .clone();
570 self.cell_renderer
571 .render_to_texture(&cursor_intermediate, true)
572 .map_err(map_err)?;
573 self.cursor_shader_renderer
574 .as_mut()
575 .ok_or_else(|| {
576 crate::error::RenderError::ShaderUnavailable(
577 "cursor_shader_renderer unavailable during render (GPU device loss?)"
578 .into(),
579 )
580 })?
581 .render(
582 self.cell_renderer.device(),
583 self.cell_renderer.queue(),
584 target_view,
585 true,
586 )
587 .map_err(map_err)?;
588 } else {
589 self.cell_renderer
591 .render_to_view(target_view)
592 .map_err(map_err)?;
593 }
594
595 Ok(())
596 }
597
598 pub fn take_screenshot(&mut self) -> Result<image::RgbaImage, crate::error::RenderError> {
603 log::info!(
604 "take_screenshot: Starting screenshot capture ({}x{})",
605 self.size.width,
606 self.size.height
607 );
608
609 let width = self.size.width;
610 let height = self.size.height;
611 let format = self.cell_renderer.surface_format();
613 log::info!("take_screenshot: Using texture format {:?}", format);
614
615 let screenshot_texture =
617 self.cell_renderer
618 .device()
619 .create_texture(&wgpu::TextureDescriptor {
620 label: Some("screenshot texture"),
621 size: wgpu::Extent3d {
622 width,
623 height,
624 depth_or_array_layers: 1,
625 },
626 mip_level_count: 1,
627 sample_count: 1,
628 dimension: wgpu::TextureDimension::D2,
629 format,
630 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
631 view_formats: &[],
632 });
633
634 let screenshot_view =
635 screenshot_texture.create_view(&wgpu::TextureViewDescriptor::default());
636
637 log::info!("take_screenshot: Rendering composited frame...");
639 self.render_cells_to_target(&screenshot_view)?;
640
641 log::info!("take_screenshot: Render complete");
642
643 let device = self.cell_renderer.device();
645 let queue = self.cell_renderer.queue();
646
647 let bytes_per_pixel = 4u32;
649 let unpadded_bytes_per_row = width * bytes_per_pixel;
650 let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
652 let padded_bytes_per_row = unpadded_bytes_per_row.div_ceil(align) * align;
653 let buffer_size = (padded_bytes_per_row * height) as u64;
654
655 let output_buffer = device.create_buffer(&wgpu::BufferDescriptor {
656 label: Some("screenshot buffer"),
657 size: buffer_size,
658 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
659 mapped_at_creation: false,
660 });
661
662 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
664 label: Some("screenshot encoder"),
665 });
666
667 encoder.copy_texture_to_buffer(
668 wgpu::TexelCopyTextureInfo {
669 texture: &screenshot_texture,
670 mip_level: 0,
671 origin: wgpu::Origin3d::ZERO,
672 aspect: wgpu::TextureAspect::All,
673 },
674 wgpu::TexelCopyBufferInfo {
675 buffer: &output_buffer,
676 layout: wgpu::TexelCopyBufferLayout {
677 offset: 0,
678 bytes_per_row: Some(padded_bytes_per_row),
679 rows_per_image: Some(height),
680 },
681 },
682 wgpu::Extent3d {
683 width,
684 height,
685 depth_or_array_layers: 1,
686 },
687 );
688
689 queue.submit(std::iter::once(encoder.finish()));
690 log::info!("take_screenshot: Texture copy submitted");
691
692 let buffer_slice = output_buffer.slice(..);
694 let (tx, rx) = std::sync::mpsc::channel();
695 buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
696 let _ = tx.send(result);
697 });
698
699 log::info!("take_screenshot: Waiting for GPU...");
701 if let Err(e) = device.poll(wgpu::PollType::wait_indefinitely()) {
702 log::warn!("take_screenshot: GPU poll returned error: {:?}", e);
703 }
704 log::info!("take_screenshot: GPU poll complete, waiting for buffer map...");
705 rx.recv()
706 .map_err(|e| {
707 crate::error::RenderError::ScreenshotMap(format!(
708 "Failed to receive map result: {}",
709 e
710 ))
711 })?
712 .map_err(|e| {
713 crate::error::RenderError::ScreenshotMap(format!("Failed to map buffer: {:?}", e))
714 })?;
715 log::info!("take_screenshot: Buffer mapped successfully");
716
717 let data = buffer_slice.get_mapped_range();
719 let mut pixels = Vec::with_capacity((width * height * 4) as usize);
720
721 let is_bgra = matches!(
723 format,
724 wgpu::TextureFormat::Bgra8Unorm | wgpu::TextureFormat::Bgra8UnormSrgb
725 );
726
727 for y in 0..height {
729 let row_start = (y * padded_bytes_per_row) as usize;
730 let row_end = row_start + (width * bytes_per_pixel) as usize;
731 let row = &data[row_start..row_end];
732
733 if is_bgra {
734 for chunk in row.chunks(4) {
736 pixels.push(chunk[2]); pixels.push(chunk[1]); pixels.push(chunk[0]); pixels.push(chunk[3]); }
741 } else {
742 pixels.extend_from_slice(row);
744 }
745 }
746
747 drop(data);
748 output_buffer.unmap();
749
750 image::RgbaImage::from_raw(width, height, pixels)
752 .ok_or(crate::error::RenderError::ScreenshotImageAssembly)
753 }
754}
755
756#[cfg(test)]
757mod tests {
758 use super::*;
759
760 #[test]
761 fn background_shader_auto_dim_requires_terminal_intermediate_texture() {
762 assert!(should_populate_terminal_intermediate_texture(
763 false, true, 0.35
764 ));
765 }
766
767 #[test]
768 fn full_content_mode_requires_terminal_intermediate_texture() {
769 assert!(should_populate_terminal_intermediate_texture(
770 true, false, 0.0
771 ));
772 }
773
774 #[test]
775 fn background_only_shader_without_auto_dim_skips_terminal_intermediate_texture() {
776 assert!(!should_populate_terminal_intermediate_texture(
777 false, false, 0.35
778 ));
779 assert!(!should_populate_terminal_intermediate_texture(
780 false, true, 0.0
781 ));
782 }
783}