1use anyhow::{Context, Result};
2use std::collections::HashMap;
3use std::sync::Arc;
4use winit::window::Window;
5
6use crate::scrollbar::Scrollbar;
7use par_term_config::SeparatorMark;
8use par_term_fonts::font_manager::FontManager;
9
10pub mod atlas;
11pub mod background;
12pub mod block_chars;
13pub mod pipeline;
14pub mod render;
15pub mod types;
16pub use types::{Cell, PaneViewport};
18pub(crate) use types::{BackgroundInstance, GlyphInfo, RowCacheEntry, TextInstance};
20
21pub struct CellRenderer {
22 pub(crate) device: Arc<wgpu::Device>,
23 pub(crate) queue: Arc<wgpu::Queue>,
24 pub(crate) surface: wgpu::Surface<'static>,
25 pub(crate) config: wgpu::SurfaceConfiguration,
26 pub(crate) supported_present_modes: Vec<wgpu::PresentMode>,
28
29 pub(crate) bg_pipeline: wgpu::RenderPipeline,
31 pub(crate) text_pipeline: wgpu::RenderPipeline,
32 pub(crate) bg_image_pipeline: wgpu::RenderPipeline,
33 #[allow(dead_code)]
34 pub(crate) visual_bell_pipeline: wgpu::RenderPipeline,
35
36 pub(crate) vertex_buffer: wgpu::Buffer,
38 pub(crate) bg_instance_buffer: wgpu::Buffer,
39 pub(crate) text_instance_buffer: wgpu::Buffer,
40 pub(crate) bg_image_uniform_buffer: wgpu::Buffer,
41 #[allow(dead_code)]
42 pub(crate) visual_bell_uniform_buffer: wgpu::Buffer,
43
44 pub(crate) text_bind_group: wgpu::BindGroup,
46 #[allow(dead_code)]
47 pub(crate) text_bind_group_layout: wgpu::BindGroupLayout,
48 pub(crate) bg_image_bind_group: Option<wgpu::BindGroup>,
49 pub(crate) bg_image_bind_group_layout: wgpu::BindGroupLayout,
50 #[allow(dead_code)]
51 pub(crate) visual_bell_bind_group: wgpu::BindGroup,
52
53 pub(crate) atlas_texture: wgpu::Texture,
55 #[allow(dead_code)]
56 pub(crate) atlas_view: wgpu::TextureView,
57 pub(crate) glyph_cache: HashMap<u64, GlyphInfo>,
58 pub(crate) lru_head: Option<u64>,
59 pub(crate) lru_tail: Option<u64>,
60 pub(crate) atlas_next_x: u32,
61 pub(crate) atlas_next_y: u32,
62 pub(crate) atlas_row_height: u32,
63
64 pub(crate) cols: usize,
66 pub(crate) rows: usize,
67 pub(crate) cell_width: f32,
68 pub(crate) cell_height: f32,
69 pub(crate) window_padding: f32,
70 pub(crate) content_offset_y: f32,
73 pub(crate) content_offset_x: f32,
76 pub(crate) content_inset_bottom: f32,
79 pub(crate) content_inset_right: f32,
82 pub(crate) egui_bottom_inset: f32,
86 pub(crate) egui_right_inset: f32,
90 #[allow(dead_code)]
91 pub(crate) scale_factor: f32,
92
93 pub(crate) font_manager: FontManager,
95 pub(crate) scrollbar: Scrollbar,
96
97 pub(crate) cells: Vec<Cell>,
99 pub(crate) dirty_rows: Vec<bool>,
100 pub(crate) row_cache: Vec<Option<RowCacheEntry>>,
101 pub(crate) cursor_pos: (usize, usize),
102 pub(crate) cursor_opacity: f32,
103 pub(crate) cursor_style: par_term_emu_core_rust::cursor::CursorStyle,
104 pub(crate) cursor_overlay: Option<BackgroundInstance>,
106 pub(crate) cursor_color: [f32; 3],
108 pub(crate) cursor_text_color: Option<[f32; 3]>,
110 pub(crate) cursor_hidden_for_shader: bool,
112 pub(crate) is_focused: bool,
114
115 pub(crate) cursor_guide_enabled: bool,
118 pub(crate) cursor_guide_color: [f32; 4],
120 pub(crate) cursor_shadow_enabled: bool,
122 pub(crate) cursor_shadow_color: [f32; 4],
124 pub(crate) cursor_shadow_offset: [f32; 2],
126 #[allow(dead_code)]
128 pub(crate) cursor_shadow_blur: f32,
129 pub(crate) cursor_boost: f32,
131 pub(crate) cursor_boost_color: [f32; 3],
133 pub(crate) unfocused_cursor_style: par_term_config::UnfocusedCursorStyle,
135 pub(crate) visual_bell_intensity: f32,
136 pub(crate) window_opacity: f32,
137 pub(crate) background_color: [f32; 4],
138
139 pub(crate) base_font_size: f32,
141 pub(crate) line_spacing: f32,
142 pub(crate) char_spacing: f32,
143
144 pub(crate) font_ascent: f32,
146 pub(crate) font_descent: f32,
147 pub(crate) font_leading: f32,
148 pub(crate) font_size_pixels: f32,
149 pub(crate) char_advance: f32,
150
151 pub(crate) bg_image_texture: Option<wgpu::Texture>,
153 pub(crate) bg_image_mode: par_term_config::BackgroundImageMode,
154 pub(crate) bg_image_opacity: f32,
155 pub(crate) bg_image_width: u32,
156 pub(crate) bg_image_height: u32,
157 pub(crate) bg_is_solid_color: bool,
161 pub(crate) solid_bg_color: [f32; 3],
164
165 pub(crate) pane_bg_cache: HashMap<String, background::PaneBackgroundEntry>,
167
168 pub(crate) max_bg_instances: usize,
170 pub(crate) max_text_instances: usize,
171
172 pub(crate) bg_instances: Vec<BackgroundInstance>,
174 pub(crate) text_instances: Vec<TextInstance>,
175
176 #[allow(dead_code)]
178 pub(crate) enable_text_shaping: bool,
179 pub(crate) enable_ligatures: bool,
180 pub(crate) enable_kerning: bool,
181
182 pub(crate) font_antialias: bool,
185 pub(crate) font_hinting: bool,
187 pub(crate) font_thin_strokes: par_term_config::ThinStrokesMode,
189 pub(crate) minimum_contrast: f32,
192
193 pub(crate) solid_pixel_offset: (u32, u32),
195
196 pub(crate) transparency_affects_only_default_background: bool,
200
201 pub(crate) keep_text_opaque: bool,
203
204 pub(crate) link_underline_style: par_term_config::LinkUnderlineStyle,
206
207 pub(crate) command_separator_enabled: bool,
210 pub(crate) command_separator_thickness: f32,
212 pub(crate) command_separator_opacity: f32,
214 pub(crate) command_separator_exit_color: bool,
216 pub(crate) command_separator_color: [f32; 3],
218 pub(crate) visible_separator_marks: Vec<SeparatorMark>,
220 pub(crate) gutter_indicators: Vec<(usize, [f32; 4])>,
222}
223
224impl CellRenderer {
225 #[allow(clippy::too_many_arguments)]
226 pub async fn new(
227 window: Arc<Window>,
228 font_family: Option<&str>,
229 font_family_bold: Option<&str>,
230 font_family_italic: Option<&str>,
231 font_family_bold_italic: Option<&str>,
232 font_ranges: &[par_term_config::FontRange],
233 font_size: f32,
234 cols: usize,
235 rows: usize,
236 window_padding: f32,
237 line_spacing: f32,
238 char_spacing: f32,
239 scrollbar_position: &str,
240 scrollbar_width: f32,
241 scrollbar_thumb_color: [f32; 4],
242 scrollbar_track_color: [f32; 4],
243 enable_text_shaping: bool,
244 enable_ligatures: bool,
245 enable_kerning: bool,
246 font_antialias: bool,
247 font_hinting: bool,
248 font_thin_strokes: par_term_config::ThinStrokesMode,
249 minimum_contrast: f32,
250 vsync_mode: par_term_config::VsyncMode,
251 power_preference: par_term_config::PowerPreference,
252 window_opacity: f32,
253 background_color: [u8; 3],
254 background_image_path: Option<&str>,
255 background_image_mode: par_term_config::BackgroundImageMode,
256 background_image_opacity: f32,
257 ) -> Result<Self> {
258 #[cfg(target_os = "windows")]
267 let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
268 backends: wgpu::Backends::DX12,
269 ..Default::default()
270 });
271 #[cfg(target_os = "macos")]
272 let instance = wgpu::Instance::default();
273 #[cfg(target_os = "linux")]
274 let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
275 backends: wgpu::Backends::VULKAN | wgpu::Backends::GL,
276 ..Default::default()
277 });
278 let surface = instance.create_surface(window.clone())?;
279 let adapter = instance
280 .request_adapter(&wgpu::RequestAdapterOptions {
281 power_preference: power_preference.to_wgpu(),
282 compatible_surface: Some(&surface),
283 force_fallback_adapter: false,
284 })
285 .await
286 .context("Failed to find wgpu adapter")?;
287
288 let (device, queue) = adapter
289 .request_device(&wgpu::DeviceDescriptor {
290 label: Some("device"),
291 required_features: wgpu::Features::empty(),
292 required_limits: wgpu::Limits::default(),
293 memory_hints: wgpu::MemoryHints::default(),
294 ..Default::default()
295 })
296 .await?;
297
298 let device = Arc::new(device);
299 let queue = Arc::new(queue);
300
301 let size = window.inner_size();
302 let surface_caps = surface.get_capabilities(&adapter);
303 let surface_format = surface_caps
304 .formats
305 .iter()
306 .copied()
307 .find(|f| !f.is_srgb())
308 .unwrap_or(surface_caps.formats[0]);
309
310 let supported_present_modes = surface_caps.present_modes.clone();
312
313 let requested_mode = vsync_mode.to_present_mode();
315 let present_mode = if supported_present_modes.contains(&requested_mode) {
316 requested_mode
317 } else {
318 log::warn!(
320 "Requested present mode {:?} not supported (available: {:?}), falling back",
321 requested_mode,
322 supported_present_modes
323 );
324 if supported_present_modes.contains(&wgpu::PresentMode::Fifo) {
325 wgpu::PresentMode::Fifo
326 } else {
327 supported_present_modes[0]
328 }
329 };
330
331 let alpha_mode = if surface_caps
334 .alpha_modes
335 .contains(&wgpu::CompositeAlphaMode::PreMultiplied)
336 {
337 wgpu::CompositeAlphaMode::PreMultiplied
338 } else if surface_caps
339 .alpha_modes
340 .contains(&wgpu::CompositeAlphaMode::PostMultiplied)
341 {
342 wgpu::CompositeAlphaMode::PostMultiplied
343 } else if surface_caps
344 .alpha_modes
345 .contains(&wgpu::CompositeAlphaMode::Auto)
346 {
347 wgpu::CompositeAlphaMode::Auto
348 } else {
349 surface_caps.alpha_modes[0]
350 };
351 log::info!(
352 "Selected alpha mode: {:?} (available: {:?})",
353 alpha_mode,
354 surface_caps.alpha_modes
355 );
356
357 let config = wgpu::SurfaceConfiguration {
358 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
359 format: surface_format,
360 width: size.width.max(1),
361 height: size.height.max(1),
362 present_mode,
363 alpha_mode,
364 view_formats: vec![],
365 desired_maximum_frame_latency: 2,
366 };
367 surface.configure(&device, &config);
368
369 let scale_factor = window.scale_factor() as f32;
370
371 let platform_dpi = if cfg!(target_os = "macos") {
372 72.0
373 } else {
374 96.0
375 };
376
377 let base_font_pixels = font_size * platform_dpi / 72.0;
378 let font_size_pixels = (base_font_pixels * scale_factor).max(1.0);
379
380 let font_manager = FontManager::new(
381 font_family,
382 font_family_bold,
383 font_family_italic,
384 font_family_bold_italic,
385 font_ranges,
386 )?;
387
388 let (font_ascent, font_descent, font_leading, char_advance) = {
390 let primary_font = font_manager.get_font(0).unwrap();
391 let metrics = primary_font.metrics(&[]);
392 let scale = font_size_pixels / metrics.units_per_em as f32;
393 let glyph_id = primary_font.charmap().map('m');
394 let advance = primary_font.glyph_metrics(&[]).advance_width(glyph_id) * scale;
395 (
396 metrics.ascent * scale,
397 metrics.descent * scale,
398 metrics.leading * scale,
399 advance,
400 )
401 };
402
403 let natural_line_height = font_ascent + font_descent + font_leading;
404 let cell_height = (natural_line_height * line_spacing).max(1.0);
405 let cell_width = (char_advance * char_spacing).max(1.0);
406
407 let scrollbar = Scrollbar::new(
408 Arc::clone(&device),
409 surface_format,
410 scrollbar_width,
411 scrollbar_position,
412 scrollbar_thumb_color,
413 scrollbar_track_color,
414 );
415
416 let bg_pipeline = pipeline::create_bg_pipeline(&device, surface_format);
418
419 let (atlas_texture, atlas_view, atlas_sampler) = pipeline::create_atlas(&device);
420 let text_bind_group_layout = pipeline::create_text_bind_group_layout(&device);
421 let text_bind_group = pipeline::create_text_bind_group(
422 &device,
423 &text_bind_group_layout,
424 &atlas_view,
425 &atlas_sampler,
426 );
427 let text_pipeline =
428 pipeline::create_text_pipeline(&device, surface_format, &text_bind_group_layout);
429
430 let bg_image_bind_group_layout = pipeline::create_bg_image_bind_group_layout(&device);
431 let bg_image_pipeline = pipeline::create_bg_image_pipeline(
432 &device,
433 surface_format,
434 &bg_image_bind_group_layout,
435 );
436 let bg_image_uniform_buffer = pipeline::create_bg_image_uniform_buffer(&device);
437
438 let (visual_bell_pipeline, visual_bell_bind_group, _, visual_bell_uniform_buffer) =
439 pipeline::create_visual_bell_pipeline(&device, surface_format);
440
441 let vertex_buffer = pipeline::create_vertex_buffer(&device);
442
443 let max_bg_instances = cols * rows + 10 + rows + rows; let max_text_instances = cols * rows * 2;
446 let (bg_instance_buffer, text_instance_buffer) =
447 pipeline::create_instance_buffers(&device, max_bg_instances, max_text_instances);
448
449 let mut renderer = Self {
450 device,
451 queue,
452 surface,
453 config,
454 supported_present_modes,
455 bg_pipeline,
456 text_pipeline,
457 bg_image_pipeline,
458 visual_bell_pipeline,
459 vertex_buffer,
460 bg_instance_buffer,
461 text_instance_buffer,
462 bg_image_uniform_buffer,
463 visual_bell_uniform_buffer,
464 text_bind_group,
465 text_bind_group_layout,
466 bg_image_bind_group: None,
467 bg_image_bind_group_layout,
468 visual_bell_bind_group,
469 atlas_texture,
470 atlas_view,
471 glyph_cache: HashMap::new(),
472 lru_head: None,
473 lru_tail: None,
474 atlas_next_x: 0,
475 atlas_next_y: 0,
476 atlas_row_height: 0,
477 cols,
478 rows,
479 cell_width,
480 cell_height,
481 window_padding,
482 content_offset_y: 0.0,
483 content_offset_x: 0.0,
484 content_inset_bottom: 0.0,
485 content_inset_right: 0.0,
486 egui_bottom_inset: 0.0,
487 egui_right_inset: 0.0,
488 scale_factor,
489 font_manager,
490 scrollbar,
491 cells: vec![Cell::default(); cols * rows],
492 dirty_rows: vec![true; rows],
493 row_cache: (0..rows).map(|_| None).collect(),
494 cursor_pos: (0, 0),
495 cursor_opacity: 0.0,
496 cursor_style: par_term_emu_core_rust::cursor::CursorStyle::SteadyBlock,
497 cursor_overlay: None,
498 cursor_color: [1.0, 1.0, 1.0],
499 cursor_text_color: None,
500 cursor_hidden_for_shader: false,
501 is_focused: true,
502 cursor_guide_enabled: false,
503 cursor_guide_color: [1.0, 1.0, 1.0, 0.08],
504 cursor_shadow_enabled: false,
505 cursor_shadow_color: [0.0, 0.0, 0.0, 0.5],
506 cursor_shadow_offset: [2.0, 2.0],
507 cursor_shadow_blur: 3.0,
508 cursor_boost: 0.0,
509 cursor_boost_color: [1.0, 1.0, 1.0],
510 unfocused_cursor_style: par_term_config::UnfocusedCursorStyle::default(),
511 visual_bell_intensity: 0.0,
512 window_opacity,
513 background_color: [
514 background_color[0] as f32 / 255.0,
515 background_color[1] as f32 / 255.0,
516 background_color[2] as f32 / 255.0,
517 1.0,
518 ],
519 base_font_size: font_size,
520 line_spacing,
521 char_spacing,
522 font_ascent,
523 font_descent,
524 font_leading,
525 font_size_pixels,
526 char_advance,
527 bg_image_texture: None,
528 bg_image_mode: background_image_mode,
529 bg_image_opacity: background_image_opacity,
530 bg_image_width: 0,
531 bg_image_height: 0,
532 bg_is_solid_color: false,
533 solid_bg_color: [0.0, 0.0, 0.0],
534 pane_bg_cache: HashMap::new(),
535 max_bg_instances,
536 max_text_instances,
537 bg_instances: vec![
538 BackgroundInstance {
539 position: [0.0, 0.0],
540 size: [0.0, 0.0],
541 color: [0.0, 0.0, 0.0, 0.0],
542 };
543 max_bg_instances
544 ],
545 text_instances: vec![
546 TextInstance {
547 position: [0.0, 0.0],
548 size: [0.0, 0.0],
549 tex_offset: [0.0, 0.0],
550 tex_size: [0.0, 0.0],
551 color: [0.0, 0.0, 0.0, 0.0],
552 is_colored: 0,
553 };
554 max_text_instances
555 ],
556 enable_text_shaping,
557 enable_ligatures,
558 enable_kerning,
559 font_antialias,
560 font_hinting,
561 font_thin_strokes,
562 minimum_contrast: minimum_contrast.clamp(1.0, 21.0),
563 solid_pixel_offset: (0, 0),
564 transparency_affects_only_default_background: false,
565 keep_text_opaque: true,
566 link_underline_style: par_term_config::LinkUnderlineStyle::default(),
567 command_separator_enabled: false,
568 command_separator_thickness: 1.0,
569 command_separator_opacity: 0.4,
570 command_separator_exit_color: true,
571 command_separator_color: [0.5, 0.5, 0.5],
572 visible_separator_marks: Vec::new(),
573 gutter_indicators: Vec::new(),
574 };
575
576 renderer.upload_solid_pixel();
578
579 log::info!(
580 "CellRenderer::new: background_image_path={:?}",
581 background_image_path
582 );
583 if let Some(path) = background_image_path {
584 if let Err(e) = renderer.load_background_image(path) {
586 log::warn!(
587 "Could not load background image '{}': {} - continuing without background image",
588 path,
589 e
590 );
591 }
592 }
593
594 Ok(renderer)
595 }
596
597 pub(crate) fn upload_solid_pixel(&mut self) {
599 let size = 2u32; let white_pixels: Vec<u8> = vec![255; (size * size * 4) as usize];
601
602 self.queue.write_texture(
603 wgpu::TexelCopyTextureInfo {
604 texture: &self.atlas_texture,
605 mip_level: 0,
606 origin: wgpu::Origin3d {
607 x: self.atlas_next_x,
608 y: self.atlas_next_y,
609 z: 0,
610 },
611 aspect: wgpu::TextureAspect::All,
612 },
613 &white_pixels,
614 wgpu::TexelCopyBufferLayout {
615 offset: 0,
616 bytes_per_row: Some(4 * size),
617 rows_per_image: Some(size),
618 },
619 wgpu::Extent3d {
620 width: size,
621 height: size,
622 depth_or_array_layers: 1,
623 },
624 );
625
626 self.solid_pixel_offset = (self.atlas_next_x, self.atlas_next_y);
627 self.atlas_next_x += size + 2; self.atlas_row_height = self.atlas_row_height.max(size);
629 }
630
631 pub fn device(&self) -> &wgpu::Device {
632 &self.device
633 }
634 pub fn queue(&self) -> &wgpu::Queue {
635 &self.queue
636 }
637 pub fn surface_format(&self) -> wgpu::TextureFormat {
638 self.config.format
639 }
640 pub fn cell_width(&self) -> f32 {
641 self.cell_width
642 }
643 pub fn cell_height(&self) -> f32 {
644 self.cell_height
645 }
646 pub fn window_padding(&self) -> f32 {
647 self.window_padding
648 }
649 pub fn content_offset_y(&self) -> f32 {
650 self.content_offset_y
651 }
652 pub fn set_content_offset_y(&mut self, offset: f32) -> Option<(usize, usize)> {
655 if (self.content_offset_y - offset).abs() > f32::EPSILON {
656 self.content_offset_y = offset;
657 let size = (self.config.width, self.config.height);
658 return Some(self.resize(size.0, size.1));
659 }
660 None
661 }
662 pub fn content_offset_x(&self) -> f32 {
663 self.content_offset_x
664 }
665 pub fn set_content_offset_x(&mut self, offset: f32) -> Option<(usize, usize)> {
668 if (self.content_offset_x - offset).abs() > f32::EPSILON {
669 self.content_offset_x = offset;
670 let size = (self.config.width, self.config.height);
671 return Some(self.resize(size.0, size.1));
672 }
673 None
674 }
675 pub fn content_inset_bottom(&self) -> f32 {
676 self.content_inset_bottom
677 }
678 pub fn set_content_inset_bottom(&mut self, inset: f32) -> Option<(usize, usize)> {
681 if (self.content_inset_bottom - inset).abs() > f32::EPSILON {
682 self.content_inset_bottom = inset;
683 let size = (self.config.width, self.config.height);
684 return Some(self.resize(size.0, size.1));
685 }
686 None
687 }
688 pub fn content_inset_right(&self) -> f32 {
689 self.content_inset_right
690 }
691 pub fn set_content_inset_right(&mut self, inset: f32) -> Option<(usize, usize)> {
694 if (self.content_inset_right - inset).abs() > f32::EPSILON {
695 log::info!(
696 "[SCROLLBAR] set_content_inset_right: {:.1} -> {:.1} (physical px)",
697 self.content_inset_right,
698 inset
699 );
700 self.content_inset_right = inset;
701 let size = (self.config.width, self.config.height);
702 return Some(self.resize(size.0, size.1));
703 }
704 None
705 }
706 pub fn grid_size(&self) -> (usize, usize) {
707 (self.cols, self.rows)
708 }
709 pub fn keep_text_opaque(&self) -> bool {
710 self.keep_text_opaque
711 }
712
713 pub fn resize(&mut self, width: u32, height: u32) -> (usize, usize) {
714 if width == 0 || height == 0 {
715 return (self.cols, self.rows);
716 }
717 self.config.width = width;
718 self.config.height = height;
719 self.surface.configure(&self.device, &self.config);
720
721 let available_width = (width as f32
722 - self.window_padding * 2.0
723 - self.content_offset_x
724 - self.content_inset_right
725 - self.scrollbar.width())
726 .max(0.0);
727 let available_height = (height as f32
728 - self.window_padding * 2.0
729 - self.content_offset_y
730 - self.content_inset_bottom
731 - self.egui_bottom_inset)
732 .max(0.0);
733 let new_cols = (available_width / self.cell_width).max(1.0) as usize;
734 let new_rows = (available_height / self.cell_height).max(1.0) as usize;
735
736 if new_cols != self.cols || new_rows != self.rows {
737 self.cols = new_cols;
738 self.rows = new_rows;
739 self.cells = vec![Cell::default(); self.cols * self.rows];
740 self.dirty_rows = vec![true; self.rows];
741 self.row_cache = (0..self.rows).map(|_| None).collect();
742 self.recreate_instance_buffers();
743 }
744
745 self.update_bg_image_uniforms();
746 (self.cols, self.rows)
747 }
748
749 fn recreate_instance_buffers(&mut self) {
750 self.max_bg_instances = self.cols * self.rows + 10 + self.rows + self.rows; self.max_text_instances = self.cols * self.rows * 2;
752 let (bg_buf, text_buf) = pipeline::create_instance_buffers(
753 &self.device,
754 self.max_bg_instances,
755 self.max_text_instances,
756 );
757 self.bg_instance_buffer = bg_buf;
758 self.text_instance_buffer = text_buf;
759
760 self.bg_instances = vec![
761 BackgroundInstance {
762 position: [0.0, 0.0],
763 size: [0.0, 0.0],
764 color: [0.0, 0.0, 0.0, 0.0],
765 };
766 self.max_bg_instances
767 ];
768 self.text_instances = vec![
769 TextInstance {
770 position: [0.0, 0.0],
771 size: [0.0, 0.0],
772 tex_offset: [0.0, 0.0],
773 tex_size: [0.0, 0.0],
774 color: [0.0, 0.0, 0.0, 0.0],
775 is_colored: 0,
776 };
777 self.max_text_instances
778 ];
779 }
780
781 pub fn update_cells(&mut self, new_cells: &[Cell]) -> bool {
783 let mut changed = false;
784 for row in 0..self.rows {
785 let start = row * self.cols;
786 let end = (row + 1) * self.cols;
787 if start < new_cells.len() && end <= new_cells.len() {
788 let row_slice = &new_cells[start..end];
789 if row_slice != &self.cells[start..end] {
790 self.cells[start..end].clone_from_slice(row_slice);
791 self.dirty_rows[row] = true;
792 changed = true;
793 }
794 }
795 }
796 changed
797 }
798
799 pub fn clear_all_cells(&mut self) {
801 for cell in &mut self.cells {
802 *cell = Cell::default();
803 }
804 for dirty in &mut self.dirty_rows {
805 *dirty = true;
806 }
807 }
808
809 pub fn update_cursor(
811 &mut self,
812 pos: (usize, usize),
813 opacity: f32,
814 style: par_term_emu_core_rust::cursor::CursorStyle,
815 ) -> bool {
816 if self.cursor_pos != pos || self.cursor_opacity != opacity || self.cursor_style != style {
817 self.dirty_rows[self.cursor_pos.1.min(self.rows - 1)] = true;
818 self.cursor_pos = pos;
819 self.cursor_opacity = opacity;
820 self.cursor_style = style;
821 self.dirty_rows[self.cursor_pos.1.min(self.rows - 1)] = true;
822
823 use par_term_emu_core_rust::cursor::CursorStyle;
825 self.cursor_overlay = if opacity > 0.0 {
826 let col = pos.0;
827 let row = pos.1;
828 let x0 =
829 (self.window_padding + self.content_offset_x + col as f32 * self.cell_width)
830 .round();
831 let x1 = (self.window_padding
832 + self.content_offset_x
833 + (col + 1) as f32 * self.cell_width)
834 .round();
835 let y0 =
836 (self.window_padding + self.content_offset_y + row as f32 * self.cell_height)
837 .round();
838 let y1 = (self.window_padding
839 + self.content_offset_y
840 + (row + 1) as f32 * self.cell_height)
841 .round();
842
843 match style {
844 CursorStyle::SteadyBlock | CursorStyle::BlinkingBlock => None,
845 CursorStyle::SteadyBar | CursorStyle::BlinkingBar => Some(BackgroundInstance {
846 position: [
847 x0 / self.config.width as f32 * 2.0 - 1.0,
848 1.0 - (y0 / self.config.height as f32 * 2.0),
849 ],
850 size: [
851 2.0 / self.config.width as f32 * 2.0,
852 (y1 - y0) / self.config.height as f32 * 2.0,
853 ],
854 color: [
855 self.cursor_color[0],
856 self.cursor_color[1],
857 self.cursor_color[2],
858 opacity,
859 ],
860 }),
861 CursorStyle::SteadyUnderline | CursorStyle::BlinkingUnderline => {
862 Some(BackgroundInstance {
863 position: [
864 x0 / self.config.width as f32 * 2.0 - 1.0,
865 1.0 - ((y1 - 2.0) / self.config.height as f32 * 2.0),
866 ],
867 size: [
868 (x1 - x0) / self.config.width as f32 * 2.0,
869 2.0 / self.config.height as f32 * 2.0,
870 ],
871 color: [
872 self.cursor_color[0],
873 self.cursor_color[1],
874 self.cursor_color[2],
875 opacity,
876 ],
877 })
878 }
879 }
880 } else {
881 None
882 };
883 return true;
884 }
885 false
886 }
887
888 pub fn clear_cursor(&mut self) -> bool {
889 self.update_cursor(self.cursor_pos, 0.0, self.cursor_style)
890 }
891
892 pub fn update_cursor_color(&mut self, color: [u8; 3]) {
894 self.cursor_color = [
895 color[0] as f32 / 255.0,
896 color[1] as f32 / 255.0,
897 color[2] as f32 / 255.0,
898 ];
899 self.dirty_rows[self.cursor_pos.1.min(self.rows - 1)] = true;
900 }
901
902 pub fn update_cursor_text_color(&mut self, color: Option<[u8; 3]>) {
904 self.cursor_text_color = color.map(|c| {
905 [
906 c[0] as f32 / 255.0,
907 c[1] as f32 / 255.0,
908 c[2] as f32 / 255.0,
909 ]
910 });
911 self.dirty_rows[self.cursor_pos.1.min(self.rows - 1)] = true;
912 }
913
914 pub fn set_cursor_hidden_for_shader(&mut self, hidden: bool) -> bool {
917 if self.cursor_hidden_for_shader != hidden {
918 self.cursor_hidden_for_shader = hidden;
919 self.dirty_rows[self.cursor_pos.1.min(self.rows - 1)] = true;
920 return true;
921 }
922 false
923 }
924
925 pub fn set_focused(&mut self, focused: bool) -> bool {
928 if self.is_focused != focused {
929 self.is_focused = focused;
930 self.dirty_rows[self.cursor_pos.1.min(self.rows - 1)] = true;
931 return true;
932 }
933 false
934 }
935
936 pub fn update_cursor_guide(&mut self, enabled: bool, color: [u8; 4]) {
938 self.cursor_guide_enabled = enabled;
939 self.cursor_guide_color = [
940 color[0] as f32 / 255.0,
941 color[1] as f32 / 255.0,
942 color[2] as f32 / 255.0,
943 color[3] as f32 / 255.0,
944 ];
945 if enabled {
946 self.dirty_rows[self.cursor_pos.1.min(self.rows - 1)] = true;
947 }
948 }
949
950 pub fn update_cursor_shadow(
952 &mut self,
953 enabled: bool,
954 color: [u8; 4],
955 offset: [f32; 2],
956 blur: f32,
957 ) {
958 self.cursor_shadow_enabled = enabled;
959 self.cursor_shadow_color = [
960 color[0] as f32 / 255.0,
961 color[1] as f32 / 255.0,
962 color[2] as f32 / 255.0,
963 color[3] as f32 / 255.0,
964 ];
965 self.cursor_shadow_offset = offset;
966 self.cursor_shadow_blur = blur;
967 if enabled {
968 self.dirty_rows[self.cursor_pos.1.min(self.rows - 1)] = true;
969 }
970 }
971
972 pub fn update_cursor_boost(&mut self, intensity: f32, color: [u8; 3]) {
974 self.cursor_boost = intensity.clamp(0.0, 1.0);
975 self.cursor_boost_color = [
976 color[0] as f32 / 255.0,
977 color[1] as f32 / 255.0,
978 color[2] as f32 / 255.0,
979 ];
980 if intensity > 0.0 {
981 self.dirty_rows[self.cursor_pos.1.min(self.rows - 1)] = true;
982 }
983 }
984
985 pub fn update_unfocused_cursor_style(&mut self, style: par_term_config::UnfocusedCursorStyle) {
987 self.unfocused_cursor_style = style;
988 if !self.is_focused {
989 self.dirty_rows[self.cursor_pos.1.min(self.rows - 1)] = true;
990 }
991 }
992
993 pub fn update_scrollbar(
994 &mut self,
995 scroll_offset: usize,
996 visible_lines: usize,
997 total_lines: usize,
998 marks: &[par_term_config::ScrollbackMark],
999 ) {
1000 let right_inset = self.content_inset_right + self.egui_right_inset;
1001 self.scrollbar.update(
1002 &self.queue,
1003 scroll_offset,
1004 visible_lines,
1005 total_lines,
1006 self.config.width,
1007 self.config.height,
1008 self.content_offset_y,
1009 self.content_inset_bottom + self.egui_bottom_inset,
1010 right_inset,
1011 marks,
1012 );
1013 }
1014
1015 pub fn update_scrollbar_for_pane(
1021 &mut self,
1022 scroll_offset: usize,
1023 visible_lines: usize,
1024 total_lines: usize,
1025 marks: &[par_term_config::ScrollbackMark],
1026 viewport: &PaneViewport,
1027 ) {
1028 let win_w = self.config.width as f32;
1029 let win_h = self.config.height as f32;
1030
1031 let pane_content_offset_y = viewport.y;
1033
1034 let pane_bottom_inset =
1036 (win_h - (viewport.y + viewport.height)).max(0.0) + self.egui_bottom_inset;
1037
1038 let pane_right_inset = (win_w - (viewport.x + viewport.width)).max(0.0)
1040 + self.content_inset_right
1041 + self.egui_right_inset;
1042
1043 self.scrollbar.update(
1044 &self.queue,
1045 scroll_offset,
1046 visible_lines,
1047 total_lines,
1048 self.config.width,
1049 self.config.height,
1050 pane_content_offset_y,
1051 pane_bottom_inset,
1052 pane_right_inset,
1053 marks,
1054 );
1055 }
1056
1057 pub fn set_visual_bell_intensity(&mut self, intensity: f32) {
1058 self.visual_bell_intensity = intensity;
1059 }
1060
1061 pub fn update_opacity(&mut self, opacity: f32) {
1062 self.window_opacity = opacity;
1063 self.update_bg_image_uniforms();
1066 }
1067
1068 pub fn set_transparency_affects_only_default_background(&mut self, value: bool) {
1071 if self.transparency_affects_only_default_background != value {
1072 log::info!(
1073 "transparency_affects_only_default_background: {} -> {} (window_opacity={})",
1074 self.transparency_affects_only_default_background,
1075 value,
1076 self.window_opacity
1077 );
1078 self.transparency_affects_only_default_background = value;
1079 self.dirty_rows.fill(true);
1081 }
1082 }
1083
1084 pub fn set_keep_text_opaque(&mut self, value: bool) {
1087 if self.keep_text_opaque != value {
1088 log::info!(
1089 "keep_text_opaque: {} -> {} (window_opacity={}, transparency_affects_only_default_bg={})",
1090 self.keep_text_opaque,
1091 value,
1092 self.window_opacity,
1093 self.transparency_affects_only_default_background
1094 );
1095 self.keep_text_opaque = value;
1096 self.dirty_rows.fill(true);
1098 }
1099 }
1100
1101 pub fn set_link_underline_style(&mut self, style: par_term_config::LinkUnderlineStyle) {
1102 if self.link_underline_style != style {
1103 self.link_underline_style = style;
1104 self.dirty_rows.fill(true);
1105 }
1106 }
1107
1108 pub fn update_command_separator(
1110 &mut self,
1111 enabled: bool,
1112 thickness: f32,
1113 opacity: f32,
1114 exit_color: bool,
1115 color: [u8; 3],
1116 ) {
1117 self.command_separator_enabled = enabled;
1118 self.command_separator_thickness = thickness;
1119 self.command_separator_opacity = opacity;
1120 self.command_separator_exit_color = exit_color;
1121 self.command_separator_color = [
1122 color[0] as f32 / 255.0,
1123 color[1] as f32 / 255.0,
1124 color[2] as f32 / 255.0,
1125 ];
1126 }
1127
1128 pub fn set_separator_marks(&mut self, marks: Vec<SeparatorMark>) -> bool {
1131 if self.visible_separator_marks != marks {
1132 self.visible_separator_marks = marks;
1133 return true;
1134 }
1135 false
1136 }
1137
1138 pub fn set_gutter_indicators(&mut self, indicators: Vec<(usize, [f32; 4])>) {
1142 self.gutter_indicators = indicators;
1143 }
1144
1145 fn separator_color(
1147 &self,
1148 exit_code: Option<i32>,
1149 custom_color: Option<(u8, u8, u8)>,
1150 opacity_mult: f32,
1151 ) -> [f32; 4] {
1152 let alpha = self.command_separator_opacity * opacity_mult;
1153 if let Some((r, g, b)) = custom_color {
1155 return [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, alpha];
1156 }
1157 if self.command_separator_exit_color {
1158 match exit_code {
1159 Some(0) => [0.3, 0.75, 0.3, alpha], Some(_) => [0.85, 0.25, 0.25, alpha], None => [0.5, 0.5, 0.5, alpha], }
1163 } else {
1164 [
1165 self.command_separator_color[0],
1166 self.command_separator_color[1],
1167 self.command_separator_color[2],
1168 alpha,
1169 ]
1170 }
1171 }
1172
1173 pub fn update_scale_factor(&mut self, scale_factor: f64) {
1176 let new_scale = scale_factor as f32;
1177
1178 if (self.scale_factor - new_scale).abs() < f32::EPSILON {
1180 return;
1181 }
1182
1183 log::info!(
1184 "Recalculating font metrics for scale factor change: {} -> {}",
1185 self.scale_factor,
1186 new_scale
1187 );
1188
1189 self.scale_factor = new_scale;
1190
1191 let platform_dpi = if cfg!(target_os = "macos") {
1193 72.0
1194 } else {
1195 96.0
1196 };
1197 let base_font_pixels = self.base_font_size * platform_dpi / 72.0;
1198 self.font_size_pixels = (base_font_pixels * new_scale).max(1.0);
1199
1200 let (font_ascent, font_descent, font_leading, char_advance) = {
1202 let primary_font = self.font_manager.get_font(0).unwrap();
1203 let metrics = primary_font.metrics(&[]);
1204 let scale = self.font_size_pixels / metrics.units_per_em as f32;
1205 let glyph_id = primary_font.charmap().map('m');
1206 let advance = primary_font.glyph_metrics(&[]).advance_width(glyph_id) * scale;
1207 (
1208 metrics.ascent * scale,
1209 metrics.descent * scale,
1210 metrics.leading * scale,
1211 advance,
1212 )
1213 };
1214
1215 self.font_ascent = font_ascent;
1216 self.font_descent = font_descent;
1217 self.font_leading = font_leading;
1218 self.char_advance = char_advance;
1219
1220 let natural_line_height = font_ascent + font_descent + font_leading;
1222 self.cell_height = (natural_line_height * self.line_spacing).max(1.0);
1223 self.cell_width = (char_advance * self.char_spacing).max(1.0);
1224
1225 log::info!(
1226 "New cell dimensions: {}x{} (font_size_pixels: {})",
1227 self.cell_width,
1228 self.cell_height,
1229 self.font_size_pixels
1230 );
1231
1232 self.clear_glyph_cache();
1234
1235 self.dirty_rows.fill(true);
1237 }
1238
1239 #[allow(dead_code)]
1240 pub fn update_window_padding(&mut self, padding: f32) -> Option<(usize, usize)> {
1241 if (self.window_padding - padding).abs() > f32::EPSILON {
1242 self.window_padding = padding;
1243 let size = (self.config.width, self.config.height);
1244 return Some(self.resize(size.0, size.1));
1245 }
1246 None
1247 }
1248
1249 pub fn update_scrollbar_appearance(
1250 &mut self,
1251 width: f32,
1252 thumb_color: [f32; 4],
1253 track_color: [f32; 4],
1254 ) {
1255 self.scrollbar
1256 .update_appearance(width, thumb_color, track_color);
1257 }
1258
1259 pub fn update_scrollbar_position(&mut self, position: &str) {
1260 self.scrollbar.update_position(position);
1261 }
1262
1263 pub fn scrollbar_contains_point(&self, x: f32, y: f32) -> bool {
1264 self.scrollbar.contains_point(x, y)
1265 }
1266
1267 pub fn scrollbar_thumb_bounds(&self) -> Option<(f32, f32)> {
1268 self.scrollbar.thumb_bounds()
1269 }
1270
1271 pub fn scrollbar_track_contains_x(&self, x: f32) -> bool {
1272 self.scrollbar.track_contains_x(x)
1273 }
1274
1275 pub fn scrollbar_mouse_y_to_scroll_offset(&self, mouse_y: f32) -> Option<usize> {
1276 self.scrollbar.mouse_y_to_scroll_offset(mouse_y)
1277 }
1278
1279 pub fn scrollbar_mark_at_position(
1282 &self,
1283 mouse_x: f32,
1284 mouse_y: f32,
1285 tolerance: f32,
1286 ) -> Option<&par_term_config::ScrollbackMark> {
1287 self.scrollbar.mark_at_position(mouse_x, mouse_y, tolerance)
1288 }
1289
1290 pub fn reconfigure_surface(&mut self) {
1291 self.surface.configure(&self.device, &self.config);
1292 }
1293
1294 pub fn update_font_antialias(&mut self, enabled: bool) -> bool {
1297 if self.font_antialias != enabled {
1298 self.font_antialias = enabled;
1299 self.clear_glyph_cache();
1300 self.dirty_rows.fill(true);
1301 true
1302 } else {
1303 false
1304 }
1305 }
1306
1307 pub fn update_font_hinting(&mut self, enabled: bool) -> bool {
1310 if self.font_hinting != enabled {
1311 self.font_hinting = enabled;
1312 self.clear_glyph_cache();
1313 self.dirty_rows.fill(true);
1314 true
1315 } else {
1316 false
1317 }
1318 }
1319
1320 pub fn update_font_thin_strokes(&mut self, mode: par_term_config::ThinStrokesMode) -> bool {
1323 if self.font_thin_strokes != mode {
1324 self.font_thin_strokes = mode;
1325 self.clear_glyph_cache();
1326 self.dirty_rows.fill(true);
1327 true
1328 } else {
1329 false
1330 }
1331 }
1332
1333 pub fn update_minimum_contrast(&mut self, ratio: f32) -> bool {
1336 let ratio = ratio.clamp(1.0, 21.0);
1338 if (self.minimum_contrast - ratio).abs() > 0.001 {
1339 self.minimum_contrast = ratio;
1340 self.dirty_rows.fill(true);
1341 true
1342 } else {
1343 false
1344 }
1345 }
1346
1347 pub(crate) fn ensure_minimum_contrast(&self, fg: [f32; 4], bg: [f32; 4]) -> [f32; 4] {
1351 if self.minimum_contrast <= 1.0 {
1353 return fg;
1354 }
1355
1356 fn luminance(color: [f32; 4]) -> f32 {
1358 let r = color[0].powf(2.2);
1359 let g = color[1].powf(2.2);
1360 let b = color[2].powf(2.2);
1361 0.2126 * r + 0.7152 * g + 0.0722 * b
1362 }
1363
1364 fn contrast_ratio(l1: f32, l2: f32) -> f32 {
1365 let (lighter, darker) = if l1 > l2 { (l1, l2) } else { (l2, l1) };
1366 (lighter + 0.05) / (darker + 0.05)
1367 }
1368
1369 let fg_lum = luminance(fg);
1370 let bg_lum = luminance(bg);
1371 let current_ratio = contrast_ratio(fg_lum, bg_lum);
1372
1373 if current_ratio >= self.minimum_contrast {
1375 return fg;
1376 }
1377
1378 let bg_is_dark = bg_lum < 0.5;
1381
1382 let mut low = 0.0f32;
1384 let mut high = 1.0f32;
1385
1386 for _ in 0..20 {
1387 let mid = (low + high) / 2.0;
1389
1390 let adjusted = if bg_is_dark {
1391 [
1393 fg[0] + (1.0 - fg[0]) * mid,
1394 fg[1] + (1.0 - fg[1]) * mid,
1395 fg[2] + (1.0 - fg[2]) * mid,
1396 fg[3],
1397 ]
1398 } else {
1399 [
1401 fg[0] * (1.0 - mid),
1402 fg[1] * (1.0 - mid),
1403 fg[2] * (1.0 - mid),
1404 fg[3],
1405 ]
1406 };
1407
1408 let adjusted_lum = luminance(adjusted);
1409 let new_ratio = contrast_ratio(adjusted_lum, bg_lum);
1410
1411 if new_ratio >= self.minimum_contrast {
1412 high = mid;
1413 } else {
1414 low = mid;
1415 }
1416 }
1417
1418 if bg_is_dark {
1420 [
1421 fg[0] + (1.0 - fg[0]) * high,
1422 fg[1] + (1.0 - fg[1]) * high,
1423 fg[2] + (1.0 - fg[2]) * high,
1424 fg[3],
1425 ]
1426 } else {
1427 [
1428 fg[0] * (1.0 - high),
1429 fg[1] * (1.0 - high),
1430 fg[2] * (1.0 - high),
1431 fg[3],
1432 ]
1433 }
1434 }
1435
1436 pub(crate) fn should_use_thin_strokes(&self) -> bool {
1438 use par_term_config::ThinStrokesMode;
1439
1440 let is_retina = self.scale_factor > 1.5;
1442
1443 let bg_brightness =
1445 (self.background_color[0] + self.background_color[1] + self.background_color[2]) / 3.0;
1446 let is_dark_background = bg_brightness < 0.5;
1447
1448 match self.font_thin_strokes {
1449 ThinStrokesMode::Never => false,
1450 ThinStrokesMode::Always => true,
1451 ThinStrokesMode::RetinaOnly => is_retina,
1452 ThinStrokesMode::DarkBackgroundsOnly => is_dark_background,
1453 ThinStrokesMode::RetinaDarkBackgroundsOnly => is_retina && is_dark_background,
1454 }
1455 }
1456
1457 #[allow(dead_code)]
1459 pub fn supported_present_modes(&self) -> &[wgpu::PresentMode] {
1460 &self.supported_present_modes
1461 }
1462
1463 pub fn is_vsync_mode_supported(&self, mode: par_term_config::VsyncMode) -> bool {
1465 self.supported_present_modes
1466 .contains(&mode.to_present_mode())
1467 }
1468
1469 pub fn update_vsync_mode(
1472 &mut self,
1473 mode: par_term_config::VsyncMode,
1474 ) -> (par_term_config::VsyncMode, bool) {
1475 let requested = mode.to_present_mode();
1476 let current = self.config.present_mode;
1477
1478 let actual = if self.supported_present_modes.contains(&requested) {
1480 requested
1481 } else {
1482 log::warn!(
1483 "Requested present mode {:?} not supported, falling back to Fifo",
1484 requested
1485 );
1486 wgpu::PresentMode::Fifo
1487 };
1488
1489 if actual != current {
1491 self.config.present_mode = actual;
1492 self.surface.configure(&self.device, &self.config);
1493 log::info!("VSync mode changed to {:?}", actual);
1494 }
1495
1496 let actual_vsync = match actual {
1498 wgpu::PresentMode::Immediate => par_term_config::VsyncMode::Immediate,
1499 wgpu::PresentMode::Mailbox => par_term_config::VsyncMode::Mailbox,
1500 wgpu::PresentMode::Fifo | wgpu::PresentMode::FifoRelaxed => {
1501 par_term_config::VsyncMode::Fifo
1502 }
1503 _ => par_term_config::VsyncMode::Fifo,
1504 };
1505
1506 (actual_vsync, actual != current)
1507 }
1508
1509 #[allow(dead_code)]
1511 pub fn current_vsync_mode(&self) -> par_term_config::VsyncMode {
1512 match self.config.present_mode {
1513 wgpu::PresentMode::Immediate => par_term_config::VsyncMode::Immediate,
1514 wgpu::PresentMode::Mailbox => par_term_config::VsyncMode::Mailbox,
1515 wgpu::PresentMode::Fifo | wgpu::PresentMode::FifoRelaxed => {
1516 par_term_config::VsyncMode::Fifo
1517 }
1518 _ => par_term_config::VsyncMode::Fifo,
1519 }
1520 }
1521
1522 #[allow(dead_code)]
1523 pub fn update_graphics(
1524 &mut self,
1525 _graphics: &[par_term_emu_core_rust::graphics::TerminalGraphic],
1526 _scroll_offset: usize,
1527 _scrollback_len: usize,
1528 _visible_lines: usize,
1529 ) -> Result<()> {
1530 Ok(())
1531 }
1532}