Skip to main content

par_term_render/cell_renderer/
background.rs

1use super::CellRenderer;
2use crate::custom_shader_renderer::textures::ChannelTexture;
3use anyhow::Result;
4
5/// Cached GPU texture for a per-pane background image
6#[allow(dead_code)]
7pub(crate) struct PaneBackgroundEntry {
8    pub(crate) texture: wgpu::Texture,
9    pub(crate) view: wgpu::TextureView,
10    pub(crate) sampler: wgpu::Sampler,
11    pub(crate) width: u32,
12    pub(crate) height: u32,
13}
14
15impl CellRenderer {
16    pub(crate) fn load_background_image(&mut self, path: &str) -> Result<()> {
17        log::info!("Loading background image from: {}", path);
18        let img = image::open(path)
19            .map_err(|e| {
20                log::error!("Failed to open background image '{}': {}", path, e);
21                e
22            })?
23            .to_rgba8();
24        log::info!("Background image loaded: {}x{}", img.width(), img.height());
25        let (width, height) = img.dimensions();
26        let texture = self.device.create_texture(&wgpu::TextureDescriptor {
27            label: Some("bg image"),
28            size: wgpu::Extent3d {
29                width,
30                height,
31                depth_or_array_layers: 1,
32            },
33            mip_level_count: 1,
34            sample_count: 1,
35            dimension: wgpu::TextureDimension::D2,
36            format: wgpu::TextureFormat::Rgba8UnormSrgb,
37            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
38            view_formats: &[],
39        });
40        self.queue.write_texture(
41            wgpu::TexelCopyTextureInfo {
42                texture: &texture,
43                mip_level: 0,
44                origin: wgpu::Origin3d::ZERO,
45                aspect: wgpu::TextureAspect::All,
46            },
47            &img,
48            wgpu::TexelCopyBufferLayout {
49                offset: 0,
50                bytes_per_row: Some(4 * width),
51                rows_per_image: Some(height),
52            },
53            wgpu::Extent3d {
54                width,
55                height,
56                depth_or_array_layers: 1,
57            },
58        );
59
60        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
61        let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
62            mag_filter: wgpu::FilterMode::Linear,
63            min_filter: wgpu::FilterMode::Linear,
64            ..Default::default()
65        });
66
67        self.bg_image_bind_group =
68            Some(self.device.create_bind_group(&wgpu::BindGroupDescriptor {
69                label: Some("bg image bind group"),
70                layout: &self.bg_image_bind_group_layout,
71                entries: &[
72                    wgpu::BindGroupEntry {
73                        binding: 0,
74                        resource: wgpu::BindingResource::TextureView(&view),
75                    },
76                    wgpu::BindGroupEntry {
77                        binding: 1,
78                        resource: wgpu::BindingResource::Sampler(&sampler),
79                    },
80                    wgpu::BindGroupEntry {
81                        binding: 2,
82                        resource: self.bg_image_uniform_buffer.as_entire_binding(),
83                    },
84                ],
85            }));
86        self.bg_image_texture = Some(texture);
87        self.bg_image_width = width;
88        self.bg_image_height = height;
89        self.bg_is_solid_color = false; // This is an image, not a solid color
90        self.update_bg_image_uniforms();
91        Ok(())
92    }
93
94    pub(crate) fn update_bg_image_uniforms(&mut self) {
95        // Shader uniform struct layout (48 bytes):
96        //   image_size: vec2<f32>    @ offset 0  (8 bytes)
97        //   window_size: vec2<f32>   @ offset 8  (8 bytes)
98        //   mode: u32                @ offset 16 (4 bytes)
99        //   opacity: f32             @ offset 20 (4 bytes)
100        //   pane_offset: vec2<f32>   @ offset 24 (8 bytes) - (0,0) for global
101        //   surface_size: vec2<f32>  @ offset 32 (8 bytes) - same as window_size for global
102        //   darken: f32              @ offset 40 (4 bytes) - 0.0 for global
103        let mut data = [0u8; 48];
104
105        let w = self.config.width as f32;
106        let h = self.config.height as f32;
107
108        // image_size (vec2<f32>)
109        data[0..4].copy_from_slice(&(self.bg_image_width as f32).to_le_bytes());
110        data[4..8].copy_from_slice(&(self.bg_image_height as f32).to_le_bytes());
111
112        // window_size (vec2<f32>)
113        data[8..12].copy_from_slice(&w.to_le_bytes());
114        data[12..16].copy_from_slice(&h.to_le_bytes());
115
116        // mode (u32)
117        data[16..20].copy_from_slice(&(self.bg_image_mode as u32).to_le_bytes());
118
119        // opacity (f32) - combine bg_image_opacity with window_opacity
120        let effective_opacity = self.bg_image_opacity * self.window_opacity;
121        data[20..24].copy_from_slice(&effective_opacity.to_le_bytes());
122
123        // pane_offset (vec2<f32>) - (0,0) for global background
124        // bytes 24..32 are already zeros
125
126        // surface_size (vec2<f32>) - same as window_size for global
127        data[32..36].copy_from_slice(&w.to_le_bytes());
128        data[36..40].copy_from_slice(&h.to_le_bytes());
129
130        // darken (f32) - 0.0 for global background (no darkening)
131        // bytes 40..44 are already zeros
132
133        self.queue
134            .write_buffer(&self.bg_image_uniform_buffer, 0, &data);
135    }
136
137    pub fn set_background_image(
138        &mut self,
139        path: Option<&str>,
140        mode: par_term_config::BackgroundImageMode,
141        opacity: f32,
142    ) {
143        self.bg_image_mode = mode;
144        self.bg_image_opacity = opacity;
145        if let Some(p) = path {
146            log::info!("Loading background image: {}", p);
147            if let Err(e) = self.load_background_image(p) {
148                log::error!("Failed to load background image '{}': {}", p, e);
149            }
150            // Note: bg_is_solid_color is set in load_background_image
151        } else {
152            self.bg_image_texture = None;
153            self.bg_image_bind_group = None;
154            self.bg_image_width = 0;
155            self.bg_image_height = 0;
156            self.bg_is_solid_color = false;
157        }
158        self.update_bg_image_uniforms();
159    }
160
161    #[allow(dead_code)]
162    pub fn update_background_image_opacity(&mut self, opacity: f32) {
163        self.bg_image_opacity = opacity;
164        self.update_bg_image_uniforms();
165    }
166
167    #[allow(dead_code)]
168    pub fn update_background_image_opacity_only(&mut self, opacity: f32) {
169        self.bg_image_opacity = opacity;
170        self.update_bg_image_uniforms();
171    }
172
173    /// Create a ChannelTexture from the current background image for use in custom shaders.
174    ///
175    /// Returns None if no background image is loaded.
176    /// The returned ChannelTexture shares the same underlying texture data with the
177    /// cell renderer's background image - no copy is made.
178    pub fn get_background_as_channel_texture(&self) -> Option<ChannelTexture> {
179        let texture = self.bg_image_texture.as_ref()?;
180
181        // Create a new view and sampler for use by the custom shader
182        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
183        let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
184            mag_filter: wgpu::FilterMode::Linear,
185            min_filter: wgpu::FilterMode::Linear,
186            address_mode_u: wgpu::AddressMode::Repeat,
187            address_mode_v: wgpu::AddressMode::Repeat,
188            address_mode_w: wgpu::AddressMode::Repeat,
189            ..Default::default()
190        });
191
192        Some(ChannelTexture::from_view(
193            view,
194            sampler,
195            self.bg_image_width,
196            self.bg_image_height,
197        ))
198    }
199
200    /// Check if a background image is currently loaded.
201    #[allow(dead_code)]
202    pub fn has_background_image(&self) -> bool {
203        self.bg_image_texture.is_some()
204    }
205
206    /// Check if a solid color background is currently set.
207    pub fn is_solid_color_background(&self) -> bool {
208        self.bg_is_solid_color
209    }
210
211    /// Get the solid background color as normalized RGB values.
212    /// Returns the color even if not in solid color mode.
213    pub fn solid_background_color(&self) -> [f32; 3] {
214        self.solid_bg_color
215    }
216
217    /// Get the solid background color as a wgpu::Color with window_opacity applied.
218    /// Returns None if not in solid color mode.
219    #[allow(dead_code)]
220    pub fn get_solid_color_as_clear(&self) -> Option<wgpu::Color> {
221        if self.bg_is_solid_color {
222            Some(wgpu::Color {
223                r: self.solid_bg_color[0] as f64 * self.window_opacity as f64,
224                g: self.solid_bg_color[1] as f64 * self.window_opacity as f64,
225                b: self.solid_bg_color[2] as f64 * self.window_opacity as f64,
226                a: self.window_opacity as f64,
227            })
228        } else {
229            None
230        }
231    }
232
233    /// Create a solid color texture for use as background.
234    ///
235    /// Creates a small (4x4) texture filled with the specified color.
236    /// Uses Stretch mode for solid colors to fill the entire window.
237    /// Transparency is controlled by window_opacity, not the texture alpha.
238    pub fn create_solid_color_texture(&mut self, color: [u8; 3]) {
239        log::info!(
240            "[BACKGROUND] create_solid_color_texture: RGB({}, {}, {}) -> normalized ({:.3}, {:.3}, {:.3})",
241            color[0],
242            color[1],
243            color[2],
244            color[0] as f32 / 255.0,
245            color[1] as f32 / 255.0,
246            color[2] as f32 / 255.0
247        );
248        let size = 4u32; // 4x4 for proper linear filtering
249        let mut pixels = Vec::with_capacity((size * size * 4) as usize);
250        for _ in 0..(size * size) {
251            pixels.push(color[0]);
252            pixels.push(color[1]);
253            pixels.push(color[2]);
254            pixels.push(255); // Fully opaque - window_opacity controls transparency
255        }
256
257        let texture = self.device.create_texture(&wgpu::TextureDescriptor {
258            label: Some("bg solid color"),
259            size: wgpu::Extent3d {
260                width: size,
261                height: size,
262                depth_or_array_layers: 1,
263            },
264            mip_level_count: 1,
265            sample_count: 1,
266            dimension: wgpu::TextureDimension::D2,
267            format: wgpu::TextureFormat::Rgba8UnormSrgb,
268            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
269            view_formats: &[],
270        });
271
272        self.queue.write_texture(
273            wgpu::TexelCopyTextureInfo {
274                texture: &texture,
275                mip_level: 0,
276                origin: wgpu::Origin3d::ZERO,
277                aspect: wgpu::TextureAspect::All,
278            },
279            &pixels,
280            wgpu::TexelCopyBufferLayout {
281                offset: 0,
282                bytes_per_row: Some(4 * size),
283                rows_per_image: Some(size),
284            },
285            wgpu::Extent3d {
286                width: size,
287                height: size,
288                depth_or_array_layers: 1,
289            },
290        );
291
292        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
293        let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
294            mag_filter: wgpu::FilterMode::Linear,
295            min_filter: wgpu::FilterMode::Linear,
296            ..Default::default()
297        });
298
299        self.bg_image_bind_group =
300            Some(self.device.create_bind_group(&wgpu::BindGroupDescriptor {
301                label: Some("bg solid color bind group"),
302                layout: &self.bg_image_bind_group_layout,
303                entries: &[
304                    wgpu::BindGroupEntry {
305                        binding: 0,
306                        resource: wgpu::BindingResource::TextureView(&view),
307                    },
308                    wgpu::BindGroupEntry {
309                        binding: 1,
310                        resource: wgpu::BindingResource::Sampler(&sampler),
311                    },
312                    wgpu::BindGroupEntry {
313                        binding: 2,
314                        resource: self.bg_image_uniform_buffer.as_entire_binding(),
315                    },
316                ],
317            }));
318
319        self.bg_image_texture = Some(texture);
320        self.bg_image_width = size;
321        self.bg_image_height = size;
322        // Use Stretch mode for solid colors to fill the window
323        self.bg_image_mode = par_term_config::BackgroundImageMode::Stretch;
324        // Use 1.0 as base opacity - window_opacity is applied in update_bg_image_uniforms()
325        self.bg_image_opacity = 1.0;
326        // Mark this as a solid color for tracking purposes
327        self.bg_is_solid_color = true;
328        self.solid_bg_color = [
329            color[0] as f32 / 255.0,
330            color[1] as f32 / 255.0,
331            color[2] as f32 / 255.0,
332        ];
333        self.update_bg_image_uniforms();
334    }
335
336    /// Create a ChannelTexture from a solid color for shader iChannel0.
337    ///
338    /// Creates a small texture with the specified color that can be used
339    /// as a channel texture in custom shaders. The texture is fully opaque;
340    /// window_opacity controls overall transparency.
341    pub fn get_solid_color_as_channel_texture(&self, color: [u8; 3]) -> ChannelTexture {
342        log::info!(
343            "get_solid_color_as_channel_texture: RGB({},{},{})",
344            color[0],
345            color[1],
346            color[2]
347        );
348        let size = 4u32;
349        let mut pixels = Vec::with_capacity((size * size * 4) as usize);
350        for _ in 0..(size * size) {
351            pixels.push(color[0]);
352            pixels.push(color[1]);
353            pixels.push(color[2]);
354            pixels.push(255); // Fully opaque
355        }
356
357        let texture = self.device.create_texture(&wgpu::TextureDescriptor {
358            label: Some("solid color channel texture"),
359            size: wgpu::Extent3d {
360                width: size,
361                height: size,
362                depth_or_array_layers: 1,
363            },
364            mip_level_count: 1,
365            sample_count: 1,
366            dimension: wgpu::TextureDimension::D2,
367            format: wgpu::TextureFormat::Rgba8UnormSrgb,
368            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
369            view_formats: &[],
370        });
371
372        self.queue.write_texture(
373            wgpu::TexelCopyTextureInfo {
374                texture: &texture,
375                mip_level: 0,
376                origin: wgpu::Origin3d::ZERO,
377                aspect: wgpu::TextureAspect::All,
378            },
379            &pixels,
380            wgpu::TexelCopyBufferLayout {
381                offset: 0,
382                bytes_per_row: Some(4 * size),
383                rows_per_image: Some(size),
384            },
385            wgpu::Extent3d {
386                width: size,
387                height: size,
388                depth_or_array_layers: 1,
389            },
390        );
391
392        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
393        let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
394            mag_filter: wgpu::FilterMode::Linear,
395            min_filter: wgpu::FilterMode::Linear,
396            address_mode_u: wgpu::AddressMode::Repeat,
397            address_mode_v: wgpu::AddressMode::Repeat,
398            address_mode_w: wgpu::AddressMode::Repeat,
399            ..Default::default()
400        });
401
402        ChannelTexture::from_view_and_texture(view, sampler, size, size, texture)
403    }
404
405    /// Set background based on mode (Default, Color, or Image).
406    ///
407    /// This unified method handles all background types and should be used
408    /// instead of calling individual methods directly.
409    pub fn set_background(
410        &mut self,
411        mode: par_term_config::BackgroundMode,
412        color: [u8; 3],
413        image_path: Option<&str>,
414        image_mode: par_term_config::BackgroundImageMode,
415        image_opacity: f32,
416        image_enabled: bool,
417    ) {
418        log::info!(
419            "[BACKGROUND] set_background: mode={:?}, color=RGB({}, {}, {}), image_path={:?}",
420            mode,
421            color[0],
422            color[1],
423            color[2],
424            image_path
425        );
426        match mode {
427            par_term_config::BackgroundMode::Default => {
428                // Clear background texture - use theme default
429                self.bg_image_texture = None;
430                self.bg_image_bind_group = None;
431                self.bg_image_width = 0;
432                self.bg_image_height = 0;
433                self.bg_is_solid_color = false;
434            }
435            par_term_config::BackgroundMode::Color => {
436                // create_solid_color_texture sets bg_is_solid_color = true
437                self.create_solid_color_texture(color);
438            }
439            par_term_config::BackgroundMode::Image => {
440                if image_enabled {
441                    // set_background_image sets bg_is_solid_color = false
442                    self.set_background_image(image_path, image_mode, image_opacity);
443                } else {
444                    // Image disabled - clear texture
445                    self.bg_image_texture = None;
446                    self.bg_image_bind_group = None;
447                    self.bg_image_width = 0;
448                    self.bg_image_height = 0;
449                    self.bg_is_solid_color = false;
450                }
451            }
452        }
453    }
454
455    /// Load a per-pane background image into the texture cache.
456    /// Returns Ok(true) if the image was newly loaded, Ok(false) if already cached.
457    pub(crate) fn load_pane_background(&mut self, path: &str) -> Result<bool> {
458        if self.pane_bg_cache.contains_key(path) {
459            return Ok(false);
460        }
461
462        // Expand tilde in path (e.g., ~/images/bg.png -> /home/user/images/bg.png)
463        let expanded = if let Some(rest) = path.strip_prefix("~/") {
464            if let Some(home) = dirs::home_dir() {
465                home.join(rest).to_string_lossy().to_string()
466            } else {
467                path.to_string()
468            }
469        } else {
470            path.to_string()
471        };
472
473        log::info!("Loading per-pane background image: {}", expanded);
474        let img = image::open(&expanded)
475            .map_err(|e| {
476                log::error!("Failed to open pane background image '{}': {}", path, e);
477                e
478            })?
479            .to_rgba8();
480
481        let (width, height) = img.dimensions();
482        let texture = self.device.create_texture(&wgpu::TextureDescriptor {
483            label: Some("pane bg image"),
484            size: wgpu::Extent3d {
485                width,
486                height,
487                depth_or_array_layers: 1,
488            },
489            mip_level_count: 1,
490            sample_count: 1,
491            dimension: wgpu::TextureDimension::D2,
492            format: wgpu::TextureFormat::Rgba8UnormSrgb,
493            usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
494            view_formats: &[],
495        });
496
497        self.queue.write_texture(
498            wgpu::TexelCopyTextureInfo {
499                texture: &texture,
500                mip_level: 0,
501                origin: wgpu::Origin3d::ZERO,
502                aspect: wgpu::TextureAspect::All,
503            },
504            &img,
505            wgpu::TexelCopyBufferLayout {
506                offset: 0,
507                bytes_per_row: Some(4 * width),
508                rows_per_image: Some(height),
509            },
510            wgpu::Extent3d {
511                width,
512                height,
513                depth_or_array_layers: 1,
514            },
515        );
516
517        let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
518        let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
519            mag_filter: wgpu::FilterMode::Linear,
520            min_filter: wgpu::FilterMode::Linear,
521            ..Default::default()
522        });
523
524        self.pane_bg_cache.insert(
525            path.to_string(),
526            super::background::PaneBackgroundEntry {
527                texture,
528                view,
529                sampler,
530                width,
531                height,
532            },
533        );
534
535        Ok(true)
536    }
537
538    /// Remove a cached pane background texture
539    #[allow(dead_code)]
540    pub(crate) fn evict_pane_background(&mut self, path: &str) {
541        self.pane_bg_cache.remove(path);
542    }
543
544    /// Clear all cached pane background textures
545    #[allow(dead_code)]
546    pub(crate) fn clear_pane_bg_cache(&mut self) {
547        self.pane_bg_cache.clear();
548    }
549
550    /// Create a bind group and uniform buffer for a per-pane background render.
551    /// The uniform buffer provides pane dimensions, position, and surface size so the
552    /// background_image.wgsl shader computes texture coords and NDC positions relative to the pane.
553    #[allow(clippy::too_many_arguments)]
554    pub(crate) fn create_pane_bg_bind_group(
555        &self,
556        entry: &super::background::PaneBackgroundEntry,
557        pane_x: f32,
558        pane_y: f32,
559        pane_width: f32,
560        pane_height: f32,
561        mode: par_term_config::BackgroundImageMode,
562        opacity: f32,
563        darken: f32,
564    ) -> (wgpu::BindGroup, wgpu::Buffer) {
565        // Shader uniform struct layout (48 bytes):
566        //   image_size: vec2<f32>    @ offset 0  (8 bytes)
567        //   window_size: vec2<f32>   @ offset 8  (8 bytes) - pane dimensions
568        //   mode: u32                @ offset 16 (4 bytes)
569        //   opacity: f32             @ offset 20 (4 bytes)
570        //   pane_offset: vec2<f32>   @ offset 24 (8 bytes) - pane position in window
571        //   surface_size: vec2<f32>  @ offset 32 (8 bytes) - window dimensions
572        //   darken: f32              @ offset 40 (4 bytes)
573        let mut data = [0u8; 48];
574        // image_size (vec2<f32>)
575        data[0..4].copy_from_slice(&(entry.width as f32).to_le_bytes());
576        data[4..8].copy_from_slice(&(entry.height as f32).to_le_bytes());
577        // window_size (pane dimensions for UV calculation)
578        data[8..12].copy_from_slice(&pane_width.to_le_bytes());
579        data[12..16].copy_from_slice(&pane_height.to_le_bytes());
580        // mode (u32)
581        data[16..20].copy_from_slice(&(mode as u32).to_le_bytes());
582        // opacity (combine with window_opacity)
583        let effective_opacity = opacity * self.window_opacity;
584        data[20..24].copy_from_slice(&effective_opacity.to_le_bytes());
585        // pane_offset (vec2<f32>) - pane position within the window
586        data[24..28].copy_from_slice(&pane_x.to_le_bytes());
587        data[28..32].copy_from_slice(&pane_y.to_le_bytes());
588        // surface_size (vec2<f32>) - full window dimensions
589        let surface_w = self.config.width as f32;
590        let surface_h = self.config.height as f32;
591        data[32..36].copy_from_slice(&surface_w.to_le_bytes());
592        data[36..40].copy_from_slice(&surface_h.to_le_bytes());
593        // darken (f32)
594        data[40..44].copy_from_slice(&darken.to_le_bytes());
595
596        let uniform_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
597            label: Some("pane bg uniform buffer"),
598            size: 48,
599            usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
600            mapped_at_creation: false,
601        });
602        self.queue.write_buffer(&uniform_buffer, 0, &data);
603
604        let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
605            label: Some("pane bg bind group"),
606            layout: &self.bg_image_bind_group_layout,
607            entries: &[
608                wgpu::BindGroupEntry {
609                    binding: 0,
610                    resource: wgpu::BindingResource::TextureView(&entry.view),
611                },
612                wgpu::BindGroupEntry {
613                    binding: 1,
614                    resource: wgpu::BindingResource::Sampler(&entry.sampler),
615                },
616                wgpu::BindGroupEntry {
617                    binding: 2,
618                    resource: uniform_buffer.as_entire_binding(),
619                },
620            ],
621        });
622
623        (bind_group, uniform_buffer)
624    }
625}