Skip to main content

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}