1use super::CellRenderer;
10use crate::custom_shader_renderer::textures::ChannelTexture;
11use crate::error::RenderError;
12use par_term_config::color_u8_to_f32;
13use std::collections::HashMap;
14
15pub(crate) struct PaneBgBindGroupParams {
17 pub pane_x: f32,
18 pub pane_y: f32,
19 pub pane_width: f32,
20 pub pane_height: f32,
21 pub mode: par_term_config::BackgroundImageMode,
22 pub opacity: f32,
23 pub darken: f32,
24}
25
26pub(crate) struct PaneBackgroundEntry {
28 #[allow(dead_code)] pub(crate) texture: wgpu::Texture,
30 pub(crate) view: wgpu::TextureView,
31 pub(crate) sampler: wgpu::Sampler,
32 pub(crate) width: u32,
33 pub(crate) height: u32,
34}
35
36pub(crate) struct PaneBgUniformEntry {
42 pub(crate) path: String,
44 pub(crate) uniform_buffer: wgpu::Buffer,
45 pub(crate) bind_group: wgpu::BindGroup,
46}
47
48pub(crate) struct BackgroundImageState {
50 pub(crate) bg_image_texture: Option<wgpu::Texture>,
51 pub(crate) bg_image_mode: par_term_config::BackgroundImageMode,
52 pub(crate) bg_image_opacity: f32,
53 pub(crate) bg_image_width: u32,
54 pub(crate) bg_image_height: u32,
55 pub(crate) bg_is_solid_color: bool,
59 pub(crate) solid_bg_color: [f32; 3],
62 pub(crate) pane_bg_cache: HashMap<String, PaneBackgroundEntry>,
64 pub(crate) pane_bg_uniform_cache: HashMap<usize, PaneBgUniformEntry>,
73}
74
75impl CellRenderer {
76 pub(crate) fn load_background_image(&mut self, path: &str) -> Result<(), RenderError> {
77 log::info!("Loading background image from: {}", path);
78 let img = image::open(path)
79 .map_err(|e| {
80 log::error!("Failed to open background image '{}': {}", path, e);
81 RenderError::ImageLoad {
82 path: path.to_string(),
83 source: e,
84 }
85 })?
86 .to_rgba8();
87 log::info!("Background image loaded: {}x{}", img.width(), img.height());
88 let (width, height) = img.dimensions();
89 let texture = self.device.create_texture(&wgpu::TextureDescriptor {
90 label: Some("bg image"),
91 size: wgpu::Extent3d {
92 width,
93 height,
94 depth_or_array_layers: 1,
95 },
96 mip_level_count: 1,
97 sample_count: 1,
98 dimension: wgpu::TextureDimension::D2,
99 format: wgpu::TextureFormat::Rgba8UnormSrgb,
100 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
101 view_formats: &[],
102 });
103 self.queue.write_texture(
104 wgpu::TexelCopyTextureInfo {
105 texture: &texture,
106 mip_level: 0,
107 origin: wgpu::Origin3d::ZERO,
108 aspect: wgpu::TextureAspect::All,
109 },
110 &img,
111 wgpu::TexelCopyBufferLayout {
112 offset: 0,
113 bytes_per_row: Some(4 * width),
114 rows_per_image: Some(height),
115 },
116 wgpu::Extent3d {
117 width,
118 height,
119 depth_or_array_layers: 1,
120 },
121 );
122
123 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
124 let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
125 mag_filter: wgpu::FilterMode::Linear,
126 min_filter: wgpu::FilterMode::Linear,
127 ..Default::default()
128 });
129
130 self.pipelines.bg_image_bind_group =
131 Some(self.device.create_bind_group(&wgpu::BindGroupDescriptor {
132 label: Some("bg image bind group"),
133 layout: &self.pipelines.bg_image_bind_group_layout,
134 entries: &[
135 wgpu::BindGroupEntry {
136 binding: 0,
137 resource: wgpu::BindingResource::TextureView(&view),
138 },
139 wgpu::BindGroupEntry {
140 binding: 1,
141 resource: wgpu::BindingResource::Sampler(&sampler),
142 },
143 wgpu::BindGroupEntry {
144 binding: 2,
145 resource: self.buffers.bg_image_uniform_buffer.as_entire_binding(),
146 },
147 ],
148 }));
149 self.bg_state.bg_image_texture = Some(texture);
150 self.bg_state.bg_image_width = width;
151 self.bg_state.bg_image_height = height;
152 self.bg_state.bg_is_solid_color = false; self.update_bg_image_uniforms(None);
154 Ok(())
155 }
156
157 pub(crate) fn update_bg_image_uniforms(&mut self, window_opacity_override: Option<f32>) {
165 let mut data = [0u8; 48];
174
175 let w = self.config.width as f32;
176 let h = self.config.height as f32;
177
178 data[0..4].copy_from_slice(&(self.bg_state.bg_image_width as f32).to_le_bytes());
180 data[4..8].copy_from_slice(&(self.bg_state.bg_image_height as f32).to_le_bytes());
181
182 data[8..12].copy_from_slice(&w.to_le_bytes());
184 data[12..16].copy_from_slice(&h.to_le_bytes());
185
186 data[16..20].copy_from_slice(&(self.bg_state.bg_image_mode as u32).to_le_bytes());
188
189 let win_opacity = window_opacity_override.unwrap_or(self.window_opacity);
191 let effective_opacity = self.bg_state.bg_image_opacity * win_opacity;
192 data[20..24].copy_from_slice(&effective_opacity.to_le_bytes());
193
194 data[32..36].copy_from_slice(&w.to_le_bytes());
199 data[36..40].copy_from_slice(&h.to_le_bytes());
200
201 self.queue
205 .write_buffer(&self.buffers.bg_image_uniform_buffer, 0, &data);
206 }
207
208 pub fn set_background_image(
209 &mut self,
210 path: Option<&str>,
211 mode: par_term_config::BackgroundImageMode,
212 opacity: f32,
213 ) {
214 self.bg_state.bg_image_mode = mode;
215 self.bg_state.bg_image_opacity = opacity;
216 if let Some(p) = path {
217 log::info!("Loading background image: {}", p);
218 if let Err(e) = self.load_background_image(p) {
219 log::error!("Failed to load background image '{}': {}", p, e);
220 }
221 } else {
223 self.bg_state.bg_image_texture = None;
224 self.pipelines.bg_image_bind_group = None;
225 self.bg_state.bg_image_width = 0;
226 self.bg_state.bg_image_height = 0;
227 self.bg_state.bg_is_solid_color = false;
228 }
229 self.update_bg_image_uniforms(None);
230 }
231
232 pub fn update_background_image_opacity(&mut self, opacity: f32) {
233 self.bg_state.bg_image_opacity = opacity;
234 self.update_bg_image_uniforms(None);
235 }
236
237 pub fn update_background_image_opacity_only(&mut self, opacity: f32) {
238 self.bg_state.bg_image_opacity = opacity;
239 self.update_bg_image_uniforms(None);
240 }
241
242 pub fn get_background_as_channel_texture(&self) -> Option<ChannelTexture> {
248 let texture = self.bg_state.bg_image_texture.as_ref()?;
249
250 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
252 let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
253 mag_filter: wgpu::FilterMode::Linear,
254 min_filter: wgpu::FilterMode::Linear,
255 address_mode_u: wgpu::AddressMode::Repeat,
256 address_mode_v: wgpu::AddressMode::Repeat,
257 address_mode_w: wgpu::AddressMode::Repeat,
258 ..Default::default()
259 });
260
261 Some(ChannelTexture::from_view(
262 view,
263 sampler,
264 self.bg_state.bg_image_width,
265 self.bg_state.bg_image_height,
266 ))
267 }
268
269 pub fn has_background_image(&self) -> bool {
271 self.bg_state.bg_image_texture.is_some()
272 }
273
274 pub fn is_solid_color_background(&self) -> bool {
276 self.bg_state.bg_is_solid_color
277 }
278
279 pub fn solid_background_color(&self) -> [f32; 3] {
282 self.bg_state.solid_bg_color
283 }
284
285 pub fn get_solid_color_as_clear(&self) -> Option<wgpu::Color> {
288 if self.bg_state.bg_is_solid_color {
289 Some(wgpu::Color {
290 r: self.bg_state.solid_bg_color[0] as f64 * self.window_opacity as f64,
291 g: self.bg_state.solid_bg_color[1] as f64 * self.window_opacity as f64,
292 b: self.bg_state.solid_bg_color[2] as f64 * self.window_opacity as f64,
293 a: self.window_opacity as f64,
294 })
295 } else {
296 None
297 }
298 }
299
300 pub fn create_solid_color_texture(&mut self, color: [u8; 3]) {
306 let norm = color_u8_to_f32(color);
307 log::info!(
308 "[BACKGROUND] create_solid_color_texture: RGB({}, {}, {}) -> normalized ({:.3}, {:.3}, {:.3})",
309 color[0],
310 color[1],
311 color[2],
312 norm[0],
313 norm[1],
314 norm[2]
315 );
316 let size = 4u32; let mut pixels = Vec::with_capacity((size * size * 4) as usize);
318 for _ in 0..(size * size) {
319 pixels.push(color[0]);
320 pixels.push(color[1]);
321 pixels.push(color[2]);
322 pixels.push(255); }
324
325 let texture = self.device.create_texture(&wgpu::TextureDescriptor {
326 label: Some("bg solid color"),
327 size: wgpu::Extent3d {
328 width: size,
329 height: size,
330 depth_or_array_layers: 1,
331 },
332 mip_level_count: 1,
333 sample_count: 1,
334 dimension: wgpu::TextureDimension::D2,
335 format: wgpu::TextureFormat::Rgba8UnormSrgb,
336 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
337 view_formats: &[],
338 });
339
340 self.queue.write_texture(
341 wgpu::TexelCopyTextureInfo {
342 texture: &texture,
343 mip_level: 0,
344 origin: wgpu::Origin3d::ZERO,
345 aspect: wgpu::TextureAspect::All,
346 },
347 &pixels,
348 wgpu::TexelCopyBufferLayout {
349 offset: 0,
350 bytes_per_row: Some(4 * size),
351 rows_per_image: Some(size),
352 },
353 wgpu::Extent3d {
354 width: size,
355 height: size,
356 depth_or_array_layers: 1,
357 },
358 );
359
360 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
361 let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
362 mag_filter: wgpu::FilterMode::Linear,
363 min_filter: wgpu::FilterMode::Linear,
364 ..Default::default()
365 });
366
367 self.pipelines.bg_image_bind_group =
368 Some(self.device.create_bind_group(&wgpu::BindGroupDescriptor {
369 label: Some("bg solid color bind group"),
370 layout: &self.pipelines.bg_image_bind_group_layout,
371 entries: &[
372 wgpu::BindGroupEntry {
373 binding: 0,
374 resource: wgpu::BindingResource::TextureView(&view),
375 },
376 wgpu::BindGroupEntry {
377 binding: 1,
378 resource: wgpu::BindingResource::Sampler(&sampler),
379 },
380 wgpu::BindGroupEntry {
381 binding: 2,
382 resource: self.buffers.bg_image_uniform_buffer.as_entire_binding(),
383 },
384 ],
385 }));
386
387 self.bg_state.bg_image_texture = Some(texture);
388 self.bg_state.bg_image_width = size;
389 self.bg_state.bg_image_height = size;
390 self.bg_state.bg_image_mode = par_term_config::BackgroundImageMode::Stretch;
392 self.bg_state.bg_image_opacity = 1.0;
394 self.bg_state.bg_is_solid_color = true;
396 self.bg_state.solid_bg_color = color_u8_to_f32(color);
397 self.update_bg_image_uniforms(None);
398 }
399
400 pub fn get_solid_color_as_channel_texture(&self, color: [u8; 3]) -> ChannelTexture {
406 log::info!(
407 "get_solid_color_as_channel_texture: RGB({},{},{})",
408 color[0],
409 color[1],
410 color[2]
411 );
412 let size = 4u32;
413 let mut pixels = Vec::with_capacity((size * size * 4) as usize);
414 for _ in 0..(size * size) {
415 pixels.push(color[0]);
416 pixels.push(color[1]);
417 pixels.push(color[2]);
418 pixels.push(255); }
420
421 let texture = self.device.create_texture(&wgpu::TextureDescriptor {
422 label: Some("solid color channel texture"),
423 size: wgpu::Extent3d {
424 width: size,
425 height: size,
426 depth_or_array_layers: 1,
427 },
428 mip_level_count: 1,
429 sample_count: 1,
430 dimension: wgpu::TextureDimension::D2,
431 format: wgpu::TextureFormat::Rgba8UnormSrgb,
432 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
433 view_formats: &[],
434 });
435
436 self.queue.write_texture(
437 wgpu::TexelCopyTextureInfo {
438 texture: &texture,
439 mip_level: 0,
440 origin: wgpu::Origin3d::ZERO,
441 aspect: wgpu::TextureAspect::All,
442 },
443 &pixels,
444 wgpu::TexelCopyBufferLayout {
445 offset: 0,
446 bytes_per_row: Some(4 * size),
447 rows_per_image: Some(size),
448 },
449 wgpu::Extent3d {
450 width: size,
451 height: size,
452 depth_or_array_layers: 1,
453 },
454 );
455
456 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
457 let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
458 mag_filter: wgpu::FilterMode::Linear,
459 min_filter: wgpu::FilterMode::Linear,
460 address_mode_u: wgpu::AddressMode::Repeat,
461 address_mode_v: wgpu::AddressMode::Repeat,
462 address_mode_w: wgpu::AddressMode::Repeat,
463 ..Default::default()
464 });
465
466 ChannelTexture::from_view_and_texture(view, sampler, size, size, texture)
467 }
468
469 pub fn set_background(
474 &mut self,
475 mode: par_term_config::BackgroundMode,
476 color: [u8; 3],
477 image_path: Option<&str>,
478 image_mode: par_term_config::BackgroundImageMode,
479 image_opacity: f32,
480 image_enabled: bool,
481 ) {
482 log::info!(
483 "[BACKGROUND] set_background: mode={:?}, color=RGB({}, {}, {}), image_path={:?}",
484 mode,
485 color[0],
486 color[1],
487 color[2],
488 image_path
489 );
490 match mode {
491 par_term_config::BackgroundMode::Default => {
492 let bg_u8: [u8; 3] = [
497 (self.background_color[0] * 255.0).round() as u8,
498 (self.background_color[1] * 255.0).round() as u8,
499 (self.background_color[2] * 255.0).round() as u8,
500 ];
501 self.create_solid_color_texture(bg_u8);
502 self.bg_state.bg_is_solid_color = false;
505 }
506 par_term_config::BackgroundMode::Color => {
507 self.create_solid_color_texture(color);
509 }
510 par_term_config::BackgroundMode::Image => {
511 if image_enabled {
512 self.set_background_image(image_path, image_mode, image_opacity);
514 } else {
515 self.bg_state.bg_image_texture = None;
517 self.pipelines.bg_image_bind_group = None;
518 self.bg_state.bg_image_width = 0;
519 self.bg_state.bg_image_height = 0;
520 self.bg_state.bg_is_solid_color = false;
521 }
522 }
523 }
524 }
525
526 pub(crate) fn load_pane_background(&mut self, path: &str) -> Result<bool, RenderError> {
529 if self.bg_state.pane_bg_cache.contains_key(path) {
530 return Ok(false);
531 }
532
533 let expanded = if let Some(rest) = path.strip_prefix("~/") {
535 if let Some(home) = dirs::home_dir() {
536 home.join(rest).to_string_lossy().to_string()
537 } else {
538 path.to_string()
539 }
540 } else {
541 path.to_string()
542 };
543
544 log::info!("Loading per-pane background image: {}", expanded);
545 let img = image::open(&expanded)
546 .map_err(|e| {
547 log::error!("Failed to open pane background image '{}': {}", path, e);
548 RenderError::ImageLoad {
549 path: expanded.clone(),
550 source: e,
551 }
552 })?
553 .to_rgba8();
554
555 let (width, height) = img.dimensions();
556 let texture = self.device.create_texture(&wgpu::TextureDescriptor {
557 label: Some("pane bg image"),
558 size: wgpu::Extent3d {
559 width,
560 height,
561 depth_or_array_layers: 1,
562 },
563 mip_level_count: 1,
564 sample_count: 1,
565 dimension: wgpu::TextureDimension::D2,
566 format: wgpu::TextureFormat::Rgba8UnormSrgb,
567 usage: wgpu::TextureUsages::TEXTURE_BINDING | wgpu::TextureUsages::COPY_DST,
568 view_formats: &[],
569 });
570
571 self.queue.write_texture(
572 wgpu::TexelCopyTextureInfo {
573 texture: &texture,
574 mip_level: 0,
575 origin: wgpu::Origin3d::ZERO,
576 aspect: wgpu::TextureAspect::All,
577 },
578 &img,
579 wgpu::TexelCopyBufferLayout {
580 offset: 0,
581 bytes_per_row: Some(4 * width),
582 rows_per_image: Some(height),
583 },
584 wgpu::Extent3d {
585 width,
586 height,
587 depth_or_array_layers: 1,
588 },
589 );
590
591 let view = texture.create_view(&wgpu::TextureViewDescriptor::default());
592 let sampler = self.device.create_sampler(&wgpu::SamplerDescriptor {
593 mag_filter: wgpu::FilterMode::Linear,
594 min_filter: wgpu::FilterMode::Linear,
595 ..Default::default()
596 });
597
598 self.bg_state.pane_bg_cache.insert(
599 path.to_string(),
600 super::background::PaneBackgroundEntry {
601 texture,
602 view,
603 sampler,
604 width,
605 height,
606 },
607 );
608
609 Ok(true)
610 }
611
612 pub(crate) fn prepare_pane_bg_bind_group(
625 &mut self,
626 pane_index: usize,
627 path: &str,
628 p: PaneBgBindGroupParams,
629 ) {
630 let PaneBgBindGroupParams {
631 pane_x,
632 pane_y,
633 pane_width,
634 pane_height,
635 mode,
636 opacity,
637 darken,
638 } = p;
639 let entry = match self.bg_state.pane_bg_cache.get(path) {
641 Some(e) => e,
642 None => return,
643 };
644
645 let mut data = [0u8; 48];
654 data[0..4].copy_from_slice(&(entry.width as f32).to_le_bytes());
656 data[4..8].copy_from_slice(&(entry.height as f32).to_le_bytes());
657 data[8..12].copy_from_slice(&pane_width.to_le_bytes());
659 data[12..16].copy_from_slice(&pane_height.to_le_bytes());
660 data[16..20].copy_from_slice(&(mode as u32).to_le_bytes());
662 let effective_opacity = opacity * self.window_opacity;
664 data[20..24].copy_from_slice(&effective_opacity.to_le_bytes());
665 data[24..28].copy_from_slice(&pane_x.to_le_bytes());
667 data[28..32].copy_from_slice(&pane_y.to_le_bytes());
668 let surface_w = self.config.width as f32;
670 let surface_h = self.config.height as f32;
671 data[32..36].copy_from_slice(&surface_w.to_le_bytes());
672 data[36..40].copy_from_slice(&surface_h.to_le_bytes());
673 data[40..44].copy_from_slice(&darken.to_le_bytes());
675
676 let reusable = self
677 .bg_state
678 .pane_bg_uniform_cache
679 .get(&pane_index)
680 .is_some_and(|cached| cached.path == path);
681 if reusable {
682 let cached = self
684 .bg_state
685 .pane_bg_uniform_cache
686 .get(&pane_index)
687 .expect("uniform cache entry must exist after the reuse check");
688 self.queue.write_buffer(&cached.uniform_buffer, 0, &data);
689 } else {
690 let uniform_buffer = self.device.create_buffer(&wgpu::BufferDescriptor {
692 label: Some("pane bg uniform buffer"),
693 size: 48,
694 usage: wgpu::BufferUsages::UNIFORM | wgpu::BufferUsages::COPY_DST,
695 mapped_at_creation: false,
696 });
697 self.queue.write_buffer(&uniform_buffer, 0, &data);
698
699 let entry = self
701 .bg_state
702 .pane_bg_cache
703 .get(path)
704 .expect("pane_bg_cache entry must exist — checked at top of function");
705
706 let bind_group = self.device.create_bind_group(&wgpu::BindGroupDescriptor {
707 label: Some("pane bg bind group"),
708 layout: &self.pipelines.bg_image_bind_group_layout,
709 entries: &[
710 wgpu::BindGroupEntry {
711 binding: 0,
712 resource: wgpu::BindingResource::TextureView(&entry.view),
713 },
714 wgpu::BindGroupEntry {
715 binding: 1,
716 resource: wgpu::BindingResource::Sampler(&entry.sampler),
717 },
718 wgpu::BindGroupEntry {
719 binding: 2,
720 resource: uniform_buffer.as_entire_binding(),
721 },
722 ],
723 });
724
725 self.bg_state.pane_bg_uniform_cache.insert(
726 pane_index,
727 super::background::PaneBgUniformEntry {
728 path: path.to_string(),
729 uniform_buffer,
730 bind_group,
731 },
732 );
733 }
734 }
735
736 pub fn evict_pane_bg_uniform_cache(&mut self) {
741 let textures = &self.bg_state.pane_bg_cache;
742 self.bg_state
743 .pane_bg_uniform_cache
744 .retain(|_, entry| textures.contains_key(entry.path.as_str()));
745 }
746}