Skip to main content

par_term_render/cell_renderer/
mod.rs

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;
16// Re-export public types for external use
17pub use types::{Cell, PaneViewport};
18// Re-export internal types for use within the cell_renderer module
19pub(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    /// Supported present modes for this surface (for vsync mode validation)
27    pub(crate) supported_present_modes: Vec<wgpu::PresentMode>,
28
29    // Pipelines
30    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    // Buffers
37    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    // Bind groups
45    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    // Glyph atlas
54    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    // Grid state
65    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    /// Vertical offset for terminal content (e.g., tab bar at top).
71    /// Content is rendered starting at y = window_padding + content_offset_y.
72    pub(crate) content_offset_y: f32,
73    /// Horizontal offset for terminal content (e.g., tab bar on left).
74    /// Content is rendered starting at x = window_padding + content_offset_x.
75    pub(crate) content_offset_x: f32,
76    /// Bottom inset for terminal content (e.g., tab bar at bottom).
77    /// Reduces available height without shifting content vertically.
78    pub(crate) content_inset_bottom: f32,
79    /// Right inset for terminal content (e.g., AI Inspector panel).
80    /// Reduces available width without shifting content horizontally.
81    pub(crate) content_inset_right: f32,
82    /// Additional bottom inset from egui panels (status bar, tmux bar).
83    /// This is added to content_inset_bottom for scrollbar bounds only,
84    /// since egui panels already claim space before wgpu rendering.
85    pub(crate) egui_bottom_inset: f32,
86    /// Additional right inset from egui panels (AI Inspector).
87    /// This is added to content_inset_right for scrollbar bounds only,
88    /// since egui panels already claim space before wgpu rendering.
89    pub(crate) egui_right_inset: f32,
90    #[allow(dead_code)]
91    pub(crate) scale_factor: f32,
92
93    // Components
94    pub(crate) font_manager: FontManager,
95    pub(crate) scrollbar: Scrollbar,
96
97    // Dynamic state
98    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    /// Separate cursor instance for beam/underline styles (rendered as overlay)
105    pub(crate) cursor_overlay: Option<BackgroundInstance>,
106    /// Cursor color [R, G, B] as floats (0.0-1.0)
107    pub(crate) cursor_color: [f32; 3],
108    /// Text color under block cursor [R, G, B] as floats (0.0-1.0), or None for auto-contrast
109    pub(crate) cursor_text_color: Option<[f32; 3]>,
110    /// Hide cursor when cursor shader is active (let shader handle cursor rendering)
111    pub(crate) cursor_hidden_for_shader: bool,
112    /// Whether the window is currently focused (for unfocused cursor style)
113    pub(crate) is_focused: bool,
114
115    // Cursor enhancement settings
116    /// Enable cursor guide (horizontal line at cursor row)
117    pub(crate) cursor_guide_enabled: bool,
118    /// Cursor guide color [R, G, B, A] as floats (0.0-1.0)
119    pub(crate) cursor_guide_color: [f32; 4],
120    /// Enable cursor shadow
121    pub(crate) cursor_shadow_enabled: bool,
122    /// Cursor shadow color [R, G, B, A] as floats (0.0-1.0)
123    pub(crate) cursor_shadow_color: [f32; 4],
124    /// Cursor shadow offset in pixels [x, y]
125    pub(crate) cursor_shadow_offset: [f32; 2],
126    /// Cursor shadow blur radius (not fully supported yet, but stores config)
127    #[allow(dead_code)]
128    pub(crate) cursor_shadow_blur: f32,
129    /// Cursor boost (glow) intensity (0.0-1.0)
130    pub(crate) cursor_boost: f32,
131    /// Cursor boost glow color [R, G, B] as floats (0.0-1.0)
132    pub(crate) cursor_boost_color: [f32; 3],
133    /// Unfocused cursor style (hollow, same, hidden)
134    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    // Font configuration (base values, before scale factor)
140    pub(crate) base_font_size: f32,
141    pub(crate) line_spacing: f32,
142    pub(crate) char_spacing: f32,
143
144    // Font metrics (scaled by current scale_factor)
145    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    // Background image
152    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    /// When true, current background is a solid color (not an image).
158    /// Solid colors should be rendered via clear color to respect window_opacity,
159    /// not via bg_image_pipeline which would cover the transparent background.
160    pub(crate) bg_is_solid_color: bool,
161    /// The solid background color [R, G, B] as floats (0.0-1.0).
162    /// Only used when bg_is_solid_color is true.
163    pub(crate) solid_bg_color: [f32; 3],
164
165    /// Cache of per-pane background textures keyed by image path
166    pub(crate) pane_bg_cache: HashMap<String, background::PaneBackgroundEntry>,
167
168    // Metrics
169    pub(crate) max_bg_instances: usize,
170    pub(crate) max_text_instances: usize,
171
172    // CPU-side instance buffers for incremental updates
173    pub(crate) bg_instances: Vec<BackgroundInstance>,
174    pub(crate) text_instances: Vec<TextInstance>,
175
176    // Shaping options
177    #[allow(dead_code)]
178    pub(crate) enable_text_shaping: bool,
179    pub(crate) enable_ligatures: bool,
180    pub(crate) enable_kerning: bool,
181
182    // Font rendering options
183    /// Enable anti-aliasing for font rendering
184    pub(crate) font_antialias: bool,
185    /// Enable hinting for font rendering
186    pub(crate) font_hinting: bool,
187    /// Thin strokes mode for font rendering
188    pub(crate) font_thin_strokes: par_term_config::ThinStrokesMode,
189    /// Minimum contrast ratio for text against background (WCAG standard)
190    /// 1.0 = disabled, 4.5 = WCAG AA, 7.0 = WCAG AAA
191    pub(crate) minimum_contrast: f32,
192
193    // Solid white pixel in atlas for geometric block rendering
194    pub(crate) solid_pixel_offset: (u32, u32),
195
196    // Transparency mode
197    /// When true, only default background cells are transparent.
198    /// Non-default (colored) backgrounds remain opaque for readability.
199    pub(crate) transparency_affects_only_default_background: bool,
200
201    /// When true, text is always rendered at full opacity regardless of window transparency.
202    pub(crate) keep_text_opaque: bool,
203
204    // Command separator line settings
205    /// Whether to render separator lines between commands
206    pub(crate) command_separator_enabled: bool,
207    /// Thickness of separator lines in pixels
208    pub(crate) command_separator_thickness: f32,
209    /// Opacity of separator lines (0.0-1.0)
210    pub(crate) command_separator_opacity: f32,
211    /// Whether to color separator lines by exit code
212    pub(crate) command_separator_exit_color: bool,
213    /// Custom separator color [R, G, B] as floats (0.0-1.0)
214    pub(crate) command_separator_color: [f32; 3],
215    /// Visible separator marks for current frame: (screen_row, exit_code, custom_color)
216    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        // Platform-specific backend selection for better VM compatibility
254        // Windows: Use DX12 (Vulkan may not work in VMs like Parallels)
255        // macOS: Use Metal (native)
256        // Linux: Try Vulkan first, fall back to GL for VM compatibility
257        // Platform-specific backend selection for better VM compatibility
258        // Windows: Use DX12 (Vulkan may not work in VMs like Parallels)
259        // macOS: Use Metal (native)
260        // Linux: Try Vulkan first, fall back to GL for VM compatibility
261        #[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        // Store supported present modes for runtime validation
306        let supported_present_modes = surface_caps.present_modes.clone();
307
308        // Select present mode with fallback if requested mode isn't supported
309        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            // Fall back to Fifo (always supported) or first available
314            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        // Select alpha mode for window transparency
327        // Prefer PreMultiplied (best for compositing) > PostMultiplied > Auto > first available
328        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        // Extract font metrics
384        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        // Create pipelines using the pipeline module
412        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        // Instance buffers
439        let max_bg_instances = cols * rows + 10 + rows; // Extra slots for cursor overlays + separator lines
440        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        // Upload a solid white 2x2 pixel block to the atlas for geometric block rendering
570        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            // Handle missing background image gracefully - don't crash, just log and continue
578            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    /// Upload a solid white pixel to the atlas for use in geometric block rendering
591    pub(crate) fn upload_solid_pixel(&mut self) {
592        let size = 2u32; // 2x2 for better sampling
593        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; // padding
621        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    /// Set the vertical content offset (e.g., tab bar height at top).
646    /// Returns Some((cols, rows)) if grid size changed, None otherwise.
647    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    /// Set the horizontal content offset (e.g., tab bar on left).
659    /// Returns Some((cols, rows)) if grid size changed, None otherwise.
660    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    /// Set the bottom content inset (e.g., tab bar at bottom).
672    /// Returns Some((cols, rows)) if grid size changed, None otherwise.
673    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    /// Set the right content inset (e.g., AI Inspector panel).
685    /// Returns Some((cols, rows)) if grid size changed, None otherwise.
686    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; // Extra slots for cursor overlays + separator lines
743        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    /// Clear all cells and mark all rows as dirty.
788    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            // Compute cursor overlay for beam/underline styles
811            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    /// Update cursor color
878    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    /// Update cursor text color (color of text under block cursor)
888    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    /// Set whether cursor should be hidden when cursor shader is active
900    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    /// Set window focus state (affects unfocused cursor rendering)
908    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    /// Update cursor guide settings
916    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    /// Update cursor shadow settings
930    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    /// Update cursor boost settings
952    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    /// Update unfocused cursor style
965    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        // update_bg_image_uniforms() multiplies bg_image_opacity by window_opacity,
1001        // so both images and solid colors respect window transparency
1002        self.update_bg_image_uniforms();
1003    }
1004
1005    /// Set whether transparency affects only default background cells.
1006    /// When true, non-default (colored) backgrounds remain opaque for readability.
1007    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            // Mark all rows dirty to re-render with new transparency behavior
1017            self.dirty_rows.fill(true);
1018        }
1019    }
1020
1021    /// Set whether text should always be rendered at full opacity.
1022    /// When true, text remains opaque regardless of window transparency settings.
1023    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            // Mark all rows dirty to re-render with new text opacity behavior
1034            self.dirty_rows.fill(true);
1035        }
1036    }
1037
1038    /// Update command separator settings from config
1039    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    /// Set the visible separator marks for the current frame
1059    pub fn set_separator_marks(&mut self, marks: Vec<SeparatorMark>) {
1060        self.visible_separator_marks = marks;
1061    }
1062
1063    /// Compute separator color based on exit code and settings
1064    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        // Custom color from trigger marks takes priority
1072        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],   // Green for success
1078                Some(_) => [0.85, 0.25, 0.25, alpha], // Red for failure
1079                None => [0.5, 0.5, 0.5, alpha],       // Gray for unknown
1080            }
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    /// Update scale factor and recalculate all font metrics and cell dimensions.
1092    /// This is called when the window is dragged between displays with different DPIs.
1093    pub fn update_scale_factor(&mut self, scale_factor: f64) {
1094        let new_scale = scale_factor as f32;
1095
1096        // Skip if scale factor hasn't changed
1097        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        // Recalculate font_size_pixels based on new scale factor
1110        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        // Re-extract font metrics at new scale
1119        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        // Recalculate cell dimensions
1139        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        // Clear glyph cache - glyphs need to be re-rasterized at new DPI
1151        self.clear_glyph_cache();
1152
1153        // Mark all rows as dirty to force re-rendering
1154        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    /// Find a scrollbar mark at the given mouse position for tooltip display.
1198    /// Returns the mark if mouse is within `tolerance` pixels of a mark.
1199    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    /// Update font anti-aliasing setting
1213    /// Returns true if the setting changed (requiring glyph cache clear)
1214    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    /// Update font hinting setting
1226    /// Returns true if the setting changed (requiring glyph cache clear)
1227    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    /// Update thin strokes mode
1239    /// Returns true if the setting changed (requiring glyph cache clear)
1240    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    /// Update minimum contrast ratio
1252    /// Returns true if the setting changed (requiring redraw)
1253    pub fn update_minimum_contrast(&mut self, ratio: f32) -> bool {
1254        // Clamp to valid range: 1.0 (disabled) to 21.0 (max possible contrast)
1255        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    /// Adjust foreground color to meet minimum contrast ratio against background
1266    /// Uses WCAG luminance formula for accurate contrast calculation.
1267    /// Returns the adjusted color [R, G, B, A] with preserved alpha.
1268    pub(crate) fn ensure_minimum_contrast(&self, fg: [f32; 4], bg: [f32; 4]) -> [f32; 4] {
1269        // If minimum_contrast is 1.0 (disabled) or less, no adjustment needed
1270        if self.minimum_contrast <= 1.0 {
1271            return fg;
1272        }
1273
1274        // Calculate luminance using WCAG formula
1275        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 already meets minimum contrast, return unchanged
1292        if current_ratio >= self.minimum_contrast {
1293            return fg;
1294        }
1295
1296        // Determine if we need to lighten or darken the foreground
1297        // If background is dark, lighten fg; if light, darken fg
1298        let bg_is_dark = bg_lum < 0.5;
1299
1300        // Binary search for the minimum adjustment needed
1301        let mut low = 0.0f32;
1302        let mut high = 1.0f32;
1303
1304        for _ in 0..20 {
1305            // 20 iterations gives ~1/1_000_000 precision
1306            let mid = (low + high) / 2.0;
1307
1308            let adjusted = if bg_is_dark {
1309                // Lighten: mix with white
1310                [
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                // Darken: mix with black
1318                [
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        // Apply the final adjustment
1337        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    /// Check if thin strokes should be applied based on current mode and context
1355    pub(crate) fn should_use_thin_strokes(&self) -> bool {
1356        use par_term_config::ThinStrokesMode;
1357
1358        // Check if we're on a Retina/HiDPI display (scale factor > 1.5)
1359        let is_retina = self.scale_factor > 1.5;
1360
1361        // Check if background is dark (average < 128)
1362        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    /// Get the list of supported present modes for this surface
1376    #[allow(dead_code)]
1377    pub fn supported_present_modes(&self) -> &[wgpu::PresentMode] {
1378        &self.supported_present_modes
1379    }
1380
1381    /// Check if a vsync mode is supported
1382    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    /// Update the vsync mode. Returns the actual mode applied (may differ if requested mode unsupported).
1388    /// Also returns whether the mode was changed.
1389    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        // Determine the actual mode to use
1397        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        // Only reconfigure if the mode actually changed
1408        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        // Convert back to VsyncMode for return
1415        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    /// Get the current vsync mode
1428    #[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}