par_term/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 debug_trace!(
49 "RENDERER",
50 "CALC: scrollback_len={}, row={}, scroll_offset_rows={}, absolute_row={}, view_start={}, screen_row={}",
51 scrollback_len,
52 row,
53 graphic.scroll_offset_rows,
54 absolute_row,
55 view_start,
56 absolute_row as isize - view_start as isize
57 );
58
59 absolute_row as isize - view_start as isize
60 };
61
62 debug_log!(
63 "RENDERER",
64 "Graphics update: id={}, protocol={:?}, pos=({},{}), screen_row={}, scrollback_row={:?}, scroll_offset_rows={}, size={}x{}, view=[{},{})",
65 id,
66 graphic.protocol,
67 col,
68 row,
69 screen_row,
70 graphic.scrollback_row,
71 graphic.scroll_offset_rows,
72 graphic.width,
73 graphic.height,
74 view_start,
75 view_end
76 );
77
78 // Create or update texture in cache
79 self.graphics_renderer.get_or_create_texture(
80 self.cell_renderer.device(),
81 self.cell_renderer.queue(),
82 id,
83 &graphic.pixels, // RGBA pixel data (Arc<Vec<u8>>)
84 graphic.width as u32,
85 graphic.height as u32,
86 )?;
87
88 // Add to render list with position and dimensions
89 // Calculate size in cells (rounding up to cover all affected cells)
90 let width_cells =
91 ((graphic.width as f32 / self.cell_renderer.cell_width()).ceil() as usize).max(1);
92 let height_cells =
93 ((graphic.height as f32 / self.cell_renderer.cell_height()).ceil() as usize).max(1);
94
95 // Calculate effective clip rows based on screen position
96 // If screen_row < 0, we need to clip that many rows from the top
97 // If screen_row >= 0, no clipping needed (we can see the full graphic)
98 let effective_clip_rows = if screen_row < 0 {
99 (-screen_row) as usize
100 } else {
101 0
102 };
103
104 self.sixel_graphics.push((
105 id,
106 screen_row, // row position (can be negative if scrolled off top)
107 col, // col position
108 width_cells,
109 height_cells,
110 1.0, // Full opacity by default
111 effective_clip_rows, // Rows to clip from top for partial rendering
112 ));
113 }
114
115 if !graphics.is_empty() {
116 self.dirty = true; // Mark dirty when graphics change
117 }
118
119 Ok(())
120 }
121
122 /// Render sixel graphics on top of terminal cells
123 pub(crate) fn render_sixel_graphics(
124 &mut self,
125 surface_texture: &wgpu::SurfaceTexture,
126 ) -> Result<()> {
127 use wgpu::TextureViewDescriptor;
128
129 // Create view of the surface texture
130 let view = surface_texture
131 .texture
132 .create_view(&TextureViewDescriptor::default());
133
134 // Create command encoder for sixel rendering
135 let mut encoder =
136 self.cell_renderer
137 .device()
138 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
139 label: Some("sixel encoder"),
140 });
141
142 // Create render pass
143 {
144 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
145 label: Some("sixel render pass"),
146 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
147 view: &view,
148 resolve_target: None,
149 ops: wgpu::Operations {
150 load: wgpu::LoadOp::Load, // Don't clear - render on top of terminal
151 store: wgpu::StoreOp::Store,
152 },
153 depth_slice: None,
154 })],
155 depth_stencil_attachment: None,
156 timestamp_writes: None,
157 occlusion_query_set: None,
158 });
159
160 // Render all sixel graphics
161 self.graphics_renderer.render(
162 self.cell_renderer.device(),
163 self.cell_renderer.queue(),
164 &mut render_pass,
165 &self.sixel_graphics,
166 self.size.width as f32,
167 self.size.height as f32,
168 )?;
169 } // render_pass dropped here
170
171 // Submit sixel commands
172 self.cell_renderer
173 .queue()
174 .submit(std::iter::once(encoder.finish()));
175
176 Ok(())
177 }
178
179 /// Clear all cached sixel textures
180 #[allow(dead_code)]
181 pub fn clear_sixel_cache(&mut self) {
182 self.graphics_renderer.clear_cache();
183 self.sixel_graphics.clear();
184 self.dirty = true;
185 }
186
187 /// Get the number of cached sixel textures
188 #[allow(dead_code)]
189 pub fn sixel_cache_size(&self) -> usize {
190 self.graphics_renderer.cache_size()
191 }
192
193 /// Remove a specific sixel texture from cache
194 #[allow(dead_code)]
195 pub fn remove_sixel_texture(&mut self, id: u64) {
196 self.graphics_renderer.remove_texture(id);
197 self.sixel_graphics
198 .retain(|(gid, _, _, _, _, _, _)| *gid != id);
199 self.dirty = true;
200 }
201}