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