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    /// Style for link underlines (solid or stipple)
205    pub(crate) link_underline_style: par_term_config::LinkUnderlineStyle,
206
207    // Command separator line settings
208    /// Whether to render separator lines between commands
209    pub(crate) command_separator_enabled: bool,
210    /// Thickness of separator lines in pixels
211    pub(crate) command_separator_thickness: f32,
212    /// Opacity of separator lines (0.0-1.0)
213    pub(crate) command_separator_opacity: f32,
214    /// Whether to color separator lines by exit code
215    pub(crate) command_separator_exit_color: bool,
216    /// Custom separator color [R, G, B] as floats (0.0-1.0)
217    pub(crate) command_separator_color: [f32; 3],
218    /// Visible separator marks for current frame: (screen_row, exit_code, custom_color)
219    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        // Platform-specific backend selection for better VM compatibility
257        // Windows: Use DX12 (Vulkan may not work in VMs like Parallels)
258        // macOS: Use Metal (native)
259        // Linux: Try Vulkan first, fall back to GL for VM compatibility
260        // Platform-specific backend selection for better VM compatibility
261        // Windows: Use DX12 (Vulkan may not work in VMs like Parallels)
262        // macOS: Use Metal (native)
263        // Linux: Try Vulkan first, fall back to GL for VM compatibility
264        #[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        // Store supported present modes for runtime validation
309        let supported_present_modes = surface_caps.present_modes.clone();
310
311        // Select present mode with fallback if requested mode isn't supported
312        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            // Fall back to Fifo (always supported) or first available
317            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        // Select alpha mode for window transparency
330        // Prefer PreMultiplied (best for compositing) > PostMultiplied > Auto > first available
331        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        // Extract font metrics
387        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        // Create pipelines using the pipeline module
415        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        // Instance buffers
442        let max_bg_instances = cols * rows + 10 + rows; // Extra slots for cursor overlays + separator lines
443        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        // Upload a solid white 2x2 pixel block to the atlas for geometric block rendering
574        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            // Handle missing background image gracefully - don't crash, just log and continue
582            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    /// Upload a solid white pixel to the atlas for use in geometric block rendering
595    pub(crate) fn upload_solid_pixel(&mut self) {
596        let size = 2u32; // 2x2 for better sampling
597        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; // padding
625        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    /// Set the vertical content offset (e.g., tab bar height at top).
650    /// Returns Some((cols, rows)) if grid size changed, None otherwise.
651    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    /// Set the horizontal content offset (e.g., tab bar on left).
663    /// Returns Some((cols, rows)) if grid size changed, None otherwise.
664    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    /// Set the bottom content inset (e.g., tab bar at bottom).
676    /// Returns Some((cols, rows)) if grid size changed, None otherwise.
677    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    /// Set the right content inset (e.g., AI Inspector panel).
689    /// Returns Some((cols, rows)) if grid size changed, None otherwise.
690    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; // Extra slots for cursor overlays + separator lines
748        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    /// Update cells. Returns `true` if any row actually changed.
779    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    /// Clear all cells and mark all rows as dirty.
797    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    /// Update cursor position, opacity and style. Returns `true` if anything changed.
807    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            // Compute cursor overlay for beam/underline styles
821            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    /// Update cursor color
890    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    /// Update cursor text color (color of text under block cursor)
900    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    /// Set whether cursor should be hidden when cursor shader is active.
912    /// Returns `true` if the value changed.
913    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    /// Set window focus state (affects unfocused cursor rendering).
923    /// Returns `true` if the value changed.
924    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    /// Update cursor guide settings
934    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    /// Update cursor shadow settings
948    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    /// Update cursor boost settings
970    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    /// Update unfocused cursor style
983    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        // update_bg_image_uniforms() multiplies bg_image_opacity by window_opacity,
1019        // so both images and solid colors respect window transparency
1020        self.update_bg_image_uniforms();
1021    }
1022
1023    /// Set whether transparency affects only default background cells.
1024    /// When true, non-default (colored) backgrounds remain opaque for readability.
1025    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            // Mark all rows dirty to re-render with new transparency behavior
1035            self.dirty_rows.fill(true);
1036        }
1037    }
1038
1039    /// Set whether text should always be rendered at full opacity.
1040    /// When true, text remains opaque regardless of window transparency settings.
1041    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            // Mark all rows dirty to re-render with new text opacity behavior
1052            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    /// Update command separator settings from config
1064    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    /// Set the visible separator marks for the current frame.
1084    /// Returns `true` if the marks changed.
1085    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    /// Compute separator color based on exit code and settings
1094    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        // Custom color from trigger marks takes priority
1102        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],   // Green for success
1108                Some(_) => [0.85, 0.25, 0.25, alpha], // Red for failure
1109                None => [0.5, 0.5, 0.5, alpha],       // Gray for unknown
1110            }
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    /// Update scale factor and recalculate all font metrics and cell dimensions.
1122    /// This is called when the window is dragged between displays with different DPIs.
1123    pub fn update_scale_factor(&mut self, scale_factor: f64) {
1124        let new_scale = scale_factor as f32;
1125
1126        // Skip if scale factor hasn't changed
1127        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        // Recalculate font_size_pixels based on new scale factor
1140        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        // Re-extract font metrics at new scale
1149        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        // Recalculate cell dimensions
1169        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        // Clear glyph cache - glyphs need to be re-rasterized at new DPI
1181        self.clear_glyph_cache();
1182
1183        // Mark all rows as dirty to force re-rendering
1184        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    /// Find a scrollbar mark at the given mouse position for tooltip display.
1228    /// Returns the mark if mouse is within `tolerance` pixels of a mark.
1229    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    /// Update font anti-aliasing setting
1243    /// Returns true if the setting changed (requiring glyph cache clear)
1244    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    /// Update font hinting setting
1256    /// Returns true if the setting changed (requiring glyph cache clear)
1257    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    /// Update thin strokes mode
1269    /// Returns true if the setting changed (requiring glyph cache clear)
1270    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    /// Update minimum contrast ratio
1282    /// Returns true if the setting changed (requiring redraw)
1283    pub fn update_minimum_contrast(&mut self, ratio: f32) -> bool {
1284        // Clamp to valid range: 1.0 (disabled) to 21.0 (max possible contrast)
1285        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    /// Adjust foreground color to meet minimum contrast ratio against background
1296    /// Uses WCAG luminance formula for accurate contrast calculation.
1297    /// Returns the adjusted color [R, G, B, A] with preserved alpha.
1298    pub(crate) fn ensure_minimum_contrast(&self, fg: [f32; 4], bg: [f32; 4]) -> [f32; 4] {
1299        // If minimum_contrast is 1.0 (disabled) or less, no adjustment needed
1300        if self.minimum_contrast <= 1.0 {
1301            return fg;
1302        }
1303
1304        // Calculate luminance using WCAG formula
1305        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 already meets minimum contrast, return unchanged
1322        if current_ratio >= self.minimum_contrast {
1323            return fg;
1324        }
1325
1326        // Determine if we need to lighten or darken the foreground
1327        // If background is dark, lighten fg; if light, darken fg
1328        let bg_is_dark = bg_lum < 0.5;
1329
1330        // Binary search for the minimum adjustment needed
1331        let mut low = 0.0f32;
1332        let mut high = 1.0f32;
1333
1334        for _ in 0..20 {
1335            // 20 iterations gives ~1/1_000_000 precision
1336            let mid = (low + high) / 2.0;
1337
1338            let adjusted = if bg_is_dark {
1339                // Lighten: mix with white
1340                [
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                // Darken: mix with black
1348                [
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        // Apply the final adjustment
1367        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    /// Check if thin strokes should be applied based on current mode and context
1385    pub(crate) fn should_use_thin_strokes(&self) -> bool {
1386        use par_term_config::ThinStrokesMode;
1387
1388        // Check if we're on a Retina/HiDPI display (scale factor > 1.5)
1389        let is_retina = self.scale_factor > 1.5;
1390
1391        // Check if background is dark (average < 128)
1392        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    /// Get the list of supported present modes for this surface
1406    #[allow(dead_code)]
1407    pub fn supported_present_modes(&self) -> &[wgpu::PresentMode] {
1408        &self.supported_present_modes
1409    }
1410
1411    /// Check if a vsync mode is supported
1412    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    /// Update the vsync mode. Returns the actual mode applied (may differ if requested mode unsupported).
1418    /// Also returns whether the mode was changed.
1419    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        // Determine the actual mode to use
1427        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        // Only reconfigure if the mode actually changed
1438        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        // Convert back to VsyncMode for return
1445        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    /// Get the current vsync mode
1458    #[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}