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