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