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}