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_link_underline_style(&mut self, style: par_term_config::LinkUnderlineStyle) {
748 self.cell_renderer.set_link_underline_style(style);
749 self.dirty = true;
750 }
751
752 pub fn set_cursor_shader_disabled_for_alt_screen(&mut self, disabled: bool) {
757 if self.cursor_shader_disabled_for_alt_screen != disabled {
758 log::debug!("[cursor-shader] Alt-screen disable set to {}", disabled);
759 self.cursor_shader_disabled_for_alt_screen = disabled;
760 } else {
761 self.cursor_shader_disabled_for_alt_screen = disabled;
762 }
763 }
764
765 #[allow(dead_code)]
769 pub fn update_window_padding(&mut self, logical_padding: f32) -> Option<(usize, usize)> {
770 let physical_padding = logical_padding * self.cell_renderer.scale_factor;
771 let result = self.cell_renderer.update_window_padding(physical_padding);
772 self.graphics_renderer.update_cell_dimensions(
774 self.cell_renderer.cell_width(),
775 self.cell_renderer.cell_height(),
776 physical_padding,
777 );
778 if let Some(ref mut custom_shader) = self.custom_shader_renderer {
780 custom_shader.update_cell_dimensions(
781 self.cell_renderer.cell_width(),
782 self.cell_renderer.cell_height(),
783 physical_padding,
784 );
785 }
786 if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
788 cursor_shader.update_cell_dimensions(
789 self.cell_renderer.cell_width(),
790 self.cell_renderer.cell_height(),
791 physical_padding,
792 );
793 }
794 self.dirty = true;
795 result
796 }
797
798 #[allow(dead_code)]
800 pub fn set_background_image_enabled(
801 &mut self,
802 enabled: bool,
803 path: Option<&str>,
804 mode: par_term_config::BackgroundImageMode,
805 opacity: f32,
806 ) {
807 let path = if enabled { path } else { None };
808 self.cell_renderer.set_background_image(path, mode, opacity);
809
810 self.sync_background_texture_to_shader();
812
813 self.dirty = true;
814 }
815
816 pub fn set_background(
820 &mut self,
821 mode: par_term_config::BackgroundMode,
822 color: [u8; 3],
823 image_path: Option<&str>,
824 image_mode: par_term_config::BackgroundImageMode,
825 image_opacity: f32,
826 image_enabled: bool,
827 ) {
828 self.cell_renderer.set_background(
829 mode,
830 color,
831 image_path,
832 image_mode,
833 image_opacity,
834 image_enabled,
835 );
836
837 self.sync_background_texture_to_shader();
839
840 let is_solid_color = matches!(mode, par_term_config::BackgroundMode::Color);
842 let is_image_mode = matches!(mode, par_term_config::BackgroundMode::Image);
843 let normalized_color = [
844 color[0] as f32 / 255.0,
845 color[1] as f32 / 255.0,
846 color[2] as f32 / 255.0,
847 ];
848
849 if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
851 let has_background_shader = self.custom_shader_renderer.is_some();
854
855 if has_background_shader {
856 cursor_shader.set_background_color([0.0, 0.0, 0.0], false);
858 cursor_shader.set_background_texture(self.cell_renderer.device(), None);
859 cursor_shader.update_use_background_as_channel0(self.cell_renderer.device(), false);
860 } else {
861 cursor_shader.set_background_color(normalized_color, is_solid_color);
862
863 if is_image_mode && image_enabled {
865 let bg_texture = self.cell_renderer.get_background_as_channel_texture();
866 cursor_shader.set_background_texture(self.cell_renderer.device(), bg_texture);
867 cursor_shader
868 .update_use_background_as_channel0(self.cell_renderer.device(), true);
869 } else {
870 cursor_shader.set_background_texture(self.cell_renderer.device(), None);
872 cursor_shader
873 .update_use_background_as_channel0(self.cell_renderer.device(), false);
874 }
875 }
876 }
877
878 if let Some(ref mut custom_shader) = self.custom_shader_renderer {
883 custom_shader.set_background_color(normalized_color, false);
884 }
885
886 self.dirty = true;
887 }
888
889 pub fn update_scrollbar_appearance(
892 &mut self,
893 logical_width: f32,
894 thumb_color: [f32; 4],
895 track_color: [f32; 4],
896 ) {
897 let physical_width = logical_width * self.cell_renderer.scale_factor;
898 self.cell_renderer
899 .update_scrollbar_appearance(physical_width, thumb_color, track_color);
900 self.dirty = true;
901 }
902
903 #[allow(dead_code)]
905 pub fn update_scrollbar_position(&mut self, position: &str) {
906 self.cell_renderer.update_scrollbar_position(position);
907 self.dirty = true;
908 }
909
910 #[allow(dead_code)]
912 pub fn update_background_image_opacity(&mut self, opacity: f32) {
913 self.cell_renderer.update_background_image_opacity(opacity);
914 self.dirty = true;
915 }
916
917 pub fn load_pane_background(&mut self, path: &str) -> anyhow::Result<bool> {
920 self.cell_renderer.load_pane_background(path)
921 }
922
923 pub fn update_image_scaling_mode(&mut self, scaling_mode: par_term_config::ImageScalingMode) {
928 self.graphics_renderer
929 .update_scaling_mode(self.cell_renderer.device(), scaling_mode);
930 self.dirty = true;
931 }
932
933 pub fn update_image_preserve_aspect_ratio(&mut self, preserve: bool) {
935 self.graphics_renderer.set_preserve_aspect_ratio(preserve);
936 self.dirty = true;
937 }
938
939 pub fn needs_continuous_render(&self) -> bool {
944 let custom_needs = self
945 .custom_shader_renderer
946 .as_ref()
947 .is_some_and(|r| r.animation_enabled() || r.cursor_needs_animation());
948 let cursor_needs = self
949 .cursor_shader_renderer
950 .as_ref()
951 .is_some_and(|r| r.animation_enabled() || r.cursor_needs_animation());
952 custom_needs || cursor_needs
953 }
954
955 pub fn render(
958 &mut self,
959 egui_data: Option<(egui::FullOutput, &egui::Context)>,
960 force_egui_opaque: bool,
961 show_scrollbar: bool,
962 pane_background: Option<&par_term_config::PaneBackground>,
963 ) -> Result<bool> {
964 let force_render = self.needs_continuous_render();
966
967 if !self.dirty && egui_data.is_none() && !force_render {
968 return Ok(false);
970 }
971
972 let has_custom_shader = self.custom_shader_renderer.is_some();
974 let use_cursor_shader =
976 self.cursor_shader_renderer.is_some() && !self.cursor_shader_disabled_for_alt_screen;
977
978 let t1 = std::time::Instant::now();
980 let surface_texture = if has_custom_shader {
981 self.cell_renderer.render_to_texture(
990 self.custom_shader_renderer
991 .as_ref()
992 .unwrap()
993 .intermediate_texture_view(),
994 true, )?
996 } else if use_cursor_shader {
997 self.cell_renderer.render_to_texture(
1001 self.cursor_shader_renderer
1002 .as_ref()
1003 .unwrap()
1004 .intermediate_texture_view(),
1005 true, )?
1007 } else {
1008 self.cell_renderer.render(show_scrollbar, pane_background)?
1011 };
1012 let cell_render_time = t1.elapsed();
1013
1014 let t_custom = std::time::Instant::now();
1016 let custom_shader_time = if let Some(ref mut custom_shader) = self.custom_shader_renderer {
1017 if use_cursor_shader {
1018 custom_shader.render(
1021 self.cell_renderer.device(),
1022 self.cell_renderer.queue(),
1023 self.cursor_shader_renderer
1024 .as_ref()
1025 .unwrap()
1026 .intermediate_texture_view(),
1027 false, )?;
1029 } else {
1030 let surface_view = surface_texture
1033 .texture
1034 .create_view(&wgpu::TextureViewDescriptor::default());
1035 custom_shader.render(
1036 self.cell_renderer.device(),
1037 self.cell_renderer.queue(),
1038 &surface_view,
1039 true, )?;
1041 }
1042 t_custom.elapsed()
1043 } else {
1044 std::time::Duration::ZERO
1045 };
1046
1047 let t_cursor = std::time::Instant::now();
1049 let cursor_shader_time = if use_cursor_shader {
1050 log::trace!("Rendering cursor shader");
1051 let cursor_shader = self.cursor_shader_renderer.as_mut().unwrap();
1052 let surface_view = surface_texture
1053 .texture
1054 .create_view(&wgpu::TextureViewDescriptor::default());
1055
1056 cursor_shader.render(
1057 self.cell_renderer.device(),
1058 self.cell_renderer.queue(),
1059 &surface_view,
1060 true, )?;
1062 t_cursor.elapsed()
1063 } else {
1064 if self.cursor_shader_disabled_for_alt_screen {
1065 log::trace!("Skipping cursor shader - alt screen active");
1066 }
1067 std::time::Duration::ZERO
1068 };
1069
1070 let t2 = std::time::Instant::now();
1072 if !self.sixel_graphics.is_empty() {
1073 self.render_sixel_graphics(&surface_texture)?;
1074 }
1075 let sixel_render_time = t2.elapsed();
1076
1077 self.cell_renderer
1081 .render_overlays(&surface_texture, show_scrollbar)?;
1082
1083 let t3 = std::time::Instant::now();
1085 if let Some((egui_output, egui_ctx)) = egui_data {
1086 self.render_egui(&surface_texture, egui_output, egui_ctx, force_egui_opaque)?;
1087 }
1088 let egui_render_time = t3.elapsed();
1089
1090 let t4 = std::time::Instant::now();
1092 surface_texture.present();
1093 let present_time = t4.elapsed();
1094
1095 let total = cell_render_time
1097 + custom_shader_time
1098 + cursor_shader_time
1099 + sixel_render_time
1100 + egui_render_time
1101 + present_time;
1102 if present_time.as_millis() > 10 || total.as_millis() > 10 {
1103 log::info!(
1104 "[RENDER] RENDER_BREAKDOWN: CellRender={:.2}ms BgShader={:.2}ms CursorShader={:.2}ms Sixel={:.2}ms Egui={:.2}ms PRESENT={:.2}ms Total={:.2}ms",
1105 cell_render_time.as_secs_f64() * 1000.0,
1106 custom_shader_time.as_secs_f64() * 1000.0,
1107 cursor_shader_time.as_secs_f64() * 1000.0,
1108 sixel_render_time.as_secs_f64() * 1000.0,
1109 egui_render_time.as_secs_f64() * 1000.0,
1110 present_time.as_secs_f64() * 1000.0,
1111 total.as_secs_f64() * 1000.0
1112 );
1113 }
1114
1115 self.dirty = false;
1117
1118 Ok(true)
1119 }
1120
1121 #[allow(dead_code)]
1134 pub fn render_panes(
1135 &mut self,
1136 panes: &[PaneRenderInfo<'_>],
1137 egui_data: Option<(egui::FullOutput, &egui::Context)>,
1138 force_egui_opaque: bool,
1139 ) -> Result<bool> {
1140 let force_render = self.needs_continuous_render();
1142 if !self.dirty && egui_data.is_none() && !force_render {
1143 return Ok(false);
1144 }
1145
1146 let surface_texture = self.cell_renderer.surface.get_current_texture()?;
1148 let surface_view = surface_texture
1149 .texture
1150 .create_view(&wgpu::TextureViewDescriptor::default());
1151
1152 {
1154 let mut encoder = self.cell_renderer.device().create_command_encoder(
1155 &wgpu::CommandEncoderDescriptor {
1156 label: Some("pane clear encoder"),
1157 },
1158 );
1159
1160 let opacity = self.cell_renderer.window_opacity as f64;
1161 let clear_color = if self.cell_renderer.bg_is_solid_color {
1162 wgpu::Color {
1163 r: self.cell_renderer.solid_bg_color[0] as f64 * opacity,
1164 g: self.cell_renderer.solid_bg_color[1] as f64 * opacity,
1165 b: self.cell_renderer.solid_bg_color[2] as f64 * opacity,
1166 a: opacity,
1167 }
1168 } else {
1169 wgpu::Color {
1170 r: self.cell_renderer.background_color[0] as f64 * opacity,
1171 g: self.cell_renderer.background_color[1] as f64 * opacity,
1172 b: self.cell_renderer.background_color[2] as f64 * opacity,
1173 a: opacity,
1174 }
1175 };
1176
1177 {
1178 let _clear_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1179 label: Some("surface clear pass"),
1180 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1181 view: &surface_view,
1182 resolve_target: None,
1183 ops: wgpu::Operations {
1184 load: wgpu::LoadOp::Clear(clear_color),
1185 store: wgpu::StoreOp::Store,
1186 },
1187 depth_slice: None,
1188 })],
1189 depth_stencil_attachment: None,
1190 timestamp_writes: None,
1191 occlusion_query_set: None,
1192 });
1193 }
1194
1195 self.cell_renderer
1196 .queue()
1197 .submit(std::iter::once(encoder.finish()));
1198 }
1199
1200 let has_background_image = self
1202 .cell_renderer
1203 .render_background_only(&surface_view, false)?;
1204
1205 for pane in panes {
1207 let separator_marks = compute_visible_separator_marks(
1208 &pane.marks,
1209 pane.scrollback_len,
1210 pane.scroll_offset,
1211 pane.grid_size.1,
1212 );
1213 self.cell_renderer.render_pane_to_view(
1214 &surface_view,
1215 &pane.viewport,
1216 pane.cells,
1217 pane.grid_size.0,
1218 pane.grid_size.1,
1219 pane.cursor_pos,
1220 pane.cursor_opacity,
1221 pane.show_scrollbar,
1222 false, has_background_image, &separator_marks,
1225 pane.background.as_ref(),
1226 )?;
1227 }
1228
1229 if let Some((egui_output, egui_ctx)) = egui_data {
1231 self.render_egui(&surface_texture, egui_output, egui_ctx, force_egui_opaque)?;
1232 }
1233
1234 surface_texture.present();
1236
1237 self.dirty = false;
1238 Ok(true)
1239 }
1240
1241 #[allow(dead_code, clippy::too_many_arguments)]
1263 pub fn render_split_panes(
1264 &mut self,
1265 panes: &[PaneRenderInfo<'_>],
1266 dividers: &[DividerRenderInfo],
1267 pane_titles: &[PaneTitleInfo],
1268 focused_viewport: Option<&PaneViewport>,
1269 divider_settings: &PaneDividerSettings,
1270 egui_data: Option<(egui::FullOutput, &egui::Context)>,
1271 force_egui_opaque: bool,
1272 ) -> Result<bool> {
1273 let force_render = self.needs_continuous_render();
1275 if !self.dirty && egui_data.is_none() && !force_render {
1276 return Ok(false);
1277 }
1278
1279 let has_custom_shader = self.custom_shader_renderer.is_some();
1280
1281 for pane in panes.iter() {
1283 if let Some(ref bg) = pane.background
1284 && let Some(ref path) = bg.image_path
1285 && let Err(e) = self.cell_renderer.load_pane_background(path)
1286 {
1287 log::error!("Failed to load pane background '{}': {}", path, e);
1288 }
1289 }
1290
1291 let surface_texture = self.cell_renderer.surface.get_current_texture()?;
1293 let surface_view = surface_texture
1294 .texture
1295 .create_view(&wgpu::TextureViewDescriptor::default());
1296
1297 let opacity = self.cell_renderer.window_opacity as f64;
1299 let clear_color = if self.cell_renderer.bg_is_solid_color {
1300 wgpu::Color {
1301 r: self.cell_renderer.solid_bg_color[0] as f64 * opacity,
1302 g: self.cell_renderer.solid_bg_color[1] as f64 * opacity,
1303 b: self.cell_renderer.solid_bg_color[2] as f64 * opacity,
1304 a: opacity,
1305 }
1306 } else {
1307 wgpu::Color {
1308 r: self.cell_renderer.background_color[0] as f64 * opacity,
1309 g: self.cell_renderer.background_color[1] as f64 * opacity,
1310 b: self.cell_renderer.background_color[2] as f64 * opacity,
1311 a: opacity,
1312 }
1313 };
1314
1315 if let Some(ref mut custom_shader) = self.custom_shader_renderer {
1318 custom_shader.clear_intermediate_texture(
1321 self.cell_renderer.device(),
1322 self.cell_renderer.queue(),
1323 );
1324
1325 custom_shader.render_with_clear_color(
1328 self.cell_renderer.device(),
1329 self.cell_renderer.queue(),
1330 &surface_view,
1331 false, clear_color,
1333 )?;
1334 } else {
1335 let mut encoder = self.cell_renderer.device().create_command_encoder(
1337 &wgpu::CommandEncoderDescriptor {
1338 label: Some("split pane clear encoder"),
1339 },
1340 );
1341
1342 {
1343 let _clear_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1344 label: Some("surface clear pass"),
1345 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1346 view: &surface_view,
1347 resolve_target: None,
1348 ops: wgpu::Operations {
1349 load: wgpu::LoadOp::Clear(clear_color),
1350 store: wgpu::StoreOp::Store,
1351 },
1352 depth_slice: None,
1353 })],
1354 depth_stencil_attachment: None,
1355 timestamp_writes: None,
1356 occlusion_query_set: None,
1357 });
1358 }
1359
1360 self.cell_renderer
1361 .queue()
1362 .submit(std::iter::once(encoder.finish()));
1363 }
1364
1365 let any_pane_has_background = panes.iter().any(|p| p.background.is_some());
1370 let has_background_image = if !has_custom_shader && !any_pane_has_background {
1371 self.cell_renderer
1372 .render_background_only(&surface_view, false)?
1373 } else {
1374 false
1375 };
1376
1377 for pane in panes {
1379 let separator_marks = compute_visible_separator_marks(
1380 &pane.marks,
1381 pane.scrollback_len,
1382 pane.scroll_offset,
1383 pane.grid_size.1,
1384 );
1385 self.cell_renderer.render_pane_to_view(
1386 &surface_view,
1387 &pane.viewport,
1388 pane.cells,
1389 pane.grid_size.0,
1390 pane.grid_size.1,
1391 pane.cursor_pos,
1392 pane.cursor_opacity,
1393 pane.show_scrollbar,
1394 false, has_background_image || has_custom_shader, &separator_marks,
1397 pane.background.as_ref(),
1398 )?;
1399 }
1400
1401 if !dividers.is_empty() {
1403 self.render_dividers(&surface_view, dividers, divider_settings)?;
1404 }
1405
1406 if !pane_titles.is_empty() {
1408 self.render_pane_titles(&surface_view, pane_titles)?;
1409 }
1410
1411 if panes.len() > 1
1413 && let Some(viewport) = focused_viewport
1414 {
1415 self.render_focus_indicator(&surface_view, viewport, divider_settings)?;
1416 }
1417
1418 if let Some((egui_output, egui_ctx)) = egui_data {
1420 self.render_egui(&surface_texture, egui_output, egui_ctx, force_egui_opaque)?;
1421 }
1422
1423 surface_texture.present();
1425
1426 self.dirty = false;
1427 Ok(true)
1428 }
1429
1430 #[allow(dead_code)]
1439 pub fn render_dividers(
1440 &mut self,
1441 surface_view: &wgpu::TextureView,
1442 dividers: &[DividerRenderInfo],
1443 settings: &PaneDividerSettings,
1444 ) -> Result<()> {
1445 if dividers.is_empty() {
1446 return Ok(());
1447 }
1448
1449 let mut instances = Vec::with_capacity(dividers.len() * 3); let w = self.size.width as f32;
1454 let h = self.size.height as f32;
1455
1456 for divider in dividers {
1457 let color = if divider.hovered {
1458 settings.hover_color
1459 } else {
1460 settings.divider_color
1461 };
1462
1463 use par_term_config::DividerStyle;
1464 match settings.divider_style {
1465 DividerStyle::Solid => {
1466 let x_ndc = divider.x / w * 2.0 - 1.0;
1467 let y_ndc = 1.0 - (divider.y / h * 2.0);
1468 let w_ndc = divider.width / w * 2.0;
1469 let h_ndc = divider.height / h * 2.0;
1470
1471 instances.push(crate::cell_renderer::types::BackgroundInstance {
1472 position: [x_ndc, y_ndc],
1473 size: [w_ndc, h_ndc],
1474 color: [color[0], color[1], color[2], 1.0],
1475 });
1476 }
1477 DividerStyle::Double => {
1478 let is_horizontal = divider.width > divider.height;
1480 let thickness = if is_horizontal {
1481 divider.height
1482 } else {
1483 divider.width
1484 };
1485
1486 if thickness >= 4.0 {
1487 if is_horizontal {
1489 instances.push(crate::cell_renderer::types::BackgroundInstance {
1491 position: [divider.x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1492 size: [divider.width / w * 2.0, 1.0 / h * 2.0],
1493 color: [color[0], color[1], color[2], 1.0],
1494 });
1495 let bottom_y = divider.y + divider.height - 1.0;
1497 instances.push(crate::cell_renderer::types::BackgroundInstance {
1498 position: [divider.x / w * 2.0 - 1.0, 1.0 - (bottom_y / h * 2.0)],
1499 size: [divider.width / w * 2.0, 1.0 / h * 2.0],
1500 color: [color[0], color[1], color[2], 1.0],
1501 });
1502 } else {
1503 instances.push(crate::cell_renderer::types::BackgroundInstance {
1505 position: [divider.x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1506 size: [1.0 / w * 2.0, divider.height / h * 2.0],
1507 color: [color[0], color[1], color[2], 1.0],
1508 });
1509 let right_x = divider.x + divider.width - 1.0;
1511 instances.push(crate::cell_renderer::types::BackgroundInstance {
1512 position: [right_x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1513 size: [1.0 / w * 2.0, divider.height / h * 2.0],
1514 color: [color[0], color[1], color[2], 1.0],
1515 });
1516 }
1517 } else {
1518 if is_horizontal {
1521 let center_y = divider.y + (divider.height - 1.0) / 2.0;
1522 instances.push(crate::cell_renderer::types::BackgroundInstance {
1523 position: [divider.x / w * 2.0 - 1.0, 1.0 - (center_y / h * 2.0)],
1524 size: [divider.width / w * 2.0, 1.0 / h * 2.0],
1525 color: [color[0], color[1], color[2], 1.0],
1526 });
1527 } else {
1528 let center_x = divider.x + (divider.width - 1.0) / 2.0;
1529 instances.push(crate::cell_renderer::types::BackgroundInstance {
1530 position: [center_x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1531 size: [1.0 / w * 2.0, divider.height / h * 2.0],
1532 color: [color[0], color[1], color[2], 1.0],
1533 });
1534 }
1535 }
1536 }
1537 DividerStyle::Dashed => {
1538 let is_horizontal = divider.width > divider.height;
1540 let dash_len: f32 = 6.0;
1541 let gap_len: f32 = 4.0;
1542
1543 if is_horizontal {
1544 let mut x = divider.x;
1545 while x < divider.x + divider.width {
1546 let seg_w = dash_len.min(divider.x + divider.width - x);
1547 instances.push(crate::cell_renderer::types::BackgroundInstance {
1548 position: [x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1549 size: [seg_w / w * 2.0, divider.height / h * 2.0],
1550 color: [color[0], color[1], color[2], 1.0],
1551 });
1552 x += dash_len + gap_len;
1553 }
1554 } else {
1555 let mut y = divider.y;
1556 while y < divider.y + divider.height {
1557 let seg_h = dash_len.min(divider.y + divider.height - y);
1558 instances.push(crate::cell_renderer::types::BackgroundInstance {
1559 position: [divider.x / w * 2.0 - 1.0, 1.0 - (y / h * 2.0)],
1560 size: [divider.width / w * 2.0, seg_h / h * 2.0],
1561 color: [color[0], color[1], color[2], 1.0],
1562 });
1563 y += dash_len + gap_len;
1564 }
1565 }
1566 }
1567 DividerStyle::Shadow => {
1568 let is_horizontal = divider.width > divider.height;
1571 let thickness = if is_horizontal {
1572 divider.height
1573 } else {
1574 divider.width
1575 };
1576
1577 let highlight = [
1579 (color[0] + 0.3).min(1.0),
1580 (color[1] + 0.3).min(1.0),
1581 (color[2] + 0.3).min(1.0),
1582 1.0,
1583 ];
1584 let shadow = [(color[0] * 0.3), (color[1] * 0.3), (color[2] * 0.3), 1.0];
1586
1587 if thickness >= 3.0 {
1588 let edge = 1.0_f32;
1590 if is_horizontal {
1591 instances.push(crate::cell_renderer::types::BackgroundInstance {
1593 position: [divider.x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1594 size: [divider.width / w * 2.0, edge / h * 2.0],
1595 color: highlight,
1596 });
1597 let body_y = divider.y + edge;
1599 let body_h = divider.height - edge * 2.0;
1600 if body_h > 0.0 {
1601 instances.push(crate::cell_renderer::types::BackgroundInstance {
1602 position: [divider.x / w * 2.0 - 1.0, 1.0 - (body_y / h * 2.0)],
1603 size: [divider.width / w * 2.0, body_h / h * 2.0],
1604 color: [color[0], color[1], color[2], 1.0],
1605 });
1606 }
1607 let shadow_y = divider.y + divider.height - edge;
1609 instances.push(crate::cell_renderer::types::BackgroundInstance {
1610 position: [divider.x / w * 2.0 - 1.0, 1.0 - (shadow_y / h * 2.0)],
1611 size: [divider.width / w * 2.0, edge / h * 2.0],
1612 color: shadow,
1613 });
1614 } else {
1615 instances.push(crate::cell_renderer::types::BackgroundInstance {
1617 position: [divider.x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1618 size: [edge / w * 2.0, divider.height / h * 2.0],
1619 color: highlight,
1620 });
1621 let body_x = divider.x + edge;
1623 let body_w = divider.width - edge * 2.0;
1624 if body_w > 0.0 {
1625 instances.push(crate::cell_renderer::types::BackgroundInstance {
1626 position: [body_x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1627 size: [body_w / w * 2.0, divider.height / h * 2.0],
1628 color: [color[0], color[1], color[2], 1.0],
1629 });
1630 }
1631 let shadow_x = divider.x + divider.width - edge;
1633 instances.push(crate::cell_renderer::types::BackgroundInstance {
1634 position: [shadow_x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1635 size: [edge / w * 2.0, divider.height / h * 2.0],
1636 color: shadow,
1637 });
1638 }
1639 } else {
1640 if is_horizontal {
1642 let half = (divider.height / 2.0).max(1.0);
1643 instances.push(crate::cell_renderer::types::BackgroundInstance {
1644 position: [divider.x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1645 size: [divider.width / w * 2.0, half / h * 2.0],
1646 color: highlight,
1647 });
1648 let bottom_y = divider.y + half;
1649 let bottom_h = divider.height - half;
1650 if bottom_h > 0.0 {
1651 instances.push(crate::cell_renderer::types::BackgroundInstance {
1652 position: [
1653 divider.x / w * 2.0 - 1.0,
1654 1.0 - (bottom_y / h * 2.0),
1655 ],
1656 size: [divider.width / w * 2.0, bottom_h / h * 2.0],
1657 color: shadow,
1658 });
1659 }
1660 } else {
1661 let half = (divider.width / 2.0).max(1.0);
1662 instances.push(crate::cell_renderer::types::BackgroundInstance {
1663 position: [divider.x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1664 size: [half / w * 2.0, divider.height / h * 2.0],
1665 color: highlight,
1666 });
1667 let right_x = divider.x + half;
1668 let right_w = divider.width - half;
1669 if right_w > 0.0 {
1670 instances.push(crate::cell_renderer::types::BackgroundInstance {
1671 position: [
1672 right_x / w * 2.0 - 1.0,
1673 1.0 - (divider.y / h * 2.0),
1674 ],
1675 size: [right_w / w * 2.0, divider.height / h * 2.0],
1676 color: shadow,
1677 });
1678 }
1679 }
1680 }
1681 }
1682 }
1683 }
1684
1685 self.cell_renderer.queue().write_buffer(
1687 &self.cell_renderer.bg_instance_buffer,
1688 0,
1689 bytemuck::cast_slice(&instances),
1690 );
1691
1692 let mut encoder =
1694 self.cell_renderer
1695 .device()
1696 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1697 label: Some("divider render encoder"),
1698 });
1699
1700 {
1701 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1702 label: Some("divider render pass"),
1703 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1704 view: surface_view,
1705 resolve_target: None,
1706 ops: wgpu::Operations {
1707 load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store,
1709 },
1710 depth_slice: None,
1711 })],
1712 depth_stencil_attachment: None,
1713 timestamp_writes: None,
1714 occlusion_query_set: None,
1715 });
1716
1717 render_pass.set_pipeline(&self.cell_renderer.bg_pipeline);
1718 render_pass.set_vertex_buffer(0, self.cell_renderer.vertex_buffer.slice(..));
1719 render_pass.set_vertex_buffer(1, self.cell_renderer.bg_instance_buffer.slice(..));
1720 render_pass.draw(0..4, 0..instances.len() as u32);
1721 }
1722
1723 self.cell_renderer
1724 .queue()
1725 .submit(std::iter::once(encoder.finish()));
1726 Ok(())
1727 }
1728
1729 #[allow(dead_code)]
1738 pub fn render_focus_indicator(
1739 &mut self,
1740 surface_view: &wgpu::TextureView,
1741 viewport: &PaneViewport,
1742 settings: &PaneDividerSettings,
1743 ) -> Result<()> {
1744 if !settings.show_focus_indicator {
1745 return Ok(());
1746 }
1747
1748 let border_w = settings.focus_width;
1749 let color = [
1750 settings.focus_color[0],
1751 settings.focus_color[1],
1752 settings.focus_color[2],
1753 1.0,
1754 ];
1755
1756 let instances = vec![
1758 crate::cell_renderer::types::BackgroundInstance {
1760 position: [
1761 viewport.x / self.size.width as f32 * 2.0 - 1.0,
1762 1.0 - (viewport.y / self.size.height as f32 * 2.0),
1763 ],
1764 size: [
1765 viewport.width / self.size.width as f32 * 2.0,
1766 border_w / self.size.height as f32 * 2.0,
1767 ],
1768 color,
1769 },
1770 crate::cell_renderer::types::BackgroundInstance {
1772 position: [
1773 viewport.x / self.size.width as f32 * 2.0 - 1.0,
1774 1.0 - ((viewport.y + viewport.height - border_w) / self.size.height as f32
1775 * 2.0),
1776 ],
1777 size: [
1778 viewport.width / self.size.width as f32 * 2.0,
1779 border_w / self.size.height as f32 * 2.0,
1780 ],
1781 color,
1782 },
1783 crate::cell_renderer::types::BackgroundInstance {
1785 position: [
1786 viewport.x / self.size.width as f32 * 2.0 - 1.0,
1787 1.0 - ((viewport.y + border_w) / self.size.height as f32 * 2.0),
1788 ],
1789 size: [
1790 border_w / self.size.width as f32 * 2.0,
1791 (viewport.height - border_w * 2.0) / self.size.height as f32 * 2.0,
1792 ],
1793 color,
1794 },
1795 crate::cell_renderer::types::BackgroundInstance {
1797 position: [
1798 (viewport.x + viewport.width - border_w) / self.size.width as f32 * 2.0 - 1.0,
1799 1.0 - ((viewport.y + border_w) / self.size.height as f32 * 2.0),
1800 ],
1801 size: [
1802 border_w / self.size.width as f32 * 2.0,
1803 (viewport.height - border_w * 2.0) / self.size.height as f32 * 2.0,
1804 ],
1805 color,
1806 },
1807 ];
1808
1809 self.cell_renderer.queue().write_buffer(
1811 &self.cell_renderer.bg_instance_buffer,
1812 0,
1813 bytemuck::cast_slice(&instances),
1814 );
1815
1816 let mut encoder =
1818 self.cell_renderer
1819 .device()
1820 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1821 label: Some("focus indicator encoder"),
1822 });
1823
1824 {
1825 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1826 label: Some("focus indicator pass"),
1827 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1828 view: surface_view,
1829 resolve_target: None,
1830 ops: wgpu::Operations {
1831 load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store,
1833 },
1834 depth_slice: None,
1835 })],
1836 depth_stencil_attachment: None,
1837 timestamp_writes: None,
1838 occlusion_query_set: None,
1839 });
1840
1841 render_pass.set_pipeline(&self.cell_renderer.bg_pipeline);
1842 render_pass.set_vertex_buffer(0, self.cell_renderer.vertex_buffer.slice(..));
1843 render_pass.set_vertex_buffer(1, self.cell_renderer.bg_instance_buffer.slice(..));
1844 render_pass.draw(0..4, 0..instances.len() as u32);
1845 }
1846
1847 self.cell_renderer
1848 .queue()
1849 .submit(std::iter::once(encoder.finish()));
1850 Ok(())
1851 }
1852
1853 #[allow(dead_code)]
1858 pub fn render_pane_titles(
1859 &mut self,
1860 surface_view: &wgpu::TextureView,
1861 titles: &[PaneTitleInfo],
1862 ) -> Result<()> {
1863 if titles.is_empty() {
1864 return Ok(());
1865 }
1866
1867 let width = self.size.width as f32;
1868 let height = self.size.height as f32;
1869
1870 let mut bg_instances = Vec::with_capacity(titles.len());
1872 for title in titles {
1873 let x_ndc = title.x / width * 2.0 - 1.0;
1874 let y_ndc = 1.0 - (title.y / height * 2.0);
1875 let w_ndc = title.width / width * 2.0;
1876 let h_ndc = title.height / height * 2.0;
1877
1878 let brightness = if title.focused { 1.0 } else { 0.7 };
1881
1882 bg_instances.push(crate::cell_renderer::types::BackgroundInstance {
1883 position: [x_ndc, y_ndc],
1884 size: [w_ndc, h_ndc],
1885 color: [
1886 title.bg_color[0] * brightness,
1887 title.bg_color[1] * brightness,
1888 title.bg_color[2] * brightness,
1889 1.0, ],
1891 });
1892 }
1893
1894 self.cell_renderer.queue().write_buffer(
1896 &self.cell_renderer.bg_instance_buffer,
1897 0,
1898 bytemuck::cast_slice(&bg_instances),
1899 );
1900
1901 let mut encoder =
1903 self.cell_renderer
1904 .device()
1905 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1906 label: Some("pane title bg encoder"),
1907 });
1908
1909 {
1910 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1911 label: Some("pane title bg pass"),
1912 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1913 view: surface_view,
1914 resolve_target: None,
1915 ops: wgpu::Operations {
1916 load: wgpu::LoadOp::Load,
1917 store: wgpu::StoreOp::Store,
1918 },
1919 depth_slice: None,
1920 })],
1921 depth_stencil_attachment: None,
1922 timestamp_writes: None,
1923 occlusion_query_set: None,
1924 });
1925
1926 render_pass.set_pipeline(&self.cell_renderer.bg_pipeline);
1927 render_pass.set_vertex_buffer(0, self.cell_renderer.vertex_buffer.slice(..));
1928 render_pass.set_vertex_buffer(1, self.cell_renderer.bg_instance_buffer.slice(..));
1929 render_pass.draw(0..4, 0..bg_instances.len() as u32);
1930 }
1931
1932 self.cell_renderer
1933 .queue()
1934 .submit(std::iter::once(encoder.finish()));
1935
1936 let mut text_instances = Vec::new();
1938 let baseline_y = self.cell_renderer.font_ascent;
1939
1940 for title in titles {
1941 let title_text = &title.title;
1942 if title_text.is_empty() {
1943 continue;
1944 }
1945
1946 let padding_x = 8.0;
1948 let mut x_pos = title.x + padding_x;
1949 let y_base = title.y + (title.height - self.cell_renderer.cell_height) / 2.0;
1950
1951 let text_color = [
1952 title.text_color[0],
1953 title.text_color[1],
1954 title.text_color[2],
1955 if title.focused { 1.0 } else { 0.8 },
1956 ];
1957
1958 let max_chars =
1960 ((title.width - padding_x * 2.0) / self.cell_renderer.cell_width) as usize;
1961 let display_text: String = if title_text.len() > max_chars && max_chars > 3 {
1962 let truncated: String = title_text.chars().take(max_chars - 1).collect();
1963 format!("{}\u{2026}", truncated) } else {
1965 title_text.clone()
1966 };
1967
1968 for ch in display_text.chars() {
1969 if x_pos >= title.x + title.width - padding_x {
1970 break;
1971 }
1972
1973 if let Some((font_idx, glyph_id)) =
1974 self.cell_renderer.font_manager.find_glyph(ch, false, false)
1975 {
1976 let cache_key = ((font_idx as u64) << 32) | (glyph_id as u64);
1977 let force_monochrome = crate::cell_renderer::atlas::should_render_as_symbol(ch);
1979 let info = if self.cell_renderer.glyph_cache.contains_key(&cache_key) {
1980 self.cell_renderer.lru_remove(cache_key);
1981 self.cell_renderer.lru_push_front(cache_key);
1982 self.cell_renderer
1983 .glyph_cache
1984 .get(&cache_key)
1985 .unwrap()
1986 .clone()
1987 } else if let Some(raster) =
1988 self.cell_renderer
1989 .rasterize_glyph(font_idx, glyph_id, force_monochrome)
1990 {
1991 let info = self.cell_renderer.upload_glyph(cache_key, &raster);
1992 self.cell_renderer
1993 .glyph_cache
1994 .insert(cache_key, info.clone());
1995 self.cell_renderer.lru_push_front(cache_key);
1996 info
1997 } else {
1998 x_pos += self.cell_renderer.cell_width;
1999 continue;
2000 };
2001
2002 let glyph_left = x_pos + info.bearing_x;
2003 let glyph_top = y_base + (baseline_y - info.bearing_y);
2004
2005 text_instances.push(crate::cell_renderer::types::TextInstance {
2006 position: [
2007 glyph_left / width * 2.0 - 1.0,
2008 1.0 - (glyph_top / height * 2.0),
2009 ],
2010 size: [
2011 info.width as f32 / width * 2.0,
2012 info.height as f32 / height * 2.0,
2013 ],
2014 tex_offset: [info.x as f32 / 2048.0, info.y as f32 / 2048.0],
2015 tex_size: [info.width as f32 / 2048.0, info.height as f32 / 2048.0],
2016 color: text_color,
2017 is_colored: if info.is_colored { 1 } else { 0 },
2018 });
2019 }
2020
2021 x_pos += self.cell_renderer.cell_width;
2022 }
2023 }
2024
2025 if text_instances.is_empty() {
2026 return Ok(());
2027 }
2028
2029 self.cell_renderer.queue().write_buffer(
2031 &self.cell_renderer.text_instance_buffer,
2032 0,
2033 bytemuck::cast_slice(&text_instances),
2034 );
2035
2036 let mut encoder =
2038 self.cell_renderer
2039 .device()
2040 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
2041 label: Some("pane title text encoder"),
2042 });
2043
2044 {
2045 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2046 label: Some("pane title text pass"),
2047 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2048 view: surface_view,
2049 resolve_target: None,
2050 ops: wgpu::Operations {
2051 load: wgpu::LoadOp::Load,
2052 store: wgpu::StoreOp::Store,
2053 },
2054 depth_slice: None,
2055 })],
2056 depth_stencil_attachment: None,
2057 timestamp_writes: None,
2058 occlusion_query_set: None,
2059 });
2060
2061 render_pass.set_pipeline(&self.cell_renderer.text_pipeline);
2062 render_pass.set_bind_group(0, &self.cell_renderer.text_bind_group, &[]);
2063 render_pass.set_vertex_buffer(0, self.cell_renderer.vertex_buffer.slice(..));
2064 render_pass.set_vertex_buffer(1, self.cell_renderer.text_instance_buffer.slice(..));
2065 render_pass.draw(0..4, 0..text_instances.len() as u32);
2066 }
2067
2068 self.cell_renderer
2069 .queue()
2070 .submit(std::iter::once(encoder.finish()));
2071
2072 Ok(())
2073 }
2074
2075 fn render_egui(
2077 &mut self,
2078 surface_texture: &wgpu::SurfaceTexture,
2079 egui_output: egui::FullOutput,
2080 egui_ctx: &egui::Context,
2081 force_opaque: bool,
2082 ) -> Result<()> {
2083 use wgpu::TextureViewDescriptor;
2084
2085 let view = surface_texture
2087 .texture
2088 .create_view(&TextureViewDescriptor::default());
2089
2090 let mut encoder =
2092 self.cell_renderer
2093 .device()
2094 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
2095 label: Some("egui encoder"),
2096 });
2097
2098 let screen_descriptor = egui_wgpu::ScreenDescriptor {
2100 size_in_pixels: [self.size.width, self.size.height],
2101 pixels_per_point: egui_output.pixels_per_point,
2102 };
2103
2104 for (id, image_delta) in &egui_output.textures_delta.set {
2106 self.egui_renderer.update_texture(
2107 self.cell_renderer.device(),
2108 self.cell_renderer.queue(),
2109 *id,
2110 image_delta,
2111 );
2112 }
2113
2114 let mut paint_jobs = egui_ctx.tessellate(egui_output.shapes, egui_output.pixels_per_point);
2116
2117 if force_opaque {
2119 for job in paint_jobs.iter_mut() {
2120 match &mut job.primitive {
2121 egui::epaint::Primitive::Mesh(mesh) => {
2122 for v in mesh.vertices.iter_mut() {
2123 v.color[3] = 255;
2124 }
2125 }
2126 egui::epaint::Primitive::Callback(_) => {}
2127 }
2128 }
2129 }
2130
2131 self.egui_renderer.update_buffers(
2133 self.cell_renderer.device(),
2134 self.cell_renderer.queue(),
2135 &mut encoder,
2136 &paint_jobs,
2137 &screen_descriptor,
2138 );
2139
2140 {
2142 let render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2143 label: Some("egui render pass"),
2144 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2145 view: &view,
2146 resolve_target: None,
2147 ops: wgpu::Operations {
2148 load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store,
2150 },
2151 depth_slice: None,
2152 })],
2153 depth_stencil_attachment: None,
2154 timestamp_writes: None,
2155 occlusion_query_set: None,
2156 });
2157
2158 let mut render_pass = render_pass.forget_lifetime();
2160
2161 self.egui_renderer
2162 .render(&mut render_pass, &paint_jobs, &screen_descriptor);
2163 } self.cell_renderer
2167 .queue()
2168 .submit(std::iter::once(encoder.finish()));
2169
2170 for id in &egui_output.textures_delta.free {
2172 self.egui_renderer.free_texture(id);
2173 }
2174
2175 Ok(())
2176 }
2177
2178 pub fn size(&self) -> PhysicalSize<u32> {
2180 self.size
2181 }
2182
2183 pub fn grid_size(&self) -> (usize, usize) {
2185 self.cell_renderer.grid_size()
2186 }
2187
2188 pub fn cell_width(&self) -> f32 {
2190 self.cell_renderer.cell_width()
2191 }
2192
2193 pub fn cell_height(&self) -> f32 {
2195 self.cell_renderer.cell_height()
2196 }
2197
2198 pub fn window_padding(&self) -> f32 {
2200 self.cell_renderer.window_padding()
2201 }
2202
2203 pub fn content_offset_y(&self) -> f32 {
2205 self.cell_renderer.content_offset_y()
2206 }
2207
2208 pub fn scale_factor(&self) -> f32 {
2210 self.cell_renderer.scale_factor
2211 }
2212
2213 pub fn set_content_offset_y(&mut self, logical_offset: f32) -> Option<(usize, usize)> {
2219 let physical_offset = logical_offset * self.cell_renderer.scale_factor;
2221 let result = self.cell_renderer.set_content_offset_y(physical_offset);
2222 self.graphics_renderer.set_content_offset_y(physical_offset);
2224 if let Some(ref mut custom_shader) = self.custom_shader_renderer {
2226 custom_shader.set_content_offset_y(physical_offset);
2227 }
2228 if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
2230 cursor_shader.set_content_offset_y(physical_offset);
2231 }
2232 if result.is_some() {
2233 self.dirty = true;
2234 }
2235 result
2236 }
2237
2238 pub fn content_offset_x(&self) -> f32 {
2240 self.cell_renderer.content_offset_x()
2241 }
2242
2243 pub fn set_content_offset_x(&mut self, logical_offset: f32) -> Option<(usize, usize)> {
2246 let physical_offset = logical_offset * self.cell_renderer.scale_factor;
2247 let result = self.cell_renderer.set_content_offset_x(physical_offset);
2248 self.graphics_renderer.set_content_offset_x(physical_offset);
2249 if let Some(ref mut custom_shader) = self.custom_shader_renderer {
2250 custom_shader.set_content_offset_x(physical_offset);
2251 }
2252 if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
2253 cursor_shader.set_content_offset_x(physical_offset);
2254 }
2255 if result.is_some() {
2256 self.dirty = true;
2257 }
2258 result
2259 }
2260
2261 pub fn content_inset_bottom(&self) -> f32 {
2263 self.cell_renderer.content_inset_bottom()
2264 }
2265
2266 pub fn content_inset_right(&self) -> f32 {
2268 self.cell_renderer.content_inset_right()
2269 }
2270
2271 pub fn set_content_inset_bottom(&mut self, logical_inset: f32) -> Option<(usize, usize)> {
2274 let physical_inset = logical_inset * self.cell_renderer.scale_factor;
2275 let result = self.cell_renderer.set_content_inset_bottom(physical_inset);
2276 if result.is_some() {
2277 self.dirty = true;
2278 }
2279 result
2280 }
2281
2282 pub fn set_content_inset_right(&mut self, logical_inset: f32) -> Option<(usize, usize)> {
2285 let physical_inset = logical_inset * self.cell_renderer.scale_factor;
2286 let result = self.cell_renderer.set_content_inset_right(physical_inset);
2287
2288 if let Some(ref mut custom_shader) = self.custom_shader_renderer {
2290 custom_shader.set_content_inset_right(physical_inset);
2291 }
2292 if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
2294 cursor_shader.set_content_inset_right(physical_inset);
2295 }
2296
2297 if result.is_some() {
2298 self.dirty = true;
2299 }
2300 result
2301 }
2302
2303 pub fn set_egui_bottom_inset(&mut self, logical_inset: f32) -> Option<(usize, usize)> {
2309 let physical_inset = logical_inset * self.cell_renderer.scale_factor;
2310 if (self.cell_renderer.egui_bottom_inset - physical_inset).abs() > f32::EPSILON {
2311 self.cell_renderer.egui_bottom_inset = physical_inset;
2312 let (w, h) = (
2313 self.cell_renderer.config.width,
2314 self.cell_renderer.config.height,
2315 );
2316 return Some(self.cell_renderer.resize(w, h));
2317 }
2318 None
2319 }
2320
2321 pub fn set_egui_right_inset(&mut self, logical_inset: f32) {
2327 let physical_inset = logical_inset * self.cell_renderer.scale_factor;
2328 self.cell_renderer.egui_right_inset = physical_inset;
2329 }
2330
2331 pub fn scrollbar_contains_point(&self, x: f32, y: f32) -> bool {
2337 self.cell_renderer.scrollbar_contains_point(x, y)
2338 }
2339
2340 pub fn scrollbar_thumb_bounds(&self) -> Option<(f32, f32)> {
2342 self.cell_renderer.scrollbar_thumb_bounds()
2343 }
2344
2345 pub fn scrollbar_track_contains_x(&self, x: f32) -> bool {
2347 self.cell_renderer.scrollbar_track_contains_x(x)
2348 }
2349
2350 pub fn scrollbar_mouse_y_to_scroll_offset(&self, mouse_y: f32) -> Option<usize> {
2358 self.cell_renderer
2359 .scrollbar_mouse_y_to_scroll_offset(mouse_y)
2360 }
2361
2362 pub fn scrollbar_mark_at_position(
2372 &self,
2373 mouse_x: f32,
2374 mouse_y: f32,
2375 tolerance: f32,
2376 ) -> Option<&par_term_config::ScrollbackMark> {
2377 self.cell_renderer
2378 .scrollbar_mark_at_position(mouse_x, mouse_y, tolerance)
2379 }
2380
2381 #[allow(dead_code)]
2383 pub fn is_dirty(&self) -> bool {
2384 self.dirty
2385 }
2386
2387 #[allow(dead_code)]
2389 pub fn mark_dirty(&mut self) {
2390 self.dirty = true;
2391 }
2392
2393 #[allow(dead_code)]
2395 #[allow(dead_code)]
2396 pub fn render_debug_overlay(&mut self, text: &str) {
2397 self.debug_text = Some(text.to_string());
2398 self.dirty = true; }
2400
2401 pub fn reconfigure_surface(&mut self) {
2404 self.cell_renderer.reconfigure_surface();
2405 self.dirty = true;
2406 }
2407
2408 pub fn is_vsync_mode_supported(&self, mode: par_term_config::VsyncMode) -> bool {
2410 self.cell_renderer.is_vsync_mode_supported(mode)
2411 }
2412
2413 pub fn update_vsync_mode(
2416 &mut self,
2417 mode: par_term_config::VsyncMode,
2418 ) -> (par_term_config::VsyncMode, bool) {
2419 let result = self.cell_renderer.update_vsync_mode(mode);
2420 if result.1 {
2421 self.dirty = true;
2422 }
2423 result
2424 }
2425
2426 #[allow(dead_code)]
2428 pub fn current_vsync_mode(&self) -> par_term_config::VsyncMode {
2429 self.cell_renderer.current_vsync_mode()
2430 }
2431
2432 pub fn clear_glyph_cache(&mut self) {
2435 self.cell_renderer.clear_glyph_cache();
2436 self.dirty = true;
2437 }
2438
2439 pub fn update_font_antialias(&mut self, enabled: bool) -> bool {
2442 let changed = self.cell_renderer.update_font_antialias(enabled);
2443 if changed {
2444 self.dirty = true;
2445 }
2446 changed
2447 }
2448
2449 pub fn update_font_hinting(&mut self, enabled: bool) -> bool {
2452 let changed = self.cell_renderer.update_font_hinting(enabled);
2453 if changed {
2454 self.dirty = true;
2455 }
2456 changed
2457 }
2458
2459 pub fn update_font_thin_strokes(&mut self, mode: par_term_config::ThinStrokesMode) -> bool {
2462 let changed = self.cell_renderer.update_font_thin_strokes(mode);
2463 if changed {
2464 self.dirty = true;
2465 }
2466 changed
2467 }
2468
2469 pub fn update_minimum_contrast(&mut self, ratio: f32) -> bool {
2472 let changed = self.cell_renderer.update_minimum_contrast(ratio);
2473 if changed {
2474 self.dirty = true;
2475 }
2476 changed
2477 }
2478
2479 pub fn pause_shader_animations(&mut self) {
2482 if let Some(ref mut custom_shader) = self.custom_shader_renderer {
2483 custom_shader.set_animation_enabled(false);
2484 }
2485 if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
2486 cursor_shader.set_animation_enabled(false);
2487 }
2488 log::info!("[SHADER] Shader animations paused");
2489 }
2490
2491 pub fn resume_shader_animations(
2494 &mut self,
2495 custom_shader_animation: bool,
2496 cursor_shader_animation: bool,
2497 ) {
2498 if let Some(ref mut custom_shader) = self.custom_shader_renderer {
2499 custom_shader.set_animation_enabled(custom_shader_animation);
2500 }
2501 if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
2502 cursor_shader.set_animation_enabled(cursor_shader_animation);
2503 }
2504 self.dirty = true;
2505 log::info!(
2506 "[SHADER] Shader animations resumed (custom: {}, cursor: {})",
2507 custom_shader_animation,
2508 cursor_shader_animation
2509 );
2510 }
2511
2512 pub fn take_screenshot(&mut self) -> Result<image::RgbaImage> {
2517 log::info!(
2518 "take_screenshot: Starting screenshot capture ({}x{})",
2519 self.size.width,
2520 self.size.height
2521 );
2522
2523 let width = self.size.width;
2524 let height = self.size.height;
2525 let format = self.cell_renderer.surface_format();
2527 log::info!("take_screenshot: Using texture format {:?}", format);
2528
2529 let screenshot_texture =
2531 self.cell_renderer
2532 .device()
2533 .create_texture(&wgpu::TextureDescriptor {
2534 label: Some("screenshot texture"),
2535 size: wgpu::Extent3d {
2536 width,
2537 height,
2538 depth_or_array_layers: 1,
2539 },
2540 mip_level_count: 1,
2541 sample_count: 1,
2542 dimension: wgpu::TextureDimension::D2,
2543 format,
2544 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
2545 view_formats: &[],
2546 });
2547
2548 let screenshot_view =
2549 screenshot_texture.create_view(&wgpu::TextureViewDescriptor::default());
2550
2551 log::info!("take_screenshot: Rendering composited frame...");
2553
2554 let has_custom_shader = self.custom_shader_renderer.is_some();
2556 let use_cursor_shader =
2557 self.cursor_shader_renderer.is_some() && !self.cursor_shader_disabled_for_alt_screen;
2558
2559 if has_custom_shader {
2560 let intermediate_view = self
2562 .custom_shader_renderer
2563 .as_ref()
2564 .unwrap()
2565 .intermediate_texture_view()
2566 .clone();
2567 self.cell_renderer
2568 .render_to_texture(&intermediate_view, true)?;
2569
2570 if use_cursor_shader {
2571 let cursor_intermediate = self
2573 .cursor_shader_renderer
2574 .as_ref()
2575 .unwrap()
2576 .intermediate_texture_view()
2577 .clone();
2578 self.custom_shader_renderer.as_mut().unwrap().render(
2579 self.cell_renderer.device(),
2580 self.cell_renderer.queue(),
2581 &cursor_intermediate,
2582 false,
2583 )?;
2584 self.cursor_shader_renderer.as_mut().unwrap().render(
2586 self.cell_renderer.device(),
2587 self.cell_renderer.queue(),
2588 &screenshot_view,
2589 true,
2590 )?;
2591 } else {
2592 self.custom_shader_renderer.as_mut().unwrap().render(
2594 self.cell_renderer.device(),
2595 self.cell_renderer.queue(),
2596 &screenshot_view,
2597 true,
2598 )?;
2599 }
2600 } else if use_cursor_shader {
2601 let cursor_intermediate = self
2603 .cursor_shader_renderer
2604 .as_ref()
2605 .unwrap()
2606 .intermediate_texture_view()
2607 .clone();
2608 self.cell_renderer
2609 .render_to_texture(&cursor_intermediate, true)?;
2610 self.cursor_shader_renderer.as_mut().unwrap().render(
2612 self.cell_renderer.device(),
2613 self.cell_renderer.queue(),
2614 &screenshot_view,
2615 true,
2616 )?;
2617 } else {
2618 self.cell_renderer.render_to_view(&screenshot_view)?;
2620 }
2621
2622 log::info!("take_screenshot: Render complete");
2623
2624 let device = self.cell_renderer.device();
2626 let queue = self.cell_renderer.queue();
2627
2628 let bytes_per_pixel = 4u32;
2630 let unpadded_bytes_per_row = width * bytes_per_pixel;
2631 let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
2633 let padded_bytes_per_row = unpadded_bytes_per_row.div_ceil(align) * align;
2634 let buffer_size = (padded_bytes_per_row * height) as u64;
2635
2636 let output_buffer = device.create_buffer(&wgpu::BufferDescriptor {
2637 label: Some("screenshot buffer"),
2638 size: buffer_size,
2639 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
2640 mapped_at_creation: false,
2641 });
2642
2643 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
2645 label: Some("screenshot encoder"),
2646 });
2647
2648 encoder.copy_texture_to_buffer(
2649 wgpu::TexelCopyTextureInfo {
2650 texture: &screenshot_texture,
2651 mip_level: 0,
2652 origin: wgpu::Origin3d::ZERO,
2653 aspect: wgpu::TextureAspect::All,
2654 },
2655 wgpu::TexelCopyBufferInfo {
2656 buffer: &output_buffer,
2657 layout: wgpu::TexelCopyBufferLayout {
2658 offset: 0,
2659 bytes_per_row: Some(padded_bytes_per_row),
2660 rows_per_image: Some(height),
2661 },
2662 },
2663 wgpu::Extent3d {
2664 width,
2665 height,
2666 depth_or_array_layers: 1,
2667 },
2668 );
2669
2670 queue.submit(std::iter::once(encoder.finish()));
2671 log::info!("take_screenshot: Texture copy submitted");
2672
2673 let buffer_slice = output_buffer.slice(..);
2675 let (tx, rx) = std::sync::mpsc::channel();
2676 buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
2677 let _ = tx.send(result);
2678 });
2679
2680 log::info!("take_screenshot: Waiting for GPU...");
2682 let _ = device.poll(wgpu::PollType::wait_indefinitely());
2683 log::info!("take_screenshot: GPU poll complete, waiting for buffer map...");
2684 rx.recv()
2685 .map_err(|e| anyhow::anyhow!("Failed to receive map result: {}", e))?
2686 .map_err(|e| anyhow::anyhow!("Failed to map buffer: {:?}", e))?;
2687 log::info!("take_screenshot: Buffer mapped successfully");
2688
2689 let data = buffer_slice.get_mapped_range();
2691 let mut pixels = Vec::with_capacity((width * height * 4) as usize);
2692
2693 let is_bgra = matches!(
2695 format,
2696 wgpu::TextureFormat::Bgra8Unorm | wgpu::TextureFormat::Bgra8UnormSrgb
2697 );
2698
2699 for y in 0..height {
2701 let row_start = (y * padded_bytes_per_row) as usize;
2702 let row_end = row_start + (width * bytes_per_pixel) as usize;
2703 let row = &data[row_start..row_end];
2704
2705 if is_bgra {
2706 for chunk in row.chunks(4) {
2708 pixels.push(chunk[2]); pixels.push(chunk[1]); pixels.push(chunk[0]); pixels.push(chunk[3]); }
2713 } else {
2714 pixels.extend_from_slice(row);
2716 }
2717 }
2718
2719 drop(data);
2720 output_buffer.unmap();
2721
2722 image::RgbaImage::from_raw(width, height, pixels)
2724 .ok_or_else(|| anyhow::anyhow!("Failed to create image from pixel data"))
2725 }
2726}