Skip to main content

par_term_render/cell_renderer/
mod.rs

1// ARC-009 TODO: This file is 742 lines (limit: 800 — approaching threshold). When it
2// exceeds 800 lines, extract into sub-modules under cell_renderer/:
3//
4//   glyph_ops.rs     — get_or_rasterize_glyph helper (see QA-006: glyph cache logic
5//                      currently duplicated 3x). Also resolves QA-013 scratch buffer.
6//   font_fallback.rs — font fallback chain construction
7//
8// Tracking: Issue ARC-009 in AUDIT.md. See also QA-006 (glyph cache deduplication).
9
10use anyhow::{Context, Result};
11use std::collections::HashMap;
12use std::sync::Arc;
13use winit::window::Window;
14
15use crate::scrollbar::Scrollbar;
16use par_term_config::{SeparatorMark, color_u8_to_f32_a};
17use par_term_fonts::font_manager::FontManager;
18
19pub mod atlas;
20pub mod background;
21mod bg_instance_builder;
22pub mod block_chars;
23mod cursor;
24mod font;
25mod instance_buffers;
26mod layout;
27pub(crate) mod pane_render;
28pub mod pipeline;
29pub mod render;
30mod settings;
31mod surface;
32mod text_instance_builder;
33pub mod types;
34// Re-export public types for external use
35pub(crate) use pane_render::PaneRenderViewParams;
36pub use types::{Cell, PaneViewport};
37// Re-export internal types for use within the cell_renderer module
38pub(crate) use types::{BackgroundInstance, GlyphInfo, RowCacheEntry, TextInstance};
39// Re-export instance buffer constants so mod.rs can reference them
40pub(crate) use instance_buffers::{CURSOR_OVERLAY_SLOTS, TEXT_INSTANCES_PER_CELL};
41// Re-export extracted sub-module types for use within this module
42pub(crate) use cursor::CursorState;
43pub(crate) use font::FontState;
44pub(crate) use layout::GridLayout;
45
46/// Physical DPI on macOS (points-based at 72 ppi).
47pub(crate) const MACOS_PLATFORM_DPI: f32 = 72.0;
48
49/// Physical DPI on non-macOS platforms (screen pixels at 96 ppi).
50pub(crate) const DEFAULT_PLATFORM_DPI: f32 = 96.0;
51
52/// Reference DPI used in the font-size conversion formula.
53/// Font sizes are specified in typographic points at 72 ppi.
54pub(crate) const FONT_REFERENCE_DPI: f32 = 72.0;
55
56/// Size (width and height) of the solid white pixel block uploaded to the glyph atlas.
57/// A 2×2 block provides better sampling behaviour than a single texel at borders.
58const SOLID_PIXEL_SIZE: u32 = 2;
59
60/// Pixel padding added around each glyph in the atlas to prevent bilinear bleed.
61pub(crate) const ATLAS_GLYPH_PADDING: u32 = 2;
62
63/// Maximum frame latency hint passed to the wgpu surface configuration.
64/// Controls how many frames may be queued ahead of the display; 2 balances
65/// throughput against input latency.
66const SURFACE_FRAME_LATENCY: u32 = 2;
67
68/// Default cursor guide line opacity.
69/// A very low value keeps the guide visible without overpowering text.
70const DEFAULT_GUIDE_OPACITY: f32 = 0.08;
71
72/// Default cursor shadow alpha component.
73const DEFAULT_SHADOW_ALPHA: f32 = 0.5;
74
75/// Default cursor shadow offset in pixels (x and y).
76const DEFAULT_SHADOW_OFFSET_PX: f32 = 2.0;
77
78/// Default cursor shadow blur radius in pixels.
79const DEFAULT_SHADOW_BLUR_PX: f32 = 3.0;
80
81/// GPU render pipelines and their associated bind group layouts.
82pub(crate) struct GpuPipelines {
83    pub(crate) bg_pipeline: wgpu::RenderPipeline,
84    pub(crate) text_pipeline: wgpu::RenderPipeline,
85    pub(crate) bg_image_pipeline: wgpu::RenderPipeline,
86    /// Full-screen flash pipeline used by `render_overlays` when `visual_bell_intensity > 0`.
87    pub(crate) visual_bell_pipeline: wgpu::RenderPipeline,
88    pub(crate) text_bind_group: wgpu::BindGroup,
89    #[allow(dead_code)] // GPU lifetime: must outlive bind groups created from this layout
90    pub(crate) text_bind_group_layout: wgpu::BindGroupLayout,
91    pub(crate) bg_image_bind_group: Option<wgpu::BindGroup>,
92    pub(crate) bg_image_bind_group_layout: wgpu::BindGroupLayout,
93    /// Bind group holding the visual bell uniform buffer; set in `render_overlays`.
94    pub(crate) visual_bell_bind_group: wgpu::BindGroup,
95    /// Pipeline that stamps alpha=1.0 over the entire surface (opaque window guard).
96    pub(crate) opaque_alpha_pipeline: wgpu::RenderPipeline,
97}
98
99/// GPU vertex, instance, and uniform buffers with capacity tracking.
100pub(crate) struct GpuBuffers {
101    pub(crate) vertex_buffer: wgpu::Buffer,
102    pub(crate) bg_instance_buffer: wgpu::Buffer,
103    pub(crate) text_instance_buffer: wgpu::Buffer,
104    pub(crate) bg_image_uniform_buffer: wgpu::Buffer,
105    /// Uniform buffer written each frame in `render_overlays` with position/color/intensity.
106    pub(crate) visual_bell_uniform_buffer: wgpu::Buffer,
107    /// Maximum capacity of the bg_instance_buffer (GPU buffer size)
108    pub(crate) max_bg_instances: usize,
109    /// Maximum capacity of the text_instance_buffer (GPU buffer size)
110    pub(crate) max_text_instances: usize,
111    /// Actual number of background instances written (used for draw calls)
112    pub(crate) actual_bg_instances: usize,
113    /// Actual number of text instances written (used for draw calls)
114    pub(crate) actual_text_instances: usize,
115}
116
117/// Glyph atlas texture, cache, and LRU eviction state.
118pub(crate) struct GlyphAtlas {
119    pub(crate) atlas_texture: wgpu::Texture,
120    #[allow(dead_code)] // GPU lifetime: must outlive text_bind_group which references this view
121    pub(crate) atlas_view: wgpu::TextureView,
122    pub(crate) glyph_cache: HashMap<u64, GlyphInfo>,
123    pub(crate) lru_head: Option<u64>,
124    pub(crate) lru_tail: Option<u64>,
125    pub(crate) atlas_next_x: u32,
126    pub(crate) atlas_next_y: u32,
127    pub(crate) atlas_row_height: u32,
128    /// Actual atlas size (may be smaller than preferred on devices with low texture limits)
129    pub(crate) atlas_size: u32,
130    /// Solid white pixel offset in atlas for geometric block rendering
131    pub(crate) solid_pixel_offset: (u32, u32),
132}
133
134/// Background image/solid-color texture state and per-pane cache.
135pub(crate) struct BackgroundImageState {
136    pub(crate) bg_image_texture: Option<wgpu::Texture>,
137    pub(crate) bg_image_mode: par_term_config::BackgroundImageMode,
138    pub(crate) bg_image_opacity: f32,
139    pub(crate) bg_image_width: u32,
140    pub(crate) bg_image_height: u32,
141    /// When true, current background is a solid color (not an image).
142    /// Solid colors should be rendered via clear color to respect window_opacity,
143    /// not via bg_image_pipeline which would cover the transparent background.
144    pub(crate) bg_is_solid_color: bool,
145    /// The solid background color [R, G, B] as floats (0.0-1.0).
146    /// Only used when bg_is_solid_color is true.
147    pub(crate) solid_bg_color: [f32; 3],
148    /// Cache of per-pane background textures keyed by image path
149    pub(crate) pane_bg_cache: HashMap<String, background::PaneBackgroundEntry>,
150    /// Cache of per-pane uniform buffers and bind groups keyed by image path.
151    /// Reused across frames via `queue.write_buffer()` to avoid per-frame GPU allocations.
152    pub(crate) pane_bg_uniform_cache: HashMap<String, background::PaneBgUniformEntry>,
153}
154
155/// Command separator line settings and visible marks.
156pub(crate) struct SeparatorConfig {
157    /// Whether to render separator lines between commands
158    pub(crate) enabled: bool,
159    /// Thickness of separator lines in pixels
160    pub(crate) thickness: f32,
161    /// Opacity of separator lines (0.0-1.0)
162    pub(crate) opacity: f32,
163    /// Whether to color separator lines by exit code
164    pub(crate) exit_color: bool,
165    /// Custom separator color [R, G, B] as floats (0.0-1.0)
166    pub(crate) color: [f32; 3],
167    /// Visible separator marks for current frame: (screen_row, exit_code, custom_color)
168    pub(crate) visible_marks: Vec<SeparatorMark>,
169}
170
171pub struct CellRenderer {
172    // Core wgpu state
173    pub(crate) device: Arc<wgpu::Device>,
174    pub(crate) queue: Arc<wgpu::Queue>,
175    pub(crate) surface: wgpu::Surface<'static>,
176    pub(crate) config: wgpu::SurfaceConfiguration,
177    /// Supported present modes for this surface (for vsync mode validation)
178    pub(crate) supported_present_modes: Vec<wgpu::PresentMode>,
179
180    // Sub-structs grouping related GPU and rendering state
181    pub(crate) pipelines: GpuPipelines,
182    pub(crate) buffers: GpuBuffers,
183    pub(crate) atlas: GlyphAtlas,
184    pub(crate) grid: GridLayout,
185    pub(crate) cursor: CursorState,
186    pub(crate) font: FontState,
187    pub(crate) bg_state: BackgroundImageState,
188    pub(crate) separator: SeparatorConfig,
189
190    /// Display scale factor (accessed directly from renderer module)
191    pub(crate) scale_factor: f32,
192
193    // Components
194    pub(crate) font_manager: FontManager,
195    pub(crate) scrollbar: Scrollbar,
196
197    // Dynamic state
198    pub(crate) cells: Vec<Cell>,
199    pub(crate) dirty_rows: Vec<bool>,
200    pub(crate) row_cache: Vec<Option<RowCacheEntry>>,
201
202    // Rendering state
203    pub(crate) visual_bell_intensity: f32,
204    pub(crate) visual_bell_color: [f32; 3],
205    pub(crate) window_opacity: f32,
206    pub(crate) background_color: [f32; 4],
207    /// Whether the window is currently focused (for unfocused cursor style)
208    pub(crate) is_focused: bool,
209
210    // CPU-side instance buffers for incremental updates
211    pub(crate) bg_instances: Vec<BackgroundInstance>,
212    pub(crate) text_instances: Vec<TextInstance>,
213
214    // Scratch buffers reused across dirty-row iterations (avoids per-row Vec allocation)
215    pub(crate) scratch_row_bg: Vec<BackgroundInstance>,
216    pub(crate) scratch_row_text: Vec<TextInstance>,
217    /// Scratch buffer for a single row of cells, reused in `build_instance_buffers` to
218    /// avoid cloning `self.cells[start..end]` into a new Vec on every dirty row.
219    pub(crate) scratch_row_cells: Vec<Cell>,
220
221    /// Reusable swash ScaleContext — holds internal caches that must be preserved
222    /// across glyph rasterization calls. Allocating one per glyph throws away these
223    /// caches unnecessarily; keeping it here allows every rasterize_glyph call to
224    /// reuse the same warmed-up context.
225    pub(crate) scale_context: swash::scale::ScaleContext,
226
227    // Transparency mode
228    /// When true, only default background cells are transparent.
229    /// Non-default (colored) backgrounds remain opaque for readability.
230    pub(crate) transparency_affects_only_default_background: bool,
231    /// When true, text is always rendered at full opacity regardless of window transparency.
232    pub(crate) keep_text_opaque: bool,
233    /// Style for link underlines (solid or stipple)
234    pub(crate) link_underline_style: par_term_config::LinkUnderlineStyle,
235
236    /// Gutter indicator marks for current frame: (screen_row, rgba_color)
237    pub(crate) gutter_indicators: Vec<(usize, [f32; 4])>,
238}
239
240/// Configuration for [`CellRenderer::new`].
241///
242/// Bundles all font, grid, scrollbar, and background parameters so the
243/// constructor does not exceed the `clippy::too_many_arguments` threshold.
244pub struct CellRendererConfig<'a> {
245    pub font_family: Option<&'a str>,
246    pub font_family_bold: Option<&'a str>,
247    pub font_family_italic: Option<&'a str>,
248    pub font_family_bold_italic: Option<&'a str>,
249    pub font_ranges: &'a [par_term_config::FontRange],
250    pub font_size: f32,
251    pub cols: usize,
252    pub rows: usize,
253    pub window_padding: f32,
254    pub line_spacing: f32,
255    pub char_spacing: f32,
256    pub scrollbar_position: &'a str,
257    pub scrollbar_width: f32,
258    pub scrollbar_thumb_color: [f32; 4],
259    pub scrollbar_track_color: [f32; 4],
260    pub enable_text_shaping: bool,
261    pub enable_ligatures: bool,
262    pub enable_kerning: bool,
263    pub font_antialias: bool,
264    pub font_hinting: bool,
265    pub font_thin_strokes: par_term_config::ThinStrokesMode,
266    pub minimum_contrast: f32,
267    pub vsync_mode: par_term_config::VsyncMode,
268    pub power_preference: par_term_config::PowerPreference,
269    pub window_opacity: f32,
270    pub background_color: [u8; 3],
271    pub background_image_path: Option<&'a str>,
272    pub background_image_mode: par_term_config::BackgroundImageMode,
273    pub background_image_opacity: f32,
274}
275
276impl CellRenderer {
277    pub async fn new(window: Arc<Window>, config: CellRendererConfig<'_>) -> Result<Self> {
278        let CellRendererConfig {
279            font_family,
280            font_family_bold,
281            font_family_italic,
282            font_family_bold_italic,
283            font_ranges,
284            font_size,
285            cols,
286            rows,
287            window_padding,
288            line_spacing,
289            char_spacing,
290            scrollbar_position,
291            scrollbar_width,
292            scrollbar_thumb_color,
293            scrollbar_track_color,
294            enable_text_shaping,
295            enable_ligatures,
296            enable_kerning,
297            font_antialias,
298            font_hinting,
299            font_thin_strokes,
300            minimum_contrast,
301            vsync_mode,
302            power_preference,
303            window_opacity,
304            background_color,
305            background_image_path,
306            background_image_mode,
307            background_image_opacity,
308        } = config;
309        // Platform-specific backend selection for better VM compatibility
310        // Windows: Use DX12 (Vulkan may not work in VMs like Parallels)
311        // macOS: Use Metal (native)
312        // Linux: Try Vulkan first, fall back to GL for VM compatibility
313        // Platform-specific backend selection for better VM compatibility
314        // Windows: Use DX12 (Vulkan may not work in VMs like Parallels)
315        // macOS: Use Metal (native)
316        // Linux: Try Vulkan first, fall back to GL for VM compatibility
317        #[cfg(target_os = "windows")]
318        let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
319            backends: wgpu::Backends::DX12,
320            ..Default::default()
321        });
322        #[cfg(target_os = "macos")]
323        let instance = wgpu::Instance::default();
324        #[cfg(target_os = "linux")]
325        let instance = wgpu::Instance::new(&wgpu::InstanceDescriptor {
326            backends: wgpu::Backends::VULKAN | wgpu::Backends::GL,
327            ..Default::default()
328        });
329        let surface = instance.create_surface(window.clone())?;
330        let adapter = instance
331            .request_adapter(&wgpu::RequestAdapterOptions {
332                power_preference: power_preference.to_wgpu(),
333                compatible_surface: Some(&surface),
334                force_fallback_adapter: false,
335            })
336            .await
337            .context("Failed to find wgpu adapter")?;
338
339        let (device, queue) = adapter
340            .request_device(&wgpu::DeviceDescriptor {
341                label: Some("device"),
342                required_features: wgpu::Features::empty(),
343                required_limits: wgpu::Limits::default(),
344                memory_hints: wgpu::MemoryHints::default(),
345                ..Default::default()
346            })
347            .await?;
348
349        let device = Arc::new(device);
350        let queue = Arc::new(queue);
351
352        let size = window.inner_size();
353        let surface_caps = surface.get_capabilities(&adapter);
354        let surface_format = surface_caps
355            .formats
356            .iter()
357            .copied()
358            .find(|f| !f.is_srgb())
359            .unwrap_or(surface_caps.formats[0]);
360
361        // Store supported present modes for runtime validation
362        let supported_present_modes = surface_caps.present_modes.clone();
363
364        // Select present mode with fallback if requested mode isn't supported
365        let requested_mode = vsync_mode.to_present_mode();
366        let present_mode = if supported_present_modes.contains(&requested_mode) {
367            requested_mode
368        } else {
369            // Fall back to Fifo (always supported) or first available
370            log::warn!(
371                "Requested present mode {:?} not supported (available: {:?}), falling back",
372                requested_mode,
373                supported_present_modes
374            );
375            if supported_present_modes.contains(&wgpu::PresentMode::Fifo) {
376                wgpu::PresentMode::Fifo
377            } else {
378                supported_present_modes[0]
379            }
380        };
381
382        // Select alpha mode for window transparency
383        // Prefer PreMultiplied (best for compositing) > PostMultiplied > Auto > first available
384        let alpha_mode = if surface_caps
385            .alpha_modes
386            .contains(&wgpu::CompositeAlphaMode::PreMultiplied)
387        {
388            wgpu::CompositeAlphaMode::PreMultiplied
389        } else if surface_caps
390            .alpha_modes
391            .contains(&wgpu::CompositeAlphaMode::PostMultiplied)
392        {
393            wgpu::CompositeAlphaMode::PostMultiplied
394        } else if surface_caps
395            .alpha_modes
396            .contains(&wgpu::CompositeAlphaMode::Auto)
397        {
398            wgpu::CompositeAlphaMode::Auto
399        } else {
400            surface_caps.alpha_modes[0]
401        };
402        log::info!(
403            "Selected alpha mode: {:?} (available: {:?})",
404            alpha_mode,
405            surface_caps.alpha_modes
406        );
407
408        let config = wgpu::SurfaceConfiguration {
409            usage: wgpu::TextureUsages::RENDER_ATTACHMENT,
410            format: surface_format,
411            width: size.width.max(1),
412            height: size.height.max(1),
413            present_mode,
414            alpha_mode,
415            view_formats: vec![],
416            desired_maximum_frame_latency: SURFACE_FRAME_LATENCY,
417        };
418        surface.configure(&device, &config);
419
420        let scale_factor = window.scale_factor() as f32;
421
422        let platform_dpi = if cfg!(target_os = "macos") {
423            MACOS_PLATFORM_DPI
424        } else {
425            DEFAULT_PLATFORM_DPI
426        };
427
428        let base_font_pixels = font_size * platform_dpi / FONT_REFERENCE_DPI;
429        let font_size_pixels = (base_font_pixels * scale_factor).max(1.0);
430
431        let font_manager = FontManager::new(
432            font_family,
433            font_family_bold,
434            font_family_italic,
435            font_family_bold_italic,
436            font_ranges,
437        )?;
438
439        // Extract font metrics
440        let (font_ascent, font_descent, font_leading, char_advance) = {
441            let primary_font = font_manager
442                .get_font(0)
443                .expect("Primary font at index 0 must exist after FontManager initialization");
444            let metrics = primary_font.metrics(&[]);
445            let scale = font_size_pixels / metrics.units_per_em as f32;
446            let glyph_id = primary_font.charmap().map('m');
447            let advance = primary_font.glyph_metrics(&[]).advance_width(glyph_id) * scale;
448            (
449                metrics.ascent * scale,
450                metrics.descent * scale,
451                metrics.leading * scale,
452                advance,
453            )
454        };
455
456        let natural_line_height = font_ascent + font_descent + font_leading;
457        // Round to integer pixels so every cell has the same width in device pixels.
458        // Without rounding, per-column scale_x alternates (e.g. 7/7.8 vs 8/7.8),
459        // causing glyphs to sample the atlas at slightly different rates and appear
460        // at different perceived brightnesses.
461        let cell_height = (natural_line_height * line_spacing).max(1.0).round();
462        let cell_width = (char_advance * char_spacing).max(1.0).round();
463
464        let scrollbar = Scrollbar::new(
465            Arc::clone(&device),
466            surface_format,
467            scrollbar_width,
468            scrollbar_position,
469            scrollbar_thumb_color,
470            scrollbar_track_color,
471        );
472
473        // Create pipelines using the pipeline module
474        let bg_pipeline = pipeline::create_bg_pipeline(&device, surface_format);
475
476        let (atlas_texture, atlas_view, atlas_sampler, atlas_size) =
477            pipeline::create_atlas(&device);
478        let text_bind_group_layout = pipeline::create_text_bind_group_layout(&device);
479        let text_bind_group = pipeline::create_text_bind_group(
480            &device,
481            &text_bind_group_layout,
482            &atlas_view,
483            &atlas_sampler,
484        );
485        let text_pipeline =
486            pipeline::create_text_pipeline(&device, surface_format, &text_bind_group_layout);
487
488        let bg_image_bind_group_layout = pipeline::create_bg_image_bind_group_layout(&device);
489        let bg_image_pipeline = pipeline::create_bg_image_pipeline(
490            &device,
491            surface_format,
492            &bg_image_bind_group_layout,
493        );
494        let bg_image_uniform_buffer = pipeline::create_bg_image_uniform_buffer(&device);
495
496        let (visual_bell_pipeline, visual_bell_bind_group, _, visual_bell_uniform_buffer) =
497            pipeline::create_visual_bell_pipeline(&device, surface_format);
498
499        let opaque_alpha_pipeline = pipeline::create_opaque_alpha_pipeline(&device, surface_format);
500
501        let vertex_buffer = pipeline::create_vertex_buffer(&device);
502
503        // Instance buffers
504        // Extra slots: CURSOR_OVERLAY_SLOTS for cursor overlays + rows for separator lines + rows for gutter indicators
505        let max_bg_instances = cols * rows + CURSOR_OVERLAY_SLOTS + rows + rows;
506        let max_text_instances = cols * rows * TEXT_INSTANCES_PER_CELL;
507        let (bg_instance_buffer, text_instance_buffer) =
508            pipeline::create_instance_buffers(&device, max_bg_instances, max_text_instances);
509
510        let mut renderer = Self {
511            device,
512            queue,
513            surface,
514            config,
515            supported_present_modes,
516            pipelines: GpuPipelines {
517                bg_pipeline,
518                text_pipeline,
519                bg_image_pipeline,
520                visual_bell_pipeline,
521                text_bind_group,
522                text_bind_group_layout,
523                bg_image_bind_group: None,
524                bg_image_bind_group_layout,
525                visual_bell_bind_group,
526                opaque_alpha_pipeline,
527            },
528            buffers: GpuBuffers {
529                vertex_buffer,
530                bg_instance_buffer,
531                text_instance_buffer,
532                bg_image_uniform_buffer,
533                visual_bell_uniform_buffer,
534                max_bg_instances,
535                max_text_instances,
536                actual_bg_instances: 0,
537                actual_text_instances: 0,
538            },
539            atlas: GlyphAtlas {
540                atlas_texture,
541                atlas_view,
542                glyph_cache: HashMap::new(),
543                lru_head: None,
544                lru_tail: None,
545                atlas_next_x: 0,
546                atlas_next_y: 0,
547                atlas_row_height: 0,
548                atlas_size,
549                solid_pixel_offset: (0, 0),
550            },
551            grid: GridLayout {
552                cols,
553                rows,
554                cell_width,
555                cell_height,
556                window_padding,
557                content_offset_y: 0.0,
558                content_offset_x: 0.0,
559                content_inset_bottom: 0.0,
560                content_inset_right: 0.0,
561                egui_bottom_inset: 0.0,
562                egui_right_inset: 0.0,
563            },
564            cursor: CursorState {
565                pos: (0, 0),
566                opacity: 0.0,
567                style: par_term_emu_core_rust::cursor::CursorStyle::SteadyBlock,
568                color: [1.0, 1.0, 1.0],
569                text_color: None,
570                hidden_for_shader: false,
571                guide_enabled: false,
572                guide_color: [1.0, 1.0, 1.0, DEFAULT_GUIDE_OPACITY],
573                shadow_enabled: false,
574                shadow_color: [0.0, 0.0, 0.0, DEFAULT_SHADOW_ALPHA],
575                shadow_offset: [DEFAULT_SHADOW_OFFSET_PX, DEFAULT_SHADOW_OFFSET_PX],
576                shadow_blur: DEFAULT_SHADOW_BLUR_PX,
577                boost: 0.0,
578                boost_color: [1.0, 1.0, 1.0],
579                unfocused_style: par_term_config::UnfocusedCursorStyle::default(),
580            },
581            font: FontState {
582                base_font_size: font_size,
583                line_spacing,
584                char_spacing,
585                font_ascent,
586                font_descent,
587                font_leading,
588                font_size_pixels,
589                char_advance,
590                enable_text_shaping,
591                enable_ligatures,
592                enable_kerning,
593                font_antialias,
594                font_hinting,
595                font_thin_strokes,
596                minimum_contrast: minimum_contrast.clamp(0.0, 1.0),
597            },
598            bg_state: BackgroundImageState {
599                bg_image_texture: None,
600                bg_image_mode: background_image_mode,
601                bg_image_opacity: background_image_opacity,
602                bg_image_width: 0,
603                bg_image_height: 0,
604                bg_is_solid_color: false,
605                solid_bg_color: [0.0, 0.0, 0.0],
606                pane_bg_cache: HashMap::new(),
607                pane_bg_uniform_cache: HashMap::new(),
608            },
609            separator: SeparatorConfig {
610                enabled: false,
611                thickness: 1.0,
612                opacity: 0.4,
613                exit_color: true,
614                color: [0.5, 0.5, 0.5],
615                visible_marks: Vec::new(),
616            },
617            scale_factor,
618            font_manager,
619            scrollbar,
620            cells: vec![Cell::default(); cols * rows],
621            dirty_rows: vec![true; rows],
622            row_cache: (0..rows).map(|_| None).collect(),
623            is_focused: true,
624            visual_bell_intensity: 0.0,
625            visual_bell_color: [1.0, 1.0, 1.0], // White flash
626            window_opacity,
627            background_color: color_u8_to_f32_a(background_color, 1.0),
628            bg_instances: vec![
629                BackgroundInstance {
630                    position: [0.0, 0.0],
631                    size: [0.0, 0.0],
632                    color: [0.0, 0.0, 0.0, 0.0],
633                };
634                max_bg_instances
635            ],
636            text_instances: vec![
637                TextInstance {
638                    position: [0.0, 0.0],
639                    size: [0.0, 0.0],
640                    tex_offset: [0.0, 0.0],
641                    tex_size: [0.0, 0.0],
642                    color: [0.0, 0.0, 0.0, 0.0],
643                    is_colored: 0,
644                };
645                max_text_instances
646            ],
647            transparency_affects_only_default_background: false,
648            keep_text_opaque: true,
649            link_underline_style: par_term_config::LinkUnderlineStyle::default(),
650            gutter_indicators: Vec::new(),
651            scratch_row_bg: Vec::with_capacity(cols),
652            scratch_row_text: Vec::with_capacity(cols * 2),
653            scratch_row_cells: Vec::with_capacity(cols),
654            scale_context: swash::scale::ScaleContext::new(),
655        };
656
657        // Upload a solid white 2x2 pixel block to the atlas for geometric block rendering
658        renderer.upload_solid_pixel();
659
660        log::info!(
661            "CellRenderer::new: background_image_path={:?}",
662            background_image_path
663        );
664        if let Some(path) = background_image_path {
665            // Handle missing background image gracefully - don't crash, just log and continue
666            if let Err(e) = renderer.load_background_image(path) {
667                log::warn!(
668                    "Could not load background image '{}': {} - continuing without background image",
669                    path,
670                    e
671                );
672            }
673        }
674
675        Ok(renderer)
676    }
677
678    /// Upload a solid white pixel to the atlas for use in geometric block rendering
679    pub(crate) fn upload_solid_pixel(&mut self) {
680        let size = SOLID_PIXEL_SIZE;
681        let white_pixels: Vec<u8> = vec![255; (size * size * 4) as usize];
682
683        self.queue.write_texture(
684            wgpu::TexelCopyTextureInfo {
685                texture: &self.atlas.atlas_texture,
686                mip_level: 0,
687                origin: wgpu::Origin3d {
688                    x: self.atlas.atlas_next_x,
689                    y: self.atlas.atlas_next_y,
690                    z: 0,
691                },
692                aspect: wgpu::TextureAspect::All,
693            },
694            &white_pixels,
695            wgpu::TexelCopyBufferLayout {
696                offset: 0,
697                bytes_per_row: Some(4 * size),
698                rows_per_image: Some(size),
699            },
700            wgpu::Extent3d {
701                width: size,
702                height: size,
703                depth_or_array_layers: 1,
704            },
705        );
706
707        self.atlas.solid_pixel_offset = (self.atlas.atlas_next_x, self.atlas.atlas_next_y);
708        self.atlas.atlas_next_x += size + ATLAS_GLYPH_PADDING;
709        self.atlas.atlas_row_height = self.atlas.atlas_row_height.max(size);
710    }
711
712    pub fn device(&self) -> &wgpu::Device {
713        &self.device
714    }
715    pub fn queue(&self) -> &wgpu::Queue {
716        &self.queue
717    }
718    pub fn surface_format(&self) -> wgpu::TextureFormat {
719        self.config.format
720    }
721    pub fn keep_text_opaque(&self) -> bool {
722        self.keep_text_opaque
723    }
724
725    /// Update cells. Returns `true` if any row actually changed.
726    pub fn update_cells(&mut self, new_cells: &[Cell]) -> bool {
727        let mut changed = false;
728        for row in 0..self.grid.rows {
729            let start = row * self.grid.cols;
730            let end = (row + 1) * self.grid.cols;
731            if start < new_cells.len() && end <= new_cells.len() {
732                let row_slice = &new_cells[start..end];
733                if row_slice != &self.cells[start..end] {
734                    self.cells[start..end].clone_from_slice(row_slice);
735                    self.dirty_rows[row] = true;
736                    changed = true;
737                }
738            }
739        }
740        changed
741    }
742
743    /// Clear all cells and mark all rows as dirty.
744    pub fn clear_all_cells(&mut self) {
745        for cell in &mut self.cells {
746            *cell = Cell::default();
747        }
748        for dirty in &mut self.dirty_rows {
749            *dirty = true;
750        }
751    }
752
753    pub fn update_graphics(
754        &mut self,
755        _graphics: &[par_term_emu_core_rust::graphics::TerminalGraphic],
756        _scroll_offset: usize,
757        _scrollback_len: usize,
758        _visible_lines: usize,
759    ) -> Result<()> {
760        Ok(())
761    }
762}