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) last_scrollbar_state: (usize, usize, usize),
181
182 pub(crate) cursor_shader_disabled_for_alt_screen: bool,
184
185 #[allow(dead_code)]
187 #[allow(dead_code)]
188 pub(crate) debug_text: Option<String>,
189}
190
191impl Renderer {
192 #[allow(clippy::too_many_arguments)]
194 pub async fn new(
195 window: Arc<Window>,
196 font_family: Option<&str>,
197 font_family_bold: Option<&str>,
198 font_family_italic: Option<&str>,
199 font_family_bold_italic: Option<&str>,
200 font_ranges: &[par_term_config::FontRange],
201 font_size: f32,
202 window_padding: f32,
203 line_spacing: f32,
204 char_spacing: f32,
205 scrollbar_position: &str,
206 scrollbar_width: f32,
207 scrollbar_thumb_color: [f32; 4],
208 scrollbar_track_color: [f32; 4],
209 enable_text_shaping: bool,
210 enable_ligatures: bool,
211 enable_kerning: bool,
212 font_antialias: bool,
213 font_hinting: bool,
214 font_thin_strokes: par_term_config::ThinStrokesMode,
215 minimum_contrast: f32,
216 vsync_mode: par_term_config::VsyncMode,
217 power_preference: par_term_config::PowerPreference,
218 window_opacity: f32,
219 background_color: [u8; 3],
220 background_image_path: Option<&str>,
221 background_image_enabled: bool,
222 background_image_mode: par_term_config::BackgroundImageMode,
223 background_image_opacity: f32,
224 custom_shader_path: Option<&str>,
225 custom_shader_enabled: bool,
226 custom_shader_animation: bool,
227 custom_shader_animation_speed: f32,
228 custom_shader_full_content: bool,
229 custom_shader_brightness: f32,
230 custom_shader_channel_paths: &[Option<std::path::PathBuf>; 4],
232 custom_shader_cubemap_path: Option<&std::path::Path>,
234 use_background_as_channel0: bool,
236 image_scaling_mode: par_term_config::ImageScalingMode,
238 image_preserve_aspect_ratio: bool,
240 cursor_shader_path: Option<&str>,
242 cursor_shader_enabled: bool,
243 cursor_shader_animation: bool,
244 cursor_shader_animation_speed: f32,
245 ) -> Result<Self> {
246 let size = window.inner_size();
247 let scale_factor = window.scale_factor();
248
249 let platform_dpi = if cfg!(target_os = "macos") {
252 72.0
253 } else {
254 96.0
255 };
256
257 let base_font_pixels = font_size * platform_dpi / 72.0;
259 let font_size_pixels = (base_font_pixels * scale_factor as f32).max(1.0);
260
261 let font_manager = par_term_fonts::font_manager::FontManager::new(
263 font_family,
264 font_family_bold,
265 font_family_italic,
266 font_family_bold_italic,
267 font_ranges,
268 )?;
269
270 let (font_ascent, font_descent, font_leading, char_advance) = {
271 let primary_font = font_manager.get_font(0).unwrap();
272 let metrics = primary_font.metrics(&[]);
273 let scale = font_size_pixels / metrics.units_per_em as f32;
274
275 let glyph_id = primary_font.charmap().map('m');
277 let advance = primary_font.glyph_metrics(&[]).advance_width(glyph_id) * scale;
278
279 (
280 metrics.ascent * scale,
281 metrics.descent * scale,
282 metrics.leading * scale,
283 advance,
284 )
285 };
286
287 let natural_line_height = font_ascent + font_descent + font_leading;
290 let char_height = (natural_line_height * line_spacing).max(1.0);
291
292 let scale = scale_factor as f32;
294 let window_padding = window_padding * scale;
295 let scrollbar_width = scrollbar_width * scale;
296
297 let available_width = (size.width as f32 - window_padding * 2.0 - scrollbar_width).max(0.0);
299 let available_height = (size.height as f32 - window_padding * 2.0).max(0.0);
300
301 let char_width = (char_advance * char_spacing).max(1.0); let cols = (available_width / char_width).max(1.0) as usize;
304 let rows = (available_height / char_height).max(1.0) as usize;
305
306 let cell_renderer = CellRenderer::new(
308 window.clone(),
309 font_family,
310 font_family_bold,
311 font_family_italic,
312 font_family_bold_italic,
313 font_ranges,
314 font_size,
315 cols,
316 rows,
317 window_padding,
318 line_spacing,
319 char_spacing,
320 scrollbar_position,
321 scrollbar_width,
322 scrollbar_thumb_color,
323 scrollbar_track_color,
324 enable_text_shaping,
325 enable_ligatures,
326 enable_kerning,
327 font_antialias,
328 font_hinting,
329 font_thin_strokes,
330 minimum_contrast,
331 vsync_mode,
332 power_preference,
333 window_opacity,
334 background_color,
335 {
336 let bg_path = if background_image_enabled {
337 background_image_path
338 } else {
339 None
340 };
341 log::info!(
342 "Renderer::new: background_image_enabled={}, path={:?}",
343 background_image_enabled,
344 bg_path
345 );
346 bg_path
347 },
348 background_image_mode,
349 background_image_opacity,
350 )
351 .await?;
352
353 let egui_renderer = egui_wgpu::Renderer::new(
355 cell_renderer.device(),
356 cell_renderer.surface_format(),
357 egui_wgpu::RendererOptions {
358 msaa_samples: 1,
359 depth_stencil_format: None,
360 dithering: false,
361 predictable_texture_filtering: false,
362 },
363 );
364
365 let graphics_renderer = GraphicsRenderer::new(
367 cell_renderer.device(),
368 cell_renderer.surface_format(),
369 cell_renderer.cell_width(),
370 cell_renderer.cell_height(),
371 cell_renderer.window_padding(),
372 image_scaling_mode,
373 image_preserve_aspect_ratio,
374 )?;
375
376 let (mut custom_shader_renderer, initial_shader_path) = shaders::init_custom_shader(
378 &cell_renderer,
379 size.width,
380 size.height,
381 window_padding,
382 custom_shader_path,
383 custom_shader_enabled,
384 custom_shader_animation,
385 custom_shader_animation_speed,
386 window_opacity,
387 custom_shader_full_content,
388 custom_shader_brightness,
389 custom_shader_channel_paths,
390 custom_shader_cubemap_path,
391 use_background_as_channel0,
392 );
393
394 let (mut cursor_shader_renderer, initial_cursor_shader_path) = shaders::init_cursor_shader(
396 &cell_renderer,
397 size.width,
398 size.height,
399 window_padding,
400 cursor_shader_path,
401 cursor_shader_enabled,
402 cursor_shader_animation,
403 cursor_shader_animation_speed,
404 window_opacity,
405 );
406
407 if let Some(ref mut cs) = custom_shader_renderer {
409 cs.set_scale_factor(scale);
410 }
411 if let Some(ref mut cs) = cursor_shader_renderer {
412 cs.set_scale_factor(scale);
413 }
414
415 log::info!(
416 "[renderer] Renderer created: custom_shader_loaded={}, cursor_shader_loaded={}",
417 initial_shader_path.is_some(),
418 initial_cursor_shader_path.is_some()
419 );
420
421 Ok(Self {
422 cell_renderer,
423 graphics_renderer,
424 sixel_graphics: Vec::new(),
425 egui_renderer,
426 custom_shader_renderer,
427 custom_shader_path: initial_shader_path,
428 cursor_shader_renderer,
429 cursor_shader_path: initial_cursor_shader_path,
430 size,
431 dirty: true, last_scrollbar_state: (usize::MAX, 0, 0), cursor_shader_disabled_for_alt_screen: false,
434 debug_text: None,
435 })
436 }
437
438 pub fn resize(&mut self, new_size: PhysicalSize<u32>) -> (usize, usize) {
440 if new_size.width > 0 && new_size.height > 0 {
441 self.size = new_size;
442 self.dirty = true; let result = self.cell_renderer.resize(new_size.width, new_size.height);
444
445 self.graphics_renderer.update_cell_dimensions(
447 self.cell_renderer.cell_width(),
448 self.cell_renderer.cell_height(),
449 self.cell_renderer.window_padding(),
450 );
451
452 if let Some(ref mut custom_shader) = self.custom_shader_renderer {
454 custom_shader.resize(self.cell_renderer.device(), new_size.width, new_size.height);
455 custom_shader.update_cell_dimensions(
457 self.cell_renderer.cell_width(),
458 self.cell_renderer.cell_height(),
459 self.cell_renderer.window_padding(),
460 );
461 }
462
463 if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
465 cursor_shader.resize(self.cell_renderer.device(), new_size.width, new_size.height);
466 cursor_shader.update_cell_dimensions(
468 self.cell_renderer.cell_width(),
469 self.cell_renderer.cell_height(),
470 self.cell_renderer.window_padding(),
471 );
472 }
473
474 return result;
475 }
476
477 self.cell_renderer.grid_size()
478 }
479
480 pub fn handle_scale_factor_change(
482 &mut self,
483 scale_factor: f64,
484 new_size: PhysicalSize<u32>,
485 ) -> (usize, usize) {
486 let old_scale = self.cell_renderer.scale_factor;
487 self.cell_renderer.update_scale_factor(scale_factor);
488 let new_scale = self.cell_renderer.scale_factor;
489
490 if old_scale > 0.0 && (old_scale - new_scale).abs() > f32::EPSILON {
492 let logical_offset_y = self.cell_renderer.content_offset_y() / old_scale;
494 let new_physical_offset_y = logical_offset_y * new_scale;
495 self.cell_renderer
496 .set_content_offset_y(new_physical_offset_y);
497 self.graphics_renderer
498 .set_content_offset_y(new_physical_offset_y);
499 if let Some(ref mut cs) = self.custom_shader_renderer {
500 cs.set_content_offset_y(new_physical_offset_y);
501 }
502 if let Some(ref mut cs) = self.cursor_shader_renderer {
503 cs.set_content_offset_y(new_physical_offset_y);
504 }
505
506 let logical_offset_x = self.cell_renderer.content_offset_x() / old_scale;
508 let new_physical_offset_x = logical_offset_x * new_scale;
509 self.cell_renderer
510 .set_content_offset_x(new_physical_offset_x);
511 self.graphics_renderer
512 .set_content_offset_x(new_physical_offset_x);
513 if let Some(ref mut cs) = self.custom_shader_renderer {
514 cs.set_content_offset_x(new_physical_offset_x);
515 }
516 if let Some(ref mut cs) = self.cursor_shader_renderer {
517 cs.set_content_offset_x(new_physical_offset_x);
518 }
519
520 let logical_inset_bottom = self.cell_renderer.content_inset_bottom() / old_scale;
522 let new_physical_inset_bottom = logical_inset_bottom * new_scale;
523 self.cell_renderer
524 .set_content_inset_bottom(new_physical_inset_bottom);
525
526 if self.cell_renderer.egui_bottom_inset > 0.0 {
528 let logical_egui_bottom = self.cell_renderer.egui_bottom_inset / old_scale;
529 self.cell_renderer.egui_bottom_inset = logical_egui_bottom * new_scale;
530 }
531
532 if self.cell_renderer.content_inset_right > 0.0 {
534 let logical_inset_right = self.cell_renderer.content_inset_right / old_scale;
535 self.cell_renderer.content_inset_right = logical_inset_right * new_scale;
536 }
537
538 if self.cell_renderer.egui_right_inset > 0.0 {
540 let logical_egui_right = self.cell_renderer.egui_right_inset / old_scale;
541 self.cell_renderer.egui_right_inset = logical_egui_right * new_scale;
542 }
543
544 let logical_padding = self.cell_renderer.window_padding() / old_scale;
546 let new_physical_padding = logical_padding * new_scale;
547 self.cell_renderer
548 .update_window_padding(new_physical_padding);
549
550 if let Some(ref mut cs) = self.custom_shader_renderer {
552 cs.set_scale_factor(new_scale);
553 }
554 if let Some(ref mut cs) = self.cursor_shader_renderer {
555 cs.set_scale_factor(new_scale);
556 }
557 }
558
559 self.resize(new_size)
560 }
561
562 pub fn update_cells(&mut self, cells: &[Cell]) {
564 if self.cell_renderer.update_cells(cells) {
565 self.dirty = true;
566 }
567 }
568
569 pub fn clear_all_cells(&mut self) {
572 self.cell_renderer.clear_all_cells();
573 self.dirty = true;
574 }
575
576 pub fn update_cursor(
578 &mut self,
579 position: (usize, usize),
580 opacity: f32,
581 style: par_term_emu_core_rust::cursor::CursorStyle,
582 ) {
583 if self.cell_renderer.update_cursor(position, opacity, style) {
584 self.dirty = true;
585 }
586 }
587
588 pub fn clear_cursor(&mut self) {
590 if self.cell_renderer.clear_cursor() {
591 self.dirty = true;
592 }
593 }
594
595 pub fn update_scrollbar(
603 &mut self,
604 scroll_offset: usize,
605 visible_lines: usize,
606 total_lines: usize,
607 marks: &[par_term_config::ScrollbackMark],
608 ) {
609 let new_state = (scroll_offset, visible_lines, total_lines);
610 if new_state == self.last_scrollbar_state {
611 return;
612 }
613 self.last_scrollbar_state = new_state;
614 self.cell_renderer
615 .update_scrollbar(scroll_offset, visible_lines, total_lines, marks);
616 self.dirty = true;
617 }
618
619 pub fn set_visual_bell_intensity(&mut self, intensity: f32) {
624 self.cell_renderer.set_visual_bell_intensity(intensity);
625 if intensity > 0.0 {
626 self.dirty = true; }
628 }
629
630 pub fn update_opacity(&mut self, opacity: f32) {
632 self.cell_renderer.update_opacity(opacity);
633
634 if let Some(ref mut custom_shader) = self.custom_shader_renderer {
636 custom_shader.set_opacity(opacity);
637 }
638
639 if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
641 cursor_shader.set_opacity(opacity);
642 }
643
644 self.dirty = true;
645 }
646
647 pub fn update_cursor_color(&mut self, color: [u8; 3]) {
649 self.cell_renderer.update_cursor_color(color);
650 self.dirty = true;
651 }
652
653 pub fn update_cursor_text_color(&mut self, color: Option<[u8; 3]>) {
655 self.cell_renderer.update_cursor_text_color(color);
656 self.dirty = true;
657 }
658
659 pub fn set_cursor_hidden_for_shader(&mut self, hidden: bool) {
661 if self.cell_renderer.set_cursor_hidden_for_shader(hidden) {
662 self.dirty = true;
663 }
664 }
665
666 pub fn set_focused(&mut self, focused: bool) {
668 if self.cell_renderer.set_focused(focused) {
669 self.dirty = true;
670 }
671 }
672
673 pub fn update_cursor_guide(&mut self, enabled: bool, color: [u8; 4]) {
675 self.cell_renderer.update_cursor_guide(enabled, color);
676 self.dirty = true;
677 }
678
679 pub fn update_cursor_shadow(
682 &mut self,
683 enabled: bool,
684 color: [u8; 4],
685 offset: [f32; 2],
686 blur: f32,
687 ) {
688 let scale = self.cell_renderer.scale_factor;
689 let physical_offset = [offset[0] * scale, offset[1] * scale];
690 let physical_blur = blur * scale;
691 self.cell_renderer
692 .update_cursor_shadow(enabled, color, physical_offset, physical_blur);
693 self.dirty = true;
694 }
695
696 pub fn update_cursor_boost(&mut self, intensity: f32, color: [u8; 3]) {
698 self.cell_renderer.update_cursor_boost(intensity, color);
699 self.dirty = true;
700 }
701
702 pub fn update_unfocused_cursor_style(&mut self, style: par_term_config::UnfocusedCursorStyle) {
704 self.cell_renderer.update_unfocused_cursor_style(style);
705 self.dirty = true;
706 }
707
708 pub fn update_command_separator(
711 &mut self,
712 enabled: bool,
713 logical_thickness: f32,
714 opacity: f32,
715 exit_color: bool,
716 color: [u8; 3],
717 ) {
718 let physical_thickness = logical_thickness * self.cell_renderer.scale_factor;
719 self.cell_renderer.update_command_separator(
720 enabled,
721 physical_thickness,
722 opacity,
723 exit_color,
724 color,
725 );
726 self.dirty = true;
727 }
728
729 pub fn set_separator_marks(&mut self, marks: Vec<SeparatorMark>) {
731 if self.cell_renderer.set_separator_marks(marks) {
732 self.dirty = true;
733 }
734 }
735
736 pub fn set_transparency_affects_only_default_background(&mut self, value: bool) {
739 self.cell_renderer
740 .set_transparency_affects_only_default_background(value);
741 self.dirty = true;
742 }
743
744 pub fn set_keep_text_opaque(&mut self, value: bool) {
747 self.cell_renderer.set_keep_text_opaque(value);
748
749 if let Some(ref mut custom_shader) = self.custom_shader_renderer {
751 custom_shader.set_keep_text_opaque(value);
752 }
753
754 if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
756 cursor_shader.set_keep_text_opaque(value);
757 }
758
759 self.dirty = true;
760 }
761
762 pub fn set_link_underline_style(&mut self, style: par_term_config::LinkUnderlineStyle) {
763 self.cell_renderer.set_link_underline_style(style);
764 self.dirty = true;
765 }
766
767 pub fn set_cursor_shader_disabled_for_alt_screen(&mut self, disabled: bool) {
772 if self.cursor_shader_disabled_for_alt_screen != disabled {
773 log::debug!("[cursor-shader] Alt-screen disable set to {}", disabled);
774 self.cursor_shader_disabled_for_alt_screen = disabled;
775 } else {
776 self.cursor_shader_disabled_for_alt_screen = disabled;
777 }
778 }
779
780 #[allow(dead_code)]
784 pub fn update_window_padding(&mut self, logical_padding: f32) -> Option<(usize, usize)> {
785 let physical_padding = logical_padding * self.cell_renderer.scale_factor;
786 let result = self.cell_renderer.update_window_padding(physical_padding);
787 self.graphics_renderer.update_cell_dimensions(
789 self.cell_renderer.cell_width(),
790 self.cell_renderer.cell_height(),
791 physical_padding,
792 );
793 if let Some(ref mut custom_shader) = self.custom_shader_renderer {
795 custom_shader.update_cell_dimensions(
796 self.cell_renderer.cell_width(),
797 self.cell_renderer.cell_height(),
798 physical_padding,
799 );
800 }
801 if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
803 cursor_shader.update_cell_dimensions(
804 self.cell_renderer.cell_width(),
805 self.cell_renderer.cell_height(),
806 physical_padding,
807 );
808 }
809 self.dirty = true;
810 result
811 }
812
813 #[allow(dead_code)]
815 pub fn set_background_image_enabled(
816 &mut self,
817 enabled: bool,
818 path: Option<&str>,
819 mode: par_term_config::BackgroundImageMode,
820 opacity: f32,
821 ) {
822 let path = if enabled { path } else { None };
823 self.cell_renderer.set_background_image(path, mode, opacity);
824
825 self.sync_background_texture_to_shader();
827
828 self.dirty = true;
829 }
830
831 pub fn set_background(
835 &mut self,
836 mode: par_term_config::BackgroundMode,
837 color: [u8; 3],
838 image_path: Option<&str>,
839 image_mode: par_term_config::BackgroundImageMode,
840 image_opacity: f32,
841 image_enabled: bool,
842 ) {
843 self.cell_renderer.set_background(
844 mode,
845 color,
846 image_path,
847 image_mode,
848 image_opacity,
849 image_enabled,
850 );
851
852 self.sync_background_texture_to_shader();
854
855 let is_solid_color = matches!(mode, par_term_config::BackgroundMode::Color);
857 let is_image_mode = matches!(mode, par_term_config::BackgroundMode::Image);
858 let normalized_color = [
859 color[0] as f32 / 255.0,
860 color[1] as f32 / 255.0,
861 color[2] as f32 / 255.0,
862 ];
863
864 if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
866 let has_background_shader = self.custom_shader_renderer.is_some();
869
870 if has_background_shader {
871 cursor_shader.set_background_color([0.0, 0.0, 0.0], false);
873 cursor_shader.set_background_texture(self.cell_renderer.device(), None);
874 cursor_shader.update_use_background_as_channel0(self.cell_renderer.device(), false);
875 } else {
876 cursor_shader.set_background_color(normalized_color, is_solid_color);
877
878 if is_image_mode && image_enabled {
880 let bg_texture = self.cell_renderer.get_background_as_channel_texture();
881 cursor_shader.set_background_texture(self.cell_renderer.device(), bg_texture);
882 cursor_shader
883 .update_use_background_as_channel0(self.cell_renderer.device(), true);
884 } else {
885 cursor_shader.set_background_texture(self.cell_renderer.device(), None);
887 cursor_shader
888 .update_use_background_as_channel0(self.cell_renderer.device(), false);
889 }
890 }
891 }
892
893 if let Some(ref mut custom_shader) = self.custom_shader_renderer {
898 custom_shader.set_background_color(normalized_color, false);
899 }
900
901 self.dirty = true;
902 }
903
904 pub fn update_scrollbar_appearance(
907 &mut self,
908 logical_width: f32,
909 thumb_color: [f32; 4],
910 track_color: [f32; 4],
911 ) {
912 let physical_width = logical_width * self.cell_renderer.scale_factor;
913 self.cell_renderer
914 .update_scrollbar_appearance(physical_width, thumb_color, track_color);
915 self.dirty = true;
916 }
917
918 #[allow(dead_code)]
920 pub fn update_scrollbar_position(&mut self, position: &str) {
921 self.cell_renderer.update_scrollbar_position(position);
922 self.dirty = true;
923 }
924
925 #[allow(dead_code)]
927 pub fn update_background_image_opacity(&mut self, opacity: f32) {
928 self.cell_renderer.update_background_image_opacity(opacity);
929 self.dirty = true;
930 }
931
932 pub fn load_pane_background(&mut self, path: &str) -> anyhow::Result<bool> {
935 self.cell_renderer.load_pane_background(path)
936 }
937
938 pub fn update_image_scaling_mode(&mut self, scaling_mode: par_term_config::ImageScalingMode) {
943 self.graphics_renderer
944 .update_scaling_mode(self.cell_renderer.device(), scaling_mode);
945 self.dirty = true;
946 }
947
948 pub fn update_image_preserve_aspect_ratio(&mut self, preserve: bool) {
950 self.graphics_renderer.set_preserve_aspect_ratio(preserve);
951 self.dirty = true;
952 }
953
954 pub fn needs_continuous_render(&self) -> bool {
959 let custom_needs = self
960 .custom_shader_renderer
961 .as_ref()
962 .is_some_and(|r| r.animation_enabled() || r.cursor_needs_animation());
963 let cursor_needs = self
964 .cursor_shader_renderer
965 .as_ref()
966 .is_some_and(|r| r.animation_enabled() || r.cursor_needs_animation());
967 custom_needs || cursor_needs
968 }
969
970 pub fn render(
973 &mut self,
974 egui_data: Option<(egui::FullOutput, &egui::Context)>,
975 force_egui_opaque: bool,
976 show_scrollbar: bool,
977 pane_background: Option<&par_term_config::PaneBackground>,
978 ) -> Result<bool> {
979 let force_render = self.needs_continuous_render();
981
982 if !self.dirty && !force_render {
985 if let Some((egui_output, egui_ctx)) = egui_data {
986 let surface_texture = self.cell_renderer.render(show_scrollbar, pane_background)?;
987 self.cell_renderer
988 .render_overlays(&surface_texture, show_scrollbar)?;
989 self.render_egui(&surface_texture, egui_output, egui_ctx, force_egui_opaque)?;
990 surface_texture.present();
991 return Ok(true);
992 }
993 return Ok(false);
994 }
995
996 let has_custom_shader = self.custom_shader_renderer.is_some();
998 let use_cursor_shader =
1000 self.cursor_shader_renderer.is_some() && !self.cursor_shader_disabled_for_alt_screen;
1001
1002 let t1 = std::time::Instant::now();
1004 let surface_texture = if has_custom_shader {
1005 self.cell_renderer.render_to_texture(
1014 self.custom_shader_renderer
1015 .as_ref()
1016 .unwrap()
1017 .intermediate_texture_view(),
1018 true, )?
1020 } else if use_cursor_shader {
1021 self.cell_renderer.render_to_texture(
1025 self.cursor_shader_renderer
1026 .as_ref()
1027 .unwrap()
1028 .intermediate_texture_view(),
1029 true, )?
1031 } else {
1032 self.cell_renderer.render(show_scrollbar, pane_background)?
1035 };
1036 let cell_render_time = t1.elapsed();
1037
1038 let t_custom = std::time::Instant::now();
1040 let custom_shader_time = if let Some(ref mut custom_shader) = self.custom_shader_renderer {
1041 if use_cursor_shader {
1042 custom_shader.render(
1045 self.cell_renderer.device(),
1046 self.cell_renderer.queue(),
1047 self.cursor_shader_renderer
1048 .as_ref()
1049 .unwrap()
1050 .intermediate_texture_view(),
1051 false, )?;
1053 } else {
1054 let surface_view = surface_texture
1057 .texture
1058 .create_view(&wgpu::TextureViewDescriptor::default());
1059 custom_shader.render(
1060 self.cell_renderer.device(),
1061 self.cell_renderer.queue(),
1062 &surface_view,
1063 true, )?;
1065 }
1066 t_custom.elapsed()
1067 } else {
1068 std::time::Duration::ZERO
1069 };
1070
1071 let t_cursor = std::time::Instant::now();
1073 let cursor_shader_time = if use_cursor_shader {
1074 log::trace!("Rendering cursor shader");
1075 let cursor_shader = self.cursor_shader_renderer.as_mut().unwrap();
1076 let surface_view = surface_texture
1077 .texture
1078 .create_view(&wgpu::TextureViewDescriptor::default());
1079
1080 cursor_shader.render(
1081 self.cell_renderer.device(),
1082 self.cell_renderer.queue(),
1083 &surface_view,
1084 true, )?;
1086 t_cursor.elapsed()
1087 } else {
1088 if self.cursor_shader_disabled_for_alt_screen {
1089 log::trace!("Skipping cursor shader - alt screen active");
1090 }
1091 std::time::Duration::ZERO
1092 };
1093
1094 let t2 = std::time::Instant::now();
1096 if !self.sixel_graphics.is_empty() {
1097 self.render_sixel_graphics(&surface_texture)?;
1098 }
1099 let sixel_render_time = t2.elapsed();
1100
1101 self.cell_renderer
1105 .render_overlays(&surface_texture, show_scrollbar)?;
1106
1107 let t3 = std::time::Instant::now();
1109 if let Some((egui_output, egui_ctx)) = egui_data {
1110 self.render_egui(&surface_texture, egui_output, egui_ctx, force_egui_opaque)?;
1111 }
1112 let egui_render_time = t3.elapsed();
1113
1114 let t4 = std::time::Instant::now();
1116 surface_texture.present();
1117 let present_time = t4.elapsed();
1118
1119 let total = cell_render_time
1121 + custom_shader_time
1122 + cursor_shader_time
1123 + sixel_render_time
1124 + egui_render_time
1125 + present_time;
1126 if present_time.as_millis() > 10 || total.as_millis() > 10 {
1127 log::info!(
1128 "[RENDER] RENDER_BREAKDOWN: CellRender={:.2}ms BgShader={:.2}ms CursorShader={:.2}ms Sixel={:.2}ms Egui={:.2}ms PRESENT={:.2}ms Total={:.2}ms",
1129 cell_render_time.as_secs_f64() * 1000.0,
1130 custom_shader_time.as_secs_f64() * 1000.0,
1131 cursor_shader_time.as_secs_f64() * 1000.0,
1132 sixel_render_time.as_secs_f64() * 1000.0,
1133 egui_render_time.as_secs_f64() * 1000.0,
1134 present_time.as_secs_f64() * 1000.0,
1135 total.as_secs_f64() * 1000.0
1136 );
1137 }
1138
1139 self.dirty = false;
1141
1142 Ok(true)
1143 }
1144
1145 #[allow(dead_code)]
1158 pub fn render_panes(
1159 &mut self,
1160 panes: &[PaneRenderInfo<'_>],
1161 egui_data: Option<(egui::FullOutput, &egui::Context)>,
1162 force_egui_opaque: bool,
1163 ) -> Result<bool> {
1164 let force_render = self.needs_continuous_render();
1166 if !self.dirty && !force_render && egui_data.is_none() {
1167 return Ok(false);
1168 }
1169
1170 let surface_texture = self.cell_renderer.surface.get_current_texture()?;
1172 let surface_view = surface_texture
1173 .texture
1174 .create_view(&wgpu::TextureViewDescriptor::default());
1175
1176 {
1178 let mut encoder = self.cell_renderer.device().create_command_encoder(
1179 &wgpu::CommandEncoderDescriptor {
1180 label: Some("pane clear encoder"),
1181 },
1182 );
1183
1184 let opacity = self.cell_renderer.window_opacity as f64;
1185 let clear_color = if self.cell_renderer.bg_is_solid_color {
1186 wgpu::Color {
1187 r: self.cell_renderer.solid_bg_color[0] as f64 * opacity,
1188 g: self.cell_renderer.solid_bg_color[1] as f64 * opacity,
1189 b: self.cell_renderer.solid_bg_color[2] as f64 * opacity,
1190 a: opacity,
1191 }
1192 } else {
1193 wgpu::Color {
1194 r: self.cell_renderer.background_color[0] as f64 * opacity,
1195 g: self.cell_renderer.background_color[1] as f64 * opacity,
1196 b: self.cell_renderer.background_color[2] as f64 * opacity,
1197 a: opacity,
1198 }
1199 };
1200
1201 {
1202 let _clear_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1203 label: Some("surface clear pass"),
1204 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1205 view: &surface_view,
1206 resolve_target: None,
1207 ops: wgpu::Operations {
1208 load: wgpu::LoadOp::Clear(clear_color),
1209 store: wgpu::StoreOp::Store,
1210 },
1211 depth_slice: None,
1212 })],
1213 depth_stencil_attachment: None,
1214 timestamp_writes: None,
1215 occlusion_query_set: None,
1216 });
1217 }
1218
1219 self.cell_renderer
1220 .queue()
1221 .submit(std::iter::once(encoder.finish()));
1222 }
1223
1224 let has_background_image = self
1226 .cell_renderer
1227 .render_background_only(&surface_view, false)?;
1228
1229 for pane in panes {
1231 let separator_marks = compute_visible_separator_marks(
1232 &pane.marks,
1233 pane.scrollback_len,
1234 pane.scroll_offset,
1235 pane.grid_size.1,
1236 );
1237 self.cell_renderer.render_pane_to_view(
1238 &surface_view,
1239 &pane.viewport,
1240 pane.cells,
1241 pane.grid_size.0,
1242 pane.grid_size.1,
1243 pane.cursor_pos,
1244 pane.cursor_opacity,
1245 pane.show_scrollbar,
1246 false, has_background_image, &separator_marks,
1249 pane.background.as_ref(),
1250 )?;
1251 }
1252
1253 if let Some((egui_output, egui_ctx)) = egui_data {
1255 self.render_egui(&surface_texture, egui_output, egui_ctx, force_egui_opaque)?;
1256 }
1257
1258 surface_texture.present();
1260
1261 self.dirty = false;
1262 Ok(true)
1263 }
1264
1265 #[allow(dead_code, clippy::too_many_arguments)]
1287 pub fn render_split_panes(
1288 &mut self,
1289 panes: &[PaneRenderInfo<'_>],
1290 dividers: &[DividerRenderInfo],
1291 pane_titles: &[PaneTitleInfo],
1292 focused_viewport: Option<&PaneViewport>,
1293 divider_settings: &PaneDividerSettings,
1294 egui_data: Option<(egui::FullOutput, &egui::Context)>,
1295 force_egui_opaque: bool,
1296 ) -> Result<bool> {
1297 let force_render = self.needs_continuous_render();
1299 if !self.dirty && !force_render && egui_data.is_none() {
1300 return Ok(false);
1301 }
1302
1303 let has_custom_shader = self.custom_shader_renderer.is_some();
1304
1305 for pane in panes.iter() {
1307 if let Some(ref bg) = pane.background
1308 && let Some(ref path) = bg.image_path
1309 && let Err(e) = self.cell_renderer.load_pane_background(path)
1310 {
1311 log::error!("Failed to load pane background '{}': {}", path, e);
1312 }
1313 }
1314
1315 let surface_texture = self.cell_renderer.surface.get_current_texture()?;
1317 let surface_view = surface_texture
1318 .texture
1319 .create_view(&wgpu::TextureViewDescriptor::default());
1320
1321 let opacity = self.cell_renderer.window_opacity as f64;
1323 let clear_color = if self.cell_renderer.bg_is_solid_color {
1324 wgpu::Color {
1325 r: self.cell_renderer.solid_bg_color[0] as f64 * opacity,
1326 g: self.cell_renderer.solid_bg_color[1] as f64 * opacity,
1327 b: self.cell_renderer.solid_bg_color[2] as f64 * opacity,
1328 a: opacity,
1329 }
1330 } else {
1331 wgpu::Color {
1332 r: self.cell_renderer.background_color[0] as f64 * opacity,
1333 g: self.cell_renderer.background_color[1] as f64 * opacity,
1334 b: self.cell_renderer.background_color[2] as f64 * opacity,
1335 a: opacity,
1336 }
1337 };
1338
1339 if let Some(ref mut custom_shader) = self.custom_shader_renderer {
1342 custom_shader.clear_intermediate_texture(
1345 self.cell_renderer.device(),
1346 self.cell_renderer.queue(),
1347 );
1348
1349 custom_shader.render_with_clear_color(
1352 self.cell_renderer.device(),
1353 self.cell_renderer.queue(),
1354 &surface_view,
1355 false, clear_color,
1357 )?;
1358 } else {
1359 let mut encoder = self.cell_renderer.device().create_command_encoder(
1361 &wgpu::CommandEncoderDescriptor {
1362 label: Some("split pane clear encoder"),
1363 },
1364 );
1365
1366 {
1367 let _clear_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1368 label: Some("surface clear pass"),
1369 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1370 view: &surface_view,
1371 resolve_target: None,
1372 ops: wgpu::Operations {
1373 load: wgpu::LoadOp::Clear(clear_color),
1374 store: wgpu::StoreOp::Store,
1375 },
1376 depth_slice: None,
1377 })],
1378 depth_stencil_attachment: None,
1379 timestamp_writes: None,
1380 occlusion_query_set: None,
1381 });
1382 }
1383
1384 self.cell_renderer
1385 .queue()
1386 .submit(std::iter::once(encoder.finish()));
1387 }
1388
1389 let any_pane_has_background = panes.iter().any(|p| p.background.is_some());
1394 let has_background_image = if !has_custom_shader && !any_pane_has_background {
1395 self.cell_renderer
1396 .render_background_only(&surface_view, false)?
1397 } else {
1398 false
1399 };
1400
1401 for pane in panes {
1403 let separator_marks = compute_visible_separator_marks(
1404 &pane.marks,
1405 pane.scrollback_len,
1406 pane.scroll_offset,
1407 pane.grid_size.1,
1408 );
1409 self.cell_renderer.render_pane_to_view(
1410 &surface_view,
1411 &pane.viewport,
1412 pane.cells,
1413 pane.grid_size.0,
1414 pane.grid_size.1,
1415 pane.cursor_pos,
1416 pane.cursor_opacity,
1417 pane.show_scrollbar,
1418 false, has_background_image || has_custom_shader, &separator_marks,
1421 pane.background.as_ref(),
1422 )?;
1423 }
1424
1425 if !dividers.is_empty() {
1427 self.render_dividers(&surface_view, dividers, divider_settings)?;
1428 }
1429
1430 if !pane_titles.is_empty() {
1432 self.render_pane_titles(&surface_view, pane_titles)?;
1433 }
1434
1435 if panes.len() > 1
1437 && let Some(viewport) = focused_viewport
1438 {
1439 self.render_focus_indicator(&surface_view, viewport, divider_settings)?;
1440 }
1441
1442 if let Some((egui_output, egui_ctx)) = egui_data {
1444 self.render_egui(&surface_texture, egui_output, egui_ctx, force_egui_opaque)?;
1445 }
1446
1447 surface_texture.present();
1449
1450 self.dirty = false;
1451 Ok(true)
1452 }
1453
1454 #[allow(dead_code)]
1463 pub fn render_dividers(
1464 &mut self,
1465 surface_view: &wgpu::TextureView,
1466 dividers: &[DividerRenderInfo],
1467 settings: &PaneDividerSettings,
1468 ) -> Result<()> {
1469 if dividers.is_empty() {
1470 return Ok(());
1471 }
1472
1473 let mut instances = Vec::with_capacity(dividers.len() * 3); let w = self.size.width as f32;
1478 let h = self.size.height as f32;
1479
1480 for divider in dividers {
1481 let color = if divider.hovered {
1482 settings.hover_color
1483 } else {
1484 settings.divider_color
1485 };
1486
1487 use par_term_config::DividerStyle;
1488 match settings.divider_style {
1489 DividerStyle::Solid => {
1490 let x_ndc = divider.x / w * 2.0 - 1.0;
1491 let y_ndc = 1.0 - (divider.y / h * 2.0);
1492 let w_ndc = divider.width / w * 2.0;
1493 let h_ndc = divider.height / h * 2.0;
1494
1495 instances.push(crate::cell_renderer::types::BackgroundInstance {
1496 position: [x_ndc, y_ndc],
1497 size: [w_ndc, h_ndc],
1498 color: [color[0], color[1], color[2], 1.0],
1499 });
1500 }
1501 DividerStyle::Double => {
1502 let is_horizontal = divider.width > divider.height;
1504 let thickness = if is_horizontal {
1505 divider.height
1506 } else {
1507 divider.width
1508 };
1509
1510 if thickness >= 4.0 {
1511 if is_horizontal {
1513 instances.push(crate::cell_renderer::types::BackgroundInstance {
1515 position: [divider.x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1516 size: [divider.width / w * 2.0, 1.0 / h * 2.0],
1517 color: [color[0], color[1], color[2], 1.0],
1518 });
1519 let bottom_y = divider.y + divider.height - 1.0;
1521 instances.push(crate::cell_renderer::types::BackgroundInstance {
1522 position: [divider.x / w * 2.0 - 1.0, 1.0 - (bottom_y / h * 2.0)],
1523 size: [divider.width / w * 2.0, 1.0 / h * 2.0],
1524 color: [color[0], color[1], color[2], 1.0],
1525 });
1526 } else {
1527 instances.push(crate::cell_renderer::types::BackgroundInstance {
1529 position: [divider.x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1530 size: [1.0 / w * 2.0, divider.height / h * 2.0],
1531 color: [color[0], color[1], color[2], 1.0],
1532 });
1533 let right_x = divider.x + divider.width - 1.0;
1535 instances.push(crate::cell_renderer::types::BackgroundInstance {
1536 position: [right_x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1537 size: [1.0 / w * 2.0, divider.height / h * 2.0],
1538 color: [color[0], color[1], color[2], 1.0],
1539 });
1540 }
1541 } else {
1542 if is_horizontal {
1545 let center_y = divider.y + (divider.height - 1.0) / 2.0;
1546 instances.push(crate::cell_renderer::types::BackgroundInstance {
1547 position: [divider.x / w * 2.0 - 1.0, 1.0 - (center_y / h * 2.0)],
1548 size: [divider.width / w * 2.0, 1.0 / h * 2.0],
1549 color: [color[0], color[1], color[2], 1.0],
1550 });
1551 } else {
1552 let center_x = divider.x + (divider.width - 1.0) / 2.0;
1553 instances.push(crate::cell_renderer::types::BackgroundInstance {
1554 position: [center_x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1555 size: [1.0 / w * 2.0, divider.height / h * 2.0],
1556 color: [color[0], color[1], color[2], 1.0],
1557 });
1558 }
1559 }
1560 }
1561 DividerStyle::Dashed => {
1562 let is_horizontal = divider.width > divider.height;
1564 let dash_len: f32 = 6.0;
1565 let gap_len: f32 = 4.0;
1566
1567 if is_horizontal {
1568 let mut x = divider.x;
1569 while x < divider.x + divider.width {
1570 let seg_w = dash_len.min(divider.x + divider.width - x);
1571 instances.push(crate::cell_renderer::types::BackgroundInstance {
1572 position: [x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1573 size: [seg_w / w * 2.0, divider.height / h * 2.0],
1574 color: [color[0], color[1], color[2], 1.0],
1575 });
1576 x += dash_len + gap_len;
1577 }
1578 } else {
1579 let mut y = divider.y;
1580 while y < divider.y + divider.height {
1581 let seg_h = dash_len.min(divider.y + divider.height - y);
1582 instances.push(crate::cell_renderer::types::BackgroundInstance {
1583 position: [divider.x / w * 2.0 - 1.0, 1.0 - (y / h * 2.0)],
1584 size: [divider.width / w * 2.0, seg_h / h * 2.0],
1585 color: [color[0], color[1], color[2], 1.0],
1586 });
1587 y += dash_len + gap_len;
1588 }
1589 }
1590 }
1591 DividerStyle::Shadow => {
1592 let is_horizontal = divider.width > divider.height;
1595 let thickness = if is_horizontal {
1596 divider.height
1597 } else {
1598 divider.width
1599 };
1600
1601 let highlight = [
1603 (color[0] + 0.3).min(1.0),
1604 (color[1] + 0.3).min(1.0),
1605 (color[2] + 0.3).min(1.0),
1606 1.0,
1607 ];
1608 let shadow = [(color[0] * 0.3), (color[1] * 0.3), (color[2] * 0.3), 1.0];
1610
1611 if thickness >= 3.0 {
1612 let edge = 1.0_f32;
1614 if is_horizontal {
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: [divider.width / w * 2.0, edge / h * 2.0],
1619 color: highlight,
1620 });
1621 let body_y = divider.y + edge;
1623 let body_h = divider.height - edge * 2.0;
1624 if body_h > 0.0 {
1625 instances.push(crate::cell_renderer::types::BackgroundInstance {
1626 position: [divider.x / w * 2.0 - 1.0, 1.0 - (body_y / h * 2.0)],
1627 size: [divider.width / w * 2.0, body_h / h * 2.0],
1628 color: [color[0], color[1], color[2], 1.0],
1629 });
1630 }
1631 let shadow_y = divider.y + divider.height - edge;
1633 instances.push(crate::cell_renderer::types::BackgroundInstance {
1634 position: [divider.x / w * 2.0 - 1.0, 1.0 - (shadow_y / h * 2.0)],
1635 size: [divider.width / w * 2.0, edge / h * 2.0],
1636 color: shadow,
1637 });
1638 } else {
1639 instances.push(crate::cell_renderer::types::BackgroundInstance {
1641 position: [divider.x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1642 size: [edge / w * 2.0, divider.height / h * 2.0],
1643 color: highlight,
1644 });
1645 let body_x = divider.x + edge;
1647 let body_w = divider.width - edge * 2.0;
1648 if body_w > 0.0 {
1649 instances.push(crate::cell_renderer::types::BackgroundInstance {
1650 position: [body_x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1651 size: [body_w / w * 2.0, divider.height / h * 2.0],
1652 color: [color[0], color[1], color[2], 1.0],
1653 });
1654 }
1655 let shadow_x = divider.x + divider.width - edge;
1657 instances.push(crate::cell_renderer::types::BackgroundInstance {
1658 position: [shadow_x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1659 size: [edge / w * 2.0, divider.height / h * 2.0],
1660 color: shadow,
1661 });
1662 }
1663 } else {
1664 if is_horizontal {
1666 let half = (divider.height / 2.0).max(1.0);
1667 instances.push(crate::cell_renderer::types::BackgroundInstance {
1668 position: [divider.x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1669 size: [divider.width / w * 2.0, half / h * 2.0],
1670 color: highlight,
1671 });
1672 let bottom_y = divider.y + half;
1673 let bottom_h = divider.height - half;
1674 if bottom_h > 0.0 {
1675 instances.push(crate::cell_renderer::types::BackgroundInstance {
1676 position: [
1677 divider.x / w * 2.0 - 1.0,
1678 1.0 - (bottom_y / h * 2.0),
1679 ],
1680 size: [divider.width / w * 2.0, bottom_h / h * 2.0],
1681 color: shadow,
1682 });
1683 }
1684 } else {
1685 let half = (divider.width / 2.0).max(1.0);
1686 instances.push(crate::cell_renderer::types::BackgroundInstance {
1687 position: [divider.x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
1688 size: [half / w * 2.0, divider.height / h * 2.0],
1689 color: highlight,
1690 });
1691 let right_x = divider.x + half;
1692 let right_w = divider.width - half;
1693 if right_w > 0.0 {
1694 instances.push(crate::cell_renderer::types::BackgroundInstance {
1695 position: [
1696 right_x / w * 2.0 - 1.0,
1697 1.0 - (divider.y / h * 2.0),
1698 ],
1699 size: [right_w / w * 2.0, divider.height / h * 2.0],
1700 color: shadow,
1701 });
1702 }
1703 }
1704 }
1705 }
1706 }
1707 }
1708
1709 self.cell_renderer.queue().write_buffer(
1711 &self.cell_renderer.bg_instance_buffer,
1712 0,
1713 bytemuck::cast_slice(&instances),
1714 );
1715
1716 let mut encoder =
1718 self.cell_renderer
1719 .device()
1720 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1721 label: Some("divider render encoder"),
1722 });
1723
1724 {
1725 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1726 label: Some("divider render pass"),
1727 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1728 view: surface_view,
1729 resolve_target: None,
1730 ops: wgpu::Operations {
1731 load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store,
1733 },
1734 depth_slice: None,
1735 })],
1736 depth_stencil_attachment: None,
1737 timestamp_writes: None,
1738 occlusion_query_set: None,
1739 });
1740
1741 render_pass.set_pipeline(&self.cell_renderer.bg_pipeline);
1742 render_pass.set_vertex_buffer(0, self.cell_renderer.vertex_buffer.slice(..));
1743 render_pass.set_vertex_buffer(1, self.cell_renderer.bg_instance_buffer.slice(..));
1744 render_pass.draw(0..4, 0..instances.len() as u32);
1745 }
1746
1747 self.cell_renderer
1748 .queue()
1749 .submit(std::iter::once(encoder.finish()));
1750 Ok(())
1751 }
1752
1753 #[allow(dead_code)]
1762 pub fn render_focus_indicator(
1763 &mut self,
1764 surface_view: &wgpu::TextureView,
1765 viewport: &PaneViewport,
1766 settings: &PaneDividerSettings,
1767 ) -> Result<()> {
1768 if !settings.show_focus_indicator {
1769 return Ok(());
1770 }
1771
1772 let border_w = settings.focus_width;
1773 let color = [
1774 settings.focus_color[0],
1775 settings.focus_color[1],
1776 settings.focus_color[2],
1777 1.0,
1778 ];
1779
1780 let instances = vec![
1782 crate::cell_renderer::types::BackgroundInstance {
1784 position: [
1785 viewport.x / self.size.width as f32 * 2.0 - 1.0,
1786 1.0 - (viewport.y / self.size.height as f32 * 2.0),
1787 ],
1788 size: [
1789 viewport.width / self.size.width as f32 * 2.0,
1790 border_w / self.size.height as f32 * 2.0,
1791 ],
1792 color,
1793 },
1794 crate::cell_renderer::types::BackgroundInstance {
1796 position: [
1797 viewport.x / self.size.width as f32 * 2.0 - 1.0,
1798 1.0 - ((viewport.y + viewport.height - border_w) / self.size.height as f32
1799 * 2.0),
1800 ],
1801 size: [
1802 viewport.width / self.size.width as f32 * 2.0,
1803 border_w / self.size.height as f32 * 2.0,
1804 ],
1805 color,
1806 },
1807 crate::cell_renderer::types::BackgroundInstance {
1809 position: [
1810 viewport.x / self.size.width as f32 * 2.0 - 1.0,
1811 1.0 - ((viewport.y + border_w) / self.size.height as f32 * 2.0),
1812 ],
1813 size: [
1814 border_w / self.size.width as f32 * 2.0,
1815 (viewport.height - border_w * 2.0) / self.size.height as f32 * 2.0,
1816 ],
1817 color,
1818 },
1819 crate::cell_renderer::types::BackgroundInstance {
1821 position: [
1822 (viewport.x + viewport.width - border_w) / self.size.width as f32 * 2.0 - 1.0,
1823 1.0 - ((viewport.y + border_w) / self.size.height as f32 * 2.0),
1824 ],
1825 size: [
1826 border_w / self.size.width as f32 * 2.0,
1827 (viewport.height - border_w * 2.0) / self.size.height as f32 * 2.0,
1828 ],
1829 color,
1830 },
1831 ];
1832
1833 self.cell_renderer.queue().write_buffer(
1835 &self.cell_renderer.bg_instance_buffer,
1836 0,
1837 bytemuck::cast_slice(&instances),
1838 );
1839
1840 let mut encoder =
1842 self.cell_renderer
1843 .device()
1844 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1845 label: Some("focus indicator encoder"),
1846 });
1847
1848 {
1849 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1850 label: Some("focus indicator pass"),
1851 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1852 view: surface_view,
1853 resolve_target: None,
1854 ops: wgpu::Operations {
1855 load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store,
1857 },
1858 depth_slice: None,
1859 })],
1860 depth_stencil_attachment: None,
1861 timestamp_writes: None,
1862 occlusion_query_set: None,
1863 });
1864
1865 render_pass.set_pipeline(&self.cell_renderer.bg_pipeline);
1866 render_pass.set_vertex_buffer(0, self.cell_renderer.vertex_buffer.slice(..));
1867 render_pass.set_vertex_buffer(1, self.cell_renderer.bg_instance_buffer.slice(..));
1868 render_pass.draw(0..4, 0..instances.len() as u32);
1869 }
1870
1871 self.cell_renderer
1872 .queue()
1873 .submit(std::iter::once(encoder.finish()));
1874 Ok(())
1875 }
1876
1877 #[allow(dead_code)]
1882 pub fn render_pane_titles(
1883 &mut self,
1884 surface_view: &wgpu::TextureView,
1885 titles: &[PaneTitleInfo],
1886 ) -> Result<()> {
1887 if titles.is_empty() {
1888 return Ok(());
1889 }
1890
1891 let width = self.size.width as f32;
1892 let height = self.size.height as f32;
1893
1894 let mut bg_instances = Vec::with_capacity(titles.len());
1896 for title in titles {
1897 let x_ndc = title.x / width * 2.0 - 1.0;
1898 let y_ndc = 1.0 - (title.y / height * 2.0);
1899 let w_ndc = title.width / width * 2.0;
1900 let h_ndc = title.height / height * 2.0;
1901
1902 let brightness = if title.focused { 1.0 } else { 0.7 };
1905
1906 bg_instances.push(crate::cell_renderer::types::BackgroundInstance {
1907 position: [x_ndc, y_ndc],
1908 size: [w_ndc, h_ndc],
1909 color: [
1910 title.bg_color[0] * brightness,
1911 title.bg_color[1] * brightness,
1912 title.bg_color[2] * brightness,
1913 1.0, ],
1915 });
1916 }
1917
1918 self.cell_renderer.queue().write_buffer(
1920 &self.cell_renderer.bg_instance_buffer,
1921 0,
1922 bytemuck::cast_slice(&bg_instances),
1923 );
1924
1925 let mut encoder =
1927 self.cell_renderer
1928 .device()
1929 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1930 label: Some("pane title bg encoder"),
1931 });
1932
1933 {
1934 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1935 label: Some("pane title bg pass"),
1936 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1937 view: surface_view,
1938 resolve_target: None,
1939 ops: wgpu::Operations {
1940 load: wgpu::LoadOp::Load,
1941 store: wgpu::StoreOp::Store,
1942 },
1943 depth_slice: None,
1944 })],
1945 depth_stencil_attachment: None,
1946 timestamp_writes: None,
1947 occlusion_query_set: None,
1948 });
1949
1950 render_pass.set_pipeline(&self.cell_renderer.bg_pipeline);
1951 render_pass.set_vertex_buffer(0, self.cell_renderer.vertex_buffer.slice(..));
1952 render_pass.set_vertex_buffer(1, self.cell_renderer.bg_instance_buffer.slice(..));
1953 render_pass.draw(0..4, 0..bg_instances.len() as u32);
1954 }
1955
1956 self.cell_renderer
1957 .queue()
1958 .submit(std::iter::once(encoder.finish()));
1959
1960 let mut text_instances = Vec::new();
1962 let baseline_y = self.cell_renderer.font_ascent;
1963
1964 for title in titles {
1965 let title_text = &title.title;
1966 if title_text.is_empty() {
1967 continue;
1968 }
1969
1970 let padding_x = 8.0;
1972 let mut x_pos = title.x + padding_x;
1973 let y_base = title.y + (title.height - self.cell_renderer.cell_height) / 2.0;
1974
1975 let text_color = [
1976 title.text_color[0],
1977 title.text_color[1],
1978 title.text_color[2],
1979 if title.focused { 1.0 } else { 0.8 },
1980 ];
1981
1982 let max_chars =
1984 ((title.width - padding_x * 2.0) / self.cell_renderer.cell_width) as usize;
1985 let display_text: String = if title_text.len() > max_chars && max_chars > 3 {
1986 let truncated: String = title_text.chars().take(max_chars - 1).collect();
1987 format!("{}\u{2026}", truncated) } else {
1989 title_text.clone()
1990 };
1991
1992 for ch in display_text.chars() {
1993 if x_pos >= title.x + title.width - padding_x {
1994 break;
1995 }
1996
1997 if let Some((font_idx, glyph_id)) =
1998 self.cell_renderer.font_manager.find_glyph(ch, false, false)
1999 {
2000 let cache_key = ((font_idx as u64) << 32) | (glyph_id as u64);
2001 let force_monochrome = crate::cell_renderer::atlas::should_render_as_symbol(ch);
2003 let info = if self.cell_renderer.glyph_cache.contains_key(&cache_key) {
2004 self.cell_renderer.lru_remove(cache_key);
2005 self.cell_renderer.lru_push_front(cache_key);
2006 self.cell_renderer
2007 .glyph_cache
2008 .get(&cache_key)
2009 .unwrap()
2010 .clone()
2011 } else if let Some(raster) =
2012 self.cell_renderer
2013 .rasterize_glyph(font_idx, glyph_id, force_monochrome)
2014 {
2015 let info = self.cell_renderer.upload_glyph(cache_key, &raster);
2016 self.cell_renderer
2017 .glyph_cache
2018 .insert(cache_key, info.clone());
2019 self.cell_renderer.lru_push_front(cache_key);
2020 info
2021 } else {
2022 x_pos += self.cell_renderer.cell_width;
2023 continue;
2024 };
2025
2026 let glyph_left = x_pos + info.bearing_x;
2027 let glyph_top = y_base + (baseline_y - info.bearing_y);
2028
2029 text_instances.push(crate::cell_renderer::types::TextInstance {
2030 position: [
2031 glyph_left / width * 2.0 - 1.0,
2032 1.0 - (glyph_top / height * 2.0),
2033 ],
2034 size: [
2035 info.width as f32 / width * 2.0,
2036 info.height as f32 / height * 2.0,
2037 ],
2038 tex_offset: [info.x as f32 / 2048.0, info.y as f32 / 2048.0],
2039 tex_size: [info.width as f32 / 2048.0, info.height as f32 / 2048.0],
2040 color: text_color,
2041 is_colored: if info.is_colored { 1 } else { 0 },
2042 });
2043 }
2044
2045 x_pos += self.cell_renderer.cell_width;
2046 }
2047 }
2048
2049 if text_instances.is_empty() {
2050 return Ok(());
2051 }
2052
2053 self.cell_renderer.queue().write_buffer(
2055 &self.cell_renderer.text_instance_buffer,
2056 0,
2057 bytemuck::cast_slice(&text_instances),
2058 );
2059
2060 let mut encoder =
2062 self.cell_renderer
2063 .device()
2064 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
2065 label: Some("pane title text encoder"),
2066 });
2067
2068 {
2069 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2070 label: Some("pane title text pass"),
2071 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2072 view: surface_view,
2073 resolve_target: None,
2074 ops: wgpu::Operations {
2075 load: wgpu::LoadOp::Load,
2076 store: wgpu::StoreOp::Store,
2077 },
2078 depth_slice: None,
2079 })],
2080 depth_stencil_attachment: None,
2081 timestamp_writes: None,
2082 occlusion_query_set: None,
2083 });
2084
2085 render_pass.set_pipeline(&self.cell_renderer.text_pipeline);
2086 render_pass.set_bind_group(0, &self.cell_renderer.text_bind_group, &[]);
2087 render_pass.set_vertex_buffer(0, self.cell_renderer.vertex_buffer.slice(..));
2088 render_pass.set_vertex_buffer(1, self.cell_renderer.text_instance_buffer.slice(..));
2089 render_pass.draw(0..4, 0..text_instances.len() as u32);
2090 }
2091
2092 self.cell_renderer
2093 .queue()
2094 .submit(std::iter::once(encoder.finish()));
2095
2096 Ok(())
2097 }
2098
2099 fn render_egui(
2101 &mut self,
2102 surface_texture: &wgpu::SurfaceTexture,
2103 egui_output: egui::FullOutput,
2104 egui_ctx: &egui::Context,
2105 force_opaque: bool,
2106 ) -> Result<()> {
2107 use wgpu::TextureViewDescriptor;
2108
2109 let view = surface_texture
2111 .texture
2112 .create_view(&TextureViewDescriptor::default());
2113
2114 let mut encoder =
2116 self.cell_renderer
2117 .device()
2118 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
2119 label: Some("egui encoder"),
2120 });
2121
2122 let screen_descriptor = egui_wgpu::ScreenDescriptor {
2124 size_in_pixels: [self.size.width, self.size.height],
2125 pixels_per_point: egui_output.pixels_per_point,
2126 };
2127
2128 for (id, image_delta) in &egui_output.textures_delta.set {
2130 self.egui_renderer.update_texture(
2131 self.cell_renderer.device(),
2132 self.cell_renderer.queue(),
2133 *id,
2134 image_delta,
2135 );
2136 }
2137
2138 let mut paint_jobs = egui_ctx.tessellate(egui_output.shapes, egui_output.pixels_per_point);
2140
2141 if force_opaque {
2143 for job in paint_jobs.iter_mut() {
2144 match &mut job.primitive {
2145 egui::epaint::Primitive::Mesh(mesh) => {
2146 for v in mesh.vertices.iter_mut() {
2147 v.color[3] = 255;
2148 }
2149 }
2150 egui::epaint::Primitive::Callback(_) => {}
2151 }
2152 }
2153 }
2154
2155 self.egui_renderer.update_buffers(
2157 self.cell_renderer.device(),
2158 self.cell_renderer.queue(),
2159 &mut encoder,
2160 &paint_jobs,
2161 &screen_descriptor,
2162 );
2163
2164 {
2166 let render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
2167 label: Some("egui render pass"),
2168 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
2169 view: &view,
2170 resolve_target: None,
2171 ops: wgpu::Operations {
2172 load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store,
2174 },
2175 depth_slice: None,
2176 })],
2177 depth_stencil_attachment: None,
2178 timestamp_writes: None,
2179 occlusion_query_set: None,
2180 });
2181
2182 let mut render_pass = render_pass.forget_lifetime();
2184
2185 self.egui_renderer
2186 .render(&mut render_pass, &paint_jobs, &screen_descriptor);
2187 } self.cell_renderer
2191 .queue()
2192 .submit(std::iter::once(encoder.finish()));
2193
2194 for id in &egui_output.textures_delta.free {
2196 self.egui_renderer.free_texture(id);
2197 }
2198
2199 Ok(())
2200 }
2201
2202 pub fn size(&self) -> PhysicalSize<u32> {
2204 self.size
2205 }
2206
2207 pub fn grid_size(&self) -> (usize, usize) {
2209 self.cell_renderer.grid_size()
2210 }
2211
2212 pub fn cell_width(&self) -> f32 {
2214 self.cell_renderer.cell_width()
2215 }
2216
2217 pub fn cell_height(&self) -> f32 {
2219 self.cell_renderer.cell_height()
2220 }
2221
2222 pub fn window_padding(&self) -> f32 {
2224 self.cell_renderer.window_padding()
2225 }
2226
2227 pub fn content_offset_y(&self) -> f32 {
2229 self.cell_renderer.content_offset_y()
2230 }
2231
2232 pub fn scale_factor(&self) -> f32 {
2234 self.cell_renderer.scale_factor
2235 }
2236
2237 pub fn set_content_offset_y(&mut self, logical_offset: f32) -> Option<(usize, usize)> {
2243 let physical_offset = logical_offset * self.cell_renderer.scale_factor;
2245 let result = self.cell_renderer.set_content_offset_y(physical_offset);
2246 self.graphics_renderer.set_content_offset_y(physical_offset);
2248 if let Some(ref mut custom_shader) = self.custom_shader_renderer {
2250 custom_shader.set_content_offset_y(physical_offset);
2251 }
2252 if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
2254 cursor_shader.set_content_offset_y(physical_offset);
2255 }
2256 if result.is_some() {
2257 self.dirty = true;
2258 }
2259 result
2260 }
2261
2262 pub fn content_offset_x(&self) -> f32 {
2264 self.cell_renderer.content_offset_x()
2265 }
2266
2267 pub fn set_content_offset_x(&mut self, logical_offset: f32) -> Option<(usize, usize)> {
2270 let physical_offset = logical_offset * self.cell_renderer.scale_factor;
2271 let result = self.cell_renderer.set_content_offset_x(physical_offset);
2272 self.graphics_renderer.set_content_offset_x(physical_offset);
2273 if let Some(ref mut custom_shader) = self.custom_shader_renderer {
2274 custom_shader.set_content_offset_x(physical_offset);
2275 }
2276 if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
2277 cursor_shader.set_content_offset_x(physical_offset);
2278 }
2279 if result.is_some() {
2280 self.dirty = true;
2281 }
2282 result
2283 }
2284
2285 pub fn content_inset_bottom(&self) -> f32 {
2287 self.cell_renderer.content_inset_bottom()
2288 }
2289
2290 pub fn content_inset_right(&self) -> f32 {
2292 self.cell_renderer.content_inset_right()
2293 }
2294
2295 pub fn set_content_inset_bottom(&mut self, logical_inset: f32) -> Option<(usize, usize)> {
2298 let physical_inset = logical_inset * self.cell_renderer.scale_factor;
2299 let result = self.cell_renderer.set_content_inset_bottom(physical_inset);
2300 if result.is_some() {
2301 self.dirty = true;
2302 }
2303 result
2304 }
2305
2306 pub fn set_content_inset_right(&mut self, logical_inset: f32) -> Option<(usize, usize)> {
2309 let physical_inset = logical_inset * self.cell_renderer.scale_factor;
2310 let result = self.cell_renderer.set_content_inset_right(physical_inset);
2311
2312 if let Some(ref mut custom_shader) = self.custom_shader_renderer {
2314 custom_shader.set_content_inset_right(physical_inset);
2315 }
2316 if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
2318 cursor_shader.set_content_inset_right(physical_inset);
2319 }
2320
2321 if result.is_some() {
2322 self.dirty = true;
2323 }
2324 result
2325 }
2326
2327 pub fn set_egui_bottom_inset(&mut self, logical_inset: f32) -> Option<(usize, usize)> {
2333 let physical_inset = logical_inset * self.cell_renderer.scale_factor;
2334 if (self.cell_renderer.egui_bottom_inset - physical_inset).abs() > f32::EPSILON {
2335 self.cell_renderer.egui_bottom_inset = physical_inset;
2336 let (w, h) = (
2337 self.cell_renderer.config.width,
2338 self.cell_renderer.config.height,
2339 );
2340 return Some(self.cell_renderer.resize(w, h));
2341 }
2342 None
2343 }
2344
2345 pub fn set_egui_right_inset(&mut self, logical_inset: f32) {
2351 let physical_inset = logical_inset * self.cell_renderer.scale_factor;
2352 self.cell_renderer.egui_right_inset = physical_inset;
2353 }
2354
2355 pub fn scrollbar_contains_point(&self, x: f32, y: f32) -> bool {
2361 self.cell_renderer.scrollbar_contains_point(x, y)
2362 }
2363
2364 pub fn scrollbar_thumb_bounds(&self) -> Option<(f32, f32)> {
2366 self.cell_renderer.scrollbar_thumb_bounds()
2367 }
2368
2369 pub fn scrollbar_track_contains_x(&self, x: f32) -> bool {
2371 self.cell_renderer.scrollbar_track_contains_x(x)
2372 }
2373
2374 pub fn scrollbar_mouse_y_to_scroll_offset(&self, mouse_y: f32) -> Option<usize> {
2382 self.cell_renderer
2383 .scrollbar_mouse_y_to_scroll_offset(mouse_y)
2384 }
2385
2386 pub fn scrollbar_mark_at_position(
2396 &self,
2397 mouse_x: f32,
2398 mouse_y: f32,
2399 tolerance: f32,
2400 ) -> Option<&par_term_config::ScrollbackMark> {
2401 self.cell_renderer
2402 .scrollbar_mark_at_position(mouse_x, mouse_y, tolerance)
2403 }
2404
2405 #[allow(dead_code)]
2407 pub fn is_dirty(&self) -> bool {
2408 self.dirty
2409 }
2410
2411 #[allow(dead_code)]
2413 pub fn mark_dirty(&mut self) {
2414 self.dirty = true;
2415 }
2416
2417 #[allow(dead_code)]
2419 #[allow(dead_code)]
2420 pub fn render_debug_overlay(&mut self, text: &str) {
2421 self.debug_text = Some(text.to_string());
2422 self.dirty = true; }
2424
2425 pub fn reconfigure_surface(&mut self) {
2428 self.cell_renderer.reconfigure_surface();
2429 self.dirty = true;
2430 }
2431
2432 pub fn is_vsync_mode_supported(&self, mode: par_term_config::VsyncMode) -> bool {
2434 self.cell_renderer.is_vsync_mode_supported(mode)
2435 }
2436
2437 pub fn update_vsync_mode(
2440 &mut self,
2441 mode: par_term_config::VsyncMode,
2442 ) -> (par_term_config::VsyncMode, bool) {
2443 let result = self.cell_renderer.update_vsync_mode(mode);
2444 if result.1 {
2445 self.dirty = true;
2446 }
2447 result
2448 }
2449
2450 #[allow(dead_code)]
2452 pub fn current_vsync_mode(&self) -> par_term_config::VsyncMode {
2453 self.cell_renderer.current_vsync_mode()
2454 }
2455
2456 pub fn clear_glyph_cache(&mut self) {
2459 self.cell_renderer.clear_glyph_cache();
2460 self.dirty = true;
2461 }
2462
2463 pub fn update_font_antialias(&mut self, enabled: bool) -> bool {
2466 let changed = self.cell_renderer.update_font_antialias(enabled);
2467 if changed {
2468 self.dirty = true;
2469 }
2470 changed
2471 }
2472
2473 pub fn update_font_hinting(&mut self, enabled: bool) -> bool {
2476 let changed = self.cell_renderer.update_font_hinting(enabled);
2477 if changed {
2478 self.dirty = true;
2479 }
2480 changed
2481 }
2482
2483 pub fn update_font_thin_strokes(&mut self, mode: par_term_config::ThinStrokesMode) -> bool {
2486 let changed = self.cell_renderer.update_font_thin_strokes(mode);
2487 if changed {
2488 self.dirty = true;
2489 }
2490 changed
2491 }
2492
2493 pub fn update_minimum_contrast(&mut self, ratio: f32) -> bool {
2496 let changed = self.cell_renderer.update_minimum_contrast(ratio);
2497 if changed {
2498 self.dirty = true;
2499 }
2500 changed
2501 }
2502
2503 pub fn pause_shader_animations(&mut self) {
2506 if let Some(ref mut custom_shader) = self.custom_shader_renderer {
2507 custom_shader.set_animation_enabled(false);
2508 }
2509 if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
2510 cursor_shader.set_animation_enabled(false);
2511 }
2512 log::info!("[SHADER] Shader animations paused");
2513 }
2514
2515 pub fn resume_shader_animations(
2518 &mut self,
2519 custom_shader_animation: bool,
2520 cursor_shader_animation: bool,
2521 ) {
2522 if let Some(ref mut custom_shader) = self.custom_shader_renderer {
2523 custom_shader.set_animation_enabled(custom_shader_animation);
2524 }
2525 if let Some(ref mut cursor_shader) = self.cursor_shader_renderer {
2526 cursor_shader.set_animation_enabled(cursor_shader_animation);
2527 }
2528 self.dirty = true;
2529 log::info!(
2530 "[SHADER] Shader animations resumed (custom: {}, cursor: {})",
2531 custom_shader_animation,
2532 cursor_shader_animation
2533 );
2534 }
2535
2536 pub fn take_screenshot(&mut self) -> Result<image::RgbaImage> {
2541 log::info!(
2542 "take_screenshot: Starting screenshot capture ({}x{})",
2543 self.size.width,
2544 self.size.height
2545 );
2546
2547 let width = self.size.width;
2548 let height = self.size.height;
2549 let format = self.cell_renderer.surface_format();
2551 log::info!("take_screenshot: Using texture format {:?}", format);
2552
2553 let screenshot_texture =
2555 self.cell_renderer
2556 .device()
2557 .create_texture(&wgpu::TextureDescriptor {
2558 label: Some("screenshot texture"),
2559 size: wgpu::Extent3d {
2560 width,
2561 height,
2562 depth_or_array_layers: 1,
2563 },
2564 mip_level_count: 1,
2565 sample_count: 1,
2566 dimension: wgpu::TextureDimension::D2,
2567 format,
2568 usage: wgpu::TextureUsages::RENDER_ATTACHMENT | wgpu::TextureUsages::COPY_SRC,
2569 view_formats: &[],
2570 });
2571
2572 let screenshot_view =
2573 screenshot_texture.create_view(&wgpu::TextureViewDescriptor::default());
2574
2575 log::info!("take_screenshot: Rendering composited frame...");
2577
2578 let has_custom_shader = self.custom_shader_renderer.is_some();
2580 let use_cursor_shader =
2581 self.cursor_shader_renderer.is_some() && !self.cursor_shader_disabled_for_alt_screen;
2582
2583 if has_custom_shader {
2584 let intermediate_view = self
2586 .custom_shader_renderer
2587 .as_ref()
2588 .unwrap()
2589 .intermediate_texture_view()
2590 .clone();
2591 self.cell_renderer
2592 .render_to_texture(&intermediate_view, true)?;
2593
2594 if use_cursor_shader {
2595 let cursor_intermediate = self
2597 .cursor_shader_renderer
2598 .as_ref()
2599 .unwrap()
2600 .intermediate_texture_view()
2601 .clone();
2602 self.custom_shader_renderer.as_mut().unwrap().render(
2603 self.cell_renderer.device(),
2604 self.cell_renderer.queue(),
2605 &cursor_intermediate,
2606 false,
2607 )?;
2608 self.cursor_shader_renderer.as_mut().unwrap().render(
2610 self.cell_renderer.device(),
2611 self.cell_renderer.queue(),
2612 &screenshot_view,
2613 true,
2614 )?;
2615 } else {
2616 self.custom_shader_renderer.as_mut().unwrap().render(
2618 self.cell_renderer.device(),
2619 self.cell_renderer.queue(),
2620 &screenshot_view,
2621 true,
2622 )?;
2623 }
2624 } else if use_cursor_shader {
2625 let cursor_intermediate = self
2627 .cursor_shader_renderer
2628 .as_ref()
2629 .unwrap()
2630 .intermediate_texture_view()
2631 .clone();
2632 self.cell_renderer
2633 .render_to_texture(&cursor_intermediate, true)?;
2634 self.cursor_shader_renderer.as_mut().unwrap().render(
2636 self.cell_renderer.device(),
2637 self.cell_renderer.queue(),
2638 &screenshot_view,
2639 true,
2640 )?;
2641 } else {
2642 self.cell_renderer.render_to_view(&screenshot_view)?;
2644 }
2645
2646 log::info!("take_screenshot: Render complete");
2647
2648 let device = self.cell_renderer.device();
2650 let queue = self.cell_renderer.queue();
2651
2652 let bytes_per_pixel = 4u32;
2654 let unpadded_bytes_per_row = width * bytes_per_pixel;
2655 let align = wgpu::COPY_BYTES_PER_ROW_ALIGNMENT;
2657 let padded_bytes_per_row = unpadded_bytes_per_row.div_ceil(align) * align;
2658 let buffer_size = (padded_bytes_per_row * height) as u64;
2659
2660 let output_buffer = device.create_buffer(&wgpu::BufferDescriptor {
2661 label: Some("screenshot buffer"),
2662 size: buffer_size,
2663 usage: wgpu::BufferUsages::COPY_DST | wgpu::BufferUsages::MAP_READ,
2664 mapped_at_creation: false,
2665 });
2666
2667 let mut encoder = device.create_command_encoder(&wgpu::CommandEncoderDescriptor {
2669 label: Some("screenshot encoder"),
2670 });
2671
2672 encoder.copy_texture_to_buffer(
2673 wgpu::TexelCopyTextureInfo {
2674 texture: &screenshot_texture,
2675 mip_level: 0,
2676 origin: wgpu::Origin3d::ZERO,
2677 aspect: wgpu::TextureAspect::All,
2678 },
2679 wgpu::TexelCopyBufferInfo {
2680 buffer: &output_buffer,
2681 layout: wgpu::TexelCopyBufferLayout {
2682 offset: 0,
2683 bytes_per_row: Some(padded_bytes_per_row),
2684 rows_per_image: Some(height),
2685 },
2686 },
2687 wgpu::Extent3d {
2688 width,
2689 height,
2690 depth_or_array_layers: 1,
2691 },
2692 );
2693
2694 queue.submit(std::iter::once(encoder.finish()));
2695 log::info!("take_screenshot: Texture copy submitted");
2696
2697 let buffer_slice = output_buffer.slice(..);
2699 let (tx, rx) = std::sync::mpsc::channel();
2700 buffer_slice.map_async(wgpu::MapMode::Read, move |result| {
2701 let _ = tx.send(result);
2702 });
2703
2704 log::info!("take_screenshot: Waiting for GPU...");
2706 let _ = device.poll(wgpu::PollType::wait_indefinitely());
2707 log::info!("take_screenshot: GPU poll complete, waiting for buffer map...");
2708 rx.recv()
2709 .map_err(|e| anyhow::anyhow!("Failed to receive map result: {}", e))?
2710 .map_err(|e| anyhow::anyhow!("Failed to map buffer: {:?}", e))?;
2711 log::info!("take_screenshot: Buffer mapped successfully");
2712
2713 let data = buffer_slice.get_mapped_range();
2715 let mut pixels = Vec::with_capacity((width * height * 4) as usize);
2716
2717 let is_bgra = matches!(
2719 format,
2720 wgpu::TextureFormat::Bgra8Unorm | wgpu::TextureFormat::Bgra8UnormSrgb
2721 );
2722
2723 for y in 0..height {
2725 let row_start = (y * padded_bytes_per_row) as usize;
2726 let row_end = row_start + (width * bytes_per_pixel) as usize;
2727 let row = &data[row_start..row_end];
2728
2729 if is_bgra {
2730 for chunk in row.chunks(4) {
2732 pixels.push(chunk[2]); pixels.push(chunk[1]); pixels.push(chunk[0]); pixels.push(chunk[3]); }
2737 } else {
2738 pixels.extend_from_slice(row);
2740 }
2741 }
2742
2743 drop(data);
2744 output_buffer.unmap();
2745
2746 image::RgbaImage::from_raw(width, height, pixels)
2748 .ok_or_else(|| anyhow::anyhow!("Failed to create image from pixel data"))
2749 }
2750}