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    /// Gutter indicator marks for current frame: (screen_row, rgba_color)
221    pub(crate) gutter_indicators: Vec<(usize, [f32; 4])>,
222}
223
224impl CellRenderer {
225    #[allow(clippy::too_many_arguments)]
226    pub async fn new(
227        window: Arc<Window>,
228        font_family: Option<&str>,
229        font_family_bold: Option<&str>,
230        font_family_italic: Option<&str>,
231        font_family_bold_italic: Option<&str>,
232        font_ranges: &[par_term_config::FontRange],
233        font_size: f32,
234        cols: usize,
235        rows: usize,
236        window_padding: f32,
237        line_spacing: f32,
238        char_spacing: f32,
239        scrollbar_position: &str,
240        scrollbar_width: f32,
241        scrollbar_thumb_color: [f32; 4],
242        scrollbar_track_color: [f32; 4],
243        enable_text_shaping: bool,
244        enable_ligatures: bool,
245        enable_kerning: bool,
246        font_antialias: bool,
247        font_hinting: bool,
248        font_thin_strokes: par_term_config::ThinStrokesMode,
249        minimum_contrast: f32,
250        vsync_mode: par_term_config::VsyncMode,
251        power_preference: par_term_config::PowerPreference,
252        window_opacity: f32,
253        background_color: [u8; 3],
254        background_image_path: Option<&str>,
255        background_image_mode: par_term_config::BackgroundImageMode,
256        background_image_opacity: f32,
257    ) -> Result<Self> {
258        // Platform-specific backend selection for better VM compatibility
259        // Windows: Use DX12 (Vulkan may not work in VMs like Parallels)
260        // macOS: Use Metal (native)
261        // Linux: Try Vulkan first, fall back to GL for VM compatibility
262        // Platform-specific backend selection for better VM compatibility
263        // Windows: Use DX12 (Vulkan may not work in VMs like Parallels)
264        // macOS: Use Metal (native)
265        // Linux: Try Vulkan first, fall back to GL for VM compatibility
266        #[cfg(target_os = "windows")]
267        let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
268            backends: wgpu::Backends::DX12,
269            ..Default::default()
270        });
271        #[cfg(target_os = "macos")]
272        let instance = wgpu::Instance::default();
273        #[cfg(target_os = "linux")]
274        let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
275            backends: wgpu::Backends::VULKAN | wgpu::Backends::GL,
276            ..Default::default()
277        });
278        let surface = instance.create_surface(window.clone())?;
279        let adapter = instance
280            .request_adapter(&wgpu::RequestAdapterOptions {
281                power_preference: power_preference.to_wgpu(),
282                compatible_surface: Some(&surface),
283                force_fallback_adapter: false,
284            })
285            .await
286            .context("Failed to find wgpu adapter")?;
287
288        let (device, queue) = adapter
289            .request_device(&wgpu::DeviceDescriptor {
290                label: Some("device"),
291                required_features: wgpu::Features::empty(),
292                required_limits: wgpu::Limits::default(),
293                memory_hints: wgpu::MemoryHints::default(),
294                ..Default::default()
295            })
296            .await?;
297
298        let device = Arc::new(device);
299        let queue = Arc::new(queue);
300
301        let size = window.inner_size();
302        let surface_caps = surface.get_capabilities(&adapter);
303        let surface_format = surface_caps
304            .formats
305            .iter()
306            .copied()
307            .find(|f| !f.is_srgb())
308            .unwrap_or(surface_caps.formats[0]);
309
310        // Store supported present modes for runtime validation
311        let supported_present_modes = surface_caps.present_modes.clone();
312
313        // Select present mode with fallback if requested mode isn't supported
314        let requested_mode = vsync_mode.to_present_mode();
315        let present_mode = if supported_present_modes.contains(&requested_mode) {
316            requested_mode
317        } else {
318            // Fall back to Fifo (always supported) or first available
319            log::warn!(
320                "Requested present mode {:?} not supported (available: {:?}), falling back",
321                requested_mode,
322                supported_present_modes
323            );
324            if supported_present_modes.contains(&wgpu::PresentMode::Fifo) {
325                wgpu::PresentMode::Fifo
326            } else {
327                supported_present_modes[0]
328            }
329        };
330
331        // Select alpha mode for window transparency
332        // Prefer PreMultiplied (best for compositing) > PostMultiplied > Auto > first available
333        let alpha_mode = if surface_caps
334            .alpha_modes
335            .contains(&wgpu::CompositeAlphaMode::PreMultiplied)
336        {
337            wgpu::CompositeAlphaMode::PreMultiplied
338        } else if surface_caps
339            .alpha_modes
340            .contains(&wgpu::CompositeAlphaMode::PostMultiplied)
341        {
342            wgpu::CompositeAlphaMode::PostMultiplied
343        } else if surface_caps
344            .alpha_modes
345            .contains(&wgpu::CompositeAlphaMode::Auto)
346        {
347            wgpu::CompositeAlphaMode::Auto
348        } else {
349            surface_caps.alpha_modes[0]
350        };
351        log::info!(
352            "Selected alpha mode: {:?} (available: {:?})",
353            alpha_mode,
354            surface_caps.alpha_modes
355        );
356
357        let config = wgpu::SurfaceConfiguration {
358            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
359            format: surface_format,
360            width: size.width.max(1),
361            height: size.height.max(1),
362            present_mode,
363            alpha_mode,
364            view_formats: vec![],
365            desired_maximum_frame_latency: 2,
366        };
367        surface.configure(&device, &config);
368
369        let scale_factor = window.scale_factor() as f32;
370
371        let platform_dpi = if cfg!(target_os = "macos") {
372            72.0
373        } else {
374            96.0
375        };
376
377        let base_font_pixels = font_size * platform_dpi / 72.0;
378        let font_size_pixels = (base_font_pixels * scale_factor).max(1.0);
379
380        let font_manager = FontManager::new(
381            font_family,
382            font_family_bold,
383            font_family_italic,
384            font_family_bold_italic,
385            font_ranges,
386        )?;
387
388        // Extract font metrics
389        let (font_ascent, font_descent, font_leading, char_advance) = {
390            let primary_font = font_manager.get_font(0).unwrap();
391            let metrics = primary_font.metrics(&[]);
392            let scale = font_size_pixels / metrics.units_per_em as f32;
393            let glyph_id = primary_font.charmap().map('m');
394            let advance = primary_font.glyph_metrics(&[]).advance_width(glyph_id) * scale;
395            (
396                metrics.ascent * scale,
397                metrics.descent * scale,
398                metrics.leading * scale,
399                advance,
400            )
401        };
402
403        let natural_line_height = font_ascent + font_descent + font_leading;
404        let cell_height = (natural_line_height * line_spacing).max(1.0);
405        let cell_width = (char_advance * char_spacing).max(1.0);
406
407        let scrollbar = Scrollbar::new(
408            Arc::clone(&device),
409            surface_format,
410            scrollbar_width,
411            scrollbar_position,
412            scrollbar_thumb_color,
413            scrollbar_track_color,
414        );
415
416        // Create pipelines using the pipeline module
417        let bg_pipeline = pipeline::create_bg_pipeline(&device, surface_format);
418
419        let (atlas_texture, atlas_view, atlas_sampler) = pipeline::create_atlas(&device);
420        let text_bind_group_layout = pipeline::create_text_bind_group_layout(&device);
421        let text_bind_group = pipeline::create_text_bind_group(
422            &device,
423            &text_bind_group_layout,
424            &atlas_view,
425            &atlas_sampler,
426        );
427        let text_pipeline =
428            pipeline::create_text_pipeline(&device, surface_format, &text_bind_group_layout);
429
430        let bg_image_bind_group_layout = pipeline::create_bg_image_bind_group_layout(&device);
431        let bg_image_pipeline = pipeline::create_bg_image_pipeline(
432            &device,
433            surface_format,
434            &bg_image_bind_group_layout,
435        );
436        let bg_image_uniform_buffer = pipeline::create_bg_image_uniform_buffer(&device);
437
438        let (visual_bell_pipeline, visual_bell_bind_group, _, visual_bell_uniform_buffer) =
439            pipeline::create_visual_bell_pipeline(&device, surface_format);
440
441        let vertex_buffer = pipeline::create_vertex_buffer(&device);
442
443        // Instance buffers
444        let max_bg_instances = cols * rows + 10 + rows + rows; // Extra slots for cursor overlays + separator lines + gutter indicators
445        let max_text_instances = cols * rows * 2;
446        let (bg_instance_buffer, text_instance_buffer) =
447            pipeline::create_instance_buffers(&device, max_bg_instances, max_text_instances);
448
449        let mut renderer = Self {
450            device,
451            queue,
452            surface,
453            config,
454            supported_present_modes,
455            bg_pipeline,
456            text_pipeline,
457            bg_image_pipeline,
458            visual_bell_pipeline,
459            vertex_buffer,
460            bg_instance_buffer,
461            text_instance_buffer,
462            bg_image_uniform_buffer,
463            visual_bell_uniform_buffer,
464            text_bind_group,
465            text_bind_group_layout,
466            bg_image_bind_group: None,
467            bg_image_bind_group_layout,
468            visual_bell_bind_group,
469            atlas_texture,
470            atlas_view,
471            glyph_cache: HashMap::new(),
472            lru_head: None,
473            lru_tail: None,
474            atlas_next_x: 0,
475            atlas_next_y: 0,
476            atlas_row_height: 0,
477            cols,
478            rows,
479            cell_width,
480            cell_height,
481            window_padding,
482            content_offset_y: 0.0,
483            content_offset_x: 0.0,
484            content_inset_bottom: 0.0,
485            content_inset_right: 0.0,
486            egui_bottom_inset: 0.0,
487            egui_right_inset: 0.0,
488            scale_factor,
489            font_manager,
490            scrollbar,
491            cells: vec![Cell::default(); cols * rows],
492            dirty_rows: vec![true; rows],
493            row_cache: (0..rows).map(|_| None).collect(),
494            cursor_pos: (0, 0),
495            cursor_opacity: 0.0,
496            cursor_style: par_term_emu_core_rust::cursor::CursorStyle::SteadyBlock,
497            cursor_overlay: None,
498            cursor_color: [1.0, 1.0, 1.0],
499            cursor_text_color: None,
500            cursor_hidden_for_shader: false,
501            is_focused: true,
502            cursor_guide_enabled: false,
503            cursor_guide_color: [1.0, 1.0, 1.0, 0.08],
504            cursor_shadow_enabled: false,
505            cursor_shadow_color: [0.0, 0.0, 0.0, 0.5],
506            cursor_shadow_offset: [2.0, 2.0],
507            cursor_shadow_blur: 3.0,
508            cursor_boost: 0.0,
509            cursor_boost_color: [1.0, 1.0, 1.0],
510            unfocused_cursor_style: par_term_config::UnfocusedCursorStyle::default(),
511            visual_bell_intensity: 0.0,
512            window_opacity,
513            background_color: [
514                background_color[0] as f32 / 255.0,
515                background_color[1] as f32 / 255.0,
516                background_color[2] as f32 / 255.0,
517                1.0,
518            ],
519            base_font_size: font_size,
520            line_spacing,
521            char_spacing,
522            font_ascent,
523            font_descent,
524            font_leading,
525            font_size_pixels,
526            char_advance,
527            bg_image_texture: None,
528            bg_image_mode: background_image_mode,
529            bg_image_opacity: background_image_opacity,
530            bg_image_width: 0,
531            bg_image_height: 0,
532            bg_is_solid_color: false,
533            solid_bg_color: [0.0, 0.0, 0.0],
534            pane_bg_cache: HashMap::new(),
535            max_bg_instances,
536            max_text_instances,
537            bg_instances: vec![
538                BackgroundInstance {
539                    position: [0.0, 0.0],
540                    size: [0.0, 0.0],
541                    color: [0.0, 0.0, 0.0, 0.0],
542                };
543                max_bg_instances
544            ],
545            text_instances: vec![
546                TextInstance {
547                    position: [0.0, 0.0],
548                    size: [0.0, 0.0],
549                    tex_offset: [0.0, 0.0],
550                    tex_size: [0.0, 0.0],
551                    color: [0.0, 0.0, 0.0, 0.0],
552                    is_colored: 0,
553                };
554                max_text_instances
555            ],
556            enable_text_shaping,
557            enable_ligatures,
558            enable_kerning,
559            font_antialias,
560            font_hinting,
561            font_thin_strokes,
562            minimum_contrast: minimum_contrast.clamp(1.0, 21.0),
563            solid_pixel_offset: (0, 0),
564            transparency_affects_only_default_background: false,
565            keep_text_opaque: true,
566            link_underline_style: par_term_config::LinkUnderlineStyle::default(),
567            command_separator_enabled: false,
568            command_separator_thickness: 1.0,
569            command_separator_opacity: 0.4,
570            command_separator_exit_color: true,
571            command_separator_color: [0.5, 0.5, 0.5],
572            visible_separator_marks: Vec::new(),
573            gutter_indicators: Vec::new(),
574        };
575
576        // Upload a solid white 2x2 pixel block to the atlas for geometric block rendering
577        renderer.upload_solid_pixel();
578
579        log::info!(
580            "CellRenderer::new: background_image_path={:?}",
581            background_image_path
582        );
583        if let Some(path) = background_image_path {
584            // Handle missing background image gracefully - don't crash, just log and continue
585            if let Err(e) = renderer.load_background_image(path) {
586                log::warn!(
587                    "Could not load background image '{}': {} - continuing without background image",
588                    path,
589                    e
590                );
591            }
592        }
593
594        Ok(renderer)
595    }
596
597    /// Upload a solid white pixel to the atlas for use in geometric block rendering
598    pub(crate) fn upload_solid_pixel(&mut self) {
599        let size = 2u32; // 2x2 for better sampling
600        let white_pixels: Vec<u8> = vec![255; (size * size * 4) as usize];
601
602        self.queue.write_texture(
603            wgpu::TexelCopyTextureInfo {
604                texture: &self.atlas_texture,
605                mip_level: 0,
606                origin: wgpu::Origin3d {
607                    x: self.atlas_next_x,
608                    y: self.atlas_next_y,
609                    z: 0,
610                },
611                aspect: wgpu::TextureAspect::All,
612            },
613            &white_pixels,
614            wgpu::TexelCopyBufferLayout {
615                offset: 0,
616                bytes_per_row: Some(4 * size),
617                rows_per_image: Some(size),
618            },
619            wgpu::Extent3d {
620                width: size,
621                height: size,
622                depth_or_array_layers: 1,
623            },
624        );
625
626        self.solid_pixel_offset = (self.atlas_next_x, self.atlas_next_y);
627        self.atlas_next_x += size + 2; // padding
628        self.atlas_row_height = self.atlas_row_height.max(size);
629    }
630
631    pub fn device(&self) -> &wgpu::Device {
632        &self.device
633    }
634    pub fn queue(&self) -> &wgpu::Queue {
635        &self.queue
636    }
637    pub fn surface_format(&self) -> wgpu::TextureFormat {
638        self.config.format
639    }
640    pub fn cell_width(&self) -> f32 {
641        self.cell_width
642    }
643    pub fn cell_height(&self) -> f32 {
644        self.cell_height
645    }
646    pub fn window_padding(&self) -> f32 {
647        self.window_padding
648    }
649    pub fn content_offset_y(&self) -> f32 {
650        self.content_offset_y
651    }
652    /// Set the vertical content offset (e.g., tab bar height at top).
653    /// Returns Some((cols, rows)) if grid size changed, None otherwise.
654    pub fn set_content_offset_y(&mut self, offset: f32) -> Option<(usize, usize)> {
655        if (self.content_offset_y - offset).abs() > f32::EPSILON {
656            self.content_offset_y = offset;
657            let size = (self.config.width, self.config.height);
658            return Some(self.resize(size.0, size.1));
659        }
660        None
661    }
662    pub fn content_offset_x(&self) -> f32 {
663        self.content_offset_x
664    }
665    /// Set the horizontal content offset (e.g., tab bar on left).
666    /// Returns Some((cols, rows)) if grid size changed, None otherwise.
667    pub fn set_content_offset_x(&mut self, offset: f32) -> Option<(usize, usize)> {
668        if (self.content_offset_x - offset).abs() > f32::EPSILON {
669            self.content_offset_x = offset;
670            let size = (self.config.width, self.config.height);
671            return Some(self.resize(size.0, size.1));
672        }
673        None
674    }
675    pub fn content_inset_bottom(&self) -> f32 {
676        self.content_inset_bottom
677    }
678    /// Set the bottom content inset (e.g., tab bar at bottom).
679    /// Returns Some((cols, rows)) if grid size changed, None otherwise.
680    pub fn set_content_inset_bottom(&mut self, inset: f32) -> Option<(usize, usize)> {
681        if (self.content_inset_bottom - inset).abs() > f32::EPSILON {
682            self.content_inset_bottom = inset;
683            let size = (self.config.width, self.config.height);
684            return Some(self.resize(size.0, size.1));
685        }
686        None
687    }
688    pub fn content_inset_right(&self) -> f32 {
689        self.content_inset_right
690    }
691    /// Set the right content inset (e.g., AI Inspector panel).
692    /// Returns Some((cols, rows)) if grid size changed, None otherwise.
693    pub fn set_content_inset_right(&mut self, inset: f32) -> Option<(usize, usize)> {
694        if (self.content_inset_right - inset).abs() > f32::EPSILON {
695            log::info!(
696                "[SCROLLBAR] set_content_inset_right: {:.1} -> {:.1} (physical px)",
697                self.content_inset_right,
698                inset
699            );
700            self.content_inset_right = inset;
701            let size = (self.config.width, self.config.height);
702            return Some(self.resize(size.0, size.1));
703        }
704        None
705    }
706    pub fn grid_size(&self) -> (usize, usize) {
707        (self.cols, self.rows)
708    }
709    pub fn keep_text_opaque(&self) -> bool {
710        self.keep_text_opaque
711    }
712
713    pub fn resize(&mut self, width: u32, height: u32) -> (usize, usize) {
714        if width == 0 || height == 0 {
715            return (self.cols, self.rows);
716        }
717        self.config.width = width;
718        self.config.height = height;
719        self.surface.configure(&self.device, &self.config);
720
721        let available_width = (width as f32
722            - self.window_padding * 2.0
723            - self.content_offset_x
724            - self.content_inset_right
725            - self.scrollbar.width())
726        .max(0.0);
727        let available_height = (height as f32
728            - self.window_padding * 2.0
729            - self.content_offset_y
730            - self.content_inset_bottom
731            - self.egui_bottom_inset)
732            .max(0.0);
733        let new_cols = (available_width / self.cell_width).max(1.0) as usize;
734        let new_rows = (available_height / self.cell_height).max(1.0) as usize;
735
736        if new_cols != self.cols || new_rows != self.rows {
737            self.cols = new_cols;
738            self.rows = new_rows;
739            self.cells = vec![Cell::default(); self.cols * self.rows];
740            self.dirty_rows = vec![true; self.rows];
741            self.row_cache = (0..self.rows).map(|_| None).collect();
742            self.recreate_instance_buffers();
743        }
744
745        self.update_bg_image_uniforms();
746        (self.cols, self.rows)
747    }
748
749    fn recreate_instance_buffers(&mut self) {
750        self.max_bg_instances = self.cols * self.rows + 10 + self.rows + self.rows; // Extra slots for cursor overlays + separator lines + gutter indicators
751        self.max_text_instances = self.cols * self.rows * 2;
752        let (bg_buf, text_buf) = pipeline::create_instance_buffers(
753            &self.device,
754            self.max_bg_instances,
755            self.max_text_instances,
756        );
757        self.bg_instance_buffer = bg_buf;
758        self.text_instance_buffer = text_buf;
759
760        self.bg_instances = vec![
761            BackgroundInstance {
762                position: [0.0, 0.0],
763                size: [0.0, 0.0],
764                color: [0.0, 0.0, 0.0, 0.0],
765            };
766            self.max_bg_instances
767        ];
768        self.text_instances = vec![
769            TextInstance {
770                position: [0.0, 0.0],
771                size: [0.0, 0.0],
772                tex_offset: [0.0, 0.0],
773                tex_size: [0.0, 0.0],
774                color: [0.0, 0.0, 0.0, 0.0],
775                is_colored: 0,
776            };
777            self.max_text_instances
778        ];
779    }
780
781    /// Update cells. Returns `true` if any row actually changed.
782    pub fn update_cells(&mut self, new_cells: &[Cell]) -> bool {
783        let mut changed = false;
784        for row in 0..self.rows {
785            let start = row * self.cols;
786            let end = (row + 1) * self.cols;
787            if start < new_cells.len() && end <= new_cells.len() {
788                let row_slice = &new_cells[start..end];
789                if row_slice != &self.cells[start..end] {
790                    self.cells[start..end].clone_from_slice(row_slice);
791                    self.dirty_rows[row] = true;
792                    changed = true;
793                }
794            }
795        }
796        changed
797    }
798
799    /// Clear all cells and mark all rows as dirty.
800    pub fn clear_all_cells(&mut self) {
801        for cell in &mut self.cells {
802            *cell = Cell::default();
803        }
804        for dirty in &mut self.dirty_rows {
805            *dirty = true;
806        }
807    }
808
809    /// Update cursor position, opacity and style. Returns `true` if anything changed.
810    pub fn update_cursor(
811        &mut self,
812        pos: (usize, usize),
813        opacity: f32,
814        style: par_term_emu_core_rust::cursor::CursorStyle,
815    ) -> bool {
816        if self.cursor_pos != pos || self.cursor_opacity != opacity || self.cursor_style != style {
817            self.dirty_rows[self.cursor_pos.1.min(self.rows - 1)] = true;
818            self.cursor_pos = pos;
819            self.cursor_opacity = opacity;
820            self.cursor_style = style;
821            self.dirty_rows[self.cursor_pos.1.min(self.rows - 1)] = true;
822
823            // Compute cursor overlay for beam/underline styles
824            use par_term_emu_core_rust::cursor::CursorStyle;
825            self.cursor_overlay = if opacity > 0.0 {
826                let col = pos.0;
827                let row = pos.1;
828                let x0 =
829                    (self.window_padding + self.content_offset_x + col as f32 * self.cell_width)
830                        .round();
831                let x1 = (self.window_padding
832                    + self.content_offset_x
833                    + (col + 1) as f32 * self.cell_width)
834                    .round();
835                let y0 =
836                    (self.window_padding + self.content_offset_y + row as f32 * self.cell_height)
837                        .round();
838                let y1 = (self.window_padding
839                    + self.content_offset_y
840                    + (row + 1) as f32 * self.cell_height)
841                    .round();
842
843                match style {
844                    CursorStyle::SteadyBlock | CursorStyle::BlinkingBlock => None,
845                    CursorStyle::SteadyBar | CursorStyle::BlinkingBar => Some(BackgroundInstance {
846                        position: [
847                            x0 / self.config.width as f32 * 2.0 - 1.0,
848                            1.0 - (y0 / self.config.height as f32 * 2.0),
849                        ],
850                        size: [
851                            2.0 / self.config.width as f32 * 2.0,
852                            (y1 - y0) / self.config.height as f32 * 2.0,
853                        ],
854                        color: [
855                            self.cursor_color[0],
856                            self.cursor_color[1],
857                            self.cursor_color[2],
858                            opacity,
859                        ],
860                    }),
861                    CursorStyle::SteadyUnderline | CursorStyle::BlinkingUnderline => {
862                        Some(BackgroundInstance {
863                            position: [
864                                x0 / self.config.width as f32 * 2.0 - 1.0,
865                                1.0 - ((y1 - 2.0) / self.config.height as f32 * 2.0),
866                            ],
867                            size: [
868                                (x1 - x0) / self.config.width as f32 * 2.0,
869                                2.0 / self.config.height as f32 * 2.0,
870                            ],
871                            color: [
872                                self.cursor_color[0],
873                                self.cursor_color[1],
874                                self.cursor_color[2],
875                                opacity,
876                            ],
877                        })
878                    }
879                }
880            } else {
881                None
882            };
883            return true;
884        }
885        false
886    }
887
888    pub fn clear_cursor(&mut self) -> bool {
889        self.update_cursor(self.cursor_pos, 0.0, self.cursor_style)
890    }
891
892    /// Update cursor color
893    pub fn update_cursor_color(&mut self, color: [u8; 3]) {
894        self.cursor_color = [
895            color[0] as f32 / 255.0,
896            color[1] as f32 / 255.0,
897            color[2] as f32 / 255.0,
898        ];
899        self.dirty_rows[self.cursor_pos.1.min(self.rows - 1)] = true;
900    }
901
902    /// Update cursor text color (color of text under block cursor)
903    pub fn update_cursor_text_color(&mut self, color: Option<[u8; 3]>) {
904        self.cursor_text_color = color.map(|c| {
905            [
906                c[0] as f32 / 255.0,
907                c[1] as f32 / 255.0,
908                c[2] as f32 / 255.0,
909            ]
910        });
911        self.dirty_rows[self.cursor_pos.1.min(self.rows - 1)] = true;
912    }
913
914    /// Set whether cursor should be hidden when cursor shader is active.
915    /// Returns `true` if the value changed.
916    pub fn set_cursor_hidden_for_shader(&mut self, hidden: bool) -> bool {
917        if self.cursor_hidden_for_shader != hidden {
918            self.cursor_hidden_for_shader = hidden;
919            self.dirty_rows[self.cursor_pos.1.min(self.rows - 1)] = true;
920            return true;
921        }
922        false
923    }
924
925    /// Set window focus state (affects unfocused cursor rendering).
926    /// Returns `true` if the value changed.
927    pub fn set_focused(&mut self, focused: bool) -> bool {
928        if self.is_focused != focused {
929            self.is_focused = focused;
930            self.dirty_rows[self.cursor_pos.1.min(self.rows - 1)] = true;
931            return true;
932        }
933        false
934    }
935
936    /// Update cursor guide settings
937    pub fn update_cursor_guide(&mut self, enabled: bool, color: [u8; 4]) {
938        self.cursor_guide_enabled = enabled;
939        self.cursor_guide_color = [
940            color[0] as f32 / 255.0,
941            color[1] as f32 / 255.0,
942            color[2] as f32 / 255.0,
943            color[3] as f32 / 255.0,
944        ];
945        if enabled {
946            self.dirty_rows[self.cursor_pos.1.min(self.rows - 1)] = true;
947        }
948    }
949
950    /// Update cursor shadow settings
951    pub fn update_cursor_shadow(
952        &mut self,
953        enabled: bool,
954        color: [u8; 4],
955        offset: [f32; 2],
956        blur: f32,
957    ) {
958        self.cursor_shadow_enabled = enabled;
959        self.cursor_shadow_color = [
960            color[0] as f32 / 255.0,
961            color[1] as f32 / 255.0,
962            color[2] as f32 / 255.0,
963            color[3] as f32 / 255.0,
964        ];
965        self.cursor_shadow_offset = offset;
966        self.cursor_shadow_blur = blur;
967        if enabled {
968            self.dirty_rows[self.cursor_pos.1.min(self.rows - 1)] = true;
969        }
970    }
971
972    /// Update cursor boost settings
973    pub fn update_cursor_boost(&mut self, intensity: f32, color: [u8; 3]) {
974        self.cursor_boost = intensity.clamp(0.0, 1.0);
975        self.cursor_boost_color = [
976            color[0] as f32 / 255.0,
977            color[1] as f32 / 255.0,
978            color[2] as f32 / 255.0,
979        ];
980        if intensity > 0.0 {
981            self.dirty_rows[self.cursor_pos.1.min(self.rows - 1)] = true;
982        }
983    }
984
985    /// Update unfocused cursor style
986    pub fn update_unfocused_cursor_style(&mut self, style: par_term_config::UnfocusedCursorStyle) {
987        self.unfocused_cursor_style = style;
988        if !self.is_focused {
989            self.dirty_rows[self.cursor_pos.1.min(self.rows - 1)] = true;
990        }
991    }
992
993    pub fn update_scrollbar(
994        &mut self,
995        scroll_offset: usize,
996        visible_lines: usize,
997        total_lines: usize,
998        marks: &[par_term_config::ScrollbackMark],
999    ) {
1000        let right_inset = self.content_inset_right + self.egui_right_inset;
1001        self.scrollbar.update(
1002            &self.queue,
1003            scroll_offset,
1004            visible_lines,
1005            total_lines,
1006            self.config.width,
1007            self.config.height,
1008            self.content_offset_y,
1009            self.content_inset_bottom + self.egui_bottom_inset,
1010            right_inset,
1011            marks,
1012        );
1013    }
1014
1015    /// Update scrollbar state constrained to a specific pane's bounds.
1016    ///
1017    /// Converts the pane viewport (pixel bounds) into the inset parameters
1018    /// that `Scrollbar::update` expects, so the track and thumb are confined
1019    /// to the pane instead of spanning the full window.
1020    pub fn update_scrollbar_for_pane(
1021        &mut self,
1022        scroll_offset: usize,
1023        visible_lines: usize,
1024        total_lines: usize,
1025        marks: &[par_term_config::ScrollbackMark],
1026        viewport: &PaneViewport,
1027    ) {
1028        let win_w = self.config.width as f32;
1029        let win_h = self.config.height as f32;
1030
1031        // Top inset: space above the pane
1032        let pane_content_offset_y = viewport.y;
1033
1034        // Bottom inset: space below the pane + existing egui bottom inset
1035        let pane_bottom_inset =
1036            (win_h - (viewport.y + viewport.height)).max(0.0) + self.egui_bottom_inset;
1037
1038        // Right inset: space to the right of the pane + existing egui/panel right inset
1039        let pane_right_inset = (win_w - (viewport.x + viewport.width)).max(0.0)
1040            + self.content_inset_right
1041            + self.egui_right_inset;
1042
1043        self.scrollbar.update(
1044            &self.queue,
1045            scroll_offset,
1046            visible_lines,
1047            total_lines,
1048            self.config.width,
1049            self.config.height,
1050            pane_content_offset_y,
1051            pane_bottom_inset,
1052            pane_right_inset,
1053            marks,
1054        );
1055    }
1056
1057    pub fn set_visual_bell_intensity(&mut self, intensity: f32) {
1058        self.visual_bell_intensity = intensity;
1059    }
1060
1061    pub fn update_opacity(&mut self, opacity: f32) {
1062        self.window_opacity = opacity;
1063        // update_bg_image_uniforms() multiplies bg_image_opacity by window_opacity,
1064        // so both images and solid colors respect window transparency
1065        self.update_bg_image_uniforms();
1066    }
1067
1068    /// Set whether transparency affects only default background cells.
1069    /// When true, non-default (colored) backgrounds remain opaque for readability.
1070    pub fn set_transparency_affects_only_default_background(&mut self, value: bool) {
1071        if self.transparency_affects_only_default_background != value {
1072            log::info!(
1073                "transparency_affects_only_default_background: {} -> {} (window_opacity={})",
1074                self.transparency_affects_only_default_background,
1075                value,
1076                self.window_opacity
1077            );
1078            self.transparency_affects_only_default_background = value;
1079            // Mark all rows dirty to re-render with new transparency behavior
1080            self.dirty_rows.fill(true);
1081        }
1082    }
1083
1084    /// Set whether text should always be rendered at full opacity.
1085    /// When true, text remains opaque regardless of window transparency settings.
1086    pub fn set_keep_text_opaque(&mut self, value: bool) {
1087        if self.keep_text_opaque != value {
1088            log::info!(
1089                "keep_text_opaque: {} -> {} (window_opacity={}, transparency_affects_only_default_bg={})",
1090                self.keep_text_opaque,
1091                value,
1092                self.window_opacity,
1093                self.transparency_affects_only_default_background
1094            );
1095            self.keep_text_opaque = value;
1096            // Mark all rows dirty to re-render with new text opacity behavior
1097            self.dirty_rows.fill(true);
1098        }
1099    }
1100
1101    pub fn set_link_underline_style(&mut self, style: par_term_config::LinkUnderlineStyle) {
1102        if self.link_underline_style != style {
1103            self.link_underline_style = style;
1104            self.dirty_rows.fill(true);
1105        }
1106    }
1107
1108    /// Update command separator settings from config
1109    pub fn update_command_separator(
1110        &mut self,
1111        enabled: bool,
1112        thickness: f32,
1113        opacity: f32,
1114        exit_color: bool,
1115        color: [u8; 3],
1116    ) {
1117        self.command_separator_enabled = enabled;
1118        self.command_separator_thickness = thickness;
1119        self.command_separator_opacity = opacity;
1120        self.command_separator_exit_color = exit_color;
1121        self.command_separator_color = [
1122            color[0] as f32 / 255.0,
1123            color[1] as f32 / 255.0,
1124            color[2] as f32 / 255.0,
1125        ];
1126    }
1127
1128    /// Set the visible separator marks for the current frame.
1129    /// Returns `true` if the marks changed.
1130    pub fn set_separator_marks(&mut self, marks: Vec<SeparatorMark>) -> bool {
1131        if self.visible_separator_marks != marks {
1132            self.visible_separator_marks = marks;
1133            return true;
1134        }
1135        false
1136    }
1137
1138    /// Set the gutter indicator data for the current frame.
1139    ///
1140    /// Each entry is `(screen_row, [r, g, b, a])` for the gutter background.
1141    pub fn set_gutter_indicators(&mut self, indicators: Vec<(usize, [f32; 4])>) {
1142        self.gutter_indicators = indicators;
1143    }
1144
1145    /// Compute separator color based on exit code and settings
1146    fn separator_color(
1147        &self,
1148        exit_code: Option<i32>,
1149        custom_color: Option<(u8, u8, u8)>,
1150        opacity_mult: f32,
1151    ) -> [f32; 4] {
1152        let alpha = self.command_separator_opacity * opacity_mult;
1153        // Custom color from trigger marks takes priority
1154        if let Some((r, g, b)) = custom_color {
1155            return [r as f32 / 255.0, g as f32 / 255.0, b as f32 / 255.0, alpha];
1156        }
1157        if self.command_separator_exit_color {
1158            match exit_code {
1159                Some(0) => [0.3, 0.75, 0.3, alpha],   // Green for success
1160                Some(_) => [0.85, 0.25, 0.25, alpha], // Red for failure
1161                None => [0.5, 0.5, 0.5, alpha],       // Gray for unknown
1162            }
1163        } else {
1164            [
1165                self.command_separator_color[0],
1166                self.command_separator_color[1],
1167                self.command_separator_color[2],
1168                alpha,
1169            ]
1170        }
1171    }
1172
1173    /// Update scale factor and recalculate all font metrics and cell dimensions.
1174    /// This is called when the window is dragged between displays with different DPIs.
1175    pub fn update_scale_factor(&mut self, scale_factor: f64) {
1176        let new_scale = scale_factor as f32;
1177
1178        // Skip if scale factor hasn't changed
1179        if (self.scale_factor - new_scale).abs() < f32::EPSILON {
1180            return;
1181        }
1182
1183        log::info!(
1184            "Recalculating font metrics for scale factor change: {} -> {}",
1185            self.scale_factor,
1186            new_scale
1187        );
1188
1189        self.scale_factor = new_scale;
1190
1191        // Recalculate font_size_pixels based on new scale factor
1192        let platform_dpi = if cfg!(target_os = "macos") {
1193            72.0
1194        } else {
1195            96.0
1196        };
1197        let base_font_pixels = self.base_font_size * platform_dpi / 72.0;
1198        self.font_size_pixels = (base_font_pixels * new_scale).max(1.0);
1199
1200        // Re-extract font metrics at new scale
1201        let (font_ascent, font_descent, font_leading, char_advance) = {
1202            let primary_font = self.font_manager.get_font(0).unwrap();
1203            let metrics = primary_font.metrics(&[]);
1204            let scale = self.font_size_pixels / metrics.units_per_em as f32;
1205            let glyph_id = primary_font.charmap().map('m');
1206            let advance = primary_font.glyph_metrics(&[]).advance_width(glyph_id) * scale;
1207            (
1208                metrics.ascent * scale,
1209                metrics.descent * scale,
1210                metrics.leading * scale,
1211                advance,
1212            )
1213        };
1214
1215        self.font_ascent = font_ascent;
1216        self.font_descent = font_descent;
1217        self.font_leading = font_leading;
1218        self.char_advance = char_advance;
1219
1220        // Recalculate cell dimensions
1221        let natural_line_height = font_ascent + font_descent + font_leading;
1222        self.cell_height = (natural_line_height * self.line_spacing).max(1.0);
1223        self.cell_width = (char_advance * self.char_spacing).max(1.0);
1224
1225        log::info!(
1226            "New cell dimensions: {}x{} (font_size_pixels: {})",
1227            self.cell_width,
1228            self.cell_height,
1229            self.font_size_pixels
1230        );
1231
1232        // Clear glyph cache - glyphs need to be re-rasterized at new DPI
1233        self.clear_glyph_cache();
1234
1235        // Mark all rows as dirty to force re-rendering
1236        self.dirty_rows.fill(true);
1237    }
1238
1239    #[allow(dead_code)]
1240    pub fn update_window_padding(&mut self, padding: f32) -> Option<(usize, usize)> {
1241        if (self.window_padding - padding).abs() > f32::EPSILON {
1242            self.window_padding = padding;
1243            let size = (self.config.width, self.config.height);
1244            return Some(self.resize(size.0, size.1));
1245        }
1246        None
1247    }
1248
1249    pub fn update_scrollbar_appearance(
1250        &mut self,
1251        width: f32,
1252        thumb_color: [f32; 4],
1253        track_color: [f32; 4],
1254    ) {
1255        self.scrollbar
1256            .update_appearance(width, thumb_color, track_color);
1257    }
1258
1259    pub fn update_scrollbar_position(&mut self, position: &str) {
1260        self.scrollbar.update_position(position);
1261    }
1262
1263    pub fn scrollbar_contains_point(&self, x: f32, y: f32) -> bool {
1264        self.scrollbar.contains_point(x, y)
1265    }
1266
1267    pub fn scrollbar_thumb_bounds(&self) -> Option<(f32, f32)> {
1268        self.scrollbar.thumb_bounds()
1269    }
1270
1271    pub fn scrollbar_track_contains_x(&self, x: f32) -> bool {
1272        self.scrollbar.track_contains_x(x)
1273    }
1274
1275    pub fn scrollbar_mouse_y_to_scroll_offset(&self, mouse_y: f32) -> Option<usize> {
1276        self.scrollbar.mouse_y_to_scroll_offset(mouse_y)
1277    }
1278
1279    /// Find a scrollbar mark at the given mouse position for tooltip display.
1280    /// Returns the mark if mouse is within `tolerance` pixels of a mark.
1281    pub fn scrollbar_mark_at_position(
1282        &self,
1283        mouse_x: f32,
1284        mouse_y: f32,
1285        tolerance: f32,
1286    ) -> Option<&par_term_config::ScrollbackMark> {
1287        self.scrollbar.mark_at_position(mouse_x, mouse_y, tolerance)
1288    }
1289
1290    pub fn reconfigure_surface(&mut self) {
1291        self.surface.configure(&self.device, &self.config);
1292    }
1293
1294    /// Update font anti-aliasing setting
1295    /// Returns true if the setting changed (requiring glyph cache clear)
1296    pub fn update_font_antialias(&mut self, enabled: bool) -> bool {
1297        if self.font_antialias != enabled {
1298            self.font_antialias = enabled;
1299            self.clear_glyph_cache();
1300            self.dirty_rows.fill(true);
1301            true
1302        } else {
1303            false
1304        }
1305    }
1306
1307    /// Update font hinting setting
1308    /// Returns true if the setting changed (requiring glyph cache clear)
1309    pub fn update_font_hinting(&mut self, enabled: bool) -> bool {
1310        if self.font_hinting != enabled {
1311            self.font_hinting = enabled;
1312            self.clear_glyph_cache();
1313            self.dirty_rows.fill(true);
1314            true
1315        } else {
1316            false
1317        }
1318    }
1319
1320    /// Update thin strokes mode
1321    /// Returns true if the setting changed (requiring glyph cache clear)
1322    pub fn update_font_thin_strokes(&mut self, mode: par_term_config::ThinStrokesMode) -> bool {
1323        if self.font_thin_strokes != mode {
1324            self.font_thin_strokes = mode;
1325            self.clear_glyph_cache();
1326            self.dirty_rows.fill(true);
1327            true
1328        } else {
1329            false
1330        }
1331    }
1332
1333    /// Update minimum contrast ratio
1334    /// Returns true if the setting changed (requiring redraw)
1335    pub fn update_minimum_contrast(&mut self, ratio: f32) -> bool {
1336        // Clamp to valid range: 1.0 (disabled) to 21.0 (max possible contrast)
1337        let ratio = ratio.clamp(1.0, 21.0);
1338        if (self.minimum_contrast - ratio).abs() > 0.001 {
1339            self.minimum_contrast = ratio;
1340            self.dirty_rows.fill(true);
1341            true
1342        } else {
1343            false
1344        }
1345    }
1346
1347    /// Adjust foreground color to meet minimum contrast ratio against background
1348    /// Uses WCAG luminance formula for accurate contrast calculation.
1349    /// Returns the adjusted color [R, G, B, A] with preserved alpha.
1350    pub(crate) fn ensure_minimum_contrast(&self, fg: [f32; 4], bg: [f32; 4]) -> [f32; 4] {
1351        // If minimum_contrast is 1.0 (disabled) or less, no adjustment needed
1352        if self.minimum_contrast <= 1.0 {
1353            return fg;
1354        }
1355
1356        // Calculate luminance using WCAG formula
1357        fn luminance(color: [f32; 4]) -> f32 {
1358            let r = color[0].powf(2.2);
1359            let g = color[1].powf(2.2);
1360            let b = color[2].powf(2.2);
1361            0.2126 * r + 0.7152 * g + 0.0722 * b
1362        }
1363
1364        fn contrast_ratio(l1: f32, l2: f32) -> f32 {
1365            let (lighter, darker) = if l1 > l2 { (l1, l2) } else { (l2, l1) };
1366            (lighter + 0.05) / (darker + 0.05)
1367        }
1368
1369        let fg_lum = luminance(fg);
1370        let bg_lum = luminance(bg);
1371        let current_ratio = contrast_ratio(fg_lum, bg_lum);
1372
1373        // If already meets minimum contrast, return unchanged
1374        if current_ratio >= self.minimum_contrast {
1375            return fg;
1376        }
1377
1378        // Determine if we need to lighten or darken the foreground
1379        // If background is dark, lighten fg; if light, darken fg
1380        let bg_is_dark = bg_lum < 0.5;
1381
1382        // Binary search for the minimum adjustment needed
1383        let mut low = 0.0f32;
1384        let mut high = 1.0f32;
1385
1386        for _ in 0..20 {
1387            // 20 iterations gives ~1/1_000_000 precision
1388            let mid = (low + high) / 2.0;
1389
1390            let adjusted = if bg_is_dark {
1391                // Lighten: mix with white
1392                [
1393                    fg[0] + (1.0 - fg[0]) * mid,
1394                    fg[1] + (1.0 - fg[1]) * mid,
1395                    fg[2] + (1.0 - fg[2]) * mid,
1396                    fg[3],
1397                ]
1398            } else {
1399                // Darken: mix with black
1400                [
1401                    fg[0] * (1.0 - mid),
1402                    fg[1] * (1.0 - mid),
1403                    fg[2] * (1.0 - mid),
1404                    fg[3],
1405                ]
1406            };
1407
1408            let adjusted_lum = luminance(adjusted);
1409            let new_ratio = contrast_ratio(adjusted_lum, bg_lum);
1410
1411            if new_ratio >= self.minimum_contrast {
1412                high = mid;
1413            } else {
1414                low = mid;
1415            }
1416        }
1417
1418        // Apply the final adjustment
1419        if bg_is_dark {
1420            [
1421                fg[0] + (1.0 - fg[0]) * high,
1422                fg[1] + (1.0 - fg[1]) * high,
1423                fg[2] + (1.0 - fg[2]) * high,
1424                fg[3],
1425            ]
1426        } else {
1427            [
1428                fg[0] * (1.0 - high),
1429                fg[1] * (1.0 - high),
1430                fg[2] * (1.0 - high),
1431                fg[3],
1432            ]
1433        }
1434    }
1435
1436    /// Check if thin strokes should be applied based on current mode and context
1437    pub(crate) fn should_use_thin_strokes(&self) -> bool {
1438        use par_term_config::ThinStrokesMode;
1439
1440        // Check if we're on a Retina/HiDPI display (scale factor > 1.5)
1441        let is_retina = self.scale_factor > 1.5;
1442
1443        // Check if background is dark (average < 128)
1444        let bg_brightness =
1445            (self.background_color[0] + self.background_color[1] + self.background_color[2]) / 3.0;
1446        let is_dark_background = bg_brightness < 0.5;
1447
1448        match self.font_thin_strokes {
1449            ThinStrokesMode::Never => false,
1450            ThinStrokesMode::Always => true,
1451            ThinStrokesMode::RetinaOnly => is_retina,
1452            ThinStrokesMode::DarkBackgroundsOnly => is_dark_background,
1453            ThinStrokesMode::RetinaDarkBackgroundsOnly => is_retina && is_dark_background,
1454        }
1455    }
1456
1457    /// Get the list of supported present modes for this surface
1458    #[allow(dead_code)]
1459    pub fn supported_present_modes(&self) -> &[wgpu::PresentMode] {
1460        &self.supported_present_modes
1461    }
1462
1463    /// Check if a vsync mode is supported
1464    pub fn is_vsync_mode_supported(&self, mode: par_term_config::VsyncMode) -> bool {
1465        self.supported_present_modes
1466            .contains(&mode.to_present_mode())
1467    }
1468
1469    /// Update the vsync mode. Returns the actual mode applied (may differ if requested mode unsupported).
1470    /// Also returns whether the mode was changed.
1471    pub fn update_vsync_mode(
1472        &mut self,
1473        mode: par_term_config::VsyncMode,
1474    ) -> (par_term_config::VsyncMode, bool) {
1475        let requested = mode.to_present_mode();
1476        let current = self.config.present_mode;
1477
1478        // Determine the actual mode to use
1479        let actual = if self.supported_present_modes.contains(&requested) {
1480            requested
1481        } else {
1482            log::warn!(
1483                "Requested present mode {:?} not supported, falling back to Fifo",
1484                requested
1485            );
1486            wgpu::PresentMode::Fifo
1487        };
1488
1489        // Only reconfigure if the mode actually changed
1490        if actual != current {
1491            self.config.present_mode = actual;
1492            self.surface.configure(&self.device, &self.config);
1493            log::info!("VSync mode changed to {:?}", actual);
1494        }
1495
1496        // Convert back to VsyncMode for return
1497        let actual_vsync = match actual {
1498            wgpu::PresentMode::Immediate => par_term_config::VsyncMode::Immediate,
1499            wgpu::PresentMode::Mailbox => par_term_config::VsyncMode::Mailbox,
1500            wgpu::PresentMode::Fifo | wgpu::PresentMode::FifoRelaxed => {
1501                par_term_config::VsyncMode::Fifo
1502            }
1503            _ => par_term_config::VsyncMode::Fifo,
1504        };
1505
1506        (actual_vsync, actual != current)
1507    }
1508
1509    /// Get the current vsync mode
1510    #[allow(dead_code)]
1511    pub fn current_vsync_mode(&self) -> par_term_config::VsyncMode {
1512        match self.config.present_mode {
1513            wgpu::PresentMode::Immediate => par_term_config::VsyncMode::Immediate,
1514            wgpu::PresentMode::Mailbox => par_term_config::VsyncMode::Mailbox,
1515            wgpu::PresentMode::Fifo | wgpu::PresentMode::FifoRelaxed => {
1516                par_term_config::VsyncMode::Fifo
1517            }
1518            _ => par_term_config::VsyncMode::Fifo,
1519        }
1520    }
1521
1522    #[allow(dead_code)]
1523    pub fn update_graphics(
1524        &mut self,
1525        _graphics: &[par_term_emu_core_rust::graphics::TerminalGraphic],
1526        _scroll_offset: usize,
1527        _scrollback_len: usize,
1528        _visible_lines: usize,
1529    ) -> Result<()> {
1530        Ok(())
1531    }
1532}