par_term_render/renderer/graphics.rs
1use super::Renderer;
2use anyhow::Result;
3
4impl Renderer {
5 /// Update graphics textures (Sixel, iTerm2, Kitty)
6 ///
7 /// # Arguments
8 /// * `graphics` - Graphics from the terminal with RGBA data
9 /// * `view_scroll_offset` - Current view scroll offset (0 = viewing current content)
10 /// * `scrollback_len` - Total lines in scrollback buffer
11 /// * `visible_rows` - Number of visible rows in terminal
12 #[allow(dead_code)]
13 pub fn update_graphics(
14 &mut self,
15 graphics: &[par_term_emu_core_rust::graphics::TerminalGraphic],
16 view_scroll_offset: usize,
17 scrollback_len: usize,
18 visible_rows: usize,
19 ) -> Result<()> {
20 // Clear old graphics list
21 self.sixel_graphics.clear();
22
23 // Calculate the view window in absolute terms
24 // total_lines = scrollback_len + visible_rows
25 // When scroll_offset = 0, we view lines [scrollback_len, scrollback_len + visible_rows)
26 // When scroll_offset > 0, we view earlier lines
27 let total_lines = scrollback_len + visible_rows;
28 let view_end = total_lines.saturating_sub(view_scroll_offset);
29 let view_start = view_end.saturating_sub(visible_rows);
30
31 // Process each graphic
32 for graphic in graphics {
33 // Use the unique ID from the graphic (stable across position changes)
34 let id = graphic.id;
35 let (col, row) = graphic.position;
36
37 // Calculate screen row based on whether this is a scrollback graphic or current
38 let screen_row: isize = if let Some(sb_row) = graphic.scrollback_row {
39 // Scrollback graphic: sb_row is absolute index in scrollback
40 // Screen row = sb_row - view_start
41 sb_row as isize - view_start as isize
42 } else {
43 // Current graphic: position is relative to visible area
44 // Absolute position = scrollback_len + row - scroll_offset_rows
45 // This keeps the graphic at its original absolute position as scrollback grows
46 let absolute_row = scrollback_len.saturating_sub(graphic.scroll_offset_rows) + row;
47
48 log::trace!(
49 "[RENDERER] CALC: scrollback_len={}, row={}, scroll_offset_rows={}, absolute_row={}, view_start={}, screen_row={}",
50 scrollback_len,
51 row,
52 graphic.scroll_offset_rows,
53 absolute_row,
54 view_start,
55 absolute_row as isize - view_start as isize
56 );
57
58 absolute_row as isize - view_start as isize
59 };
60
61 log::debug!(
62 "[RENDERER] Graphics update: id={}, protocol={:?}, pos=({},{}), screen_row={}, scrollback_row={:?}, scroll_offset_rows={}, size={}x{}, view=[{},{})",
63 id,
64 graphic.protocol,
65 col,
66 row,
67 screen_row,
68 graphic.scrollback_row,
69 graphic.scroll_offset_rows,
70 graphic.width,
71 graphic.height,
72 view_start,
73 view_end
74 );
75
76 // Create or update texture in cache
77 self.graphics_renderer.get_or_create_texture(
78 self.cell_renderer.device(),
79 self.cell_renderer.queue(),
80 id,
81 &graphic.pixels, // RGBA pixel data (Arc<Vec<u8>>)
82 graphic.width as u32,
83 graphic.height as u32,
84 )?;
85
86 // Add to render list with position and dimensions
87 // Calculate size in cells (rounding up to cover all affected cells)
88 let width_cells =
89 ((graphic.width as f32 / self.cell_renderer.cell_width()).ceil() as usize).max(1);
90 let height_cells =
91 ((graphic.height as f32 / self.cell_renderer.cell_height()).ceil() as usize).max(1);
92
93 // Calculate effective clip rows based on screen position
94 // If screen_row < 0, we need to clip that many rows from the top
95 // If screen_row >= 0, no clipping needed (we can see the full graphic)
96 let effective_clip_rows = if screen_row < 0 {
97 (-screen_row) as usize
98 } else {
99 0
100 };
101
102 self.sixel_graphics.push((
103 id,
104 screen_row, // row position (can be negative if scrolled off top)
105 col, // col position
106 width_cells,
107 height_cells,
108 1.0, // Full opacity by default
109 effective_clip_rows, // Rows to clip from top for partial rendering
110 ));
111 }
112
113 if !graphics.is_empty() {
114 self.dirty = true; // Mark dirty when graphics change
115 }
116
117 Ok(())
118 }
119
120 /// Render sixel graphics on top of terminal cells
121 pub(crate) fn render_sixel_graphics(
122 &mut self,
123 surface_texture: &wgpu::SurfaceTexture,
124 ) -> Result<()> {
125 use wgpu::TextureViewDescriptor;
126
127 // Create view of the surface texture
128 let view = surface_texture
129 .texture
130 .create_view(&TextureViewDescriptor::default());
131
132 // Create command encoder for sixel rendering
133 let mut encoder =
134 self.cell_renderer
135 .device()
136 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
137 label: Some("sixel encoder"),
138 });
139
140 // Create render pass
141 {
142 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
143 label: Some("sixel render pass"),
144 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
145 view: &view,
146 resolve_target: None,
147 ops: wgpu::Operations {
148 load: wgpu::LoadOp::Load, // Don't clear - render on top of terminal
149 store: wgpu::StoreOp::Store,
150 },
151 depth_slice: None,
152 })],
153 depth_stencil_attachment: None,
154 timestamp_writes: None,
155 occlusion_query_set: None,
156 });
157
158 // Render all sixel graphics
159 self.graphics_renderer.render(
160 self.cell_renderer.device(),
161 self.cell_renderer.queue(),
162 &mut render_pass,
163 &self.sixel_graphics,
164 self.size.width as f32,
165 self.size.height as f32,
166 )?;
167 } // render_pass dropped here
168
169 // Submit sixel commands
170 self.cell_renderer
171 .queue()
172 .submit(std::iter::once(encoder.finish()));
173
174 Ok(())
175 }
176
177 /// Clear all cached sixel textures
178 #[allow(dead_code)]
179 pub fn clear_sixel_cache(&mut self) {
180 self.graphics_renderer.clear_cache();
181 self.sixel_graphics.clear();
182 self.dirty = true;
183 }
184
185 /// Get the number of cached sixel textures
186 #[allow(dead_code)]
187 pub fn sixel_cache_size(&self) -> usize {
188 self.graphics_renderer.cache_size()
189 }
190
191 /// Remove a specific sixel texture from cache
192 #[allow(dead_code)]
193 pub fn remove_sixel_texture(&mut self, id: u64) {
194 self.graphics_renderer.remove_texture(id);
195 self.sixel_graphics
196 .retain(|(gid, _, _, _, _, _, _)| *gid != id);
197 self.dirty = true;
198 }
199}