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) command_separator_enabled: bool,
207 pub(crate) command_separator_thickness: f32,
209 pub(crate) command_separator_opacity: f32,
211 pub(crate) command_separator_exit_color: bool,
213 pub(crate) command_separator_color: [f32; 3],
215 pub(crate) visible_separator_marks: Vec<SeparatorMark>,
217}
218
219impl CellRenderer {
220 #[allow(clippy::too_many_arguments)]
221 pub async fn new(
222 window: Arc<Window>,
223 font_family: Option<&str>,
224 font_family_bold: Option<&str>,
225 font_family_italic: Option<&str>,
226 font_family_bold_italic: Option<&str>,
227 font_ranges: &[par_term_config::FontRange],
228 font_size: f32,
229 cols: usize,
230 rows: usize,
231 window_padding: f32,
232 line_spacing: f32,
233 char_spacing: f32,
234 scrollbar_position: &str,
235 scrollbar_width: f32,
236 scrollbar_thumb_color: [f32; 4],
237 scrollbar_track_color: [f32; 4],
238 enable_text_shaping: bool,
239 enable_ligatures: bool,
240 enable_kerning: bool,
241 font_antialias: bool,
242 font_hinting: bool,
243 font_thin_strokes: par_term_config::ThinStrokesMode,
244 minimum_contrast: f32,
245 vsync_mode: par_term_config::VsyncMode,
246 power_preference: par_term_config::PowerPreference,
247 window_opacity: f32,
248 background_color: [u8; 3],
249 background_image_path: Option<&str>,
250 background_image_mode: par_term_config::BackgroundImageMode,
251 background_image_opacity: f32,
252 ) -> Result<Self> {
253 #[cfg(target_os = "windows")]
262 let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
263 backends: wgpu::Backends::DX12,
264 ..Default::default()
265 });
266 #[cfg(target_os = "macos")]
267 let instance = wgpu::Instance::default();
268 #[cfg(target_os = "linux")]
269 let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
270 backends: wgpu::Backends::VULKAN | wgpu::Backends::GL,
271 ..Default::default()
272 });
273 let surface = instance.create_surface(window.clone())?;
274 let adapter = instance
275 .request_adapter(&wgpu::RequestAdapterOptions {
276 power_preference: power_preference.to_wgpu(),
277 compatible_surface: Some(&surface),
278 force_fallback_adapter: false,
279 })
280 .await
281 .context("Failed to find wgpu adapter")?;
282
283 let (device, queue) = adapter
284 .request_device(&wgpu::DeviceDescriptor {
285 label: Some("device"),
286 required_features: wgpu::Features::empty(),
287 required_limits: wgpu::Limits::default(),
288 memory_hints: wgpu::MemoryHints::default(),
289 ..Default::default()
290 })
291 .await?;
292
293 let device = Arc::new(device);
294 let queue = Arc::new(queue);
295
296 let size = window.inner_size();
297 let surface_caps = surface.get_capabilities(&adapter);
298 let surface_format = surface_caps
299 .formats
300 .iter()
301 .copied()
302 .find(|f| !f.is_srgb())
303 .unwrap_or(surface_caps.formats[0]);
304
305 let supported_present_modes = surface_caps.present_modes.clone();
307
308 let requested_mode = vsync_mode.to_present_mode();
310 let present_mode = if supported_present_modes.contains(&requested_mode) {
311 requested_mode
312 } else {
313 log::warn!(
315 "Requested present mode {:?} not supported (available: {:?}), falling back",
316 requested_mode,
317 supported_present_modes
318 );
319 if supported_present_modes.contains(&wgpu::PresentMode::Fifo) {
320 wgpu::PresentMode::Fifo
321 } else {
322 supported_present_modes[0]
323 }
324 };
325
326 let alpha_mode = if surface_caps
329 .alpha_modes
330 .contains(&wgpu::CompositeAlphaMode::PreMultiplied)
331 {
332 wgpu::CompositeAlphaMode::PreMultiplied
333 } else if surface_caps
334 .alpha_modes
335 .contains(&wgpu::CompositeAlphaMode::PostMultiplied)
336 {
337 wgpu::CompositeAlphaMode::PostMultiplied
338 } else if surface_caps
339 .alpha_modes
340 .contains(&wgpu::CompositeAlphaMode::Auto)
341 {
342 wgpu::CompositeAlphaMode::Auto
343 } else {
344 surface_caps.alpha_modes[0]
345 };
346 log::info!(
347 "Selected alpha mode: {:?} (available: {:?})",
348 alpha_mode,
349 surface_caps.alpha_modes
350 );
351
352 let config = wgpu::SurfaceConfiguration {
353 usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
354 format: surface_format,
355 width: size.width.max(1),
356 height: size.height.max(1),
357 present_mode,
358 alpha_mode,
359 view_formats: vec![],
360 desired_maximum_frame_latency: 2,
361 };
362 surface.configure(&device, &config);
363
364 let scale_factor = window.scale_factor() as f32;
365
366 let platform_dpi = if cfg!(target_os = "macos") {
367 72.0
368 } else {
369 96.0
370 };
371
372 let base_font_pixels = font_size * platform_dpi / 72.0;
373 let font_size_pixels = (base_font_pixels * scale_factor).max(1.0);
374
375 let font_manager = FontManager::new(
376 font_family,
377 font_family_bold,
378 font_family_italic,
379 font_family_bold_italic,
380 font_ranges,
381 )?;
382
383 let (font_ascent, font_descent, font_leading, char_advance) = {
385 let primary_font = font_manager.get_font(0).unwrap();
386 let metrics = primary_font.metrics(&[]);
387 let scale = font_size_pixels / metrics.units_per_em as f32;
388 let glyph_id = primary_font.charmap().map('m');
389 let advance = primary_font.glyph_metrics(&[]).advance_width(glyph_id) * scale;
390 (
391 metrics.ascent * scale,
392 metrics.descent * scale,
393 metrics.leading * scale,
394 advance,
395 )
396 };
397
398 let natural_line_height = font_ascent + font_descent + font_leading;
399 let cell_height = (natural_line_height * line_spacing).max(1.0);
400 let cell_width = (char_advance * char_spacing).max(1.0);
401
402 let scrollbar = Scrollbar::new(
403 Arc::clone(&device),
404 surface_format,
405 scrollbar_width,
406 scrollbar_position,
407 scrollbar_thumb_color,
408 scrollbar_track_color,
409 );
410
411 let bg_pipeline = pipeline::create_bg_pipeline(&device, surface_format);
413
414 let (atlas_texture, atlas_view, atlas_sampler) = pipeline::create_atlas(&device);
415 let text_bind_group_layout = pipeline::create_text_bind_group_layout(&device);
416 let text_bind_group = pipeline::create_text_bind_group(
417 &device,
418 &text_bind_group_layout,
419 &atlas_view,
420 &atlas_sampler,
421 );
422 let text_pipeline =
423 pipeline::create_text_pipeline(&device, surface_format, &text_bind_group_layout);
424
425 let bg_image_bind_group_layout = pipeline::create_bg_image_bind_group_layout(&device);
426 let bg_image_pipeline = pipeline::create_bg_image_pipeline(
427 &device,
428 surface_format,
429 &bg_image_bind_group_layout,
430 );
431 let bg_image_uniform_buffer = pipeline::create_bg_image_uniform_buffer(&device);
432
433 let (visual_bell_pipeline, visual_bell_bind_group, _, visual_bell_uniform_buffer) =
434 pipeline::create_visual_bell_pipeline(&device, surface_format);
435
436 let vertex_buffer = pipeline::create_vertex_buffer(&device);
437
438 let max_bg_instances = cols * rows + 10 + rows; let max_text_instances = cols * rows * 2;
441 let (bg_instance_buffer, text_instance_buffer) =
442 pipeline::create_instance_buffers(&device, max_bg_instances, max_text_instances);
443
444 let mut renderer = Self {
445 device,
446 queue,
447 surface,
448 config,
449 supported_present_modes,
450 bg_pipeline,
451 text_pipeline,
452 bg_image_pipeline,
453 visual_bell_pipeline,
454 vertex_buffer,
455 bg_instance_buffer,
456 text_instance_buffer,
457 bg_image_uniform_buffer,
458 visual_bell_uniform_buffer,
459 text_bind_group,
460 text_bind_group_layout,
461 bg_image_bind_group: None,
462 bg_image_bind_group_layout,
463 visual_bell_bind_group,
464 atlas_texture,
465 atlas_view,
466 glyph_cache: HashMap::new(),
467 lru_head: None,
468 lru_tail: None,
469 atlas_next_x: 0,
470 atlas_next_y: 0,
471 atlas_row_height: 0,
472 cols,
473 rows,
474 cell_width,
475 cell_height,
476 window_padding,
477 content_offset_y: 0.0,
478 content_offset_x: 0.0,
479 content_inset_bottom: 0.0,
480 content_inset_right: 0.0,
481 egui_bottom_inset: 0.0,
482 egui_right_inset: 0.0,
483 scale_factor,
484 font_manager,
485 scrollbar,
486 cells: vec![Cell::default(); cols * rows],
487 dirty_rows: vec![true; rows],
488 row_cache: (0..rows).map(|_| None).collect(),
489 cursor_pos: (0, 0),
490 cursor_opacity: 0.0,
491 cursor_style: par_term_emu_core_rust::cursor::CursorStyle::SteadyBlock,
492 cursor_overlay: None,
493 cursor_color: [1.0, 1.0, 1.0],
494 cursor_text_color: None,
495 cursor_hidden_for_shader: false,
496 is_focused: true,
497 cursor_guide_enabled: false,
498 cursor_guide_color: [1.0, 1.0, 1.0, 0.08],
499 cursor_shadow_enabled: false,
500 cursor_shadow_color: [0.0, 0.0, 0.0, 0.5],
501 cursor_shadow_offset: [2.0, 2.0],
502 cursor_shadow_blur: 3.0,
503 cursor_boost: 0.0,
504 cursor_boost_color: [1.0, 1.0, 1.0],
505 unfocused_cursor_style: par_term_config::UnfocusedCursorStyle::default(),
506 visual_bell_intensity: 0.0,
507 window_opacity,
508 background_color: [
509 background_color[0] as f32 / 255.0,
510 background_color[1] as f32 / 255.0,
511 background_color[2] as f32 / 255.0,
512 1.0,
513 ],
514 base_font_size: font_size,
515 line_spacing,
516 char_spacing,
517 font_ascent,
518 font_descent,
519 font_leading,
520 font_size_pixels,
521 char_advance,
522 bg_image_texture: None,
523 bg_image_mode: background_image_mode,
524 bg_image_opacity: background_image_opacity,
525 bg_image_width: 0,
526 bg_image_height: 0,
527 bg_is_solid_color: false,
528 solid_bg_color: [0.0, 0.0, 0.0],
529 pane_bg_cache: HashMap::new(),
530 max_bg_instances,
531 max_text_instances,
532 bg_instances: vec![
533 BackgroundInstance {
534 position: [0.0, 0.0],
535 size: [0.0, 0.0],
536 color: [0.0, 0.0, 0.0, 0.0],
537 };
538 max_bg_instances
539 ],
540 text_instances: vec![
541 TextInstance {
542 position: [0.0, 0.0],
543 size: [0.0, 0.0],
544 tex_offset: [0.0, 0.0],
545 tex_size: [0.0, 0.0],
546 color: [0.0, 0.0, 0.0, 0.0],
547 is_colored: 0,
548 };
549 max_text_instances
550 ],
551 enable_text_shaping,
552 enable_ligatures,
553 enable_kerning,
554 font_antialias,
555 font_hinting,
556 font_thin_strokes,
557 minimum_contrast: minimum_contrast.clamp(1.0, 21.0),
558 solid_pixel_offset: (0, 0),
559 transparency_affects_only_default_background: false,
560 keep_text_opaque: true,
561 command_separator_enabled: false,
562 command_separator_thickness: 1.0,
563 command_separator_opacity: 0.4,
564 command_separator_exit_color: true,
565 command_separator_color: [0.5, 0.5, 0.5],
566 visible_separator_marks: Vec::new(),
567 };
568
569 renderer.upload_solid_pixel();
571
572 log::info!(
573 "CellRenderer::new: background_image_path={:?}",
574 background_image_path
575 );
576 if let Some(path) = background_image_path {
577 if let Err(e) = renderer.load_background_image(path) {
579 log::warn!(
580 "Could not load background image '{}': {} - continuing without background image",
581 path,
582 e
583 );
584 }
585 }
586
587 Ok(renderer)
588 }
589
590 pub(crate) fn upload_solid_pixel(&mut self) {
592 let size = 2u32; let white_pixels: Vec<u8> = vec![255; (size * size * 4) as usize];
594
595 self.queue.write_texture(
596 wgpu::TexelCopyTextureInfo {
597 texture: &self.atlas_texture,
598 mip_level: 0,
599 origin: wgpu::Origin3d {
600 x: self.atlas_next_x,
601 y: self.atlas_next_y,
602 z: 0,
603 },
604 aspect: wgpu::TextureAspect::All,
605 },
606 &white_pixels,
607 wgpu::TexelCopyBufferLayout {
608 offset: 0,
609 bytes_per_row: Some(4 * size),
610 rows_per_image: Some(size),
611 },
612 wgpu::Extent3d {
613 width: size,
614 height: size,
615 depth_or_array_layers: 1,
616 },
617 );
618
619 self.solid_pixel_offset = (self.atlas_next_x, self.atlas_next_y);
620 self.atlas_next_x += size + 2; self.atlas_row_height = self.atlas_row_height.max(size);
622 }
623
624 pub fn device(&self) -> &wgpu::Device {
625 &self.device
626 }
627 pub fn queue(&self) -> &wgpu::Queue {
628 &self.queue
629 }
630 pub fn surface_format(&self) -> wgpu::TextureFormat {
631 self.config.format
632 }
633 pub fn cell_width(&self) -> f32 {
634 self.cell_width
635 }
636 pub fn cell_height(&self) -> f32 {
637 self.cell_height
638 }
639 pub fn window_padding(&self) -> f32 {
640 self.window_padding
641 }
642 pub fn content_offset_y(&self) -> f32 {
643 self.content_offset_y
644 }
645 pub fn set_content_offset_y(&mut self, offset: f32) -> Option<(usize, usize)> {
648 if (self.content_offset_y - offset).abs() > f32::EPSILON {
649 self.content_offset_y = offset;
650 let size = (self.config.width, self.config.height);
651 return Some(self.resize(size.0, size.1));
652 }
653 None
654 }
655 pub fn content_offset_x(&self) -> f32 {
656 self.content_offset_x
657 }
658 pub fn set_content_offset_x(&mut self, offset: f32) -> Option<(usize, usize)> {
661 if (self.content_offset_x - offset).abs() > f32::EPSILON {
662 self.content_offset_x = offset;
663 let size = (self.config.width, self.config.height);
664 return Some(self.resize(size.0, size.1));
665 }
666 None
667 }
668 pub fn content_inset_bottom(&self) -> f32 {
669 self.content_inset_bottom
670 }
671 pub fn set_content_inset_bottom(&mut self, inset: f32) -> Option<(usize, usize)> {
674 if (self.content_inset_bottom - inset).abs() > f32::EPSILON {
675 self.content_inset_bottom = inset;
676 let size = (self.config.width, self.config.height);
677 return Some(self.resize(size.0, size.1));
678 }
679 None
680 }
681 pub fn content_inset_right(&self) -> f32 {
682 self.content_inset_right
683 }
684 pub fn set_content_inset_right(&mut self, inset: f32) -> Option<(usize, usize)> {
687 if (self.content_inset_right - inset).abs() > f32::EPSILON {
688 log::info!(
689 "[SCROLLBAR] set_content_inset_right: {:.1} -> {:.1} (physical px)",
690 self.content_inset_right,
691 inset
692 );
693 self.content_inset_right = inset;
694 let size = (self.config.width, self.config.height);
695 return Some(self.resize(size.0, size.1));
696 }
697 None
698 }
699 pub fn grid_size(&self) -> (usize, usize) {
700 (self.cols, self.rows)
701 }
702 pub fn keep_text_opaque(&self) -> bool {
703 self.keep_text_opaque
704 }
705
706 pub fn resize(&mut self, width: u32, height: u32) -> (usize, usize) {
707 if width == 0 || height == 0 {
708 return (self.cols, self.rows);
709 }
710 self.config.width = width;
711 self.config.height = height;
712 self.surface.configure(&self.device, &self.config);
713
714 let available_width = (width as f32
715 - self.window_padding * 2.0
716 - self.content_offset_x
717 - self.content_inset_right)
718 .max(0.0);
719 let available_height = (height as f32
720 - self.window_padding * 2.0
721 - self.content_offset_y
722 - self.content_inset_bottom
723 - self.egui_bottom_inset)
724 .max(0.0);
725 let new_cols = (available_width / self.cell_width).max(1.0) as usize;
726 let new_rows = (available_height / self.cell_height).max(1.0) as usize;
727
728 if new_cols != self.cols || new_rows != self.rows {
729 self.cols = new_cols;
730 self.rows = new_rows;
731 self.cells = vec![Cell::default(); self.cols * self.rows];
732 self.dirty_rows = vec![true; self.rows];
733 self.row_cache = (0..self.rows).map(|_| None).collect();
734 self.recreate_instance_buffers();
735 }
736
737 self.update_bg_image_uniforms();
738 (self.cols, self.rows)
739 }
740
741 fn recreate_instance_buffers(&mut self) {
742 self.max_bg_instances = self.cols * self.rows + 10 + self.rows; self.max_text_instances = self.cols * self.rows * 2;
744 let (bg_buf, text_buf) = pipeline::create_instance_buffers(
745 &self.device,
746 self.max_bg_instances,
747 self.max_text_instances,
748 );
749 self.bg_instance_buffer = bg_buf;
750 self.text_instance_buffer = text_buf;
751
752 self.bg_instances = vec![
753 BackgroundInstance {
754 position: [0.0, 0.0],
755 size: [0.0, 0.0],
756 color: [0.0, 0.0, 0.0, 0.0],
757 };
758 self.max_bg_instances
759 ];
760 self.text_instances = vec![
761 TextInstance {
762 position: [0.0, 0.0],
763 size: [0.0, 0.0],
764 tex_offset: [0.0, 0.0],
765 tex_size: [0.0, 0.0],
766 color: [0.0, 0.0, 0.0, 0.0],
767 is_colored: 0,
768 };
769 self.max_text_instances
770 ];
771 }
772
773 pub fn update_cells(&mut self, new_cells: &[Cell]) {
774 for row in 0..self.rows {
775 let start = row * self.cols;
776 let end = (row + 1) * self.cols;
777 if start < new_cells.len() && end <= new_cells.len() {
778 let row_slice = &new_cells[start..end];
779 if row_slice != &self.cells[start..end] {
780 self.cells[start..end].clone_from_slice(row_slice);
781 self.dirty_rows[row] = true;
782 }
783 }
784 }
785 }
786
787 pub fn clear_all_cells(&mut self) {
789 for cell in &mut self.cells {
790 *cell = Cell::default();
791 }
792 for dirty in &mut self.dirty_rows {
793 *dirty = true;
794 }
795 }
796
797 pub fn update_cursor(
798 &mut self,
799 pos: (usize, usize),
800 opacity: f32,
801 style: par_term_emu_core_rust::cursor::CursorStyle,
802 ) {
803 if self.cursor_pos != pos || self.cursor_opacity != opacity || self.cursor_style != style {
804 self.dirty_rows[self.cursor_pos.1.min(self.rows - 1)] = true;
805 self.cursor_pos = pos;
806 self.cursor_opacity = opacity;
807 self.cursor_style = style;
808 self.dirty_rows[self.cursor_pos.1.min(self.rows - 1)] = true;
809
810 use par_term_emu_core_rust::cursor::CursorStyle;
812 self.cursor_overlay = if opacity > 0.0 {
813 let col = pos.0;
814 let row = pos.1;
815 let x0 =
816 (self.window_padding + self.content_offset_x + col as f32 * self.cell_width)
817 .round();
818 let x1 = (self.window_padding
819 + self.content_offset_x
820 + (col + 1) as f32 * self.cell_width)
821 .round();
822 let y0 =
823 (self.window_padding + self.content_offset_y + row as f32 * self.cell_height)
824 .round();
825 let y1 = (self.window_padding
826 + self.content_offset_y
827 + (row + 1) as f32 * self.cell_height)
828 .round();
829
830 match style {
831 CursorStyle::SteadyBlock | CursorStyle::BlinkingBlock => None,
832 CursorStyle::SteadyBar | CursorStyle::BlinkingBar => Some(BackgroundInstance {
833 position: [
834 x0 / self.config.width as f32 * 2.0 - 1.0,
835 1.0 - (y0 / self.config.height as f32 * 2.0),
836 ],
837 size: [
838 2.0 / self.config.width as f32 * 2.0,
839 (y1 - y0) / self.config.height as f32 * 2.0,
840 ],
841 color: [
842 self.cursor_color[0],
843 self.cursor_color[1],
844 self.cursor_color[2],
845 opacity,
846 ],
847 }),
848 CursorStyle::SteadyUnderline | CursorStyle::BlinkingUnderline => {
849 Some(BackgroundInstance {
850 position: [
851 x0 / self.config.width as f32 * 2.0 - 1.0,
852 1.0 - ((y1 - 2.0) / self.config.height as f32 * 2.0),
853 ],
854 size: [
855 (x1 - x0) / self.config.width as f32 * 2.0,
856 2.0 / self.config.height as f32 * 2.0,
857 ],
858 color: [
859 self.cursor_color[0],
860 self.cursor_color[1],
861 self.cursor_color[2],
862 opacity,
863 ],
864 })
865 }
866 }
867 } else {
868 None
869 };
870 }
871 }
872
873 pub fn clear_cursor(&mut self) {
874 self.update_cursor(self.cursor_pos, 0.0, self.cursor_style);
875 }
876
877 pub fn update_cursor_color(&mut self, color: [u8; 3]) {
879 self.cursor_color = [
880 color[0] as f32 / 255.0,
881 color[1] as f32 / 255.0,
882 color[2] as f32 / 255.0,
883 ];
884 self.dirty_rows[self.cursor_pos.1.min(self.rows - 1)] = true;
885 }
886
887 pub fn update_cursor_text_color(&mut self, color: Option<[u8; 3]>) {
889 self.cursor_text_color = color.map(|c| {
890 [
891 c[0] as f32 / 255.0,
892 c[1] as f32 / 255.0,
893 c[2] as f32 / 255.0,
894 ]
895 });
896 self.dirty_rows[self.cursor_pos.1.min(self.rows - 1)] = true;
897 }
898
899 pub fn set_cursor_hidden_for_shader(&mut self, hidden: bool) {
901 if self.cursor_hidden_for_shader != hidden {
902 self.cursor_hidden_for_shader = hidden;
903 self.dirty_rows[self.cursor_pos.1.min(self.rows - 1)] = true;
904 }
905 }
906
907 pub fn set_focused(&mut self, focused: bool) {
909 if self.is_focused != focused {
910 self.is_focused = focused;
911 self.dirty_rows[self.cursor_pos.1.min(self.rows - 1)] = true;
912 }
913 }
914
915 pub fn update_cursor_guide(&mut self, enabled: bool, color: [u8; 4]) {
917 self.cursor_guide_enabled = enabled;
918 self.cursor_guide_color = [
919 color[0] as f32 / 255.0,
920 color[1] as f32 / 255.0,
921 color[2] as f32 / 255.0,
922 color[3] as f32 / 255.0,
923 ];
924 if enabled {
925 self.dirty_rows[self.cursor_pos.1.min(self.rows - 1)] = true;
926 }
927 }
928
929 pub fn update_cursor_shadow(
931 &mut self,
932 enabled: bool,
933 color: [u8; 4],
934 offset: [f32; 2],
935 blur: f32,
936 ) {
937 self.cursor_shadow_enabled = enabled;
938 self.cursor_shadow_color = [
939 color[0] as f32 / 255.0,
940 color[1] as f32 / 255.0,
941 color[2] as f32 / 255.0,
942 color[3] as f32 / 255.0,
943 ];
944 self.cursor_shadow_offset = offset;
945 self.cursor_shadow_blur = blur;
946 if enabled {
947 self.dirty_rows[self.cursor_pos.1.min(self.rows - 1)] = true;
948 }
949 }
950
951 pub fn update_cursor_boost(&mut self, intensity: f32, color: [u8; 3]) {
953 self.cursor_boost = intensity.clamp(0.0, 1.0);
954 self.cursor_boost_color = [
955 color[0] as f32 / 255.0,
956 color[1] as f32 / 255.0,
957 color[2] as f32 / 255.0,
958 ];
959 if intensity > 0.0 {
960 self.dirty_rows[self.cursor_pos.1.min(self.rows - 1)] = true;
961 }
962 }
963
964 pub fn update_unfocused_cursor_style(&mut self, style: par_term_config::UnfocusedCursorStyle) {
966 self.unfocused_cursor_style = style;
967 if !self.is_focused {
968 self.dirty_rows[self.cursor_pos.1.min(self.rows - 1)] = true;
969 }
970 }
971
972 pub fn update_scrollbar(
973 &mut self,
974 scroll_offset: usize,
975 visible_lines: usize,
976 total_lines: usize,
977 marks: &[par_term_config::ScrollbackMark],
978 ) {
979 let right_inset = self.content_inset_right + self.egui_right_inset;
980 self.scrollbar.update(
981 &self.queue,
982 scroll_offset,
983 visible_lines,
984 total_lines,
985 self.config.width,
986 self.config.height,
987 self.content_offset_y,
988 self.content_inset_bottom + self.egui_bottom_inset,
989 right_inset,
990 marks,
991 );
992 }
993
994 pub fn set_visual_bell_intensity(&mut self, intensity: f32) {
995 self.visual_bell_intensity = intensity;
996 }
997
998 pub fn update_opacity(&mut self, opacity: f32) {
999 self.window_opacity = opacity;
1000 self.update_bg_image_uniforms();
1003 }
1004
1005 pub fn set_transparency_affects_only_default_background(&mut self, value: bool) {
1008 if self.transparency_affects_only_default_background != value {
1009 log::info!(
1010 "transparency_affects_only_default_background: {} -> {} (window_opacity={})",
1011 self.transparency_affects_only_default_background,
1012 value,
1013 self.window_opacity
1014 );
1015 self.transparency_affects_only_default_background = value;
1016 self.dirty_rows.fill(true);
1018 }
1019 }
1020
1021 pub fn set_keep_text_opaque(&mut self, value: bool) {
1024 if self.keep_text_opaque != value {
1025 log::info!(
1026 "keep_text_opaque: {} -> {} (window_opacity={}, transparency_affects_only_default_bg={})",
1027 self.keep_text_opaque,
1028 value,
1029 self.window_opacity,
1030 self.transparency_affects_only_default_background
1031 );
1032 self.keep_text_opaque = value;
1033 self.dirty_rows.fill(true);
1035 }
1036 }
1037
1038 pub fn update_command_separator(
1040 &mut self,
1041 enabled: bool,
1042 thickness: f32,
1043 opacity: f32,
1044 exit_color: bool,
1045 color: [u8; 3],
1046 ) {
1047 self.command_separator_enabled = enabled;
1048 self.command_separator_thickness = thickness;
1049 self.command_separator_opacity = opacity;
1050 self.command_separator_exit_color = exit_color;
1051 self.command_separator_color = [
1052 color[0] as f32 / 255.0,
1053 color[1] as f32 / 255.0,
1054 color[2] as f32 / 255.0,
1055 ];
1056 }
1057
1058 pub fn set_separator_marks(&mut self, marks: Vec<SeparatorMark>) {
1060 self.visible_separator_marks = marks;
1061 }
1062
1063 fn separator_color(
1065 &self,
1066 exit_code: Option<i32>,
1067 custom_color: Option<(u8, u8, u8)>,
1068 opacity_mult: f32,
1069 ) -> [f32; 4] {
1070 let alpha = self.command_separator_opacity * opacity_mult;
1071 if let Some((r, g, b)) = custom_color {
1073 return [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, alpha];
1074 }
1075 if self.command_separator_exit_color {
1076 match exit_code {
1077 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], }
1081 } else {
1082 [
1083 self.command_separator_color[0],
1084 self.command_separator_color[1],
1085 self.command_separator_color[2],
1086 alpha,
1087 ]
1088 }
1089 }
1090
1091 pub fn update_scale_factor(&mut self, scale_factor: f64) {
1094 let new_scale = scale_factor as f32;
1095
1096 if (self.scale_factor - new_scale).abs() < f32::EPSILON {
1098 return;
1099 }
1100
1101 log::info!(
1102 "Recalculating font metrics for scale factor change: {} -> {}",
1103 self.scale_factor,
1104 new_scale
1105 );
1106
1107 self.scale_factor = new_scale;
1108
1109 let platform_dpi = if cfg!(target_os = "macos") {
1111 72.0
1112 } else {
1113 96.0
1114 };
1115 let base_font_pixels = self.base_font_size * platform_dpi / 72.0;
1116 self.font_size_pixels = (base_font_pixels * new_scale).max(1.0);
1117
1118 let (font_ascent, font_descent, font_leading, char_advance) = {
1120 let primary_font = self.font_manager.get_font(0).unwrap();
1121 let metrics = primary_font.metrics(&[]);
1122 let scale = self.font_size_pixels / metrics.units_per_em as f32;
1123 let glyph_id = primary_font.charmap().map('m');
1124 let advance = primary_font.glyph_metrics(&[]).advance_width(glyph_id) * scale;
1125 (
1126 metrics.ascent * scale,
1127 metrics.descent * scale,
1128 metrics.leading * scale,
1129 advance,
1130 )
1131 };
1132
1133 self.font_ascent = font_ascent;
1134 self.font_descent = font_descent;
1135 self.font_leading = font_leading;
1136 self.char_advance = char_advance;
1137
1138 let natural_line_height = font_ascent + font_descent + font_leading;
1140 self.cell_height = (natural_line_height * self.line_spacing).max(1.0);
1141 self.cell_width = (char_advance * self.char_spacing).max(1.0);
1142
1143 log::info!(
1144 "New cell dimensions: {}x{} (font_size_pixels: {})",
1145 self.cell_width,
1146 self.cell_height,
1147 self.font_size_pixels
1148 );
1149
1150 self.clear_glyph_cache();
1152
1153 self.dirty_rows.fill(true);
1155 }
1156
1157 #[allow(dead_code)]
1158 pub fn update_window_padding(&mut self, padding: f32) -> Option<(usize, usize)> {
1159 if (self.window_padding - padding).abs() > f32::EPSILON {
1160 self.window_padding = padding;
1161 let size = (self.config.width, self.config.height);
1162 return Some(self.resize(size.0, size.1));
1163 }
1164 None
1165 }
1166
1167 pub fn update_scrollbar_appearance(
1168 &mut self,
1169 width: f32,
1170 thumb_color: [f32; 4],
1171 track_color: [f32; 4],
1172 ) {
1173 self.scrollbar
1174 .update_appearance(width, thumb_color, track_color);
1175 }
1176
1177 pub fn update_scrollbar_position(&mut self, position: &str) {
1178 self.scrollbar.update_position(position);
1179 }
1180
1181 pub fn scrollbar_contains_point(&self, x: f32, y: f32) -> bool {
1182 self.scrollbar.contains_point(x, y)
1183 }
1184
1185 pub fn scrollbar_thumb_bounds(&self) -> Option<(f32, f32)> {
1186 self.scrollbar.thumb_bounds()
1187 }
1188
1189 pub fn scrollbar_track_contains_x(&self, x: f32) -> bool {
1190 self.scrollbar.track_contains_x(x)
1191 }
1192
1193 pub fn scrollbar_mouse_y_to_scroll_offset(&self, mouse_y: f32) -> Option<usize> {
1194 self.scrollbar.mouse_y_to_scroll_offset(mouse_y)
1195 }
1196
1197 pub fn scrollbar_mark_at_position(
1200 &self,
1201 mouse_x: f32,
1202 mouse_y: f32,
1203 tolerance: f32,
1204 ) -> Option<&par_term_config::ScrollbackMark> {
1205 self.scrollbar.mark_at_position(mouse_x, mouse_y, tolerance)
1206 }
1207
1208 pub fn reconfigure_surface(&mut self) {
1209 self.surface.configure(&self.device, &self.config);
1210 }
1211
1212 pub fn update_font_antialias(&mut self, enabled: bool) -> bool {
1215 if self.font_antialias != enabled {
1216 self.font_antialias = enabled;
1217 self.clear_glyph_cache();
1218 self.dirty_rows.fill(true);
1219 true
1220 } else {
1221 false
1222 }
1223 }
1224
1225 pub fn update_font_hinting(&mut self, enabled: bool) -> bool {
1228 if self.font_hinting != enabled {
1229 self.font_hinting = enabled;
1230 self.clear_glyph_cache();
1231 self.dirty_rows.fill(true);
1232 true
1233 } else {
1234 false
1235 }
1236 }
1237
1238 pub fn update_font_thin_strokes(&mut self, mode: par_term_config::ThinStrokesMode) -> bool {
1241 if self.font_thin_strokes != mode {
1242 self.font_thin_strokes = mode;
1243 self.clear_glyph_cache();
1244 self.dirty_rows.fill(true);
1245 true
1246 } else {
1247 false
1248 }
1249 }
1250
1251 pub fn update_minimum_contrast(&mut self, ratio: f32) -> bool {
1254 let ratio = ratio.clamp(1.0, 21.0);
1256 if (self.minimum_contrast - ratio).abs() > 0.001 {
1257 self.minimum_contrast = ratio;
1258 self.dirty_rows.fill(true);
1259 true
1260 } else {
1261 false
1262 }
1263 }
1264
1265 pub(crate) fn ensure_minimum_contrast(&self, fg: [f32; 4], bg: [f32; 4]) -> [f32; 4] {
1269 if self.minimum_contrast <= 1.0 {
1271 return fg;
1272 }
1273
1274 fn luminance(color: [f32; 4]) -> f32 {
1276 let r = color[0].powf(2.2);
1277 let g = color[1].powf(2.2);
1278 let b = color[2].powf(2.2);
1279 0.2126 * r + 0.7152 * g + 0.0722 * b
1280 }
1281
1282 fn contrast_ratio(l1: f32, l2: f32) -> f32 {
1283 let (lighter, darker) = if l1 > l2 { (l1, l2) } else { (l2, l1) };
1284 (lighter + 0.05) / (darker + 0.05)
1285 }
1286
1287 let fg_lum = luminance(fg);
1288 let bg_lum = luminance(bg);
1289 let current_ratio = contrast_ratio(fg_lum, bg_lum);
1290
1291 if current_ratio >= self.minimum_contrast {
1293 return fg;
1294 }
1295
1296 let bg_is_dark = bg_lum < 0.5;
1299
1300 let mut low = 0.0f32;
1302 let mut high = 1.0f32;
1303
1304 for _ in 0..20 {
1305 let mid = (low + high) / 2.0;
1307
1308 let adjusted = if bg_is_dark {
1309 [
1311 fg[0] + (1.0 - fg[0]) * mid,
1312 fg[1] + (1.0 - fg[1]) * mid,
1313 fg[2] + (1.0 - fg[2]) * mid,
1314 fg[3],
1315 ]
1316 } else {
1317 [
1319 fg[0] * (1.0 - mid),
1320 fg[1] * (1.0 - mid),
1321 fg[2] * (1.0 - mid),
1322 fg[3],
1323 ]
1324 };
1325
1326 let adjusted_lum = luminance(adjusted);
1327 let new_ratio = contrast_ratio(adjusted_lum, bg_lum);
1328
1329 if new_ratio >= self.minimum_contrast {
1330 high = mid;
1331 } else {
1332 low = mid;
1333 }
1334 }
1335
1336 if bg_is_dark {
1338 [
1339 fg[0] + (1.0 - fg[0]) * high,
1340 fg[1] + (1.0 - fg[1]) * high,
1341 fg[2] + (1.0 - fg[2]) * high,
1342 fg[3],
1343 ]
1344 } else {
1345 [
1346 fg[0] * (1.0 - high),
1347 fg[1] * (1.0 - high),
1348 fg[2] * (1.0 - high),
1349 fg[3],
1350 ]
1351 }
1352 }
1353
1354 pub(crate) fn should_use_thin_strokes(&self) -> bool {
1356 use par_term_config::ThinStrokesMode;
1357
1358 let is_retina = self.scale_factor > 1.5;
1360
1361 let bg_brightness =
1363 (self.background_color[0] + self.background_color[1] + self.background_color[2]) / 3.0;
1364 let is_dark_background = bg_brightness < 0.5;
1365
1366 match self.font_thin_strokes {
1367 ThinStrokesMode::Never => false,
1368 ThinStrokesMode::Always => true,
1369 ThinStrokesMode::RetinaOnly => is_retina,
1370 ThinStrokesMode::DarkBackgroundsOnly => is_dark_background,
1371 ThinStrokesMode::RetinaDarkBackgroundsOnly => is_retina && is_dark_background,
1372 }
1373 }
1374
1375 #[allow(dead_code)]
1377 pub fn supported_present_modes(&self) -> &[wgpu::PresentMode] {
1378 &self.supported_present_modes
1379 }
1380
1381 pub fn is_vsync_mode_supported(&self, mode: par_term_config::VsyncMode) -> bool {
1383 self.supported_present_modes
1384 .contains(&mode.to_present_mode())
1385 }
1386
1387 pub fn update_vsync_mode(
1390 &mut self,
1391 mode: par_term_config::VsyncMode,
1392 ) -> (par_term_config::VsyncMode, bool) {
1393 let requested = mode.to_present_mode();
1394 let current = self.config.present_mode;
1395
1396 let actual = if self.supported_present_modes.contains(&requested) {
1398 requested
1399 } else {
1400 log::warn!(
1401 "Requested present mode {:?} not supported, falling back to Fifo",
1402 requested
1403 );
1404 wgpu::PresentMode::Fifo
1405 };
1406
1407 if actual != current {
1409 self.config.present_mode = actual;
1410 self.surface.configure(&self.device, &self.config);
1411 log::info!("VSync mode changed to {:?}", actual);
1412 }
1413
1414 let actual_vsync = match actual {
1416 wgpu::PresentMode::Immediate => par_term_config::VsyncMode::Immediate,
1417 wgpu::PresentMode::Mailbox => par_term_config::VsyncMode::Mailbox,
1418 wgpu::PresentMode::Fifo | wgpu::PresentMode::FifoRelaxed => {
1419 par_term_config::VsyncMode::Fifo
1420 }
1421 _ => par_term_config::VsyncMode::Fifo,
1422 };
1423
1424 (actual_vsync, actual != current)
1425 }
1426
1427 #[allow(dead_code)]
1429 pub fn current_vsync_mode(&self) -> par_term_config::VsyncMode {
1430 match self.config.present_mode {
1431 wgpu::PresentMode::Immediate => par_term_config::VsyncMode::Immediate,
1432 wgpu::PresentMode::Mailbox => par_term_config::VsyncMode::Mailbox,
1433 wgpu::PresentMode::Fifo | wgpu::PresentMode::FifoRelaxed => {
1434 par_term_config::VsyncMode::Fifo
1435 }
1436 _ => par_term_config::VsyncMode::Fifo,
1437 }
1438 }
1439
1440 #[allow(dead_code)]
1441 pub fn update_graphics(
1442 &mut self,
1443 _graphics: &[par_term_emu_core_rust::graphics::TerminalGraphic],
1444 _scroll_offset: usize,
1445 _scrollback_len: usize,
1446 _visible_lines: usize,
1447 ) -> Result<()> {
1448 Ok(())
1449 }
1450}