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