1use super::CellRenderer;
10use crate::custom_shader_renderer::textures::ChannelTexture;
11use crate::error::RenderError;
12use par_term_config::color_u8_to_f32;
13
14pub(crate) struct PaneBgBindGroupParams {
16 pub pane_x: f32,
17 pub pane_y: f32,
18 pub pane_width: f32,
19 pub pane_height: f32,
20 pub mode: par_term_config::BackgroundImageMode,
21 pub opacity: f32,
22 pub darken: f32,
23}
24
25pub(crate) struct PaneBackgroundEntry {
27 #[allow(dead_code)] pub(crate) texture: wgpu::Texture,
29 pub(crate) view: wgpu::TextureView,
30 pub(crate) sampler: wgpu::Sampler,
31 pub(crate) width: u32,
32 pub(crate) height: u32,
33}
34
35pub(crate) struct PaneBgUniformEntry {
40 pub(crate) uniform_buffer: wgpu::Buffer,
41 pub(crate) bind_group: wgpu::BindGroup,
42}
43
44impl CellRenderer {
45 pub(crate) fn load_background_image(&mut self, path: &str) -> Result<(), RenderError> {
46 log::info!("Loading background image from: {}", path);
47 let img = image::open(path)
48 .map_err(|e| {
49 log::error!("Failed to open background image '{}': {}", path, e);
50 RenderError::ImageLoad {
51 path: path.to_string(),
52 source: e,
53 }
54 })?
55 .to_rgba8();
56 log::info!("Background image loaded: {}x{}", img.width(), img.height());
57 let (width, height) = img.dimensions();
58 let texture = self.device.create_texture(&wgpu::TextureDescriptor {
59 label: Some("bg image"),
60 size: wgpu::Extent3d {
61 width,
62 height,
63 depth_or_array_layers: 1,
64 },
65 mip_level_count: 1,
66 sample_count: 1,
67 dimension: wgpu::TextureDimension::D2,
68 format: wgpu::TextureFormat::Rgba8UnormSrgb,
69 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
70 view_formats: &[],
71 });
72 self.queue.write_texture(
73 wgpu::TexelCopyTextureInfo {
74 texture: &texture,
75 mip_level: 0,
76 origin: wgpu::Origin3d::ZERO,
77 aspect: wgpu::TextureAspect::All,
78 },
79 &img,
80 wgpu::TexelCopyBufferLayout {
81 offset: 0,
82 bytes_per_row: Some(4 * width),
83 rows_per_image: Some(height),
84 },
85 wgpu::Extent3d {
86 width,
87 height,
88 depth_or_array_layers: 1,
89 },
90 );
91
92 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
93 let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
94 mag_filter: wgpu::FilterMode::Linear,
95 min_filter: wgpu::FilterMode::Linear,
96 ..Default::default()
97 });
98
99 self.pipelines.bg_image_bind_group =
100 Some(self.device.create_bind_group(&wgpu::BindGroupDescriptor {
101 label: Some("bg image bind group"),
102 layout: &self.pipelines.bg_image_bind_group_layout,
103 entries: &[
104 wgpu::BindGroupEntry {
105 binding: 0,
106 resource: wgpu::BindingResource::TextureView(&view),
107 },
108 wgpu::BindGroupEntry {
109 binding: 1,
110 resource: wgpu::BindingResource::Sampler(&sampler),
111 },
112 wgpu::BindGroupEntry {
113 binding: 2,
114 resource: self.buffers.bg_image_uniform_buffer.as_entire_binding(),
115 },
116 ],
117 }));
118 self.bg_state.bg_image_texture = Some(texture);
119 self.bg_state.bg_image_width = width;
120 self.bg_state.bg_image_height = height;
121 self.bg_state.bg_is_solid_color = false; self.update_bg_image_uniforms(None);
123 Ok(())
124 }
125
126 pub(crate) fn update_bg_image_uniforms(&mut self, window_opacity_override: Option<f32>) {
134 let mut data = [0u8; 48];
143
144 let w = self.config.width as f32;
145 let h = self.config.height as f32;
146
147 data[0..4].copy_from_slice(&(self.bg_state.bg_image_width as f32).to_le_bytes());
149 data[4..8].copy_from_slice(&(self.bg_state.bg_image_height as f32).to_le_bytes());
150
151 data[8..12].copy_from_slice(&w.to_le_bytes());
153 data[12..16].copy_from_slice(&h.to_le_bytes());
154
155 data[16..20].copy_from_slice(&(self.bg_state.bg_image_mode as u32).to_le_bytes());
157
158 let win_opacity = window_opacity_override.unwrap_or(self.window_opacity);
160 let effective_opacity = self.bg_state.bg_image_opacity * win_opacity;
161 data[20..24].copy_from_slice(&effective_opacity.to_le_bytes());
162
163 data[32..36].copy_from_slice(&w.to_le_bytes());
168 data[36..40].copy_from_slice(&h.to_le_bytes());
169
170 self.queue
174 .write_buffer(&self.buffers.bg_image_uniform_buffer, 0, &data);
175 }
176
177 pub fn set_background_image(
178 &mut self,
179 path: Option<&str>,
180 mode: par_term_config::BackgroundImageMode,
181 opacity: f32,
182 ) {
183 self.bg_state.bg_image_mode = mode;
184 self.bg_state.bg_image_opacity = opacity;
185 if let Some(p) = path {
186 log::info!("Loading background image: {}", p);
187 if let Err(e) = self.load_background_image(p) {
188 log::error!("Failed to load background image '{}': {}", p, e);
189 }
190 } else {
192 self.bg_state.bg_image_texture = None;
193 self.pipelines.bg_image_bind_group = None;
194 self.bg_state.bg_image_width = 0;
195 self.bg_state.bg_image_height = 0;
196 self.bg_state.bg_is_solid_color = false;
197 }
198 self.update_bg_image_uniforms(None);
199 }
200
201 pub fn update_background_image_opacity(&mut self, opacity: f32) {
202 self.bg_state.bg_image_opacity = opacity;
203 self.update_bg_image_uniforms(None);
204 }
205
206 pub fn update_background_image_opacity_only(&mut self, opacity: f32) {
207 self.bg_state.bg_image_opacity = opacity;
208 self.update_bg_image_uniforms(None);
209 }
210
211 pub fn get_background_as_channel_texture(&self) -> Option<ChannelTexture> {
217 let texture = self.bg_state.bg_image_texture.as_ref()?;
218
219 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
221 let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
222 mag_filter: wgpu::FilterMode::Linear,
223 min_filter: wgpu::FilterMode::Linear,
224 address_mode_u: wgpu::AddressMode::Repeat,
225 address_mode_v: wgpu::AddressMode::Repeat,
226 address_mode_w: wgpu::AddressMode::Repeat,
227 ..Default::default()
228 });
229
230 Some(ChannelTexture::from_view(
231 view,
232 sampler,
233 self.bg_state.bg_image_width,
234 self.bg_state.bg_image_height,
235 ))
236 }
237
238 pub fn has_background_image(&self) -> bool {
240 self.bg_state.bg_image_texture.is_some()
241 }
242
243 pub fn is_solid_color_background(&self) -> bool {
245 self.bg_state.bg_is_solid_color
246 }
247
248 pub fn solid_background_color(&self) -> [f32; 3] {
251 self.bg_state.solid_bg_color
252 }
253
254 pub fn get_solid_color_as_clear(&self) -> Option<wgpu::Color> {
257 if self.bg_state.bg_is_solid_color {
258 Some(wgpu::Color {
259 r: self.bg_state.solid_bg_color[0] as f64 * self.window_opacity as f64,
260 g: self.bg_state.solid_bg_color[1] as f64 * self.window_opacity as f64,
261 b: self.bg_state.solid_bg_color[2] as f64 * self.window_opacity as f64,
262 a: self.window_opacity as f64,
263 })
264 } else {
265 None
266 }
267 }
268
269 pub fn create_solid_color_texture(&mut self, color: [u8; 3]) {
275 let norm = color_u8_to_f32(color);
276 log::info!(
277 "[BACKGROUND] create_solid_color_texture: RGB({}, {}, {}) -> normalized ({:.3}, {:.3}, {:.3})",
278 color[0],
279 color[1],
280 color[2],
281 norm[0],
282 norm[1],
283 norm[2]
284 );
285 let size = 4u32; let mut pixels = Vec::with_capacity((size * size * 4) as usize);
287 for _ in 0..(size * size) {
288 pixels.push(color[0]);
289 pixels.push(color[1]);
290 pixels.push(color[2]);
291 pixels.push(255); }
293
294 let texture = self.device.create_texture(&wgpu::TextureDescriptor {
295 label: Some("bg solid color"),
296 size: wgpu::Extent3d {
297 width: size,
298 height: size,
299 depth_or_array_layers: 1,
300 },
301 mip_level_count: 1,
302 sample_count: 1,
303 dimension: wgpu::TextureDimension::D2,
304 format: wgpu::TextureFormat::Rgba8UnormSrgb,
305 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
306 view_formats: &[],
307 });
308
309 self.queue.write_texture(
310 wgpu::TexelCopyTextureInfo {
311 texture: &texture,
312 mip_level: 0,
313 origin: wgpu::Origin3d::ZERO,
314 aspect: wgpu::TextureAspect::All,
315 },
316 &pixels,
317 wgpu::TexelCopyBufferLayout {
318 offset: 0,
319 bytes_per_row: Some(4 * size),
320 rows_per_image: Some(size),
321 },
322 wgpu::Extent3d {
323 width: size,
324 height: size,
325 depth_or_array_layers: 1,
326 },
327 );
328
329 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
330 let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
331 mag_filter: wgpu::FilterMode::Linear,
332 min_filter: wgpu::FilterMode::Linear,
333 ..Default::default()
334 });
335
336 self.pipelines.bg_image_bind_group =
337 Some(self.device.create_bind_group(&wgpu::BindGroupDescriptor {
338 label: Some("bg solid color bind group"),
339 layout: &self.pipelines.bg_image_bind_group_layout,
340 entries: &[
341 wgpu::BindGroupEntry {
342 binding: 0,
343 resource: wgpu::BindingResource::TextureView(&view),
344 },
345 wgpu::BindGroupEntry {
346 binding: 1,
347 resource: wgpu::BindingResource::Sampler(&sampler),
348 },
349 wgpu::BindGroupEntry {
350 binding: 2,
351 resource: self.buffers.bg_image_uniform_buffer.as_entire_binding(),
352 },
353 ],
354 }));
355
356 self.bg_state.bg_image_texture = Some(texture);
357 self.bg_state.bg_image_width = size;
358 self.bg_state.bg_image_height = size;
359 self.bg_state.bg_image_mode = par_term_config::BackgroundImageMode::Stretch;
361 self.bg_state.bg_image_opacity = 1.0;
363 self.bg_state.bg_is_solid_color = true;
365 self.bg_state.solid_bg_color = color_u8_to_f32(color);
366 self.update_bg_image_uniforms(None);
367 }
368
369 pub fn get_solid_color_as_channel_texture(&self, color: [u8; 3]) -> ChannelTexture {
375 log::info!(
376 "get_solid_color_as_channel_texture: RGB({},{},{})",
377 color[0],
378 color[1],
379 color[2]
380 );
381 let size = 4u32;
382 let mut pixels = Vec::with_capacity((size * size * 4) as usize);
383 for _ in 0..(size * size) {
384 pixels.push(color[0]);
385 pixels.push(color[1]);
386 pixels.push(color[2]);
387 pixels.push(255); }
389
390 let texture = self.device.create_texture(&wgpu::TextureDescriptor {
391 label: Some("solid color channel texture"),
392 size: wgpu::Extent3d {
393 width: size,
394 height: size,
395 depth_or_array_layers: 1,
396 },
397 mip_level_count: 1,
398 sample_count: 1,
399 dimension: wgpu::TextureDimension::D2,
400 format: wgpu::TextureFormat::Rgba8UnormSrgb,
401 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
402 view_formats: &[],
403 });
404
405 self.queue.write_texture(
406 wgpu::TexelCopyTextureInfo {
407 texture: &texture,
408 mip_level: 0,
409 origin: wgpu::Origin3d::ZERO,
410 aspect: wgpu::TextureAspect::All,
411 },
412 &pixels,
413 wgpu::TexelCopyBufferLayout {
414 offset: 0,
415 bytes_per_row: Some(4 * size),
416 rows_per_image: Some(size),
417 },
418 wgpu::Extent3d {
419 width: size,
420 height: size,
421 depth_or_array_layers: 1,
422 },
423 );
424
425 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
426 let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
427 mag_filter: wgpu::FilterMode::Linear,
428 min_filter: wgpu::FilterMode::Linear,
429 address_mode_u: wgpu::AddressMode::Repeat,
430 address_mode_v: wgpu::AddressMode::Repeat,
431 address_mode_w: wgpu::AddressMode::Repeat,
432 ..Default::default()
433 });
434
435 ChannelTexture::from_view_and_texture(view, sampler, size, size, texture)
436 }
437
438 pub fn set_background(
443 &mut self,
444 mode: par_term_config::BackgroundMode,
445 color: [u8; 3],
446 image_path: Option<&str>,
447 image_mode: par_term_config::BackgroundImageMode,
448 image_opacity: f32,
449 image_enabled: bool,
450 ) {
451 log::info!(
452 "[BACKGROUND] set_background: mode={:?}, color=RGB({}, {}, {}), image_path={:?}",
453 mode,
454 color[0],
455 color[1],
456 color[2],
457 image_path
458 );
459 match mode {
460 par_term_config::BackgroundMode::Default => {
461 let bg_u8: [u8; 3] = [
466 (self.background_color[0] * 255.0).round() as u8,
467 (self.background_color[1] * 255.0).round() as u8,
468 (self.background_color[2] * 255.0).round() as u8,
469 ];
470 self.create_solid_color_texture(bg_u8);
471 self.bg_state.bg_is_solid_color = false;
474 }
475 par_term_config::BackgroundMode::Color => {
476 self.create_solid_color_texture(color);
478 }
479 par_term_config::BackgroundMode::Image => {
480 if image_enabled {
481 self.set_background_image(image_path, image_mode, image_opacity);
483 } else {
484 self.bg_state.bg_image_texture = None;
486 self.pipelines.bg_image_bind_group = None;
487 self.bg_state.bg_image_width = 0;
488 self.bg_state.bg_image_height = 0;
489 self.bg_state.bg_is_solid_color = false;
490 }
491 }
492 }
493 }
494
495 pub(crate) fn load_pane_background(&mut self, path: &str) -> Result<bool, RenderError> {
498 if self.bg_state.pane_bg_cache.contains_key(path) {
499 return Ok(false);
500 }
501
502 let expanded = if let Some(rest) = path.strip_prefix("~/") {
504 if let Some(home) = dirs::home_dir() {
505 home.join(rest).to_string_lossy().to_string()
506 } else {
507 path.to_string()
508 }
509 } else {
510 path.to_string()
511 };
512
513 log::info!("Loading per-pane background image: {}", expanded);
514 let img = image::open(&expanded)
515 .map_err(|e| {
516 log::error!("Failed to open pane background image '{}': {}", path, e);
517 RenderError::ImageLoad {
518 path: expanded.clone(),
519 source: e,
520 }
521 })?
522 .to_rgba8();
523
524 let (width, height) = img.dimensions();
525 let texture = self.device.create_texture(&wgpu::TextureDescriptor {
526 label: Some("pane bg image"),
527 size: wgpu::Extent3d {
528 width,
529 height,
530 depth_or_array_layers: 1,
531 },
532 mip_level_count: 1,
533 sample_count: 1,
534 dimension: wgpu::TextureDimension::D2,
535 format: wgpu::TextureFormat::Rgba8UnormSrgb,
536 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
537 view_formats: &[],
538 });
539
540 self.queue.write_texture(
541 wgpu::TexelCopyTextureInfo {
542 texture: &texture,
543 mip_level: 0,
544 origin: wgpu::Origin3d::ZERO,
545 aspect: wgpu::TextureAspect::All,
546 },
547 &img,
548 wgpu::TexelCopyBufferLayout {
549 offset: 0,
550 bytes_per_row: Some(4 * width),
551 rows_per_image: Some(height),
552 },
553 wgpu::Extent3d {
554 width,
555 height,
556 depth_or_array_layers: 1,
557 },
558 );
559
560 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
561 let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
562 mag_filter: wgpu::FilterMode::Linear,
563 min_filter: wgpu::FilterMode::Linear,
564 ..Default::default()
565 });
566
567 self.bg_state.pane_bg_cache.insert(
568 path.to_string(),
569 super::background::PaneBackgroundEntry {
570 texture,
571 view,
572 sampler,
573 width,
574 height,
575 },
576 );
577
578 Ok(true)
579 }
580
581 pub(crate) fn prepare_pane_bg_bind_group(&mut self, path: &str, p: PaneBgBindGroupParams) {
592 let PaneBgBindGroupParams {
593 pane_x,
594 pane_y,
595 pane_width,
596 pane_height,
597 mode,
598 opacity,
599 darken,
600 } = p;
601 let entry = match self.bg_state.pane_bg_cache.get(path) {
603 Some(e) => e,
604 None => return,
605 };
606
607 let mut data = [0u8; 48];
616 data[0..4].copy_from_slice(&(entry.width as f32).to_le_bytes());
618 data[4..8].copy_from_slice(&(entry.height as f32).to_le_bytes());
619 data[8..12].copy_from_slice(&pane_width.to_le_bytes());
621 data[12..16].copy_from_slice(&pane_height.to_le_bytes());
622 data[16..20].copy_from_slice(&(mode as u32).to_le_bytes());
624 let effective_opacity = opacity * self.window_opacity;
626 data[20..24].copy_from_slice(&effective_opacity.to_le_bytes());
627 data[24..28].copy_from_slice(&pane_x.to_le_bytes());
629 data[28..32].copy_from_slice(&pane_y.to_le_bytes());
630 let surface_w = self.config.width as f32;
632 let surface_h = self.config.height as f32;
633 data[32..36].copy_from_slice(&surface_w.to_le_bytes());
634 data[36..40].copy_from_slice(&surface_h.to_le_bytes());
635 data[40..44].copy_from_slice(&darken.to_le_bytes());
637
638 if self.bg_state.pane_bg_uniform_cache.contains_key(path) {
639 let cached = self
641 .bg_state
642 .pane_bg_uniform_cache
643 .get(path)
644 .expect("uniform cache entry must exist after contains_key check");
645 self.queue.write_buffer(&cached.uniform_buffer, 0, &data);
646 } else {
647 let uniform_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
649 label: Some("pane bg uniform buffer"),
650 size: 48,
651 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
652 mapped_at_creation: false,
653 });
654 self.queue.write_buffer(&uniform_buffer, 0, &data);
655
656 let entry = self
658 .bg_state
659 .pane_bg_cache
660 .get(path)
661 .expect("pane_bg_cache entry must exist — checked at top of function");
662
663 let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
664 label: Some("pane bg bind group"),
665 layout: &self.pipelines.bg_image_bind_group_layout,
666 entries: &[
667 wgpu::BindGroupEntry {
668 binding: 0,
669 resource: wgpu::BindingResource::TextureView(&entry.view),
670 },
671 wgpu::BindGroupEntry {
672 binding: 1,
673 resource: wgpu::BindingResource::Sampler(&entry.sampler),
674 },
675 wgpu::BindGroupEntry {
676 binding: 2,
677 resource: uniform_buffer.as_entire_binding(),
678 },
679 ],
680 });
681
682 self.bg_state.pane_bg_uniform_cache.insert(
683 path.to_string(),
684 super::background::PaneBgUniformEntry {
685 uniform_buffer,
686 bind_group,
687 },
688 );
689 }
690 }
691
692 pub fn evict_pane_bg_uniform_cache(&mut self) {
697 self.bg_state
698 .pane_bg_uniform_cache
699 .retain(|path, _| self.bg_state.pane_bg_cache.contains_key(path.as_str()));
700 }
701}