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 = match self.cell_renderer.surface.get_current_texture() {
94 wgpu::CurrentSurfaceTexture::Success(t)
95 | wgpu::CurrentSurfaceTexture::Suboptimal(t) => t,
96 other => return Err(crate::error::RenderError::Surface(format!("{other:?}")).into()),
97 };
98 let surface_view = surface_texture
99 .texture
100 .create_view(&wgpu::TextureViewDescriptor::default());
101
102 let cursor_intermediate: Option<wgpu::TextureView> = if use_cursor_shader {
105 Some(
106 self.cursor_shader_renderer
107 .as_ref()
108 .ok_or_else(|| {
109 crate::error::RenderError::ShaderUnavailable(
110 "cursor_shader_renderer unavailable (GPU device loss?)".into(),
111 )
112 })?
113 .intermediate_texture_view()
114 .clone(),
115 )
116 } else {
117 None
118 };
119 let content_view = cursor_intermediate.as_ref().unwrap_or(&surface_view);
121
122 let opacity = self.cell_renderer.window_opacity as f64;
125 let clear_color = if self.cell_renderer.pipelines.bg_image_bind_group.is_some() {
126 wgpu::Color::TRANSPARENT
127 } else if use_cursor_shader {
128 wgpu::Color {
130 r: self.cell_renderer.background_color[0] as f64,
131 g: self.cell_renderer.background_color[1] as f64,
132 b: self.cell_renderer.background_color[2] as f64,
133 a: 1.0,
134 }
135 } else {
136 wgpu::Color {
137 r: self.cell_renderer.background_color[0] as f64 * opacity,
138 g: self.cell_renderer.background_color[1] as f64 * opacity,
139 b: self.cell_renderer.background_color[2] as f64 * opacity,
140 a: opacity,
141 }
142 };
143
144 let (full_content_mode, populate_terminal_intermediate_texture) = self
148 .custom_shader_renderer
149 .as_ref()
150 .map(|s| {
151 let full_content_mode = s.full_content_mode();
152 (
153 full_content_mode,
154 should_populate_terminal_intermediate_texture(
155 full_content_mode,
156 s.auto_dim_under_text,
157 s.auto_dim_strength,
158 ),
159 )
160 })
161 .unwrap_or((false, false));
162
163 if populate_terminal_intermediate_texture {
168 let custom_shader = self.custom_shader_renderer.as_mut().ok_or_else(|| {
169 crate::error::RenderError::ShaderUnavailable(
170 "custom_shader_renderer unavailable for iChannel4 content (GPU device loss?)"
171 .into(),
172 )
173 })?;
174 custom_shader.clear_intermediate_texture(
175 self.cell_renderer.device(),
176 self.cell_renderer.queue(),
177 );
178 let intermediate_view = custom_shader.intermediate_texture_view().clone();
179
180 let mut scratch: Vec<SeparatorMark> = Vec::new();
186 for pane in panes.iter() {
187 if pane.show_scrollbar {
188 let total_lines = pane.scrollback_len + pane.grid_size.1;
189 self.cell_renderer.update_scrollbar_for_pane(
190 pane.scroll_offset,
191 pane.grid_size.1,
192 total_lines,
193 &pane.marks,
194 &pane.viewport,
195 );
196 }
197 fill_visible_separator_marks(
198 &mut scratch,
199 &pane.marks,
200 pane.scrollback_len,
201 pane.scroll_offset,
202 pane.grid_size.1,
203 );
204 self.cell_renderer.render_pane_to_view(
205 &intermediate_view,
206 crate::cell_renderer::PaneRenderViewParams {
207 viewport: &pane.viewport,
208 cells: pane.cells,
209 cols: pane.grid_size.0,
210 rows: pane.grid_size.1,
211 cursor_pos: pane.cursor_pos,
212 cursor_opacity: pane.cursor_opacity,
213 show_scrollbar: pane.show_scrollbar,
214 clear_first: false,
215 skip_background_image: true, fill_default_bg_cells: false, separator_marks: &scratch,
218 pane_background: pane.background.as_ref(),
219 },
220 )?;
221 }
222
223 for pane in panes.iter() {
225 if !pane.graphics.is_empty() || !pane.virtual_placements.is_empty() {
226 self.render_pane_sixel_graphics(
227 &intermediate_view,
228 &pane.viewport,
229 &pane.graphics,
230 pane.scroll_offset,
231 pane.scrollback_len,
232 pane.grid_size.1,
233 pane.cells,
234 pane.grid_size.0,
235 &pane.virtual_placements,
236 )?;
237 }
238 }
239 }
240
241 if let Some(ref mut custom_shader) = self.custom_shader_renderer {
244 if !populate_terminal_intermediate_texture {
245 custom_shader.clear_intermediate_texture(
248 self.cell_renderer.device(),
249 self.cell_renderer.queue(),
250 );
251 }
252
253 custom_shader.render_with_clear_color(
257 self.cell_renderer.device(),
258 self.cell_renderer.queue(),
259 content_view,
260 !use_cursor_shader, clear_color,
262 )?;
263 } else {
264 let mut encoder = self.cell_renderer.device().create_command_encoder(
266 &wgpu::CommandEncoderDescriptor {
267 label: Some("split pane clear encoder"),
268 },
269 );
270
271 {
272 let _clear_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
273 label: Some("surface clear pass"),
274 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
275 view: content_view,
276 resolve_target: None,
277 ops: wgpu::Operations {
278 load: wgpu::LoadOp::Clear(clear_color),
279 store: wgpu::StoreOp::Store,
280 },
281 depth_slice: None,
282 })],
283 depth_stencil_attachment: None,
284 timestamp_writes: None,
285 occlusion_query_set: None,
286 multiview_mask: None,
287 });
288 }
289
290 self.cell_renderer
291 .queue()
292 .submit(std::iter::once(encoder.finish()));
293 }
294
295 let any_pane_has_background = panes.iter().any(|p| p.background.is_some());
300 let has_background_image = if !has_custom_shader && !any_pane_has_background {
301 self.cell_renderer
302 .render_background_only(content_view, false)?
303 } else {
304 false
305 };
306
307 if !full_content_mode {
311 let mut scratch: Vec<SeparatorMark> = Vec::new();
317 for pane in panes {
318 if pane.show_scrollbar {
319 let total_lines = pane.scrollback_len + pane.grid_size.1;
320 self.cell_renderer.update_scrollbar_for_pane(
321 pane.scroll_offset,
322 pane.grid_size.1,
323 total_lines,
324 &pane.marks,
325 &pane.viewport,
326 );
327 }
328 fill_visible_separator_marks(
329 &mut scratch,
330 &pane.marks,
331 pane.scrollback_len,
332 pane.scroll_offset,
333 pane.grid_size.1,
334 );
335 self.cell_renderer.render_pane_to_view(
336 content_view,
337 crate::cell_renderer::PaneRenderViewParams {
338 viewport: &pane.viewport,
339 cells: pane.cells,
340 cols: pane.grid_size.0,
341 rows: pane.grid_size.1,
342 cursor_pos: pane.cursor_pos,
343 cursor_opacity: pane.cursor_opacity,
344 show_scrollbar: pane.show_scrollbar,
345 clear_first: false, skip_background_image: has_background_image || has_custom_shader,
347 fill_default_bg_cells: has_background_image, separator_marks: &scratch,
349 pane_background: pane.background.as_ref(),
350 },
351 )?;
352 }
353
354 for pane in panes {
356 if !pane.graphics.is_empty() || !pane.virtual_placements.is_empty() {
357 self.render_pane_sixel_graphics(
358 content_view,
359 &pane.viewport,
360 &pane.graphics,
361 pane.scroll_offset,
362 pane.scrollback_len,
363 pane.grid_size.1,
364 pane.cells,
365 pane.grid_size.0,
366 &pane.virtual_placements,
367 )?;
368 }
369 }
370 }
371
372 if !dividers.is_empty() {
374 self.render_dividers(content_view, dividers, divider_settings)?;
375 }
376
377 if !pane_titles.is_empty() {
379 self.render_pane_titles(content_view, pane_titles)?;
380 }
381
382 if self.cell_renderer.visual_bell_intensity > 0.0 {
384 let uniforms: [f32; 8] = [
385 -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, ];
394 self.cell_renderer.queue().write_buffer(
395 &self.cell_renderer.buffers.visual_bell_uniform_buffer,
396 0,
397 bytemuck::cast_slice(&uniforms),
398 );
399
400 let mut encoder = self.cell_renderer.device().create_command_encoder(
401 &wgpu::CommandEncoderDescriptor {
402 label: Some("visual bell encoder"),
403 },
404 );
405 {
406 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
407 label: Some("visual bell pass"),
408 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
409 view: content_view,
410 resolve_target: None,
411 ops: wgpu::Operations {
412 load: wgpu::LoadOp::Load,
413 store: wgpu::StoreOp::Store,
414 },
415 depth_slice: None,
416 })],
417 depth_stencil_attachment: None,
418 timestamp_writes: None,
419 occlusion_query_set: None,
420 multiview_mask: None,
421 });
422 render_pass.set_pipeline(&self.cell_renderer.pipelines.visual_bell_pipeline);
423 render_pass.set_bind_group(
424 0,
425 &self.cell_renderer.pipelines.visual_bell_bind_group,
426 &[],
427 );
428 render_pass.draw(0..4, 0..1); }
430 self.cell_renderer
431 .queue()
432 .submit(std::iter::once(encoder.finish()));
433 }
434
435 if panes.len() > 1
437 && let Some(viewport) = focused_viewport
438 {
439 self.render_focus_indicator(content_view, viewport, divider_settings)?;
440 }
441
442 if use_cursor_shader {
444 self.cursor_shader_renderer
445 .as_mut()
446 .ok_or_else(|| crate::error::RenderError::ShaderUnavailable(
447 "cursor_shader_renderer unavailable during final composite (GPU device loss?)".into(),
448 ))?
449 .render(
450 self.cell_renderer.device(),
451 self.cell_renderer.queue(),
452 &surface_view,
453 true, )?;
455 }
456
457 if let Some((egui_output, egui_ctx)) = egui_data {
459 self.render_egui(&surface_texture, egui_output, egui_ctx, force_egui_opaque)?;
460 }
461
462 self.cell_renderer.render_opaque_alpha(&surface_texture)?;
464
465 surface_texture.present();
467
468 self.dirty = false;
469 Ok(true)
470 }
471
472 fn render_cells_to_target(
485 &mut self,
486 target_view: &wgpu::TextureView,
487 ) -> Result<(), crate::error::RenderError> {
488 let has_custom_shader = self.custom_shader_renderer.is_some();
489 let use_cursor_shader =
490 self.cursor_shader_renderer.is_some() && !self.cursor_shader_disabled_for_alt_screen;
491
492 let map_err = |e: anyhow::Error| {
493 crate::error::RenderError::ScreenshotMap(format!("Render failed: {:#}", e))
494 };
495
496 if has_custom_shader {
497 let intermediate_view = self
499 .custom_shader_renderer
500 .as_ref()
501 .ok_or_else(|| {
502 crate::error::RenderError::ShaderUnavailable(
503 "custom_shader_renderer unavailable (GPU device loss?)".into(),
504 )
505 })?
506 .intermediate_texture_view()
507 .clone();
508 self.cell_renderer
509 .render_to_texture(&intermediate_view, true)
510 .map_err(map_err)?;
511
512 if use_cursor_shader {
513 let cursor_intermediate = self
515 .cursor_shader_renderer
516 .as_ref()
517 .ok_or_else(|| crate::error::RenderError::ShaderUnavailable(
518 "cursor_shader_renderer unavailable during shader chain (GPU device loss?)".into(),
519 ))?
520 .intermediate_texture_view()
521 .clone();
522 self.custom_shader_renderer
523 .as_mut()
524 .ok_or_else(|| crate::error::RenderError::ShaderUnavailable(
525 "custom_shader_renderer unavailable during shader chain (GPU device loss?)".into(),
526 ))?
527 .render(
528 self.cell_renderer.device(),
529 self.cell_renderer.queue(),
530 &cursor_intermediate,
531 false,
532 )
533 .map_err(map_err)?;
534 self.cursor_shader_renderer
535 .as_mut()
536 .ok_or_else(|| crate::error::RenderError::ShaderUnavailable(
537 "cursor_shader_renderer unavailable during shader chain (GPU device loss?)".into(),
538 ))?
539 .render(
540 self.cell_renderer.device(),
541 self.cell_renderer.queue(),
542 target_view,
543 true,
544 )
545 .map_err(map_err)?;
546 } else {
547 self.custom_shader_renderer
549 .as_mut()
550 .ok_or_else(|| {
551 crate::error::RenderError::ShaderUnavailable(
552 "custom_shader_renderer unavailable during render (GPU device loss?)"
553 .into(),
554 )
555 })?
556 .render(
557 self.cell_renderer.device(),
558 self.cell_renderer.queue(),
559 target_view,
560 true,
561 )
562 .map_err(map_err)?;
563 }
564 } else if use_cursor_shader {
565 let cursor_intermediate = self
567 .cursor_shader_renderer
568 .as_ref()
569 .ok_or_else(|| {
570 crate::error::RenderError::ShaderUnavailable(
571 "cursor_shader_renderer unavailable (GPU device loss?)".into(),
572 )
573 })?
574 .intermediate_texture_view()
575 .clone();
576 self.cell_renderer
577 .render_to_texture(&cursor_intermediate, true)
578 .map_err(map_err)?;
579 self.cursor_shader_renderer
580 .as_mut()
581 .ok_or_else(|| {
582 crate::error::RenderError::ShaderUnavailable(
583 "cursor_shader_renderer unavailable during render (GPU device loss?)"
584 .into(),
585 )
586 })?
587 .render(
588 self.cell_renderer.device(),
589 self.cell_renderer.queue(),
590 target_view,
591 true,
592 )
593 .map_err(map_err)?;
594 } else {
595 self.cell_renderer
597 .render_to_view(target_view)
598 .map_err(map_err)?;
599 }
600
601 Ok(())
602 }
603
604 pub fn take_screenshot(&mut self) -> Result<image::RgbaImage, crate::error::RenderError> {
609 log::info!(
610 "take_screenshot: Starting screenshot capture ({}x{})",
611 self.size.width,
612 self.size.height
613 );
614
615 let width = self.size.width;
616 let height = self.size.height;
617 let format = self.cell_renderer.surface_format();
619 log::info!("take_screenshot: Using texture format {:?}", format);
620
621 let screenshot_texture =
623 self.cell_renderer
624 .device()
625 .create_texture(&wgpu::TextureDescriptor {
626 label: Some("screenshot texture"),
627 size: wgpu::Extent3d {
628 width,
629 height,
630 depth_or_array_layers: 1,
631 },
632 mip_level_count: 1,
633 sample_count: 1,
634 dimension: wgpu::TextureDimension::D2,
635 format,
636 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
637 view_formats: &[],
638 });
639
640 let screenshot_view =
641 screenshot_texture.create_view(&wgpu::TextureViewDescriptor::default());
642
643 log::info!("take_screenshot: Rendering composited frame...");
645 self.render_cells_to_target(&screenshot_view)?;
646
647 log::info!("take_screenshot: Render complete");
648
649 let device = self.cell_renderer.device();
651 let queue = self.cell_renderer.queue();
652
653 let bytes_per_pixel = 4u32;
655 let unpadded_bytes_per_row = width * bytes_per_pixel;
656 let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
658 let padded_bytes_per_row = unpadded_bytes_per_row.div_ceil(align) * align;
659 let buffer_size = (padded_bytes_per_row * height) as u64;
660
661 let output_buffer = device.create_buffer(&wgpu::BufferDescriptor {
662 label: Some("screenshot buffer"),
663 size: buffer_size,
664 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
665 mapped_at_creation: false,
666 });
667
668 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
670 label: Some("screenshot encoder"),
671 });
672
673 encoder.copy_texture_to_buffer(
674 wgpu::TexelCopyTextureInfo {
675 texture: &screenshot_texture,
676 mip_level: 0,
677 origin: wgpu::Origin3d::ZERO,
678 aspect: wgpu::TextureAspect::All,
679 },
680 wgpu::TexelCopyBufferInfo {
681 buffer: &output_buffer,
682 layout: wgpu::TexelCopyBufferLayout {
683 offset: 0,
684 bytes_per_row: Some(padded_bytes_per_row),
685 rows_per_image: Some(height),
686 },
687 },
688 wgpu::Extent3d {
689 width,
690 height,
691 depth_or_array_layers: 1,
692 },
693 );
694
695 queue.submit(std::iter::once(encoder.finish()));
696 log::info!("take_screenshot: Texture copy submitted");
697
698 let buffer_slice = output_buffer.slice(..);
700 let (tx, rx) = std::sync::mpsc::channel();
701 buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
702 let _ = tx.send(result);
703 });
704
705 log::info!("take_screenshot: Waiting for GPU...");
707 if let Err(e) = device.poll(wgpu::PollType::wait_indefinitely()) {
708 log::warn!("take_screenshot: GPU poll returned error: {:?}", e);
709 }
710 log::info!("take_screenshot: GPU poll complete, waiting for buffer map...");
711 rx.recv()
712 .map_err(|e| {
713 crate::error::RenderError::ScreenshotMap(format!(
714 "Failed to receive map result: {}",
715 e
716 ))
717 })?
718 .map_err(|e| {
719 crate::error::RenderError::ScreenshotMap(format!("Failed to map buffer: {:?}", e))
720 })?;
721 log::info!("take_screenshot: Buffer mapped successfully");
722
723 let data = buffer_slice.get_mapped_range();
725 let mut pixels = Vec::with_capacity((width * height * 4) as usize);
726
727 let is_bgra = matches!(
729 format,
730 wgpu::TextureFormat::Bgra8Unorm | wgpu::TextureFormat::Bgra8UnormSrgb
731 );
732
733 for y in 0..height {
735 let row_start = (y * padded_bytes_per_row) as usize;
736 let row_end = row_start + (width * bytes_per_pixel) as usize;
737 let row = &data[row_start..row_end];
738
739 if is_bgra {
740 for chunk in row.chunks(4) {
742 pixels.push(chunk[2]); pixels.push(chunk[1]); pixels.push(chunk[0]); pixels.push(chunk[3]); }
747 } else {
748 pixels.extend_from_slice(row);
750 }
751 }
752
753 drop(data);
754 output_buffer.unmap();
755
756 image::RgbaImage::from_raw(width, height, pixels)
758 .ok_or(crate::error::RenderError::ScreenshotImageAssembly)
759 }
760}
761
762#[cfg(test)]
763mod tests {
764 use super::*;
765
766 #[test]
767 fn background_shader_auto_dim_requires_terminal_intermediate_texture() {
768 assert!(should_populate_terminal_intermediate_texture(
769 false, true, 0.35
770 ));
771 }
772
773 #[test]
774 fn full_content_mode_requires_terminal_intermediate_texture() {
775 assert!(should_populate_terminal_intermediate_texture(
776 true, false, 0.0
777 ));
778 }
779
780 #[test]
781 fn background_only_shader_without_auto_dim_skips_terminal_intermediate_texture() {
782 assert!(!should_populate_terminal_intermediate_texture(
783 false, false, 0.35
784 ));
785 assert!(!should_populate_terminal_intermediate_texture(
786 false, true, 0.0
787 ));
788 }
789}