Skip to main content

par_term_render/renderer/
render_passes.rs

1use anyhow::Result;
2
3use super::{DividerRenderInfo, PaneDividerSettings, PaneTitleInfo, Renderer};
4use crate::cell_renderer::PaneViewport;
5use crate::cell_renderer::pane_render::ATLAS_SIZE;
6
7impl Renderer {
8    /// Render pane dividers on top of pane content
9    ///
10    /// This should be called after rendering pane content but before egui.
11    ///
12    /// # Arguments
13    /// * `surface_view` - The texture view to render to
14    /// * `dividers` - List of dividers to render with hover state
15    /// * `settings` - Divider appearance settings
16    pub fn render_dividers(
17        &mut self,
18        surface_view: &wgpu::TextureView,
19        dividers: &[DividerRenderInfo],
20        settings: &PaneDividerSettings,
21    ) -> Result<()> {
22        if dividers.is_empty() {
23            return Ok(());
24        }
25
26        // Build divider instances using the cell renderer's background pipeline.
27        // We take the scratch buffer from self so the borrow checker allows us to
28        // also borrow self.cell_renderer mutably later in this method.
29        let mut instances = std::mem::take(&mut self.scratch_divider_instances);
30        instances.clear();
31
32        let w = self.size.width as f32;
33        let h = self.size.height as f32;
34
35        for divider in dividers {
36            let color = if divider.hovered {
37                settings.hover_color
38            } else {
39                settings.divider_color
40            };
41
42            use par_term_config::DividerStyle;
43            match settings.divider_style {
44                DividerStyle::Solid => {
45                    let x_ndc = divider.x / w * 2.0 - 1.0;
46                    let y_ndc = 1.0 - (divider.y / h * 2.0);
47                    let w_ndc = divider.width / w * 2.0;
48                    let h_ndc = divider.height / h * 2.0;
49
50                    instances.push(crate::cell_renderer::types::BackgroundInstance {
51                        position: [x_ndc, y_ndc],
52                        size: [w_ndc, h_ndc],
53                        color: [color[0], color[1], color[2], 1.0],
54                    });
55                }
56                DividerStyle::Double => {
57                    // Two parallel lines with a visible gap between them
58                    let is_horizontal = divider.width > divider.height;
59                    let thickness = if is_horizontal {
60                        divider.height
61                    } else {
62                        divider.width
63                    };
64
65                    if thickness >= 4.0 {
66                        // Enough space for two 1px lines with visible gap
67                        if is_horizontal {
68                            // Top line
69                            instances.push(crate::cell_renderer::types::BackgroundInstance {
70                                position: [divider.x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
71                                size: [divider.width / w * 2.0, 1.0 / h * 2.0],
72                                color: [color[0], color[1], color[2], 1.0],
73                            });
74                            // Bottom line (gap in between shows background)
75                            let bottom_y = divider.y + divider.height - 1.0;
76                            instances.push(crate::cell_renderer::types::BackgroundInstance {
77                                position: [divider.x / w * 2.0 - 1.0, 1.0 - (bottom_y / h * 2.0)],
78                                size: [divider.width / w * 2.0, 1.0 / h * 2.0],
79                                color: [color[0], color[1], color[2], 1.0],
80                            });
81                        } else {
82                            // Left line
83                            instances.push(crate::cell_renderer::types::BackgroundInstance {
84                                position: [divider.x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
85                                size: [1.0 / w * 2.0, divider.height / h * 2.0],
86                                color: [color[0], color[1], color[2], 1.0],
87                            });
88                            // Right line
89                            let right_x = divider.x + divider.width - 1.0;
90                            instances.push(crate::cell_renderer::types::BackgroundInstance {
91                                position: [right_x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
92                                size: [1.0 / w * 2.0, divider.height / h * 2.0],
93                                color: [color[0], color[1], color[2], 1.0],
94                            });
95                        }
96                    } else {
97                        // Divider too thin for double lines — render centered 1px line
98                        // (visibly thinner than Solid to differentiate)
99                        if is_horizontal {
100                            let center_y = divider.y + (divider.height - 1.0) / 2.0;
101                            instances.push(crate::cell_renderer::types::BackgroundInstance {
102                                position: [divider.x / w * 2.0 - 1.0, 1.0 - (center_y / h * 2.0)],
103                                size: [divider.width / w * 2.0, 1.0 / h * 2.0],
104                                color: [color[0], color[1], color[2], 1.0],
105                            });
106                        } else {
107                            let center_x = divider.x + (divider.width - 1.0) / 2.0;
108                            instances.push(crate::cell_renderer::types::BackgroundInstance {
109                                position: [center_x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
110                                size: [1.0 / w * 2.0, divider.height / h * 2.0],
111                                color: [color[0], color[1], color[2], 1.0],
112                            });
113                        }
114                    }
115                }
116                DividerStyle::Dashed => {
117                    // Dashed line effect using segments
118                    let is_horizontal = divider.width > divider.height;
119                    let dash_len: f32 = 6.0;
120                    let gap_len: f32 = 4.0;
121
122                    if is_horizontal {
123                        let mut x = divider.x;
124                        while x < divider.x + divider.width {
125                            let seg_w = dash_len.min(divider.x + divider.width - x);
126                            instances.push(crate::cell_renderer::types::BackgroundInstance {
127                                position: [x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
128                                size: [seg_w / w * 2.0, divider.height / h * 2.0],
129                                color: [color[0], color[1], color[2], 1.0],
130                            });
131                            x += dash_len + gap_len;
132                        }
133                    } else {
134                        let mut y = divider.y;
135                        while y < divider.y + divider.height {
136                            let seg_h = dash_len.min(divider.y + divider.height - y);
137                            instances.push(crate::cell_renderer::types::BackgroundInstance {
138                                position: [divider.x / w * 2.0 - 1.0, 1.0 - (y / h * 2.0)],
139                                size: [divider.width / w * 2.0, seg_h / h * 2.0],
140                                color: [color[0], color[1], color[2], 1.0],
141                            });
142                            y += dash_len + gap_len;
143                        }
144                    }
145                }
146                DividerStyle::Shadow => {
147                    // Beveled/embossed effect — all rendering stays within divider bounds
148                    // Highlight on top/left edge, shadow on bottom/right edge
149                    let is_horizontal = divider.width > divider.height;
150                    let thickness = if is_horizontal {
151                        divider.height
152                    } else {
153                        divider.width
154                    };
155
156                    // Brighter highlight color
157                    let highlight = [
158                        (color[0] + 0.3).min(1.0),
159                        (color[1] + 0.3).min(1.0),
160                        (color[2] + 0.3).min(1.0),
161                        1.0,
162                    ];
163                    // Darker shadow color
164                    let shadow = [(color[0] * 0.3), (color[1] * 0.3), (color[2] * 0.3), 1.0];
165
166                    if thickness >= 3.0 {
167                        // 3+ px: highlight line / main body / shadow line
168                        let edge = 1.0_f32;
169                        if is_horizontal {
170                            // Top highlight
171                            instances.push(crate::cell_renderer::types::BackgroundInstance {
172                                position: [divider.x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
173                                size: [divider.width / w * 2.0, edge / h * 2.0],
174                                color: highlight,
175                            });
176                            // Main body (middle portion)
177                            let body_y = divider.y + edge;
178                            let body_h = divider.height - edge * 2.0;
179                            if body_h > 0.0 {
180                                instances.push(crate::cell_renderer::types::BackgroundInstance {
181                                    position: [divider.x / w * 2.0 - 1.0, 1.0 - (body_y / h * 2.0)],
182                                    size: [divider.width / w * 2.0, body_h / h * 2.0],
183                                    color: [color[0], color[1], color[2], 1.0],
184                                });
185                            }
186                            // Bottom shadow
187                            let shadow_y = divider.y + divider.height - edge;
188                            instances.push(crate::cell_renderer::types::BackgroundInstance {
189                                position: [divider.x / w * 2.0 - 1.0, 1.0 - (shadow_y / h * 2.0)],
190                                size: [divider.width / w * 2.0, edge / h * 2.0],
191                                color: shadow,
192                            });
193                        } else {
194                            // Left highlight
195                            instances.push(crate::cell_renderer::types::BackgroundInstance {
196                                position: [divider.x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
197                                size: [edge / w * 2.0, divider.height / h * 2.0],
198                                color: highlight,
199                            });
200                            // Main body
201                            let body_x = divider.x + edge;
202                            let body_w = divider.width - edge * 2.0;
203                            if body_w > 0.0 {
204                                instances.push(crate::cell_renderer::types::BackgroundInstance {
205                                    position: [body_x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
206                                    size: [body_w / w * 2.0, divider.height / h * 2.0],
207                                    color: [color[0], color[1], color[2], 1.0],
208                                });
209                            }
210                            // Right shadow
211                            let shadow_x = divider.x + divider.width - edge;
212                            instances.push(crate::cell_renderer::types::BackgroundInstance {
213                                position: [shadow_x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
214                                size: [edge / w * 2.0, divider.height / h * 2.0],
215                                color: shadow,
216                            });
217                        }
218                    } else {
219                        // 2px or less: top/left half highlight, bottom/right half shadow
220                        if is_horizontal {
221                            let half = (divider.height / 2.0).max(1.0);
222                            instances.push(crate::cell_renderer::types::BackgroundInstance {
223                                position: [divider.x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
224                                size: [divider.width / w * 2.0, half / h * 2.0],
225                                color: highlight,
226                            });
227                            let bottom_y = divider.y + half;
228                            let bottom_h = divider.height - half;
229                            if bottom_h > 0.0 {
230                                instances.push(crate::cell_renderer::types::BackgroundInstance {
231                                    position: [
232                                        divider.x / w * 2.0 - 1.0,
233                                        1.0 - (bottom_y / h * 2.0),
234                                    ],
235                                    size: [divider.width / w * 2.0, bottom_h / h * 2.0],
236                                    color: shadow,
237                                });
238                            }
239                        } else {
240                            let half = (divider.width / 2.0).max(1.0);
241                            instances.push(crate::cell_renderer::types::BackgroundInstance {
242                                position: [divider.x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
243                                size: [half / w * 2.0, divider.height / h * 2.0],
244                                color: highlight,
245                            });
246                            let right_x = divider.x + half;
247                            let right_w = divider.width - half;
248                            if right_w > 0.0 {
249                                instances.push(crate::cell_renderer::types::BackgroundInstance {
250                                    position: [
251                                        right_x / w * 2.0 - 1.0,
252                                        1.0 - (divider.y / h * 2.0),
253                                    ],
254                                    size: [right_w / w * 2.0, divider.height / h * 2.0],
255                                    color: shadow,
256                                });
257                            }
258                        }
259                    }
260                }
261            }
262        }
263
264        // Write instances to GPU buffer
265        self.cell_renderer.queue().write_buffer(
266            &self.cell_renderer.buffers.bg_instance_buffer,
267            0,
268            bytemuck::cast_slice(&instances),
269        );
270
271        // Render dividers
272        let mut encoder =
273            self.cell_renderer
274                .device()
275                .create_command_encoder(&wgpu::CommandEncoderDescriptor {
276                    label: Some("divider render encoder"),
277                });
278
279        {
280            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
281                label: Some("divider render pass"),
282                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
283                    view: surface_view,
284                    resolve_target: None,
285                    ops: wgpu::Operations {
286                        load: wgpu::LoadOp::Load, // Don't clear - render on top
287                        store: wgpu::StoreOp::Store,
288                    },
289                    depth_slice: None,
290                })],
291                depth_stencil_attachment: None,
292                timestamp_writes: None,
293                occlusion_query_set: None,
294                multiview_mask: None,
295            });
296
297            render_pass.set_pipeline(&self.cell_renderer.pipelines.bg_pipeline);
298            render_pass.set_vertex_buffer(0, self.cell_renderer.buffers.vertex_buffer.slice(..));
299            render_pass
300                .set_vertex_buffer(1, self.cell_renderer.buffers.bg_instance_buffer.slice(..));
301            render_pass.draw(0..4, 0..instances.len() as u32);
302        }
303
304        self.cell_renderer
305            .queue()
306            .submit(std::iter::once(encoder.finish()));
307
308        // Restore the scratch buffer so its capacity is retained for the next frame.
309        instances.clear();
310        self.scratch_divider_instances = instances;
311
312        Ok(())
313    }
314
315    /// Render focus indicator around a pane
316    ///
317    /// This draws a colored border around the focused pane to highlight it.
318    ///
319    /// # Arguments
320    /// * `surface_view` - The texture view to render to
321    /// * `viewport` - The focused pane's viewport
322    /// * `settings` - Divider/focus settings
323    pub fn render_focus_indicator(
324        &mut self,
325        surface_view: &wgpu::TextureView,
326        viewport: &PaneViewport,
327        settings: &PaneDividerSettings,
328    ) -> Result<()> {
329        if !settings.show_focus_indicator {
330            return Ok(());
331        }
332
333        let border_w = settings.focus_width;
334        let color = [
335            settings.focus_color[0],
336            settings.focus_color[1],
337            settings.focus_color[2],
338            1.0,
339        ];
340
341        // Create 4 border rectangles (top, bottom, left, right)
342        let instances = vec![
343            // Top border
344            crate::cell_renderer::types::BackgroundInstance {
345                position: [
346                    viewport.x / self.size.width as f32 * 2.0 - 1.0,
347                    1.0 - (viewport.y / self.size.height as f32 * 2.0),
348                ],
349                size: [
350                    viewport.width / self.size.width as f32 * 2.0,
351                    border_w / self.size.height as f32 * 2.0,
352                ],
353                color,
354            },
355            // Bottom border
356            crate::cell_renderer::types::BackgroundInstance {
357                position: [
358                    viewport.x / self.size.width as f32 * 2.0 - 1.0,
359                    1.0 - ((viewport.y + viewport.height - border_w) / self.size.height as f32
360                        * 2.0),
361                ],
362                size: [
363                    viewport.width / self.size.width as f32 * 2.0,
364                    border_w / self.size.height as f32 * 2.0,
365                ],
366                color,
367            },
368            // Left border (between top and bottom)
369            crate::cell_renderer::types::BackgroundInstance {
370                position: [
371                    viewport.x / self.size.width as f32 * 2.0 - 1.0,
372                    1.0 - ((viewport.y + border_w) / self.size.height as f32 * 2.0),
373                ],
374                size: [
375                    border_w / self.size.width as f32 * 2.0,
376                    (viewport.height - border_w * 2.0) / self.size.height as f32 * 2.0,
377                ],
378                color,
379            },
380            // Right border (between top and bottom)
381            crate::cell_renderer::types::BackgroundInstance {
382                position: [
383                    (viewport.x + viewport.width - border_w) / self.size.width as f32 * 2.0 - 1.0,
384                    1.0 - ((viewport.y + border_w) / self.size.height as f32 * 2.0),
385                ],
386                size: [
387                    border_w / self.size.width as f32 * 2.0,
388                    (viewport.height - border_w * 2.0) / self.size.height as f32 * 2.0,
389                ],
390                color,
391            },
392        ];
393
394        // Write instances to GPU buffer
395        self.cell_renderer.queue().write_buffer(
396            &self.cell_renderer.buffers.bg_instance_buffer,
397            0,
398            bytemuck::cast_slice(&instances),
399        );
400
401        // Render focus indicator
402        let mut encoder =
403            self.cell_renderer
404                .device()
405                .create_command_encoder(&wgpu::CommandEncoderDescriptor {
406                    label: Some("focus indicator encoder"),
407                });
408
409        {
410            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
411                label: Some("focus indicator pass"),
412                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
413                    view: surface_view,
414                    resolve_target: None,
415                    ops: wgpu::Operations {
416                        load: wgpu::LoadOp::Load, // Don't clear - render on top
417                        store: wgpu::StoreOp::Store,
418                    },
419                    depth_slice: None,
420                })],
421                depth_stencil_attachment: None,
422                timestamp_writes: None,
423                occlusion_query_set: None,
424                multiview_mask: None,
425            });
426
427            render_pass.set_pipeline(&self.cell_renderer.pipelines.bg_pipeline);
428            render_pass.set_vertex_buffer(0, self.cell_renderer.buffers.vertex_buffer.slice(..));
429            render_pass
430                .set_vertex_buffer(1, self.cell_renderer.buffers.bg_instance_buffer.slice(..));
431            render_pass.draw(0..4, 0..instances.len() as u32);
432        }
433
434        self.cell_renderer
435            .queue()
436            .submit(std::iter::once(encoder.finish()));
437        Ok(())
438    }
439
440    /// Render pane title bars (background rectangles + text)
441    ///
442    /// Title bars are rendered on top of pane content and dividers.
443    /// Each title bar consists of a colored background rectangle and centered text.
444    pub fn render_pane_titles(
445        &mut self,
446        surface_view: &wgpu::TextureView,
447        titles: &[PaneTitleInfo],
448    ) -> Result<()> {
449        if titles.is_empty() {
450            return Ok(());
451        }
452
453        let width = self.size.width as f32;
454        let height = self.size.height as f32;
455
456        // Phase 1: Render title bar backgrounds
457        let mut bg_instances = Vec::with_capacity(titles.len());
458        for title in titles {
459            let x_ndc = title.x / width * 2.0 - 1.0;
460            let y_ndc = 1.0 - (title.y / height * 2.0);
461            let w_ndc = title.width / width * 2.0;
462            let h_ndc = title.height / height * 2.0;
463
464            // Title bar must be fully opaque (alpha=1.0) to cover the background.
465            // Differentiate focused/unfocused by lightening/darkening the color.
466            let brightness = if title.focused { 1.0 } else { 0.7 };
467
468            bg_instances.push(crate::cell_renderer::types::BackgroundInstance {
469                position: [x_ndc, y_ndc],
470                size: [w_ndc, h_ndc],
471                color: [
472                    title.bg_color[0] * brightness,
473                    title.bg_color[1] * brightness,
474                    title.bg_color[2] * brightness,
475                    1.0, // Always fully opaque
476                ],
477            });
478        }
479
480        // Write background instances to GPU buffer
481        self.cell_renderer.queue().write_buffer(
482            &self.cell_renderer.buffers.bg_instance_buffer,
483            0,
484            bytemuck::cast_slice(&bg_instances),
485        );
486
487        // Render title backgrounds
488        let mut encoder =
489            self.cell_renderer
490                .device()
491                .create_command_encoder(&wgpu::CommandEncoderDescriptor {
492                    label: Some("pane title bg encoder"),
493                });
494
495        {
496            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
497                label: Some("pane title bg pass"),
498                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
499                    view: surface_view,
500                    resolve_target: None,
501                    ops: wgpu::Operations {
502                        load: wgpu::LoadOp::Load,
503                        store: wgpu::StoreOp::Store,
504                    },
505                    depth_slice: None,
506                })],
507                depth_stencil_attachment: None,
508                timestamp_writes: None,
509                occlusion_query_set: None,
510                multiview_mask: None,
511            });
512
513            render_pass.set_pipeline(&self.cell_renderer.pipelines.bg_pipeline);
514            render_pass.set_vertex_buffer(0, self.cell_renderer.buffers.vertex_buffer.slice(..));
515            render_pass
516                .set_vertex_buffer(1, self.cell_renderer.buffers.bg_instance_buffer.slice(..));
517            render_pass.draw(0..4, 0..bg_instances.len() as u32);
518        }
519
520        self.cell_renderer
521            .queue()
522            .submit(std::iter::once(encoder.finish()));
523
524        // Phase 2: Render title text using glyph atlas
525        let mut text_instances = Vec::new();
526        let baseline_y = self.cell_renderer.font.font_ascent;
527
528        for title in titles {
529            let title_text = &title.title;
530            if title_text.is_empty() {
531                continue;
532            }
533
534            // Calculate starting X position (centered in title bar with left padding)
535            let padding_x = 8.0;
536            let mut x_pos = title.x + padding_x;
537            let y_base = title.y + (title.height - self.cell_renderer.grid.cell_height) / 2.0;
538
539            let text_color = [
540                title.text_color[0],
541                title.text_color[1],
542                title.text_color[2],
543                if title.focused { 1.0 } else { 0.8 },
544            ];
545
546            // Truncate title if it would overflow the title bar
547            let max_chars =
548                ((title.width - padding_x * 2.0) / self.cell_renderer.grid.cell_width) as usize;
549            let display_text: String = if title_text.len() > max_chars && max_chars > 3 {
550                let truncated: String = title_text.chars().take(max_chars - 1).collect();
551                format!("{}\u{2026}", truncated) // ellipsis
552            } else {
553                title_text.clone()
554            };
555
556            for ch in display_text.chars() {
557                if x_pos >= title.x + title.width - padding_x {
558                    break;
559                }
560
561                if let Some((font_idx, glyph_id)) =
562                    self.cell_renderer.font_manager.find_glyph(ch, false, false)
563                {
564                    let cache_key = ((font_idx as u64) << 32) | (glyph_id as u64);
565                    // Check if this character should be rendered as a monochrome symbol
566                    let force_monochrome = crate::cell_renderer::atlas::should_render_as_symbol(ch);
567                    let info = if self
568                        .cell_renderer
569                        .atlas
570                        .glyph_cache
571                        .contains_key(&cache_key)
572                    {
573                        self.cell_renderer.lru_remove(cache_key);
574                        self.cell_renderer.lru_push_front(cache_key);
575                        self.cell_renderer
576                            .atlas
577                            .glyph_cache
578                            .get(&cache_key)
579                            .expect("Glyph cache entry must exist after contains_key check")
580                            .clone()
581                    } else if let Some(raster) =
582                        self.cell_renderer
583                            .rasterize_glyph(font_idx, glyph_id, force_monochrome)
584                    {
585                        let info = self.cell_renderer.upload_glyph(cache_key, &raster);
586                        self.cell_renderer
587                            .atlas
588                            .glyph_cache
589                            .insert(cache_key, info.clone());
590                        self.cell_renderer.lru_push_front(cache_key);
591                        info
592                    } else {
593                        x_pos += self.cell_renderer.grid.cell_width;
594                        continue;
595                    };
596
597                    let glyph_left = x_pos + info.bearing_x;
598                    let glyph_top = y_base + (baseline_y - info.bearing_y);
599
600                    text_instances.push(crate::cell_renderer::types::TextInstance {
601                        position: [
602                            glyph_left / width * 2.0 - 1.0,
603                            1.0 - (glyph_top / height * 2.0),
604                        ],
605                        size: [
606                            info.width as f32 / width * 2.0,
607                            info.height as f32 / height * 2.0,
608                        ],
609                        tex_offset: [info.x as f32 / ATLAS_SIZE, info.y as f32 / ATLAS_SIZE],
610                        tex_size: [
611                            info.width as f32 / ATLAS_SIZE,
612                            info.height as f32 / ATLAS_SIZE,
613                        ],
614                        color: text_color,
615                        is_colored: if info.is_colored { 1 } else { 0 },
616                    });
617                }
618
619                x_pos += self.cell_renderer.grid.cell_width;
620            }
621        }
622
623        if text_instances.is_empty() {
624            return Ok(());
625        }
626
627        // Write text instances to GPU buffer
628        self.cell_renderer.queue().write_buffer(
629            &self.cell_renderer.buffers.text_instance_buffer,
630            0,
631            bytemuck::cast_slice(&text_instances),
632        );
633
634        // Render title text
635        let mut encoder =
636            self.cell_renderer
637                .device()
638                .create_command_encoder(&wgpu::CommandEncoderDescriptor {
639                    label: Some("pane title text encoder"),
640                });
641
642        {
643            let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
644                label: Some("pane title text pass"),
645                color_attachments: &[Some(wgpu::RenderPassColorAttachment {
646                    view: surface_view,
647                    resolve_target: None,
648                    ops: wgpu::Operations {
649                        load: wgpu::LoadOp::Load,
650                        store: wgpu::StoreOp::Store,
651                    },
652                    depth_slice: None,
653                })],
654                depth_stencil_attachment: None,
655                timestamp_writes: None,
656                occlusion_query_set: None,
657                multiview_mask: None,
658            });
659
660            render_pass.set_pipeline(&self.cell_renderer.pipelines.text_pipeline);
661            render_pass.set_bind_group(0, &self.cell_renderer.pipelines.text_bind_group, &[]);
662            render_pass.set_vertex_buffer(0, self.cell_renderer.buffers.vertex_buffer.slice(..));
663            render_pass
664                .set_vertex_buffer(1, self.cell_renderer.buffers.text_instance_buffer.slice(..));
665            render_pass.draw(0..4, 0..text_instances.len() as u32);
666        }
667
668        self.cell_renderer
669            .queue()
670            .submit(std::iter::once(encoder.finish()));
671
672        Ok(())
673    }
674}