1use crate::cell_renderer::{Cell, CellRenderer, PaneViewport};
2use crate::custom_shader_renderer::CustomShaderRenderer;
3use crate::graphics_renderer::GraphicsRenderer;
4use anyhow::Result;
5use std::sync::Arc;
6use winit::dpi::PhysicalSize;
7use winit::window::Window;
8
9pub mod graphics;
10pub mod shaders;
11
12pub use par_term_config::SeparatorMark;
14
15pub fn compute_visible_separator_marks(
24 marks: &[par_term_config::ScrollbackMark],
25 scrollback_len: usize,
26 scroll_offset: usize,
27 visible_lines: usize,
28) -> Vec<SeparatorMark> {
29 let viewport_start = scrollback_len.saturating_sub(scroll_offset);
30 let viewport_end = viewport_start + visible_lines;
31
32 marks
33 .iter()
34 .filter_map(|mark| {
35 if mark.line >= viewport_start && mark.line < viewport_end {
36 let screen_row = mark.line - viewport_start;
37 Some((screen_row, mark.exit_code, mark.color))
38 } else {
39 None
40 }
41 })
42 .collect()
43}
44
45pub struct PaneRenderInfo<'a> {
47 pub viewport: PaneViewport,
49 pub cells: &'a [Cell],
51 pub grid_size: (usize, usize),
53 pub cursor_pos: Option<(usize, usize)>,
55 pub cursor_opacity: f32,
57 pub show_scrollbar: bool,
59 pub marks: Vec<par_term_config::ScrollbackMark>,
61 pub scrollback_len: usize,
63 pub scroll_offset: usize,
65 pub background: Option<par_term_config::PaneBackground>,
67}
68
69#[derive(Clone, Copy, Debug)]
71pub struct DividerRenderInfo {
72 pub x: f32,
74 pub y: f32,
76 pub width: f32,
78 pub height: f32,
80 pub hovered: bool,
82}
83
84impl DividerRenderInfo {
85 pub fn from_rect(rect: &par_term_config::DividerRect, hovered: bool) -> Self {
87 Self {
88 x: rect.x,
89 y: rect.y,
90 width: rect.width,
91 height: rect.height,
92 hovered,
93 }
94 }
95}
96
97#[derive(Clone, Debug)]
99pub struct PaneTitleInfo {
100 pub x: f32,
102 pub y: f32,
104 pub width: f32,
106 pub height: f32,
108 pub title: String,
110 pub focused: bool,
112 pub text_color: [f32; 3],
114 pub bg_color: [f32; 3],
116}
117
118#[derive(Clone, Copy, Debug)]
120pub struct PaneDividerSettings {
121 pub divider_color: [f32; 3],
123 pub hover_color: [f32; 3],
125 pub show_focus_indicator: bool,
127 pub focus_color: [f32; 3],
129 pub focus_width: f32,
131 pub divider_style: par_term_config::DividerStyle,
133}
134
135impl Default for PaneDividerSettings {
136 fn default() -> Self {
137 Self {
138 divider_color: [0.3, 0.3, 0.3],
139 hover_color: [0.5, 0.6, 0.8],
140 show_focus_indicator: true,
141 focus_color: [0.4, 0.6, 1.0],
142 focus_width: 2.0,
143 divider_style: par_term_config::DividerStyle::default(),
144 }
145 }
146}
147
148pub struct Renderer {
150 pub(crate) cell_renderer: CellRenderer,
152
153 pub(crate) graphics_renderer: GraphicsRenderer,
155
156 pub(crate) sixel_graphics: Vec<(u64, isize, usize, usize, usize, f32, usize)>,
159
160 pub(crate) egui_renderer: egui_wgpu::Renderer,
162
163 pub(crate) custom_shader_renderer: Option<CustomShaderRenderer>,
165 pub(crate) custom_shader_path: Option<String>,
167
168 pub(crate) cursor_shader_renderer: Option<CustomShaderRenderer>,
170 pub(crate) cursor_shader_path: Option<String>,
172
173 pub(crate) size: PhysicalSize<u32>,
175
176 pub(crate) dirty: bool,
178
179 pub(crate) cursor_shader_disabled_for_alt_screen: bool,
181
182 #[allow(dead_code)]
184 #[allow(dead_code)]
185 pub(crate) debug_text: Option<String>,
186}
187
188impl Renderer {
189 #[allow(clippy::too_many_arguments)]
191 pub async fn new(
192 window: Arc<Window>,
193 font_family: Option<&str>,
194 font_family_bold: Option<&str>,
195 font_family_italic: Option<&str>,
196 font_family_bold_italic: Option<&str>,
197 font_ranges: &[par_term_config::FontRange],
198 font_size: f32,
199 window_padding: f32,
200 line_spacing: f32,
201 char_spacing: f32,
202 scrollbar_position: &str,
203 scrollbar_width: f32,
204 scrollbar_thumb_color: [f32; 4],
205 scrollbar_track_color: [f32; 4],
206 enable_text_shaping: bool,
207 enable_ligatures: bool,
208 enable_kerning: bool,
209 font_antialias: bool,
210 font_hinting: bool,
211 font_thin_strokes: par_term_config::ThinStrokesMode,
212 minimum_contrast: f32,
213 vsync_mode: par_term_config::VsyncMode,
214 power_preference: par_term_config::PowerPreference,
215 window_opacity: f32,
216 background_color: [u8; 3],
217 background_image_path: Option<&str>,
218 background_image_enabled: bool,
219 background_image_mode: par_term_config::BackgroundImageMode,
220 background_image_opacity: f32,
221 custom_shader_path: Option<&str>,
222 custom_shader_enabled: bool,
223 custom_shader_animation: bool,
224 custom_shader_animation_speed: f32,
225 custom_shader_full_content: bool,
226 custom_shader_brightness: f32,
227 custom_shader_channel_paths: &[Option<std::path::PathBuf>; 4],
229 custom_shader_cubemap_path: Option<&std::path::Path>,
231 use_background_as_channel0: bool,
233 image_scaling_mode: par_term_config::ImageScalingMode,
235 image_preserve_aspect_ratio: bool,
237 cursor_shader_path: Option<&str>,
239 cursor_shader_enabled: bool,
240 cursor_shader_animation: bool,
241 cursor_shader_animation_speed: f32,
242 ) -> Result<Self> {
243 let size = window.inner_size();
244 let scale_factor = window.scale_factor();
245
246 let platform_dpi = if cfg!(target_os = "macos") {
249 72.0
250 } else {
251 96.0
252 };
253
254 let base_font_pixels = font_size * platform_dpi / 72.0;
256 let font_size_pixels = (base_font_pixels * scale_factor as f32).max(1.0);
257
258 let font_manager = par_term_fonts::font_manager::FontManager::new(
260 font_family,
261 font_family_bold,
262 font_family_italic,
263 font_family_bold_italic,
264 font_ranges,
265 )?;
266
267 let (font_ascent, font_descent, font_leading, char_advance) = {
268 let primary_font = font_manager.get_font(0).unwrap();
269 let metrics = primary_font.metrics(&[]);
270 let scale = font_size_pixels / metrics.units_per_em as f32;
271
272 let glyph_id = primary_font.charmap().map('m');
274 let advance = primary_font.glyph_metrics(&[]).advance_width(glyph_id) * scale;
275
276 (
277 metrics.ascent * scale,
278 metrics.descent * scale,
279 metrics.leading * scale,
280 advance,
281 )
282 };
283
284 let natural_line_height = font_ascent + font_descent + font_leading;
287 let char_height = (natural_line_height * line_spacing).max(1.0);
288
289 let scale = scale_factor as f32;
291 let window_padding = window_padding * scale;
292 let scrollbar_width = scrollbar_width * scale;
293
294 let available_width = (size.width as f32 - window_padding * 2.0).max(0.0);
296 let available_height = (size.height as f32 - window_padding * 2.0).max(0.0);
297
298 let char_width = (char_advance * char_spacing).max(1.0); let cols = (available_width / char_width).max(1.0) as usize;
301 let rows = (available_height / char_height).max(1.0) as usize;
302
303 let cell_renderer = CellRenderer::new(
305 window.clone(),
306 font_family,
307 font_family_bold,
308 font_family_italic,
309 font_family_bold_italic,
310 font_ranges,
311 font_size,
312 cols,
313 rows,
314 window_padding,
315 line_spacing,
316 char_spacing,
317 scrollbar_position,
318 scrollbar_width,
319 scrollbar_thumb_color,
320 scrollbar_track_color,
321 enable_text_shaping,
322 enable_ligatures,
323 enable_kerning,
324 font_antialias,
325 font_hinting,
326 font_thin_strokes,
327 minimum_contrast,
328 vsync_mode,
329 power_preference,
330 window_opacity,
331 background_color,
332 {
333 let bg_path = if background_image_enabled {
334 background_image_path
335 } else {
336 None
337 };
338 log::info!(
339 "Renderer::new: background_image_enabled={}, path={:?}",
340 background_image_enabled,
341 bg_path
342 );
343 bg_path
344 },
345 background_image_mode,
346 background_image_opacity,
347 )
348 .await?;
349
350 let egui_renderer = egui_wgpu::Renderer::new(
352 cell_renderer.device(),
353 cell_renderer.surface_format(),
354 egui_wgpu::RendererOptions {
355 msaa_samples: 1,
356 depth_stencil_format: None,
357 dithering: false,
358 predictable_texture_filtering: false,
359 },
360 );
361
362 let graphics_renderer = GraphicsRenderer::new(
364 cell_renderer.device(),
365 cell_renderer.surface_format(),
366 cell_renderer.cell_width(),
367 cell_renderer.cell_height(),
368 cell_renderer.window_padding(),
369 image_scaling_mode,
370 image_preserve_aspect_ratio,
371 )?;
372
373 let (mut custom_shader_renderer, initial_shader_path) = shaders::init_custom_shader(
375 &cell_renderer,
376 size.width,
377 size.height,
378 window_padding,
379 custom_shader_path,
380 custom_shader_enabled,
381 custom_shader_animation,
382 custom_shader_animation_speed,
383 window_opacity,
384 custom_shader_full_content,
385 custom_shader_brightness,
386 custom_shader_channel_paths,
387 custom_shader_cubemap_path,
388 use_background_as_channel0,
389 );
390
391 let (mut cursor_shader_renderer, initial_cursor_shader_path) = shaders::init_cursor_shader(
393 &cell_renderer,
394 size.width,
395 size.height,
396 window_padding,
397 cursor_shader_path,
398 cursor_shader_enabled,
399 cursor_shader_animation,
400 cursor_shader_animation_speed,
401 window_opacity,
402 );
403
404 if let Some(ref mut cs) = custom_shader_renderer {
406 cs.set_scale_factor(scale);
407 }
408 if let Some(ref mut cs) = cursor_shader_renderer {
409 cs.set_scale_factor(scale);
410 }
411
412 log::info!(
413 "[renderer] Renderer created: custom_shader_loaded={}, cursor_shader_loaded={}",
414 initial_shader_path.is_some(),
415 initial_cursor_shader_path.is_some()
416 );
417
418 Ok(Self {
419 cell_renderer,
420 graphics_renderer,
421 sixel_graphics: Vec::new(),
422 egui_renderer,
423 custom_shader_renderer,
424 custom_shader_path: initial_shader_path,
425 cursor_shader_renderer,
426 cursor_shader_path: initial_cursor_shader_path,
427 size,
428 dirty: true, cursor_shader_disabled_for_alt_screen: false,
430 debug_text: None,
431 })
432 }
433
434 pub fn resize(&mut self, new_size: PhysicalSize<u32>) -> (usize, usize) {
436 if new_size.width > 0 && new_size.height > 0 {
437 self.size = new_size;
438 self.dirty = true; let result = self.cell_renderer.resize(new_size.width, new_size.height);
440
441 self.graphics_renderer.update_cell_dimensions(
443 self.cell_renderer.cell_width(),
444 self.cell_renderer.cell_height(),
445 self.cell_renderer.window_padding(),
446 );
447
448 if let Some(ref mut custom_shader) = self.custom_shader_renderer {
450 custom_shader.resize(self.cell_renderer.device(), new_size.width, new_size.height);
451 custom_shader.update_cell_dimensions(
453 self.cell_renderer.cell_width(),
454 self.cell_renderer.cell_height(),
455 self.cell_renderer.window_padding(),
456 );
457 }
458
459 if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
461 cursor_shader.resize(self.cell_renderer.device(), new_size.width, new_size.height);
462 cursor_shader.update_cell_dimensions(
464 self.cell_renderer.cell_width(),
465 self.cell_renderer.cell_height(),
466 self.cell_renderer.window_padding(),
467 );
468 }
469
470 return result;
471 }
472
473 self.cell_renderer.grid_size()
474 }
475
476 pub fn handle_scale_factor_change(
478 &mut self,
479 scale_factor: f64,
480 new_size: PhysicalSize<u32>,
481 ) -> (usize, usize) {
482 let old_scale = self.cell_renderer.scale_factor;
483 self.cell_renderer.update_scale_factor(scale_factor);
484 let new_scale = self.cell_renderer.scale_factor;
485
486 if old_scale > 0.0 && (old_scale - new_scale).abs() > f32::EPSILON {
488 let logical_offset_y = self.cell_renderer.content_offset_y() / old_scale;
490 let new_physical_offset_y = logical_offset_y * new_scale;
491 self.cell_renderer
492 .set_content_offset_y(new_physical_offset_y);
493 self.graphics_renderer
494 .set_content_offset_y(new_physical_offset_y);
495 if let Some(ref mut cs) = self.custom_shader_renderer {
496 cs.set_content_offset_y(new_physical_offset_y);
497 }
498 if let Some(ref mut cs) = self.cursor_shader_renderer {
499 cs.set_content_offset_y(new_physical_offset_y);
500 }
501
502 let logical_offset_x = self.cell_renderer.content_offset_x() / old_scale;
504 let new_physical_offset_x = logical_offset_x * new_scale;
505 self.cell_renderer
506 .set_content_offset_x(new_physical_offset_x);
507 self.graphics_renderer
508 .set_content_offset_x(new_physical_offset_x);
509 if let Some(ref mut cs) = self.custom_shader_renderer {
510 cs.set_content_offset_x(new_physical_offset_x);
511 }
512 if let Some(ref mut cs) = self.cursor_shader_renderer {
513 cs.set_content_offset_x(new_physical_offset_x);
514 }
515
516 let logical_inset_bottom = self.cell_renderer.content_inset_bottom() / old_scale;
518 let new_physical_inset_bottom = logical_inset_bottom * new_scale;
519 self.cell_renderer
520 .set_content_inset_bottom(new_physical_inset_bottom);
521
522 if self.cell_renderer.egui_bottom_inset > 0.0 {
524 let logical_egui_bottom = self.cell_renderer.egui_bottom_inset / old_scale;
525 self.cell_renderer.egui_bottom_inset = logical_egui_bottom * new_scale;
526 }
527
528 if self.cell_renderer.content_inset_right > 0.0 {
530 let logical_inset_right = self.cell_renderer.content_inset_right / old_scale;
531 self.cell_renderer.content_inset_right = logical_inset_right * new_scale;
532 }
533
534 if self.cell_renderer.egui_right_inset > 0.0 {
536 let logical_egui_right = self.cell_renderer.egui_right_inset / old_scale;
537 self.cell_renderer.egui_right_inset = logical_egui_right * new_scale;
538 }
539
540 let logical_padding = self.cell_renderer.window_padding() / old_scale;
542 let new_physical_padding = logical_padding * new_scale;
543 self.cell_renderer
544 .update_window_padding(new_physical_padding);
545
546 if let Some(ref mut cs) = self.custom_shader_renderer {
548 cs.set_scale_factor(new_scale);
549 }
550 if let Some(ref mut cs) = self.cursor_shader_renderer {
551 cs.set_scale_factor(new_scale);
552 }
553 }
554
555 self.resize(new_size)
556 }
557
558 pub fn update_cells(&mut self, cells: &[Cell]) {
560 self.cell_renderer.update_cells(cells);
561 self.dirty = true; }
563
564 pub fn clear_all_cells(&mut self) {
567 self.cell_renderer.clear_all_cells();
568 self.dirty = true;
569 }
570
571 pub fn update_cursor(
573 &mut self,
574 position: (usize, usize),
575 opacity: f32,
576 style: par_term_emu_core_rust::cursor::CursorStyle,
577 ) {
578 self.cell_renderer.update_cursor(position, opacity, style);
579 self.dirty = true;
580 }
581
582 pub fn clear_cursor(&mut self) {
584 self.cell_renderer.clear_cursor();
585 self.dirty = true;
586 }
587
588 pub fn update_scrollbar(
596 &mut self,
597 scroll_offset: usize,
598 visible_lines: usize,
599 total_lines: usize,
600 marks: &[par_term_config::ScrollbackMark],
601 ) {
602 self.cell_renderer
603 .update_scrollbar(scroll_offset, visible_lines, total_lines, marks);
604 self.dirty = true; }
606
607 pub fn set_visual_bell_intensity(&mut self, intensity: f32) {
612 self.cell_renderer.set_visual_bell_intensity(intensity);
613 if intensity > 0.0 {
614 self.dirty = true; }
616 }
617
618 pub fn update_opacity(&mut self, opacity: f32) {
620 self.cell_renderer.update_opacity(opacity);
621
622 if let Some(ref mut custom_shader) = self.custom_shader_renderer {
624 custom_shader.set_opacity(opacity);
625 }
626
627 if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
629 cursor_shader.set_opacity(opacity);
630 }
631
632 self.dirty = true;
633 }
634
635 pub fn update_cursor_color(&mut self, color: [u8; 3]) {
637 self.cell_renderer.update_cursor_color(color);
638 self.dirty = true;
639 }
640
641 pub fn update_cursor_text_color(&mut self, color: Option<[u8; 3]>) {
643 self.cell_renderer.update_cursor_text_color(color);
644 self.dirty = true;
645 }
646
647 pub fn set_cursor_hidden_for_shader(&mut self, hidden: bool) {
649 self.cell_renderer.set_cursor_hidden_for_shader(hidden);
650 self.dirty = true;
651 }
652
653 pub fn set_focused(&mut self, focused: bool) {
655 self.cell_renderer.set_focused(focused);
656 self.dirty = true;
657 }
658
659 pub fn update_cursor_guide(&mut self, enabled: bool, color: [u8; 4]) {
661 self.cell_renderer.update_cursor_guide(enabled, color);
662 self.dirty = true;
663 }
664
665 pub fn update_cursor_shadow(
668 &mut self,
669 enabled: bool,
670 color: [u8; 4],
671 offset: [f32; 2],
672 blur: f32,
673 ) {
674 let scale = self.cell_renderer.scale_factor;
675 let physical_offset = [offset[0] * scale, offset[1] * scale];
676 let physical_blur = blur * scale;
677 self.cell_renderer
678 .update_cursor_shadow(enabled, color, physical_offset, physical_blur);
679 self.dirty = true;
680 }
681
682 pub fn update_cursor_boost(&mut self, intensity: f32, color: [u8; 3]) {
684 self.cell_renderer.update_cursor_boost(intensity, color);
685 self.dirty = true;
686 }
687
688 pub fn update_unfocused_cursor_style(&mut self, style: par_term_config::UnfocusedCursorStyle) {
690 self.cell_renderer.update_unfocused_cursor_style(style);
691 self.dirty = true;
692 }
693
694 pub fn update_command_separator(
697 &mut self,
698 enabled: bool,
699 logical_thickness: f32,
700 opacity: f32,
701 exit_color: bool,
702 color: [u8; 3],
703 ) {
704 let physical_thickness = logical_thickness * self.cell_renderer.scale_factor;
705 self.cell_renderer.update_command_separator(
706 enabled,
707 physical_thickness,
708 opacity,
709 exit_color,
710 color,
711 );
712 self.dirty = true;
713 }
714
715 pub fn set_separator_marks(&mut self, marks: Vec<SeparatorMark>) {
717 self.cell_renderer.set_separator_marks(marks);
718 self.dirty = true;
719 }
720
721 pub fn set_transparency_affects_only_default_background(&mut self, value: bool) {
724 self.cell_renderer
725 .set_transparency_affects_only_default_background(value);
726 self.dirty = true;
727 }
728
729 pub fn set_keep_text_opaque(&mut self, value: bool) {
732 self.cell_renderer.set_keep_text_opaque(value);
733
734 if let Some(ref mut custom_shader) = self.custom_shader_renderer {
736 custom_shader.set_keep_text_opaque(value);
737 }
738
739 if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
741 cursor_shader.set_keep_text_opaque(value);
742 }
743
744 self.dirty = true;
745 }
746
747 pub fn set_cursor_shader_disabled_for_alt_screen(&mut self, disabled: bool) {
752 if self.cursor_shader_disabled_for_alt_screen != disabled {
753 log::debug!("[cursor-shader] Alt-screen disable set to {}", disabled);
754 self.cursor_shader_disabled_for_alt_screen = disabled;
755 } else {
756 self.cursor_shader_disabled_for_alt_screen = disabled;
757 }
758 }
759
760 #[allow(dead_code)]
764 pub fn update_window_padding(&mut self, logical_padding: f32) -> Option<(usize, usize)> {
765 let physical_padding = logical_padding * self.cell_renderer.scale_factor;
766 let result = self.cell_renderer.update_window_padding(physical_padding);
767 self.graphics_renderer.update_cell_dimensions(
769 self.cell_renderer.cell_width(),
770 self.cell_renderer.cell_height(),
771 physical_padding,
772 );
773 if let Some(ref mut custom_shader) = self.custom_shader_renderer {
775 custom_shader.update_cell_dimensions(
776 self.cell_renderer.cell_width(),
777 self.cell_renderer.cell_height(),
778 physical_padding,
779 );
780 }
781 if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
783 cursor_shader.update_cell_dimensions(
784 self.cell_renderer.cell_width(),
785 self.cell_renderer.cell_height(),
786 physical_padding,
787 );
788 }
789 self.dirty = true;
790 result
791 }
792
793 #[allow(dead_code)]
795 pub fn set_background_image_enabled(
796 &mut self,
797 enabled: bool,
798 path: Option<&str>,
799 mode: par_term_config::BackgroundImageMode,
800 opacity: f32,
801 ) {
802 let path = if enabled { path } else { None };
803 self.cell_renderer.set_background_image(path, mode, opacity);
804
805 self.sync_background_texture_to_shader();
807
808 self.dirty = true;
809 }
810
811 pub fn set_background(
815 &mut self,
816 mode: par_term_config::BackgroundMode,
817 color: [u8; 3],
818 image_path: Option<&str>,
819 image_mode: par_term_config::BackgroundImageMode,
820 image_opacity: f32,
821 image_enabled: bool,
822 ) {
823 self.cell_renderer.set_background(
824 mode,
825 color,
826 image_path,
827 image_mode,
828 image_opacity,
829 image_enabled,
830 );
831
832 self.sync_background_texture_to_shader();
834
835 let is_solid_color = matches!(mode, par_term_config::BackgroundMode::Color);
837 let is_image_mode = matches!(mode, par_term_config::BackgroundMode::Image);
838 let normalized_color = [
839 color[0] as f32 / 255.0,
840 color[1] as f32 / 255.0,
841 color[2] as f32 / 255.0,
842 ];
843
844 if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
846 let has_background_shader = self.custom_shader_renderer.is_some();
849
850 if has_background_shader {
851 cursor_shader.set_background_color([0.0, 0.0, 0.0], false);
853 cursor_shader.set_background_texture(self.cell_renderer.device(), None);
854 cursor_shader.update_use_background_as_channel0(self.cell_renderer.device(), false);
855 } else {
856 cursor_shader.set_background_color(normalized_color, is_solid_color);
857
858 if is_image_mode && image_enabled {
860 let bg_texture = self.cell_renderer.get_background_as_channel_texture();
861 cursor_shader.set_background_texture(self.cell_renderer.device(), bg_texture);
862 cursor_shader
863 .update_use_background_as_channel0(self.cell_renderer.device(), true);
864 } else {
865 cursor_shader.set_background_texture(self.cell_renderer.device(), None);
867 cursor_shader
868 .update_use_background_as_channel0(self.cell_renderer.device(), false);
869 }
870 }
871 }
872
873 if let Some(ref mut custom_shader) = self.custom_shader_renderer {
878 custom_shader.set_background_color(normalized_color, false);
879 }
880
881 self.dirty = true;
882 }
883
884 pub fn update_scrollbar_appearance(
887 &mut self,
888 logical_width: f32,
889 thumb_color: [f32; 4],
890 track_color: [f32; 4],
891 ) {
892 let physical_width = logical_width * self.cell_renderer.scale_factor;
893 self.cell_renderer
894 .update_scrollbar_appearance(physical_width, thumb_color, track_color);
895 self.dirty = true;
896 }
897
898 #[allow(dead_code)]
900 pub fn update_scrollbar_position(&mut self, position: &str) {
901 self.cell_renderer.update_scrollbar_position(position);
902 self.dirty = true;
903 }
904
905 #[allow(dead_code)]
907 pub fn update_background_image_opacity(&mut self, opacity: f32) {
908 self.cell_renderer.update_background_image_opacity(opacity);
909 self.dirty = true;
910 }
911
912 pub fn load_pane_background(&mut self, path: &str) -> anyhow::Result<bool> {
915 self.cell_renderer.load_pane_background(path)
916 }
917
918 pub fn update_image_scaling_mode(&mut self, scaling_mode: par_term_config::ImageScalingMode) {
923 self.graphics_renderer
924 .update_scaling_mode(self.cell_renderer.device(), scaling_mode);
925 self.dirty = true;
926 }
927
928 pub fn update_image_preserve_aspect_ratio(&mut self, preserve: bool) {
930 self.graphics_renderer.set_preserve_aspect_ratio(preserve);
931 self.dirty = true;
932 }
933
934 pub fn needs_continuous_render(&self) -> bool {
939 let custom_needs = self
940 .custom_shader_renderer
941 .as_ref()
942 .is_some_and(|r| r.animation_enabled() || r.cursor_needs_animation());
943 let cursor_needs = self
944 .cursor_shader_renderer
945 .as_ref()
946 .is_some_and(|r| r.animation_enabled() || r.cursor_needs_animation());
947 custom_needs || cursor_needs
948 }
949
950 pub fn render(
953 &mut self,
954 egui_data: Option<(egui::FullOutput, &egui::Context)>,
955 force_egui_opaque: bool,
956 show_scrollbar: bool,
957 pane_background: Option<&par_term_config::PaneBackground>,
958 ) -> Result<bool> {
959 let force_render = self.needs_continuous_render();
961
962 if !self.dirty && egui_data.is_none() && !force_render {
963 return Ok(false);
965 }
966
967 let has_custom_shader = self.custom_shader_renderer.is_some();
969 let use_cursor_shader =
971 self.cursor_shader_renderer.is_some() && !self.cursor_shader_disabled_for_alt_screen;
972
973 let t1 = std::time::Instant::now();
975 let surface_texture = if has_custom_shader {
976 self.cell_renderer.render_to_texture(
985 self.custom_shader_renderer
986 .as_ref()
987 .unwrap()
988 .intermediate_texture_view(),
989 true, )?
991 } else if use_cursor_shader {
992 self.cell_renderer.render_to_texture(
996 self.cursor_shader_renderer
997 .as_ref()
998 .unwrap()
999 .intermediate_texture_view(),
1000 true, )?
1002 } else {
1003 self.cell_renderer.render(show_scrollbar, pane_background)?
1006 };
1007 let cell_render_time = t1.elapsed();
1008
1009 let t_custom = std::time::Instant::now();
1011 let custom_shader_time = if let Some(ref mut custom_shader) = self.custom_shader_renderer {
1012 if use_cursor_shader {
1013 custom_shader.render(
1016 self.cell_renderer.device(),
1017 self.cell_renderer.queue(),
1018 self.cursor_shader_renderer
1019 .as_ref()
1020 .unwrap()
1021 .intermediate_texture_view(),
1022 false, )?;
1024 } else {
1025 let surface_view = surface_texture
1028 .texture
1029 .create_view(&wgpu::TextureViewDescriptor::default());
1030 custom_shader.render(
1031 self.cell_renderer.device(),
1032 self.cell_renderer.queue(),
1033 &surface_view,
1034 true, )?;
1036 }
1037 t_custom.elapsed()
1038 } else {
1039 std::time::Duration::ZERO
1040 };
1041
1042 let t_cursor = std::time::Instant::now();
1044 let cursor_shader_time = if use_cursor_shader {
1045 log::trace!("Rendering cursor shader");
1046 let cursor_shader = self.cursor_shader_renderer.as_mut().unwrap();
1047 let surface_view = surface_texture
1048 .texture
1049 .create_view(&wgpu::TextureViewDescriptor::default());
1050
1051 cursor_shader.render(
1052 self.cell_renderer.device(),
1053 self.cell_renderer.queue(),
1054 &surface_view,
1055 true, )?;
1057 t_cursor.elapsed()
1058 } else {
1059 if self.cursor_shader_disabled_for_alt_screen {
1060 log::trace!("Skipping cursor shader - alt screen active");
1061 }
1062 std::time::Duration::ZERO
1063 };
1064
1065 let t2 = std::time::Instant::now();
1067 if !self.sixel_graphics.is_empty() {
1068 self.render_sixel_graphics(&surface_texture)?;
1069 }
1070 let sixel_render_time = t2.elapsed();
1071
1072 self.cell_renderer
1076 .render_overlays(&surface_texture, show_scrollbar)?;
1077
1078 let t3 = std::time::Instant::now();
1080 if let Some((egui_output, egui_ctx)) = egui_data {
1081 self.render_egui(&surface_texture, egui_output, egui_ctx, force_egui_opaque)?;
1082 }
1083 let egui_render_time = t3.elapsed();
1084
1085 let t4 = std::time::Instant::now();
1087 surface_texture.present();
1088 let present_time = t4.elapsed();
1089
1090 let total = cell_render_time
1092 + custom_shader_time
1093 + cursor_shader_time
1094 + sixel_render_time
1095 + egui_render_time
1096 + present_time;
1097 if present_time.as_millis() > 10 || total.as_millis() > 10 {
1098 log::info!(
1099 "[RENDER] RENDER_BREAKDOWN: CellRender={:.2}ms BgShader={:.2}ms CursorShader={:.2}ms Sixel={:.2}ms Egui={:.2}ms PRESENT={:.2}ms Total={:.2}ms",
1100 cell_render_time.as_secs_f64() * 1000.0,
1101 custom_shader_time.as_secs_f64() * 1000.0,
1102 cursor_shader_time.as_secs_f64() * 1000.0,
1103 sixel_render_time.as_secs_f64() * 1000.0,
1104 egui_render_time.as_secs_f64() * 1000.0,
1105 present_time.as_secs_f64() * 1000.0,
1106 total.as_secs_f64() * 1000.0
1107 );
1108 }
1109
1110 self.dirty = false;
1112
1113 Ok(true)
1114 }
1115
1116 #[allow(dead_code)]
1129 pub fn render_panes(
1130 &mut self,
1131 panes: &[PaneRenderInfo<'_>],
1132 egui_data: Option<(egui::FullOutput, &egui::Context)>,
1133 force_egui_opaque: bool,
1134 ) -> Result<bool> {
1135 let force_render = self.needs_continuous_render();
1137 if !self.dirty && egui_data.is_none() && !force_render {
1138 return Ok(false);
1139 }
1140
1141 let surface_texture = self.cell_renderer.surface.get_current_texture()?;
1143 let surface_view = surface_texture
1144 .texture
1145 .create_view(&wgpu::TextureViewDescriptor::default());
1146
1147 {
1149 let mut encoder = self.cell_renderer.device().create_command_encoder(
1150 &wgpu::CommandEncoderDescriptor {
1151 label: Some("pane clear encoder"),
1152 },
1153 );
1154
1155 let opacity = self.cell_renderer.window_opacity as f64;
1156 let clear_color = if self.cell_renderer.bg_is_solid_color {
1157 wgpu::Color {
1158 r: self.cell_renderer.solid_bg_color[0] as f64 * opacity,
1159 g: self.cell_renderer.solid_bg_color[1] as f64 * opacity,
1160 b: self.cell_renderer.solid_bg_color[2] as f64 * opacity,
1161 a: opacity,
1162 }
1163 } else {
1164 wgpu::Color {
1165 r: self.cell_renderer.background_color[0] as f64 * opacity,
1166 g: self.cell_renderer.background_color[1] as f64 * opacity,
1167 b: self.cell_renderer.background_color[2] as f64 * opacity,
1168 a: opacity,
1169 }
1170 };
1171
1172 {
1173 let _clear_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1174 label: Some("surface clear pass"),
1175 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1176 view: &surface_view,
1177 resolve_target: None,
1178 ops: wgpu::Operations {
1179 load: wgpu::LoadOp::Clear(clear_color),
1180 store: wgpu::StoreOp::Store,
1181 },
1182 depth_slice: None,
1183 })],
1184 depth_stencil_attachment: None,
1185 timestamp_writes: None,
1186 occlusion_query_set: None,
1187 });
1188 }
1189
1190 self.cell_renderer
1191 .queue()
1192 .submit(std::iter::once(encoder.finish()));
1193 }
1194
1195 let has_background_image = self
1197 .cell_renderer
1198 .render_background_only(&surface_view, false)?;
1199
1200 for pane in panes {
1202 let separator_marks = compute_visible_separator_marks(
1203 &pane.marks,
1204 pane.scrollback_len,
1205 pane.scroll_offset,
1206 pane.grid_size.1,
1207 );
1208 self.cell_renderer.render_pane_to_view(
1209 &surface_view,
1210 &pane.viewport,
1211 pane.cells,
1212 pane.grid_size.0,
1213 pane.grid_size.1,
1214 pane.cursor_pos,
1215 pane.cursor_opacity,
1216 pane.show_scrollbar,
1217 false, has_background_image, &separator_marks,
1220 pane.background.as_ref(),
1221 )?;
1222 }
1223
1224 if let Some((egui_output, egui_ctx)) = egui_data {
1226 self.render_egui(&surface_texture, egui_output, egui_ctx, force_egui_opaque)?;
1227 }
1228
1229 surface_texture.present();
1231
1232 self.dirty = false;
1233 Ok(true)
1234 }
1235
1236 #[allow(dead_code, clippy::too_many_arguments)]
1258 pub fn render_split_panes(
1259 &mut self,
1260 panes: &[PaneRenderInfo<'_>],
1261 dividers: &[DividerRenderInfo],
1262 pane_titles: &[PaneTitleInfo],
1263 focused_viewport: Option<&PaneViewport>,
1264 divider_settings: &PaneDividerSettings,
1265 egui_data: Option<(egui::FullOutput, &egui::Context)>,
1266 force_egui_opaque: bool,
1267 ) -> Result<bool> {
1268 let force_render = self.needs_continuous_render();
1270 if !self.dirty && egui_data.is_none() && !force_render {
1271 return Ok(false);
1272 }
1273
1274 let has_custom_shader = self.custom_shader_renderer.is_some();
1275
1276 for pane in panes.iter() {
1278 if let Some(ref bg) = pane.background
1279 && let Some(ref path) = bg.image_path
1280 && let Err(e) = self.cell_renderer.load_pane_background(path)
1281 {
1282 log::error!("Failed to load pane background '{}': {}", path, e);
1283 }
1284 }
1285
1286 let surface_texture = self.cell_renderer.surface.get_current_texture()?;
1288 let surface_view = surface_texture
1289 .texture
1290 .create_view(&wgpu::TextureViewDescriptor::default());
1291
1292 let opacity = self.cell_renderer.window_opacity as f64;
1294 let clear_color = if self.cell_renderer.bg_is_solid_color {
1295 wgpu::Color {
1296 r: self.cell_renderer.solid_bg_color[0] as f64 * opacity,
1297 g: self.cell_renderer.solid_bg_color[1] as f64 * opacity,
1298 b: self.cell_renderer.solid_bg_color[2] as f64 * opacity,
1299 a: opacity,
1300 }
1301 } else {
1302 wgpu::Color {
1303 r: self.cell_renderer.background_color[0] as f64 * opacity,
1304 g: self.cell_renderer.background_color[1] as f64 * opacity,
1305 b: self.cell_renderer.background_color[2] as f64 * opacity,
1306 a: opacity,
1307 }
1308 };
1309
1310 if let Some(ref mut custom_shader) = self.custom_shader_renderer {
1313 custom_shader.clear_intermediate_texture(
1316 self.cell_renderer.device(),
1317 self.cell_renderer.queue(),
1318 );
1319
1320 custom_shader.render_with_clear_color(
1323 self.cell_renderer.device(),
1324 self.cell_renderer.queue(),
1325 &surface_view,
1326 false, clear_color,
1328 )?;
1329 } else {
1330 let mut encoder = self.cell_renderer.device().create_command_encoder(
1332 &wgpu::CommandEncoderDescriptor {
1333 label: Some("split pane clear encoder"),
1334 },
1335 );
1336
1337 {
1338 let _clear_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1339 label: Some("surface clear pass"),
1340 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1341 view: &surface_view,
1342 resolve_target: None,
1343 ops: wgpu::Operations {
1344 load: wgpu::LoadOp::Clear(clear_color),
1345 store: wgpu::StoreOp::Store,
1346 },
1347 depth_slice: None,
1348 })],
1349 depth_stencil_attachment: None,
1350 timestamp_writes: None,
1351 occlusion_query_set: None,
1352 });
1353 }
1354
1355 self.cell_renderer
1356 .queue()
1357 .submit(std::iter::once(encoder.finish()));
1358 }
1359
1360 let any_pane_has_background = panes.iter().any(|p| p.background.is_some());
1365 let has_background_image = if !has_custom_shader && !any_pane_has_background {
1366 self.cell_renderer
1367 .render_background_only(&surface_view, false)?
1368 } else {
1369 false
1370 };
1371
1372 for pane in panes {
1374 let separator_marks = compute_visible_separator_marks(
1375 &pane.marks,
1376 pane.scrollback_len,
1377 pane.scroll_offset,
1378 pane.grid_size.1,
1379 );
1380 self.cell_renderer.render_pane_to_view(
1381 &surface_view,
1382 &pane.viewport,
1383 pane.cells,
1384 pane.grid_size.0,
1385 pane.grid_size.1,
1386 pane.cursor_pos,
1387 pane.cursor_opacity,
1388 pane.show_scrollbar,
1389 false, has_background_image || has_custom_shader, &separator_marks,
1392 pane.background.as_ref(),
1393 )?;
1394 }
1395
1396 if !dividers.is_empty() {
1398 self.render_dividers(&surface_view, dividers, divider_settings)?;
1399 }
1400
1401 if !pane_titles.is_empty() {
1403 self.render_pane_titles(&surface_view, pane_titles)?;
1404 }
1405
1406 if panes.len() > 1
1408 && let Some(viewport) = focused_viewport
1409 {
1410 self.render_focus_indicator(&surface_view, viewport, divider_settings)?;
1411 }
1412
1413 if let Some((egui_output, egui_ctx)) = egui_data {
1415 self.render_egui(&surface_texture, egui_output, egui_ctx, force_egui_opaque)?;
1416 }
1417
1418 surface_texture.present();
1420
1421 self.dirty = false;
1422 Ok(true)
1423 }
1424
1425 #[allow(dead_code)]
1434 pub fn render_dividers(
1435 &mut self,
1436 surface_view: &wgpu::TextureView,
1437 dividers: &[DividerRenderInfo],
1438 settings: &PaneDividerSettings,
1439 ) -> Result<()> {
1440 if dividers.is_empty() {
1441 return Ok(());
1442 }
1443
1444 let mut instances = Vec::with_capacity(dividers.len() * 3); let w = self.size.width as f32;
1449 let h = self.size.height as f32;
1450
1451 for divider in dividers {
1452 let color = if divider.hovered {
1453 settings.hover_color
1454 } else {
1455 settings.divider_color
1456 };
1457
1458 use par_term_config::DividerStyle;
1459 match settings.divider_style {
1460 DividerStyle::Solid => {
1461 let x_ndc = divider.x / w * 2.0 - 1.0;
1462 let y_ndc = 1.0 - (divider.y / h * 2.0);
1463 let w_ndc = divider.width / w * 2.0;
1464 let h_ndc = divider.height / h * 2.0;
1465
1466 instances.push(crate::cell_renderer::types::BackgroundInstance {
1467 position: [x_ndc, y_ndc],
1468 size: [w_ndc, h_ndc],
1469 color: [color[0], color[1], color[2], 1.0],
1470 });
1471 }
1472 DividerStyle::Double => {
1473 let is_horizontal = divider.width > divider.height;
1475 let thickness = if is_horizontal {
1476 divider.height
1477 } else {
1478 divider.width
1479 };
1480
1481 if thickness >= 4.0 {
1482 if is_horizontal {
1484 instances.push(crate::cell_renderer::types::BackgroundInstance {
1486 position: [divider.x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1487 size: [divider.width / w * 2.0, 1.0 / h * 2.0],
1488 color: [color[0], color[1], color[2], 1.0],
1489 });
1490 let bottom_y = divider.y + divider.height - 1.0;
1492 instances.push(crate::cell_renderer::types::BackgroundInstance {
1493 position: [divider.x / w * 2.0 - 1.0, 1.0 - (bottom_y / h * 2.0)],
1494 size: [divider.width / w * 2.0, 1.0 / h * 2.0],
1495 color: [color[0], color[1], color[2], 1.0],
1496 });
1497 } else {
1498 instances.push(crate::cell_renderer::types::BackgroundInstance {
1500 position: [divider.x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1501 size: [1.0 / w * 2.0, divider.height / h * 2.0],
1502 color: [color[0], color[1], color[2], 1.0],
1503 });
1504 let right_x = divider.x + divider.width - 1.0;
1506 instances.push(crate::cell_renderer::types::BackgroundInstance {
1507 position: [right_x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1508 size: [1.0 / w * 2.0, divider.height / h * 2.0],
1509 color: [color[0], color[1], color[2], 1.0],
1510 });
1511 }
1512 } else {
1513 if is_horizontal {
1516 let center_y = divider.y + (divider.height - 1.0) / 2.0;
1517 instances.push(crate::cell_renderer::types::BackgroundInstance {
1518 position: [divider.x / w * 2.0 - 1.0, 1.0 - (center_y / h * 2.0)],
1519 size: [divider.width / w * 2.0, 1.0 / h * 2.0],
1520 color: [color[0], color[1], color[2], 1.0],
1521 });
1522 } else {
1523 let center_x = divider.x + (divider.width - 1.0) / 2.0;
1524 instances.push(crate::cell_renderer::types::BackgroundInstance {
1525 position: [center_x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1526 size: [1.0 / w * 2.0, divider.height / h * 2.0],
1527 color: [color[0], color[1], color[2], 1.0],
1528 });
1529 }
1530 }
1531 }
1532 DividerStyle::Dashed => {
1533 let is_horizontal = divider.width > divider.height;
1535 let dash_len: f32 = 6.0;
1536 let gap_len: f32 = 4.0;
1537
1538 if is_horizontal {
1539 let mut x = divider.x;
1540 while x < divider.x + divider.width {
1541 let seg_w = dash_len.min(divider.x + divider.width - x);
1542 instances.push(crate::cell_renderer::types::BackgroundInstance {
1543 position: [x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1544 size: [seg_w / w * 2.0, divider.height / h * 2.0],
1545 color: [color[0], color[1], color[2], 1.0],
1546 });
1547 x += dash_len + gap_len;
1548 }
1549 } else {
1550 let mut y = divider.y;
1551 while y < divider.y + divider.height {
1552 let seg_h = dash_len.min(divider.y + divider.height - y);
1553 instances.push(crate::cell_renderer::types::BackgroundInstance {
1554 position: [divider.x / w * 2.0 - 1.0, 1.0 - (y / h * 2.0)],
1555 size: [divider.width / w * 2.0, seg_h / h * 2.0],
1556 color: [color[0], color[1], color[2], 1.0],
1557 });
1558 y += dash_len + gap_len;
1559 }
1560 }
1561 }
1562 DividerStyle::Shadow => {
1563 let is_horizontal = divider.width > divider.height;
1566 let thickness = if is_horizontal {
1567 divider.height
1568 } else {
1569 divider.width
1570 };
1571
1572 let highlight = [
1574 (color[0] + 0.3).min(1.0),
1575 (color[1] + 0.3).min(1.0),
1576 (color[2] + 0.3).min(1.0),
1577 1.0,
1578 ];
1579 let shadow = [(color[0] * 0.3), (color[1] * 0.3), (color[2] * 0.3), 1.0];
1581
1582 if thickness >= 3.0 {
1583 let edge = 1.0_f32;
1585 if is_horizontal {
1586 instances.push(crate::cell_renderer::types::BackgroundInstance {
1588 position: [divider.x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1589 size: [divider.width / w * 2.0, edge / h * 2.0],
1590 color: highlight,
1591 });
1592 let body_y = divider.y + edge;
1594 let body_h = divider.height - edge * 2.0;
1595 if body_h > 0.0 {
1596 instances.push(crate::cell_renderer::types::BackgroundInstance {
1597 position: [divider.x / w * 2.0 - 1.0, 1.0 - (body_y / h * 2.0)],
1598 size: [divider.width / w * 2.0, body_h / h * 2.0],
1599 color: [color[0], color[1], color[2], 1.0],
1600 });
1601 }
1602 let shadow_y = divider.y + divider.height - edge;
1604 instances.push(crate::cell_renderer::types::BackgroundInstance {
1605 position: [divider.x / w * 2.0 - 1.0, 1.0 - (shadow_y / h * 2.0)],
1606 size: [divider.width / w * 2.0, edge / h * 2.0],
1607 color: shadow,
1608 });
1609 } else {
1610 instances.push(crate::cell_renderer::types::BackgroundInstance {
1612 position: [divider.x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1613 size: [edge / w * 2.0, divider.height / h * 2.0],
1614 color: highlight,
1615 });
1616 let body_x = divider.x + edge;
1618 let body_w = divider.width - edge * 2.0;
1619 if body_w > 0.0 {
1620 instances.push(crate::cell_renderer::types::BackgroundInstance {
1621 position: [body_x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1622 size: [body_w / w * 2.0, divider.height / h * 2.0],
1623 color: [color[0], color[1], color[2], 1.0],
1624 });
1625 }
1626 let shadow_x = divider.x + divider.width - edge;
1628 instances.push(crate::cell_renderer::types::BackgroundInstance {
1629 position: [shadow_x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1630 size: [edge / w * 2.0, divider.height / h * 2.0],
1631 color: shadow,
1632 });
1633 }
1634 } else {
1635 if is_horizontal {
1637 let half = (divider.height / 2.0).max(1.0);
1638 instances.push(crate::cell_renderer::types::BackgroundInstance {
1639 position: [divider.x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1640 size: [divider.width / w * 2.0, half / h * 2.0],
1641 color: highlight,
1642 });
1643 let bottom_y = divider.y + half;
1644 let bottom_h = divider.height - half;
1645 if bottom_h > 0.0 {
1646 instances.push(crate::cell_renderer::types::BackgroundInstance {
1647 position: [
1648 divider.x / w * 2.0 - 1.0,
1649 1.0 - (bottom_y / h * 2.0),
1650 ],
1651 size: [divider.width / w * 2.0, bottom_h / h * 2.0],
1652 color: shadow,
1653 });
1654 }
1655 } else {
1656 let half = (divider.width / 2.0).max(1.0);
1657 instances.push(crate::cell_renderer::types::BackgroundInstance {
1658 position: [divider.x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1659 size: [half / w * 2.0, divider.height / h * 2.0],
1660 color: highlight,
1661 });
1662 let right_x = divider.x + half;
1663 let right_w = divider.width - half;
1664 if right_w > 0.0 {
1665 instances.push(crate::cell_renderer::types::BackgroundInstance {
1666 position: [
1667 right_x / w * 2.0 - 1.0,
1668 1.0 - (divider.y / h * 2.0),
1669 ],
1670 size: [right_w / w * 2.0, divider.height / h * 2.0],
1671 color: shadow,
1672 });
1673 }
1674 }
1675 }
1676 }
1677 }
1678 }
1679
1680 self.cell_renderer.queue().write_buffer(
1682 &self.cell_renderer.bg_instance_buffer,
1683 0,
1684 bytemuck::cast_slice(&instances),
1685 );
1686
1687 let mut encoder =
1689 self.cell_renderer
1690 .device()
1691 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1692 label: Some("divider render encoder"),
1693 });
1694
1695 {
1696 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1697 label: Some("divider render pass"),
1698 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1699 view: surface_view,
1700 resolve_target: None,
1701 ops: wgpu::Operations {
1702 load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store,
1704 },
1705 depth_slice: None,
1706 })],
1707 depth_stencil_attachment: None,
1708 timestamp_writes: None,
1709 occlusion_query_set: None,
1710 });
1711
1712 render_pass.set_pipeline(&self.cell_renderer.bg_pipeline);
1713 render_pass.set_vertex_buffer(0, self.cell_renderer.vertex_buffer.slice(..));
1714 render_pass.set_vertex_buffer(1, self.cell_renderer.bg_instance_buffer.slice(..));
1715 render_pass.draw(0..4, 0..instances.len() as u32);
1716 }
1717
1718 self.cell_renderer
1719 .queue()
1720 .submit(std::iter::once(encoder.finish()));
1721 Ok(())
1722 }
1723
1724 #[allow(dead_code)]
1733 pub fn render_focus_indicator(
1734 &mut self,
1735 surface_view: &wgpu::TextureView,
1736 viewport: &PaneViewport,
1737 settings: &PaneDividerSettings,
1738 ) -> Result<()> {
1739 if !settings.show_focus_indicator {
1740 return Ok(());
1741 }
1742
1743 let border_w = settings.focus_width;
1744 let color = [
1745 settings.focus_color[0],
1746 settings.focus_color[1],
1747 settings.focus_color[2],
1748 1.0,
1749 ];
1750
1751 let instances = vec![
1753 crate::cell_renderer::types::BackgroundInstance {
1755 position: [
1756 viewport.x / self.size.width as f32 * 2.0 - 1.0,
1757 1.0 - (viewport.y / self.size.height as f32 * 2.0),
1758 ],
1759 size: [
1760 viewport.width / self.size.width as f32 * 2.0,
1761 border_w / self.size.height as f32 * 2.0,
1762 ],
1763 color,
1764 },
1765 crate::cell_renderer::types::BackgroundInstance {
1767 position: [
1768 viewport.x / self.size.width as f32 * 2.0 - 1.0,
1769 1.0 - ((viewport.y + viewport.height - border_w) / self.size.height as f32
1770 * 2.0),
1771 ],
1772 size: [
1773 viewport.width / self.size.width as f32 * 2.0,
1774 border_w / self.size.height as f32 * 2.0,
1775 ],
1776 color,
1777 },
1778 crate::cell_renderer::types::BackgroundInstance {
1780 position: [
1781 viewport.x / self.size.width as f32 * 2.0 - 1.0,
1782 1.0 - ((viewport.y + border_w) / self.size.height as f32 * 2.0),
1783 ],
1784 size: [
1785 border_w / self.size.width as f32 * 2.0,
1786 (viewport.height - border_w * 2.0) / self.size.height as f32 * 2.0,
1787 ],
1788 color,
1789 },
1790 crate::cell_renderer::types::BackgroundInstance {
1792 position: [
1793 (viewport.x + viewport.width - border_w) / self.size.width as f32 * 2.0 - 1.0,
1794 1.0 - ((viewport.y + border_w) / self.size.height as f32 * 2.0),
1795 ],
1796 size: [
1797 border_w / self.size.width as f32 * 2.0,
1798 (viewport.height - border_w * 2.0) / self.size.height as f32 * 2.0,
1799 ],
1800 color,
1801 },
1802 ];
1803
1804 self.cell_renderer.queue().write_buffer(
1806 &self.cell_renderer.bg_instance_buffer,
1807 0,
1808 bytemuck::cast_slice(&instances),
1809 );
1810
1811 let mut encoder =
1813 self.cell_renderer
1814 .device()
1815 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1816 label: Some("focus indicator encoder"),
1817 });
1818
1819 {
1820 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1821 label: Some("focus indicator pass"),
1822 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1823 view: surface_view,
1824 resolve_target: None,
1825 ops: wgpu::Operations {
1826 load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store,
1828 },
1829 depth_slice: None,
1830 })],
1831 depth_stencil_attachment: None,
1832 timestamp_writes: None,
1833 occlusion_query_set: None,
1834 });
1835
1836 render_pass.set_pipeline(&self.cell_renderer.bg_pipeline);
1837 render_pass.set_vertex_buffer(0, self.cell_renderer.vertex_buffer.slice(..));
1838 render_pass.set_vertex_buffer(1, self.cell_renderer.bg_instance_buffer.slice(..));
1839 render_pass.draw(0..4, 0..instances.len() as u32);
1840 }
1841
1842 self.cell_renderer
1843 .queue()
1844 .submit(std::iter::once(encoder.finish()));
1845 Ok(())
1846 }
1847
1848 #[allow(dead_code)]
1853 pub fn render_pane_titles(
1854 &mut self,
1855 surface_view: &wgpu::TextureView,
1856 titles: &[PaneTitleInfo],
1857 ) -> Result<()> {
1858 if titles.is_empty() {
1859 return Ok(());
1860 }
1861
1862 let width = self.size.width as f32;
1863 let height = self.size.height as f32;
1864
1865 let mut bg_instances = Vec::with_capacity(titles.len());
1867 for title in titles {
1868 let x_ndc = title.x / width * 2.0 - 1.0;
1869 let y_ndc = 1.0 - (title.y / height * 2.0);
1870 let w_ndc = title.width / width * 2.0;
1871 let h_ndc = title.height / height * 2.0;
1872
1873 let brightness = if title.focused { 1.0 } else { 0.7 };
1876
1877 bg_instances.push(crate::cell_renderer::types::BackgroundInstance {
1878 position: [x_ndc, y_ndc],
1879 size: [w_ndc, h_ndc],
1880 color: [
1881 title.bg_color[0] * brightness,
1882 title.bg_color[1] * brightness,
1883 title.bg_color[2] * brightness,
1884 1.0, ],
1886 });
1887 }
1888
1889 self.cell_renderer.queue().write_buffer(
1891 &self.cell_renderer.bg_instance_buffer,
1892 0,
1893 bytemuck::cast_slice(&bg_instances),
1894 );
1895
1896 let mut encoder =
1898 self.cell_renderer
1899 .device()
1900 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1901 label: Some("pane title bg encoder"),
1902 });
1903
1904 {
1905 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1906 label: Some("pane title bg pass"),
1907 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1908 view: surface_view,
1909 resolve_target: None,
1910 ops: wgpu::Operations {
1911 load: wgpu::LoadOp::Load,
1912 store: wgpu::StoreOp::Store,
1913 },
1914 depth_slice: None,
1915 })],
1916 depth_stencil_attachment: None,
1917 timestamp_writes: None,
1918 occlusion_query_set: None,
1919 });
1920
1921 render_pass.set_pipeline(&self.cell_renderer.bg_pipeline);
1922 render_pass.set_vertex_buffer(0, self.cell_renderer.vertex_buffer.slice(..));
1923 render_pass.set_vertex_buffer(1, self.cell_renderer.bg_instance_buffer.slice(..));
1924 render_pass.draw(0..4, 0..bg_instances.len() as u32);
1925 }
1926
1927 self.cell_renderer
1928 .queue()
1929 .submit(std::iter::once(encoder.finish()));
1930
1931 let mut text_instances = Vec::new();
1933 let baseline_y = self.cell_renderer.font_ascent;
1934
1935 for title in titles {
1936 let title_text = &title.title;
1937 if title_text.is_empty() {
1938 continue;
1939 }
1940
1941 let padding_x = 8.0;
1943 let mut x_pos = title.x + padding_x;
1944 let y_base = title.y + (title.height - self.cell_renderer.cell_height) / 2.0;
1945
1946 let text_color = [
1947 title.text_color[0],
1948 title.text_color[1],
1949 title.text_color[2],
1950 if title.focused { 1.0 } else { 0.8 },
1951 ];
1952
1953 let max_chars =
1955 ((title.width - padding_x * 2.0) / self.cell_renderer.cell_width) as usize;
1956 let display_text: String = if title_text.len() > max_chars && max_chars > 3 {
1957 let truncated: String = title_text.chars().take(max_chars - 1).collect();
1958 format!("{}\u{2026}", truncated) } else {
1960 title_text.clone()
1961 };
1962
1963 for ch in display_text.chars() {
1964 if x_pos >= title.x + title.width - padding_x {
1965 break;
1966 }
1967
1968 if let Some((font_idx, glyph_id)) =
1969 self.cell_renderer.font_manager.find_glyph(ch, false, false)
1970 {
1971 let cache_key = ((font_idx as u64) << 32) | (glyph_id as u64);
1972 let force_monochrome = crate::cell_renderer::atlas::should_render_as_symbol(ch);
1974 let info = if self.cell_renderer.glyph_cache.contains_key(&cache_key) {
1975 self.cell_renderer.lru_remove(cache_key);
1976 self.cell_renderer.lru_push_front(cache_key);
1977 self.cell_renderer
1978 .glyph_cache
1979 .get(&cache_key)
1980 .unwrap()
1981 .clone()
1982 } else if let Some(raster) =
1983 self.cell_renderer
1984 .rasterize_glyph(font_idx, glyph_id, force_monochrome)
1985 {
1986 let info = self.cell_renderer.upload_glyph(cache_key, &raster);
1987 self.cell_renderer
1988 .glyph_cache
1989 .insert(cache_key, info.clone());
1990 self.cell_renderer.lru_push_front(cache_key);
1991 info
1992 } else {
1993 x_pos += self.cell_renderer.cell_width;
1994 continue;
1995 };
1996
1997 let glyph_left = x_pos + info.bearing_x;
1998 let glyph_top = y_base + (baseline_y - info.bearing_y);
1999
2000 text_instances.push(crate::cell_renderer::types::TextInstance {
2001 position: [
2002 glyph_left / width * 2.0 - 1.0,
2003 1.0 - (glyph_top / height * 2.0),
2004 ],
2005 size: [
2006 info.width as f32 / width * 2.0,
2007 info.height as f32 / height * 2.0,
2008 ],
2009 tex_offset: [info.x as f32 / 2048.0, info.y as f32 / 2048.0],
2010 tex_size: [info.width as f32 / 2048.0, info.height as f32 / 2048.0],
2011 color: text_color,
2012 is_colored: if info.is_colored { 1 } else { 0 },
2013 });
2014 }
2015
2016 x_pos += self.cell_renderer.cell_width;
2017 }
2018 }
2019
2020 if text_instances.is_empty() {
2021 return Ok(());
2022 }
2023
2024 self.cell_renderer.queue().write_buffer(
2026 &self.cell_renderer.text_instance_buffer,
2027 0,
2028 bytemuck::cast_slice(&text_instances),
2029 );
2030
2031 let mut encoder =
2033 self.cell_renderer
2034 .device()
2035 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
2036 label: Some("pane title text encoder"),
2037 });
2038
2039 {
2040 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2041 label: Some("pane title text pass"),
2042 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2043 view: surface_view,
2044 resolve_target: None,
2045 ops: wgpu::Operations {
2046 load: wgpu::LoadOp::Load,
2047 store: wgpu::StoreOp::Store,
2048 },
2049 depth_slice: None,
2050 })],
2051 depth_stencil_attachment: None,
2052 timestamp_writes: None,
2053 occlusion_query_set: None,
2054 });
2055
2056 render_pass.set_pipeline(&self.cell_renderer.text_pipeline);
2057 render_pass.set_bind_group(0, &self.cell_renderer.text_bind_group, &[]);
2058 render_pass.set_vertex_buffer(0, self.cell_renderer.vertex_buffer.slice(..));
2059 render_pass.set_vertex_buffer(1, self.cell_renderer.text_instance_buffer.slice(..));
2060 render_pass.draw(0..4, 0..text_instances.len() as u32);
2061 }
2062
2063 self.cell_renderer
2064 .queue()
2065 .submit(std::iter::once(encoder.finish()));
2066
2067 Ok(())
2068 }
2069
2070 fn render_egui(
2072 &mut self,
2073 surface_texture: &wgpu::SurfaceTexture,
2074 egui_output: egui::FullOutput,
2075 egui_ctx: &egui::Context,
2076 force_opaque: bool,
2077 ) -> Result<()> {
2078 use wgpu::TextureViewDescriptor;
2079
2080 let view = surface_texture
2082 .texture
2083 .create_view(&TextureViewDescriptor::default());
2084
2085 let mut encoder =
2087 self.cell_renderer
2088 .device()
2089 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
2090 label: Some("egui encoder"),
2091 });
2092
2093 let screen_descriptor = egui_wgpu::ScreenDescriptor {
2095 size_in_pixels: [self.size.width, self.size.height],
2096 pixels_per_point: egui_output.pixels_per_point,
2097 };
2098
2099 for (id, image_delta) in &egui_output.textures_delta.set {
2101 self.egui_renderer.update_texture(
2102 self.cell_renderer.device(),
2103 self.cell_renderer.queue(),
2104 *id,
2105 image_delta,
2106 );
2107 }
2108
2109 let mut paint_jobs = egui_ctx.tessellate(egui_output.shapes, egui_output.pixels_per_point);
2111
2112 if force_opaque {
2114 for job in paint_jobs.iter_mut() {
2115 match &mut job.primitive {
2116 egui::epaint::Primitive::Mesh(mesh) => {
2117 for v in mesh.vertices.iter_mut() {
2118 v.color[3] = 255;
2119 }
2120 }
2121 egui::epaint::Primitive::Callback(_) => {}
2122 }
2123 }
2124 }
2125
2126 self.egui_renderer.update_buffers(
2128 self.cell_renderer.device(),
2129 self.cell_renderer.queue(),
2130 &mut encoder,
2131 &paint_jobs,
2132 &screen_descriptor,
2133 );
2134
2135 {
2137 let render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2138 label: Some("egui render pass"),
2139 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2140 view: &view,
2141 resolve_target: None,
2142 ops: wgpu::Operations {
2143 load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store,
2145 },
2146 depth_slice: None,
2147 })],
2148 depth_stencil_attachment: None,
2149 timestamp_writes: None,
2150 occlusion_query_set: None,
2151 });
2152
2153 let mut render_pass = render_pass.forget_lifetime();
2155
2156 self.egui_renderer
2157 .render(&mut render_pass, &paint_jobs, &screen_descriptor);
2158 } self.cell_renderer
2162 .queue()
2163 .submit(std::iter::once(encoder.finish()));
2164
2165 for id in &egui_output.textures_delta.free {
2167 self.egui_renderer.free_texture(id);
2168 }
2169
2170 Ok(())
2171 }
2172
2173 pub fn size(&self) -> PhysicalSize<u32> {
2175 self.size
2176 }
2177
2178 pub fn grid_size(&self) -> (usize, usize) {
2180 self.cell_renderer.grid_size()
2181 }
2182
2183 pub fn cell_width(&self) -> f32 {
2185 self.cell_renderer.cell_width()
2186 }
2187
2188 pub fn cell_height(&self) -> f32 {
2190 self.cell_renderer.cell_height()
2191 }
2192
2193 pub fn window_padding(&self) -> f32 {
2195 self.cell_renderer.window_padding()
2196 }
2197
2198 pub fn content_offset_y(&self) -> f32 {
2200 self.cell_renderer.content_offset_y()
2201 }
2202
2203 pub fn scale_factor(&self) -> f32 {
2205 self.cell_renderer.scale_factor
2206 }
2207
2208 pub fn set_content_offset_y(&mut self, logical_offset: f32) -> Option<(usize, usize)> {
2214 let physical_offset = logical_offset * self.cell_renderer.scale_factor;
2216 let result = self.cell_renderer.set_content_offset_y(physical_offset);
2217 self.graphics_renderer.set_content_offset_y(physical_offset);
2219 if let Some(ref mut custom_shader) = self.custom_shader_renderer {
2221 custom_shader.set_content_offset_y(physical_offset);
2222 }
2223 if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
2225 cursor_shader.set_content_offset_y(physical_offset);
2226 }
2227 if result.is_some() {
2228 self.dirty = true;
2229 }
2230 result
2231 }
2232
2233 pub fn content_offset_x(&self) -> f32 {
2235 self.cell_renderer.content_offset_x()
2236 }
2237
2238 pub fn set_content_offset_x(&mut self, logical_offset: f32) -> Option<(usize, usize)> {
2241 let physical_offset = logical_offset * self.cell_renderer.scale_factor;
2242 let result = self.cell_renderer.set_content_offset_x(physical_offset);
2243 self.graphics_renderer.set_content_offset_x(physical_offset);
2244 if let Some(ref mut custom_shader) = self.custom_shader_renderer {
2245 custom_shader.set_content_offset_x(physical_offset);
2246 }
2247 if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
2248 cursor_shader.set_content_offset_x(physical_offset);
2249 }
2250 if result.is_some() {
2251 self.dirty = true;
2252 }
2253 result
2254 }
2255
2256 pub fn content_inset_bottom(&self) -> f32 {
2258 self.cell_renderer.content_inset_bottom()
2259 }
2260
2261 pub fn content_inset_right(&self) -> f32 {
2263 self.cell_renderer.content_inset_right()
2264 }
2265
2266 pub fn set_content_inset_bottom(&mut self, logical_inset: f32) -> Option<(usize, usize)> {
2269 let physical_inset = logical_inset * self.cell_renderer.scale_factor;
2270 let result = self.cell_renderer.set_content_inset_bottom(physical_inset);
2271 if result.is_some() {
2272 self.dirty = true;
2273 }
2274 result
2275 }
2276
2277 pub fn set_content_inset_right(&mut self, logical_inset: f32) -> Option<(usize, usize)> {
2280 let physical_inset = logical_inset * self.cell_renderer.scale_factor;
2281 let result = self.cell_renderer.set_content_inset_right(physical_inset);
2282
2283 if let Some(ref mut custom_shader) = self.custom_shader_renderer {
2285 custom_shader.set_content_inset_right(physical_inset);
2286 }
2287 if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
2289 cursor_shader.set_content_inset_right(physical_inset);
2290 }
2291
2292 if result.is_some() {
2293 self.dirty = true;
2294 }
2295 result
2296 }
2297
2298 pub fn set_egui_bottom_inset(&mut self, logical_inset: f32) -> Option<(usize, usize)> {
2304 let physical_inset = logical_inset * self.cell_renderer.scale_factor;
2305 if (self.cell_renderer.egui_bottom_inset - physical_inset).abs() > f32::EPSILON {
2306 self.cell_renderer.egui_bottom_inset = physical_inset;
2307 let (w, h) = (
2308 self.cell_renderer.config.width,
2309 self.cell_renderer.config.height,
2310 );
2311 return Some(self.cell_renderer.resize(w, h));
2312 }
2313 None
2314 }
2315
2316 pub fn set_egui_right_inset(&mut self, logical_inset: f32) {
2322 let physical_inset = logical_inset * self.cell_renderer.scale_factor;
2323 self.cell_renderer.egui_right_inset = physical_inset;
2324 }
2325
2326 pub fn scrollbar_contains_point(&self, x: f32, y: f32) -> bool {
2332 self.cell_renderer.scrollbar_contains_point(x, y)
2333 }
2334
2335 pub fn scrollbar_thumb_bounds(&self) -> Option<(f32, f32)> {
2337 self.cell_renderer.scrollbar_thumb_bounds()
2338 }
2339
2340 pub fn scrollbar_track_contains_x(&self, x: f32) -> bool {
2342 self.cell_renderer.scrollbar_track_contains_x(x)
2343 }
2344
2345 pub fn scrollbar_mouse_y_to_scroll_offset(&self, mouse_y: f32) -> Option<usize> {
2353 self.cell_renderer
2354 .scrollbar_mouse_y_to_scroll_offset(mouse_y)
2355 }
2356
2357 pub fn scrollbar_mark_at_position(
2367 &self,
2368 mouse_x: f32,
2369 mouse_y: f32,
2370 tolerance: f32,
2371 ) -> Option<&par_term_config::ScrollbackMark> {
2372 self.cell_renderer
2373 .scrollbar_mark_at_position(mouse_x, mouse_y, tolerance)
2374 }
2375
2376 #[allow(dead_code)]
2378 pub fn is_dirty(&self) -> bool {
2379 self.dirty
2380 }
2381
2382 #[allow(dead_code)]
2384 pub fn mark_dirty(&mut self) {
2385 self.dirty = true;
2386 }
2387
2388 #[allow(dead_code)]
2390 #[allow(dead_code)]
2391 pub fn render_debug_overlay(&mut self, text: &str) {
2392 self.debug_text = Some(text.to_string());
2393 self.dirty = true; }
2395
2396 pub fn reconfigure_surface(&mut self) {
2399 self.cell_renderer.reconfigure_surface();
2400 self.dirty = true;
2401 }
2402
2403 pub fn is_vsync_mode_supported(&self, mode: par_term_config::VsyncMode) -> bool {
2405 self.cell_renderer.is_vsync_mode_supported(mode)
2406 }
2407
2408 pub fn update_vsync_mode(
2411 &mut self,
2412 mode: par_term_config::VsyncMode,
2413 ) -> (par_term_config::VsyncMode, bool) {
2414 let result = self.cell_renderer.update_vsync_mode(mode);
2415 if result.1 {
2416 self.dirty = true;
2417 }
2418 result
2419 }
2420
2421 #[allow(dead_code)]
2423 pub fn current_vsync_mode(&self) -> par_term_config::VsyncMode {
2424 self.cell_renderer.current_vsync_mode()
2425 }
2426
2427 pub fn clear_glyph_cache(&mut self) {
2430 self.cell_renderer.clear_glyph_cache();
2431 self.dirty = true;
2432 }
2433
2434 pub fn update_font_antialias(&mut self, enabled: bool) -> bool {
2437 let changed = self.cell_renderer.update_font_antialias(enabled);
2438 if changed {
2439 self.dirty = true;
2440 }
2441 changed
2442 }
2443
2444 pub fn update_font_hinting(&mut self, enabled: bool) -> bool {
2447 let changed = self.cell_renderer.update_font_hinting(enabled);
2448 if changed {
2449 self.dirty = true;
2450 }
2451 changed
2452 }
2453
2454 pub fn update_font_thin_strokes(&mut self, mode: par_term_config::ThinStrokesMode) -> bool {
2457 let changed = self.cell_renderer.update_font_thin_strokes(mode);
2458 if changed {
2459 self.dirty = true;
2460 }
2461 changed
2462 }
2463
2464 pub fn update_minimum_contrast(&mut self, ratio: f32) -> bool {
2467 let changed = self.cell_renderer.update_minimum_contrast(ratio);
2468 if changed {
2469 self.dirty = true;
2470 }
2471 changed
2472 }
2473
2474 pub fn pause_shader_animations(&mut self) {
2477 if let Some(ref mut custom_shader) = self.custom_shader_renderer {
2478 custom_shader.set_animation_enabled(false);
2479 }
2480 if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
2481 cursor_shader.set_animation_enabled(false);
2482 }
2483 log::info!("[SHADER] Shader animations paused");
2484 }
2485
2486 pub fn resume_shader_animations(
2489 &mut self,
2490 custom_shader_animation: bool,
2491 cursor_shader_animation: bool,
2492 ) {
2493 if let Some(ref mut custom_shader) = self.custom_shader_renderer {
2494 custom_shader.set_animation_enabled(custom_shader_animation);
2495 }
2496 if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
2497 cursor_shader.set_animation_enabled(cursor_shader_animation);
2498 }
2499 self.dirty = true;
2500 log::info!(
2501 "[SHADER] Shader animations resumed (custom: {}, cursor: {})",
2502 custom_shader_animation,
2503 cursor_shader_animation
2504 );
2505 }
2506
2507 pub fn take_screenshot(&mut self) -> Result<image::RgbaImage> {
2512 log::info!(
2513 "take_screenshot: Starting screenshot capture ({}x{})",
2514 self.size.width,
2515 self.size.height
2516 );
2517
2518 let width = self.size.width;
2519 let height = self.size.height;
2520 let format = self.cell_renderer.surface_format();
2522 log::info!("take_screenshot: Using texture format {:?}", format);
2523
2524 let screenshot_texture =
2526 self.cell_renderer
2527 .device()
2528 .create_texture(&wgpu::TextureDescriptor {
2529 label: Some("screenshot texture"),
2530 size: wgpu::Extent3d {
2531 width,
2532 height,
2533 depth_or_array_layers: 1,
2534 },
2535 mip_level_count: 1,
2536 sample_count: 1,
2537 dimension: wgpu::TextureDimension::D2,
2538 format,
2539 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
2540 view_formats: &[],
2541 });
2542
2543 let screenshot_view =
2544 screenshot_texture.create_view(&wgpu::TextureViewDescriptor::default());
2545
2546 log::info!("take_screenshot: Rendering composited frame...");
2548
2549 let has_custom_shader = self.custom_shader_renderer.is_some();
2551 let use_cursor_shader =
2552 self.cursor_shader_renderer.is_some() && !self.cursor_shader_disabled_for_alt_screen;
2553
2554 if has_custom_shader {
2555 let intermediate_view = self
2557 .custom_shader_renderer
2558 .as_ref()
2559 .unwrap()
2560 .intermediate_texture_view()
2561 .clone();
2562 self.cell_renderer
2563 .render_to_texture(&intermediate_view, true)?;
2564
2565 if use_cursor_shader {
2566 let cursor_intermediate = self
2568 .cursor_shader_renderer
2569 .as_ref()
2570 .unwrap()
2571 .intermediate_texture_view()
2572 .clone();
2573 self.custom_shader_renderer.as_mut().unwrap().render(
2574 self.cell_renderer.device(),
2575 self.cell_renderer.queue(),
2576 &cursor_intermediate,
2577 false,
2578 )?;
2579 self.cursor_shader_renderer.as_mut().unwrap().render(
2581 self.cell_renderer.device(),
2582 self.cell_renderer.queue(),
2583 &screenshot_view,
2584 true,
2585 )?;
2586 } else {
2587 self.custom_shader_renderer.as_mut().unwrap().render(
2589 self.cell_renderer.device(),
2590 self.cell_renderer.queue(),
2591 &screenshot_view,
2592 true,
2593 )?;
2594 }
2595 } else if use_cursor_shader {
2596 let cursor_intermediate = self
2598 .cursor_shader_renderer
2599 .as_ref()
2600 .unwrap()
2601 .intermediate_texture_view()
2602 .clone();
2603 self.cell_renderer
2604 .render_to_texture(&cursor_intermediate, true)?;
2605 self.cursor_shader_renderer.as_mut().unwrap().render(
2607 self.cell_renderer.device(),
2608 self.cell_renderer.queue(),
2609 &screenshot_view,
2610 true,
2611 )?;
2612 } else {
2613 self.cell_renderer.render_to_view(&screenshot_view)?;
2615 }
2616
2617 log::info!("take_screenshot: Render complete");
2618
2619 let device = self.cell_renderer.device();
2621 let queue = self.cell_renderer.queue();
2622
2623 let bytes_per_pixel = 4u32;
2625 let unpadded_bytes_per_row = width * bytes_per_pixel;
2626 let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
2628 let padded_bytes_per_row = unpadded_bytes_per_row.div_ceil(align) * align;
2629 let buffer_size = (padded_bytes_per_row * height) as u64;
2630
2631 let output_buffer = device.create_buffer(&wgpu::BufferDescriptor {
2632 label: Some("screenshot buffer"),
2633 size: buffer_size,
2634 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
2635 mapped_at_creation: false,
2636 });
2637
2638 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
2640 label: Some("screenshot encoder"),
2641 });
2642
2643 encoder.copy_texture_to_buffer(
2644 wgpu::TexelCopyTextureInfo {
2645 texture: &screenshot_texture,
2646 mip_level: 0,
2647 origin: wgpu::Origin3d::ZERO,
2648 aspect: wgpu::TextureAspect::All,
2649 },
2650 wgpu::TexelCopyBufferInfo {
2651 buffer: &output_buffer,
2652 layout: wgpu::TexelCopyBufferLayout {
2653 offset: 0,
2654 bytes_per_row: Some(padded_bytes_per_row),
2655 rows_per_image: Some(height),
2656 },
2657 },
2658 wgpu::Extent3d {
2659 width,
2660 height,
2661 depth_or_array_layers: 1,
2662 },
2663 );
2664
2665 queue.submit(std::iter::once(encoder.finish()));
2666 log::info!("take_screenshot: Texture copy submitted");
2667
2668 let buffer_slice = output_buffer.slice(..);
2670 let (tx, rx) = std::sync::mpsc::channel();
2671 buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
2672 let _ = tx.send(result);
2673 });
2674
2675 log::info!("take_screenshot: Waiting for GPU...");
2677 let _ = device.poll(wgpu::PollType::wait_indefinitely());
2678 log::info!("take_screenshot: GPU poll complete, waiting for buffer map...");
2679 rx.recv()
2680 .map_err(|e| anyhow::anyhow!("Failed to receive map result: {}", e))?
2681 .map_err(|e| anyhow::anyhow!("Failed to map buffer: {:?}", e))?;
2682 log::info!("take_screenshot: Buffer mapped successfully");
2683
2684 let data = buffer_slice.get_mapped_range();
2686 let mut pixels = Vec::with_capacity((width * height * 4) as usize);
2687
2688 let is_bgra = matches!(
2690 format,
2691 wgpu::TextureFormat::Bgra8Unorm | wgpu::TextureFormat::Bgra8UnormSrgb
2692 );
2693
2694 for y in 0..height {
2696 let row_start = (y * padded_bytes_per_row) as usize;
2697 let row_end = row_start + (width * bytes_per_pixel) as usize;
2698 let row = &data[row_start..row_end];
2699
2700 if is_bgra {
2701 for chunk in row.chunks(4) {
2703 pixels.push(chunk[2]); pixels.push(chunk[1]); pixels.push(chunk[0]); pixels.push(chunk[3]); }
2708 } else {
2709 pixels.extend_from_slice(row);
2711 }
2712 }
2713
2714 drop(data);
2715 output_buffer.unmap();
2716
2717 image::RgbaImage::from_raw(width, height, pixels)
2719 .ok_or_else(|| anyhow::anyhow!("Failed to create image from pixel data"))
2720 }
2721}