1use super::block_chars;
2use super::{BackgroundInstance, Cell, CellRenderer, PaneViewport, RowCacheEntry, TextInstance};
3use anyhow::Result;
4use par_term_config::SeparatorMark;
5use par_term_fonts::text_shaper::ShapingOptions;
6
7impl CellRenderer {
8 pub fn render(
9 &mut self,
10 _show_scrollbar: bool,
11 pane_background: Option<&par_term_config::PaneBackground>,
12 ) -> Result<wgpu::SurfaceTexture> {
13 let output = self.surface.get_current_texture()?;
14 let view = output
15 .texture
16 .create_view(&wgpu::TextureViewDescriptor::default());
17 self.build_instance_buffers()?;
18
19 let pane_bg_resources = if !self.bg_is_solid_color {
22 if let Some(pane_bg) = pane_background {
23 if let Some(ref path) = pane_bg.image_path {
24 self.pane_bg_cache.get(path.as_str()).map(|entry| {
25 self.create_pane_bg_bind_group(
26 entry,
27 0.0, 0.0, self.config.width as f32,
30 self.config.height as f32,
31 pane_bg.mode,
32 pane_bg.opacity,
33 )
34 })
35 } else {
36 None
37 }
38 } else {
39 None
40 }
41 } else {
42 None
43 };
44
45 let mut encoder = self
46 .device
47 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
48 label: Some("render encoder"),
49 });
50
51 let has_pane_bg = pane_bg_resources.is_some();
57 let (clear_color, use_bg_image_pipeline) = if has_pane_bg {
58 (wgpu::Color::TRANSPARENT, false)
60 } else if self.bg_is_solid_color {
61 log::info!(
64 "[BACKGROUND] Solid color mode: RGB({:.3}, {:.3}, {:.3}) * opacity {:.3}",
65 self.solid_bg_color[0],
66 self.solid_bg_color[1],
67 self.solid_bg_color[2],
68 self.window_opacity
69 );
70 (
71 wgpu::Color {
72 r: self.solid_bg_color[0] as f64 * self.window_opacity as f64,
73 g: self.solid_bg_color[1] as f64 * self.window_opacity as f64,
74 b: self.solid_bg_color[2] as f64 * self.window_opacity as f64,
75 a: self.window_opacity as f64,
76 },
77 false,
78 )
79 } else if self.bg_image_bind_group.is_some() {
80 (wgpu::Color::TRANSPARENT, true)
82 } else {
83 (
85 wgpu::Color {
86 r: self.background_color[0] as f64 * self.window_opacity as f64,
87 g: self.background_color[1] as f64 * self.window_opacity as f64,
88 b: self.background_color[2] as f64 * self.window_opacity as f64,
89 a: self.window_opacity as f64,
90 },
91 false,
92 )
93 };
94
95 {
96 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
97 label: Some("render pass"),
98 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
99 view: &view,
100 resolve_target: None,
101 ops: wgpu::Operations {
102 load: wgpu::LoadOp::Clear(clear_color),
103 store: wgpu::StoreOp::Store,
104 },
105 depth_slice: None,
106 })],
107 depth_stencil_attachment: None,
108 timestamp_writes: None,
109 occlusion_query_set: None,
110 });
111
112 if let Some((ref bind_group, _)) = pane_bg_resources {
114 render_pass.set_pipeline(&self.bg_image_pipeline);
115 render_pass.set_bind_group(0, bind_group, &[]);
116 render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
117 render_pass.draw(0..4, 0..1);
118 }
119
120 if use_bg_image_pipeline && let Some(ref bg_bind_group) = self.bg_image_bind_group {
122 render_pass.set_pipeline(&self.bg_image_pipeline);
123 render_pass.set_bind_group(0, bg_bind_group, &[]);
124 render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
125 render_pass.draw(0..4, 0..1);
126 }
127
128 render_pass.set_pipeline(&self.bg_pipeline);
129 render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
130 render_pass.set_vertex_buffer(1, self.bg_instance_buffer.slice(..));
131 render_pass.draw(0..4, 0..self.max_bg_instances as u32);
132
133 render_pass.set_pipeline(&self.text_pipeline);
134 render_pass.set_bind_group(0, &self.text_bind_group, &[]);
135 render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
136 render_pass.set_vertex_buffer(1, self.text_instance_buffer.slice(..));
137 render_pass.draw(0..4, 0..self.max_text_instances as u32);
138 }
139
140 self.queue.submit(std::iter::once(encoder.finish()));
141 Ok(output)
142 }
143
144 pub fn render_to_texture(
154 &mut self,
155 target_view: &wgpu::TextureView,
156 skip_background_image: bool,
157 ) -> Result<wgpu::SurfaceTexture> {
158 let output = self.surface.get_current_texture()?;
159 self.build_instance_buffers()?;
160
161 let render_background_image =
164 !skip_background_image && !self.bg_is_solid_color && self.bg_image_bind_group.is_some();
165 let saved_window_opacity = self.window_opacity;
166
167 if render_background_image {
168 self.window_opacity = 1.0;
171 self.update_bg_image_uniforms();
172 }
173
174 let mut encoder = self
175 .device
176 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
177 label: Some("render to texture encoder"),
178 });
179
180 let clear_color = wgpu::Color::TRANSPARENT;
182
183 {
184 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
185 label: Some("render pass"),
186 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
187 view: target_view,
188 resolve_target: None,
189 ops: wgpu::Operations {
190 load: wgpu::LoadOp::Clear(clear_color),
191 store: wgpu::StoreOp::Store,
192 },
193 depth_slice: None,
194 })],
195 depth_stencil_attachment: None,
196 timestamp_writes: None,
197 occlusion_query_set: None,
198 });
199
200 if render_background_image && let Some(ref bg_bind_group) = self.bg_image_bind_group {
202 log::info!(
203 "[BACKGROUND] render_to_texture: bg_image_pipeline (image, window_opacity={:.3} applied by shader)",
204 saved_window_opacity
205 );
206 render_pass.set_pipeline(&self.bg_image_pipeline);
207 render_pass.set_bind_group(0, bg_bind_group, &[]);
208 render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
209 render_pass.draw(0..4, 0..1);
210 }
211
212 render_pass.set_pipeline(&self.bg_pipeline);
213 render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
214 render_pass.set_vertex_buffer(1, self.bg_instance_buffer.slice(..));
215 render_pass.draw(0..4, 0..self.max_bg_instances as u32);
216
217 render_pass.set_pipeline(&self.text_pipeline);
218 render_pass.set_bind_group(0, &self.text_bind_group, &[]);
219 render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
220 render_pass.set_vertex_buffer(1, self.text_instance_buffer.slice(..));
221 render_pass.draw(0..4, 0..self.max_text_instances as u32);
222 }
223
224 self.queue.submit(std::iter::once(encoder.finish()));
225
226 if render_background_image {
228 self.window_opacity = saved_window_opacity;
229 self.update_bg_image_uniforms();
230 }
231
232 Ok(output)
233 }
234
235 pub fn render_background_only(
247 &self,
248 target_view: &wgpu::TextureView,
249 clear_first: bool,
250 ) -> Result<bool> {
251 let mut encoder = self
252 .device
253 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
254 label: Some("background only encoder"),
255 });
256
257 let (clear_color, use_bg_image_pipeline) = if self.bg_is_solid_color {
259 (
260 wgpu::Color {
261 r: self.solid_bg_color[0] as f64 * self.window_opacity as f64,
262 g: self.solid_bg_color[1] as f64 * self.window_opacity as f64,
263 b: self.solid_bg_color[2] as f64 * self.window_opacity as f64,
264 a: self.window_opacity as f64,
265 },
266 false,
267 )
268 } else if self.bg_image_bind_group.is_some() {
269 (wgpu::Color::TRANSPARENT, true)
270 } else {
271 (
272 wgpu::Color {
273 r: self.background_color[0] as f64 * self.window_opacity as f64,
274 g: self.background_color[1] as f64 * self.window_opacity as f64,
275 b: self.background_color[2] as f64 * self.window_opacity as f64,
276 a: self.window_opacity as f64,
277 },
278 false,
279 )
280 };
281
282 let load_op = if clear_first {
283 wgpu::LoadOp::Clear(clear_color)
284 } else {
285 wgpu::LoadOp::Load
286 };
287
288 {
289 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
290 label: Some("background only render pass"),
291 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
292 view: target_view,
293 resolve_target: None,
294 ops: wgpu::Operations {
295 load: load_op,
296 store: wgpu::StoreOp::Store,
297 },
298 depth_slice: None,
299 })],
300 depth_stencil_attachment: None,
301 timestamp_writes: None,
302 occlusion_query_set: None,
303 });
304
305 if use_bg_image_pipeline && let Some(ref bg_bind_group) = self.bg_image_bind_group {
307 render_pass.set_pipeline(&self.bg_image_pipeline);
308 render_pass.set_bind_group(0, bg_bind_group, &[]);
309 render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
310 render_pass.draw(0..4, 0..1);
311 }
312 }
313
314 self.queue.submit(std::iter::once(encoder.finish()));
315 Ok(use_bg_image_pipeline)
316 }
317
318 pub fn render_to_view(&self, target_view: &wgpu::TextureView) -> Result<()> {
321 let mut encoder = self
325 .device
326 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
327 label: Some("screenshot render encoder"),
328 });
329
330 let (clear_color, use_bg_image_pipeline) = if self.bg_is_solid_color {
332 (
333 wgpu::Color {
334 r: self.solid_bg_color[0] as f64 * self.window_opacity as f64,
335 g: self.solid_bg_color[1] as f64 * self.window_opacity as f64,
336 b: self.solid_bg_color[2] as f64 * self.window_opacity as f64,
337 a: self.window_opacity as f64,
338 },
339 false,
340 )
341 } else if self.bg_image_bind_group.is_some() {
342 (wgpu::Color::TRANSPARENT, true)
343 } else {
344 (
345 wgpu::Color {
346 r: self.background_color[0] as f64 * self.window_opacity as f64,
347 g: self.background_color[1] as f64 * self.window_opacity as f64,
348 b: self.background_color[2] as f64 * self.window_opacity as f64,
349 a: self.window_opacity as f64,
350 },
351 false,
352 )
353 };
354
355 {
356 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
357 label: Some("screenshot render pass"),
358 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
359 view: target_view,
360 resolve_target: None,
361 ops: wgpu::Operations {
362 load: wgpu::LoadOp::Clear(clear_color),
363 store: wgpu::StoreOp::Store,
364 },
365 depth_slice: None,
366 })],
367 depth_stencil_attachment: None,
368 timestamp_writes: None,
369 occlusion_query_set: None,
370 });
371
372 if use_bg_image_pipeline && let Some(ref bg_bind_group) = self.bg_image_bind_group {
374 render_pass.set_pipeline(&self.bg_image_pipeline);
375 render_pass.set_bind_group(0, bg_bind_group, &[]);
376 render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
377 render_pass.draw(0..4, 0..1);
378 }
379
380 render_pass.set_pipeline(&self.bg_pipeline);
381 render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
382 render_pass.set_vertex_buffer(1, self.bg_instance_buffer.slice(..));
383 render_pass.draw(0..4, 0..self.max_bg_instances as u32);
384
385 render_pass.set_pipeline(&self.text_pipeline);
386 render_pass.set_bind_group(0, &self.text_bind_group, &[]);
387 render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
388 render_pass.set_vertex_buffer(1, self.text_instance_buffer.slice(..));
389 render_pass.draw(0..4, 0..self.max_text_instances as u32);
390
391 self.scrollbar.render(&mut render_pass);
393 }
394
395 self.queue.submit(std::iter::once(encoder.finish()));
396 Ok(())
397 }
398
399 pub fn render_overlays(
400 &mut self,
401 surface_texture: &wgpu::SurfaceTexture,
402 show_scrollbar: bool,
403 ) -> Result<()> {
404 let view = surface_texture
405 .texture
406 .create_view(&wgpu::TextureViewDescriptor::default());
407 let mut encoder = self
408 .device
409 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
410 label: Some("overlay encoder"),
411 });
412
413 {
414 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
415 label: Some("overlay pass"),
416 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
417 view: &view,
418 resolve_target: None,
419 ops: wgpu::Operations {
420 load: wgpu::LoadOp::Load,
421 store: wgpu::StoreOp::Store,
422 },
423 depth_slice: None,
424 })],
425 depth_stencil_attachment: None,
426 timestamp_writes: None,
427 occlusion_query_set: None,
428 });
429
430 if show_scrollbar {
431 self.scrollbar.render(&mut render_pass);
432 }
433
434 if self.visual_bell_intensity > 0.0 {
435 }
437 }
438
439 self.queue.submit(std::iter::once(encoder.finish()));
440 Ok(())
441 }
442
443 pub(crate) fn build_instance_buffers(&mut self) -> Result<()> {
444 let _shaping_options = ShapingOptions {
445 enable_ligatures: self.enable_ligatures,
446 enable_kerning: self.enable_kerning,
447 ..Default::default()
448 };
449
450 for row in 0..self.rows {
451 if self.dirty_rows[row] || self.row_cache[row].is_none() {
452 let start = row * self.cols;
453 let end = (row + 1) * self.cols;
454 let row_cells = &self.cells[start..end];
455
456 let mut row_bg = Vec::with_capacity(self.cols);
457 let mut row_text = Vec::with_capacity(self.cols);
458
459 let mut col = 0;
462 while col < row_cells.len() {
463 let cell = &row_cells[col];
464 let is_default_bg =
465 (cell.bg_color[0] as f32 / 255.0 - self.background_color[0]).abs() < 0.001
466 && (cell.bg_color[1] as f32 / 255.0 - self.background_color[1]).abs()
467 < 0.001
468 && (cell.bg_color[2] as f32 / 255.0 - self.background_color[2]).abs()
469 < 0.001;
470
471 let cursor_visible = self.cursor_opacity > 0.0
473 && !self.cursor_hidden_for_shader
474 && self.cursor_pos.1 == row
475 && self.cursor_pos.0 == col;
476
477 let has_cursor = if cursor_visible && !self.is_focused {
479 match self.unfocused_cursor_style {
480 par_term_config::UnfocusedCursorStyle::Hidden => false,
481 par_term_config::UnfocusedCursorStyle::Hollow
482 | par_term_config::UnfocusedCursorStyle::Same => true,
483 }
484 } else {
485 cursor_visible
486 };
487
488 if is_default_bg && !has_cursor {
489 col += 1;
490 continue;
491 }
492
493 let bg_alpha =
495 if self.transparency_affects_only_default_background && !is_default_bg {
496 1.0
497 } else {
498 self.window_opacity
499 };
500 let mut bg_color = [
501 cell.bg_color[0] as f32 / 255.0,
502 cell.bg_color[1] as f32 / 255.0,
503 cell.bg_color[2] as f32 / 255.0,
504 bg_alpha,
505 ];
506
507 if has_cursor && self.cursor_opacity > 0.0 {
509 use par_term_emu_core_rust::cursor::CursorStyle;
510
511 let render_hollow = !self.is_focused
513 && self.unfocused_cursor_style
514 == par_term_config::UnfocusedCursorStyle::Hollow;
515
516 match self.cursor_style {
517 CursorStyle::SteadyBlock | CursorStyle::BlinkingBlock => {
518 if render_hollow {
519 } else {
522 for (bg, &cursor) in
524 bg_color.iter_mut().take(3).zip(&self.cursor_color)
525 {
526 *bg = *bg * (1.0 - self.cursor_opacity)
527 + cursor * self.cursor_opacity;
528 }
529 bg_color[3] = bg_color[3].max(self.cursor_opacity);
530 }
531 }
532 _ => {}
533 }
534 let x0 = self.window_padding
536 + self.content_offset_x
537 + col as f32 * self.cell_width;
538 let x1 = self.window_padding
539 + self.content_offset_x
540 + (col + 1) as f32 * self.cell_width;
541 let y0 = self.window_padding
542 + self.content_offset_y
543 + row as f32 * self.cell_height;
544 let y1 = y0 + self.cell_height;
545 row_bg.push(BackgroundInstance {
546 position: [
547 x0 / self.config.width as f32 * 2.0 - 1.0,
548 1.0 - (y0 / self.config.height as f32 * 2.0),
549 ],
550 size: [
551 (x1 - x0) / self.config.width as f32 * 2.0,
552 (y1 - y0) / self.config.height as f32 * 2.0,
553 ],
554 color: bg_color,
555 });
556 col += 1;
557 continue;
558 }
559
560 let start_col = col;
562 let run_color = cell.bg_color;
563 col += 1;
564 while col < row_cells.len() {
565 let next_cell = &row_cells[col];
566 let next_has_cursor = self.cursor_opacity > 0.0
567 && !self.cursor_hidden_for_shader
568 && self.cursor_pos.1 == row
569 && self.cursor_pos.0 == col;
570 if next_cell.bg_color != run_color || next_has_cursor {
572 break;
573 }
574 col += 1;
575 }
576 let run_length = col - start_col;
577
578 let x0 = self.window_padding
580 + self.content_offset_x
581 + start_col as f32 * self.cell_width;
582 let x1 = self.window_padding
583 + self.content_offset_x
584 + (start_col + run_length) as f32 * self.cell_width;
585 let y0 =
586 self.window_padding + self.content_offset_y + row as f32 * self.cell_height;
587 let y1 = y0 + self.cell_height;
588
589 row_bg.push(BackgroundInstance {
590 position: [
591 x0 / self.config.width as f32 * 2.0 - 1.0,
592 1.0 - (y0 / self.config.height as f32 * 2.0),
593 ],
594 size: [
595 (x1 - x0) / self.config.width as f32 * 2.0,
596 (y1 - y0) / self.config.height as f32 * 2.0,
597 ],
598 color: bg_color,
599 });
600 }
601
602 while row_bg.len() < self.cols {
605 row_bg.push(BackgroundInstance {
606 position: [0.0, 0.0],
607 size: [0.0, 0.0],
608 color: [0.0, 0.0, 0.0, 0.0],
609 });
610 }
611
612 let mut x_offset = 0.0;
614 #[allow(clippy::type_complexity)]
615 let cell_data: Vec<(
616 String,
617 bool,
618 bool,
619 [u8; 4],
620 [u8; 4],
621 bool,
622 bool,
623 )> = row_cells
624 .iter()
625 .map(|c| {
626 (
627 c.grapheme.clone(),
628 c.bold,
629 c.italic,
630 c.fg_color,
631 c.bg_color,
632 c.wide_char_spacer,
633 c.wide_char,
634 )
635 })
636 .collect();
637
638 let natural_line_height = self.font_ascent + self.font_descent + self.font_leading;
640 let vertical_padding = (self.cell_height - natural_line_height).max(0.0) / 2.0;
641 let baseline_y_unrounded = self.window_padding
642 + self.content_offset_y
643 + (row as f32 * self.cell_height)
644 + vertical_padding
645 + self.font_ascent;
646
647 let cursor_is_block_on_this_row = {
650 use par_term_emu_core_rust::cursor::CursorStyle;
651 self.cursor_pos.1 == row
652 && self.cursor_opacity > 0.0
653 && !self.cursor_hidden_for_shader
654 && matches!(
655 self.cursor_style,
656 CursorStyle::SteadyBlock | CursorStyle::BlinkingBlock
657 )
658 && (self.is_focused
659 || self.unfocused_cursor_style
660 == par_term_config::UnfocusedCursorStyle::Same)
661 };
662
663 let mut current_col = 0usize;
664 for (grapheme, bold, italic, fg_color, bg_color, is_spacer, is_wide) in cell_data {
665 if is_spacer || grapheme == " " {
666 x_offset += self.cell_width;
667 current_col += 1;
668 continue;
669 }
670
671 let text_alpha = if self.keep_text_opaque {
674 1.0
675 } else {
676 self.window_opacity
677 };
678
679 let render_fg_color: [f32; 4] =
682 if cursor_is_block_on_this_row && current_col == self.cursor_pos.0 {
683 if let Some(cursor_text) = self.cursor_text_color {
684 [cursor_text[0], cursor_text[1], cursor_text[2], text_alpha]
685 } else {
686 let cursor_brightness = (self.cursor_color[0]
689 + self.cursor_color[1]
690 + self.cursor_color[2])
691 / 3.0;
692 if cursor_brightness > 0.5 {
693 [0.0, 0.0, 0.0, text_alpha] } else {
695 [1.0, 1.0, 1.0, text_alpha] }
697 }
698 } else {
699 let effective_bg = if bg_color[3] > 0 {
702 [
704 bg_color[0] as f32 / 255.0,
705 bg_color[1] as f32 / 255.0,
706 bg_color[2] as f32 / 255.0,
707 1.0,
708 ]
709 } else {
710 [
712 self.background_color[0],
713 self.background_color[1],
714 self.background_color[2],
715 1.0,
716 ]
717 };
718
719 let base_fg = [
720 fg_color[0] as f32 / 255.0,
721 fg_color[1] as f32 / 255.0,
722 fg_color[2] as f32 / 255.0,
723 text_alpha,
724 ];
725
726 self.ensure_minimum_contrast(base_fg, effective_bg)
728 };
729
730 let chars: Vec<char> = grapheme.chars().collect();
731 #[allow(clippy::collapsible_if)]
732 if let Some(ch) = chars.first() {
733 let char_type = block_chars::classify_char(*ch);
736
737 if chars.len() == 1 && block_chars::should_render_geometrically(char_type) {
740 let char_w = if is_wide {
741 self.cell_width * 2.0
742 } else {
743 self.cell_width
744 };
745 let x0 =
746 (self.window_padding + self.content_offset_x + x_offset).round();
747 let y0 = (self.window_padding
748 + self.content_offset_y
749 + row as f32 * self.cell_height)
750 .round();
751
752 let aspect_ratio = self.cell_height / char_w;
755 if let Some(box_geo) =
756 block_chars::get_box_drawing_geometry(*ch, aspect_ratio)
757 {
758 for segment in &box_geo.segments {
759 let rect =
760 segment.to_pixel_rect(x0, y0, char_w, self.cell_height);
761
762 let extension = 1.0;
764 let ext_x = if segment.x <= 0.01 { extension } else { 0.0 };
765 let ext_y = if segment.y <= 0.01 { extension } else { 0.0 };
766 let ext_w = if segment.x + segment.width >= 0.99 {
767 extension
768 } else {
769 0.0
770 };
771 let ext_h = if segment.y + segment.height >= 0.99 {
772 extension
773 } else {
774 0.0
775 };
776
777 let final_x = rect.x - ext_x;
778 let final_y = rect.y - ext_y;
779 let final_w = rect.width + ext_x + ext_w;
780 let final_h = rect.height + ext_y + ext_h;
781
782 row_text.push(TextInstance {
783 position: [
784 final_x / self.config.width as f32 * 2.0 - 1.0,
785 1.0 - (final_y / self.config.height as f32 * 2.0),
786 ],
787 size: [
788 final_w / self.config.width as f32 * 2.0,
789 final_h / self.config.height as f32 * 2.0,
790 ],
791 tex_offset: [
792 self.solid_pixel_offset.0 as f32 / 2048.0,
793 self.solid_pixel_offset.1 as f32 / 2048.0,
794 ],
795 tex_size: [1.0 / 2048.0, 1.0 / 2048.0],
796 color: render_fg_color,
797 is_colored: 0,
798 });
799 }
800 x_offset += self.cell_width;
801 current_col += 1;
802 continue;
803 }
804
805 if let Some(geo_block) = block_chars::get_geometric_block(*ch) {
807 let rect =
808 geo_block.to_pixel_rect(x0, y0, char_w, self.cell_height);
809
810 let extension = 1.0;
812 let ext_x = if geo_block.x == 0.0 { extension } else { 0.0 };
813 let ext_y = if geo_block.y == 0.0 { extension } else { 0.0 };
814 let ext_w = if geo_block.x + geo_block.width >= 1.0 {
815 extension
816 } else {
817 0.0
818 };
819 let ext_h = if geo_block.y + geo_block.height >= 1.0 {
820 extension
821 } else {
822 0.0
823 };
824
825 let final_x = rect.x - ext_x;
826 let final_y = rect.y - ext_y;
827 let final_w = rect.width + ext_x + ext_w;
828 let final_h = rect.height + ext_y + ext_h;
829
830 row_text.push(TextInstance {
833 position: [
834 final_x / self.config.width as f32 * 2.0 - 1.0,
835 1.0 - (final_y / self.config.height as f32 * 2.0),
836 ],
837 size: [
838 final_w / self.config.width as f32 * 2.0,
839 final_h / self.config.height as f32 * 2.0,
840 ],
841 tex_offset: [
843 self.solid_pixel_offset.0 as f32 / 2048.0,
844 self.solid_pixel_offset.1 as f32 / 2048.0,
845 ],
846 tex_size: [1.0 / 2048.0, 1.0 / 2048.0],
847 color: render_fg_color,
848 is_colored: 0,
849 });
850
851 x_offset += self.cell_width;
852 current_col += 1;
853 continue;
854 }
855 }
856
857 let glyph_result = if chars.len() > 1 {
860 self.font_manager
861 .find_grapheme_glyph(&grapheme, bold, italic)
862 } else {
863 self.font_manager.find_glyph(*ch, bold, italic)
864 };
865
866 if let Some((font_idx, glyph_id)) = glyph_result {
867 let cache_key = ((font_idx as u64) << 32) | (glyph_id as u64);
868 let info = if self.glyph_cache.contains_key(&cache_key) {
869 self.lru_remove(cache_key);
871 self.lru_push_front(cache_key);
872 self.glyph_cache.get(&cache_key).unwrap().clone()
873 } else if let Some(raster) = self.rasterize_glyph(font_idx, glyph_id) {
874 let info = self.upload_glyph(cache_key, &raster);
875 self.glyph_cache.insert(cache_key, info.clone());
876 self.lru_push_front(cache_key);
877 info
878 } else {
879 x_offset += self.cell_width;
880 continue;
881 };
882
883 let char_w = if is_wide {
884 self.cell_width * 2.0
885 } else {
886 self.cell_width
887 };
888 let x0 =
889 (self.window_padding + self.content_offset_x + x_offset).round();
890 let x1 =
891 (self.window_padding + self.content_offset_x + x_offset + char_w)
892 .round();
893 let y0 = (self.window_padding
894 + self.content_offset_y
895 + row as f32 * self.cell_height)
896 .round();
897 let y1 = (self.window_padding
898 + self.content_offset_y
899 + (row + 1) as f32 * self.cell_height)
900 .round();
901
902 let cell_w = x1 - x0;
903 let cell_h = y1 - y0;
904
905 let scale_x = cell_w / char_w;
906 let scale_y = cell_h / self.cell_height;
907
908 let baseline_offset = baseline_y_unrounded
915 - (self.window_padding
916 + self.content_offset_y
917 + row as f32 * self.cell_height);
918 let glyph_left = x0 + (info.bearing_x * scale_x).round();
919 let baseline_in_cell = (baseline_offset * scale_y).round();
920 let glyph_top = y0 + baseline_in_cell - info.bearing_y;
921
922 let render_w = info.width as f32 * scale_x;
923 let render_h = info.height as f32 * scale_y;
924
925 let (final_left, final_top, final_w, final_h) = if chars.len() == 1
929 && block_chars::should_snap_to_boundaries(char_type)
930 {
931 block_chars::snap_glyph_to_cell(
933 glyph_left, glyph_top, render_w, render_h, x0, y0, x1, y1, 3.0,
934 0.5,
935 )
936 } else {
937 (glyph_left, glyph_top, render_w, render_h)
938 };
939
940 row_text.push(TextInstance {
941 position: [
942 final_left / self.config.width as f32 * 2.0 - 1.0,
943 1.0 - (final_top / self.config.height as f32 * 2.0),
944 ],
945 size: [
946 final_w / self.config.width as f32 * 2.0,
947 final_h / self.config.height as f32 * 2.0,
948 ],
949 tex_offset: [info.x as f32 / 2048.0, info.y as f32 / 2048.0],
950 tex_size: [info.width as f32 / 2048.0, info.height as f32 / 2048.0],
951 color: render_fg_color,
952 is_colored: if info.is_colored { 1 } else { 0 },
953 });
954 }
955 }
956 x_offset += self.cell_width;
957 current_col += 1;
958 }
959
960 let bg_start = row * self.cols;
962 self.bg_instances[bg_start..bg_start + self.cols].copy_from_slice(&row_bg);
963
964 let text_start = row * self.cols * 2;
965 for i in 0..(self.cols * 2) {
967 self.text_instances[text_start + i].size = [0.0, 0.0];
968 }
969 let text_count = row_text.len().min(self.cols * 2);
971 self.text_instances[text_start..text_start + text_count]
972 .copy_from_slice(&row_text[..text_count]);
973
974 self.queue.write_buffer(
976 &self.bg_instance_buffer,
977 (bg_start * std::mem::size_of::<BackgroundInstance>()) as u64,
978 bytemuck::cast_slice(&row_bg),
979 );
980 self.queue.write_buffer(
981 &self.text_instance_buffer,
982 (text_start * std::mem::size_of::<TextInstance>()) as u64,
983 bytemuck::cast_slice(
984 &self.text_instances[text_start..text_start + self.cols * 2],
985 ),
986 );
987
988 self.row_cache[row] = Some(RowCacheEntry {});
989 self.dirty_rows[row] = false;
990 }
991 }
992
993 let base_overlay_index = self.cols * self.rows;
996 let mut overlay_instances = vec![
997 BackgroundInstance {
998 position: [0.0, 0.0],
999 size: [0.0, 0.0],
1000 color: [0.0, 0.0, 0.0, 0.0],
1001 };
1002 10
1003 ];
1004
1005 let cursor_visible = self.cursor_opacity > 0.0
1007 && !self.cursor_hidden_for_shader
1008 && (self.is_focused
1009 || self.unfocused_cursor_style != par_term_config::UnfocusedCursorStyle::Hidden);
1010
1011 let cursor_col = self.cursor_pos.0;
1013 let cursor_row = self.cursor_pos.1;
1014 let cursor_x0 =
1015 self.window_padding + self.content_offset_x + cursor_col as f32 * self.cell_width;
1016 let cursor_x1 = cursor_x0 + self.cell_width;
1017 let cursor_y0 =
1018 self.window_padding + self.content_offset_y + cursor_row as f32 * self.cell_height;
1019 let cursor_y1 = cursor_y0 + self.cell_height;
1020
1021 overlay_instances[0] = self.cursor_overlay.unwrap_or(BackgroundInstance {
1023 position: [0.0, 0.0],
1024 size: [0.0, 0.0],
1025 color: [0.0, 0.0, 0.0, 0.0],
1026 });
1027
1028 if cursor_visible && self.cursor_guide_enabled {
1030 let guide_x0 = self.window_padding + self.content_offset_x;
1031 let guide_x1 =
1032 self.config.width as f32 - self.window_padding - self.content_inset_right;
1033 overlay_instances[1] = BackgroundInstance {
1034 position: [
1035 guide_x0 / self.config.width as f32 * 2.0 - 1.0,
1036 1.0 - (cursor_y0 / self.config.height as f32 * 2.0),
1037 ],
1038 size: [
1039 (guide_x1 - guide_x0) / self.config.width as f32 * 2.0,
1040 (cursor_y1 - cursor_y0) / self.config.height as f32 * 2.0,
1041 ],
1042 color: self.cursor_guide_color,
1043 };
1044 }
1045
1046 if cursor_visible && self.cursor_shadow_enabled {
1048 let shadow_x0 = cursor_x0 + self.cursor_shadow_offset[0];
1049 let shadow_y0 = cursor_y0 + self.cursor_shadow_offset[1];
1050 overlay_instances[2] = BackgroundInstance {
1051 position: [
1052 shadow_x0 / self.config.width as f32 * 2.0 - 1.0,
1053 1.0 - (shadow_y0 / self.config.height as f32 * 2.0),
1054 ],
1055 size: [
1056 self.cell_width / self.config.width as f32 * 2.0,
1057 self.cell_height / self.config.height as f32 * 2.0,
1058 ],
1059 color: self.cursor_shadow_color,
1060 };
1061 }
1062
1063 if cursor_visible && self.cursor_boost > 0.0 {
1065 let glow_expand = 4.0 * self.scale_factor * self.cursor_boost; let glow_x0 = cursor_x0 - glow_expand;
1067 let glow_y0 = cursor_y0 - glow_expand;
1068 let glow_w = self.cell_width + glow_expand * 2.0;
1069 let glow_h = self.cell_height + glow_expand * 2.0;
1070 overlay_instances[3] = BackgroundInstance {
1071 position: [
1072 glow_x0 / self.config.width as f32 * 2.0 - 1.0,
1073 1.0 - (glow_y0 / self.config.height as f32 * 2.0),
1074 ],
1075 size: [
1076 glow_w / self.config.width as f32 * 2.0,
1077 glow_h / self.config.height as f32 * 2.0,
1078 ],
1079 color: [
1080 self.cursor_boost_color[0],
1081 self.cursor_boost_color[1],
1082 self.cursor_boost_color[2],
1083 self.cursor_boost * 0.3 * self.cursor_opacity, ],
1085 };
1086 }
1087
1088 let render_hollow = cursor_visible
1091 && !self.is_focused
1092 && self.unfocused_cursor_style == par_term_config::UnfocusedCursorStyle::Hollow;
1093
1094 if render_hollow {
1095 use par_term_emu_core_rust::cursor::CursorStyle;
1096 let is_block = matches!(
1097 self.cursor_style,
1098 CursorStyle::SteadyBlock | CursorStyle::BlinkingBlock
1099 );
1100
1101 if is_block {
1102 let border_width = 2.0; let color = [
1104 self.cursor_color[0],
1105 self.cursor_color[1],
1106 self.cursor_color[2],
1107 self.cursor_opacity,
1108 ];
1109
1110 overlay_instances[4] = BackgroundInstance {
1112 position: [
1113 cursor_x0 / self.config.width as f32 * 2.0 - 1.0,
1114 1.0 - (cursor_y0 / self.config.height as f32 * 2.0),
1115 ],
1116 size: [
1117 self.cell_width / self.config.width as f32 * 2.0,
1118 border_width / self.config.height as f32 * 2.0,
1119 ],
1120 color,
1121 };
1122
1123 overlay_instances[5] = BackgroundInstance {
1125 position: [
1126 cursor_x0 / self.config.width as f32 * 2.0 - 1.0,
1127 1.0 - ((cursor_y1 - border_width) / self.config.height as f32 * 2.0),
1128 ],
1129 size: [
1130 self.cell_width / self.config.width as f32 * 2.0,
1131 border_width / self.config.height as f32 * 2.0,
1132 ],
1133 color,
1134 };
1135
1136 overlay_instances[6] = BackgroundInstance {
1138 position: [
1139 cursor_x0 / self.config.width as f32 * 2.0 - 1.0,
1140 1.0 - ((cursor_y0 + border_width) / self.config.height as f32 * 2.0),
1141 ],
1142 size: [
1143 border_width / self.config.width as f32 * 2.0,
1144 (self.cell_height - border_width * 2.0) / self.config.height as f32 * 2.0,
1145 ],
1146 color,
1147 };
1148
1149 overlay_instances[7] = BackgroundInstance {
1151 position: [
1152 (cursor_x1 - border_width) / self.config.width as f32 * 2.0 - 1.0,
1153 1.0 - ((cursor_y0 + border_width) / self.config.height as f32 * 2.0),
1154 ],
1155 size: [
1156 border_width / self.config.width as f32 * 2.0,
1157 (self.cell_height - border_width * 2.0) / self.config.height as f32 * 2.0,
1158 ],
1159 color,
1160 };
1161 }
1162 }
1163
1164 for (i, instance) in overlay_instances.iter().enumerate() {
1166 self.bg_instances[base_overlay_index + i] = *instance;
1167 }
1168 self.queue.write_buffer(
1169 &self.bg_instance_buffer,
1170 (base_overlay_index * std::mem::size_of::<BackgroundInstance>()) as u64,
1171 bytemuck::cast_slice(&overlay_instances),
1172 );
1173
1174 let separator_base = self.cols * self.rows + 10;
1176 let mut separator_instances = vec![
1177 BackgroundInstance {
1178 position: [0.0, 0.0],
1179 size: [0.0, 0.0],
1180 color: [0.0, 0.0, 0.0, 0.0],
1181 };
1182 self.rows
1183 ];
1184
1185 if self.command_separator_enabled {
1186 let width_f = self.config.width as f32;
1187 let height_f = self.config.height as f32;
1188 for &(screen_row, exit_code, custom_color) in &self.visible_separator_marks {
1189 if screen_row < self.rows {
1190 let x0 = self.window_padding + self.content_offset_x;
1191 let x1 = width_f - self.window_padding - self.content_inset_right;
1192 let y0 = self.window_padding
1193 + self.content_offset_y
1194 + screen_row as f32 * self.cell_height;
1195 let color = self.separator_color(exit_code, custom_color, 1.0);
1196 separator_instances[screen_row] = BackgroundInstance {
1197 position: [x0 / width_f * 2.0 - 1.0, 1.0 - (y0 / height_f * 2.0)],
1198 size: [
1199 (x1 - x0) / width_f * 2.0,
1200 self.command_separator_thickness / height_f * 2.0,
1201 ],
1202 color,
1203 };
1204 }
1205 }
1206 }
1207
1208 for (i, instance) in separator_instances.iter().enumerate() {
1209 if separator_base + i < self.max_bg_instances {
1210 self.bg_instances[separator_base + i] = *instance;
1211 }
1212 }
1213 let separator_byte_offset = separator_base * std::mem::size_of::<BackgroundInstance>();
1214 let separator_byte_count =
1215 separator_instances.len() * std::mem::size_of::<BackgroundInstance>();
1216 if separator_byte_offset + separator_byte_count
1217 <= self.max_bg_instances * std::mem::size_of::<BackgroundInstance>()
1218 {
1219 self.queue.write_buffer(
1220 &self.bg_instance_buffer,
1221 separator_byte_offset as u64,
1222 bytemuck::cast_slice(&separator_instances),
1223 );
1224 }
1225
1226 Ok(())
1227 }
1228
1229 #[allow(dead_code, clippy::too_many_arguments)]
1247 pub fn render_pane_to_view(
1248 &mut self,
1249 surface_view: &wgpu::TextureView,
1250 viewport: &PaneViewport,
1251 cells: &[Cell],
1252 cols: usize,
1253 rows: usize,
1254 cursor_pos: Option<(usize, usize)>,
1255 cursor_opacity: f32,
1256 show_scrollbar: bool,
1257 clear_first: bool,
1258 skip_background_image: bool,
1259 separator_marks: &[SeparatorMark],
1260 pane_background: Option<&par_term_config::PaneBackground>,
1261 ) -> Result<()> {
1262 self.build_pane_instance_buffers(
1265 viewport,
1266 cells,
1267 cols,
1268 rows,
1269 cursor_pos,
1270 cursor_opacity,
1271 skip_background_image,
1272 separator_marks,
1273 )?;
1274
1275 let pane_bg_resources = if let Some(pane_bg) = pane_background
1279 && let Some(ref path) = pane_bg.image_path
1280 {
1281 self.pane_bg_cache.get(path.as_str()).map(|entry| {
1282 self.create_pane_bg_bind_group(
1283 entry,
1284 viewport.x,
1285 viewport.y,
1286 viewport.width,
1287 viewport.height,
1288 pane_bg.mode,
1289 pane_bg.opacity,
1290 )
1291 })
1292 } else {
1293 None
1294 };
1295
1296 let mut encoder = self
1297 .device
1298 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1299 label: Some("pane render encoder"),
1300 });
1301
1302 let load_op = if clear_first {
1304 let clear_color = if self.bg_is_solid_color {
1305 wgpu::Color {
1306 r: self.solid_bg_color[0] as f64
1307 * self.window_opacity as f64
1308 * viewport.opacity as f64,
1309 g: self.solid_bg_color[1] as f64
1310 * self.window_opacity as f64
1311 * viewport.opacity as f64,
1312 b: self.solid_bg_color[2] as f64
1313 * self.window_opacity as f64
1314 * viewport.opacity as f64,
1315 a: self.window_opacity as f64 * viewport.opacity as f64,
1316 }
1317 } else {
1318 wgpu::Color {
1319 r: self.background_color[0] as f64
1320 * self.window_opacity as f64
1321 * viewport.opacity as f64,
1322 g: self.background_color[1] as f64
1323 * self.window_opacity as f64
1324 * viewport.opacity as f64,
1325 b: self.background_color[2] as f64
1326 * self.window_opacity as f64
1327 * viewport.opacity as f64,
1328 a: self.window_opacity as f64 * viewport.opacity as f64,
1329 }
1330 };
1331 wgpu::LoadOp::Clear(clear_color)
1332 } else {
1333 wgpu::LoadOp::Load
1334 };
1335
1336 {
1337 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1338 label: Some("pane render pass"),
1339 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1340 view: surface_view,
1341 resolve_target: None,
1342 ops: wgpu::Operations {
1343 load: load_op,
1344 store: wgpu::StoreOp::Store,
1345 },
1346 depth_slice: None,
1347 })],
1348 depth_stencil_attachment: None,
1349 timestamp_writes: None,
1350 occlusion_query_set: None,
1351 });
1352
1353 let (sx, sy, sw, sh) = viewport.to_scissor_rect();
1355 render_pass.set_scissor_rect(sx, sy, sw, sh);
1356
1357 if let Some((ref bind_group, ref _buf)) = pane_bg_resources {
1361 render_pass.set_pipeline(&self.bg_image_pipeline);
1362 render_pass.set_bind_group(0, bind_group, &[]);
1363 render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
1364 render_pass.draw(0..4, 0..1);
1365 }
1366
1367 render_pass.set_pipeline(&self.bg_pipeline);
1369 render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
1370 render_pass.set_vertex_buffer(1, self.bg_instance_buffer.slice(..));
1371 render_pass.draw(0..4, 0..self.max_bg_instances as u32);
1372
1373 render_pass.set_pipeline(&self.text_pipeline);
1375 render_pass.set_bind_group(0, &self.text_bind_group, &[]);
1376 render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
1377 render_pass.set_vertex_buffer(1, self.text_instance_buffer.slice(..));
1378 render_pass.draw(0..4, 0..self.max_text_instances as u32);
1379
1380 if show_scrollbar {
1382 render_pass.set_scissor_rect(0, 0, self.config.width, self.config.height);
1384 self.scrollbar.render(&mut render_pass);
1385 }
1386 }
1387
1388 self.queue.submit(std::iter::once(encoder.finish()));
1389 Ok(())
1390 }
1391
1392 #[allow(clippy::too_many_arguments)]
1401 fn build_pane_instance_buffers(
1402 &mut self,
1403 viewport: &PaneViewport,
1404 cells: &[Cell],
1405 cols: usize,
1406 rows: usize,
1407 cursor_pos: Option<(usize, usize)>,
1408 cursor_opacity: f32,
1409 skip_solid_background: bool,
1410 separator_marks: &[SeparatorMark],
1411 ) -> Result<()> {
1412 let _shaping_options = ShapingOptions {
1413 enable_ligatures: self.enable_ligatures,
1414 enable_kerning: self.enable_kerning,
1415 ..Default::default()
1416 };
1417
1418 for instance in &mut self.bg_instances {
1420 instance.size = [0.0, 0.0];
1421 instance.color = [0.0, 0.0, 0.0, 0.0];
1422 }
1423
1424 let bg_start_index = if !skip_solid_background && !self.bg_instances.is_empty() {
1428 let bg_color = self.background_color;
1429 let opacity = self.window_opacity * viewport.opacity;
1430 self.bg_instances[0] = super::types::BackgroundInstance {
1431 position: [viewport.x, viewport.y],
1432 size: [viewport.width, viewport.height],
1433 color: [
1434 bg_color[0] * opacity,
1435 bg_color[1] * opacity,
1436 bg_color[2] * opacity,
1437 opacity,
1438 ],
1439 };
1440 1 } else {
1442 0 };
1444
1445 for instance in &mut self.text_instances {
1446 instance.size = [0.0, 0.0];
1447 }
1448
1449 let mut bg_index = bg_start_index;
1451 let mut text_index = 0;
1452
1453 let (content_x, content_y) = viewport.content_origin();
1455 let opacity_multiplier = viewport.opacity;
1456
1457 for row in 0..rows {
1458 let row_start = row * cols;
1459 let row_end = (row + 1) * cols;
1460 if row_start >= cells.len() {
1461 break;
1462 }
1463 let row_cells = &cells[row_start..row_end.min(cells.len())];
1464
1465 let mut col = 0;
1467 while col < row_cells.len() {
1468 let cell = &row_cells[col];
1469 let is_default_bg = (cell.bg_color[0] as f32 / 255.0 - self.background_color[0])
1470 .abs()
1471 < 0.001
1472 && (cell.bg_color[1] as f32 / 255.0 - self.background_color[1]).abs() < 0.001
1473 && (cell.bg_color[2] as f32 / 255.0 - self.background_color[2]).abs() < 0.001;
1474
1475 let has_cursor = cursor_pos.is_some_and(|(cx, cy)| cx == col && cy == row)
1477 && cursor_opacity > 0.0
1478 && !self.cursor_hidden_for_shader;
1479
1480 if is_default_bg && !has_cursor {
1481 col += 1;
1482 continue;
1483 }
1484
1485 let bg_alpha =
1487 if self.transparency_affects_only_default_background && !is_default_bg {
1488 1.0
1489 } else {
1490 self.window_opacity
1491 };
1492 let pane_alpha = bg_alpha * opacity_multiplier;
1493 let mut bg_color = [
1494 cell.bg_color[0] as f32 / 255.0,
1495 cell.bg_color[1] as f32 / 255.0,
1496 cell.bg_color[2] as f32 / 255.0,
1497 pane_alpha,
1498 ];
1499
1500 if has_cursor {
1502 use par_term_emu_core_rust::cursor::CursorStyle;
1503 match self.cursor_style {
1504 CursorStyle::SteadyBlock | CursorStyle::BlinkingBlock => {
1505 for (bg, &cursor) in bg_color.iter_mut().take(3).zip(&self.cursor_color)
1506 {
1507 *bg = *bg * (1.0 - cursor_opacity) + cursor * cursor_opacity;
1508 }
1509 bg_color[3] = bg_color[3].max(cursor_opacity * opacity_multiplier);
1510 }
1511 _ => {}
1512 }
1513
1514 let x0 = content_x + col as f32 * self.cell_width;
1516 let y0 = content_y + row as f32 * self.cell_height;
1517 let x1 = x0 + self.cell_width;
1518 let y1 = y0 + self.cell_height;
1519
1520 if bg_index < self.max_bg_instances {
1521 self.bg_instances[bg_index] = BackgroundInstance {
1522 position: [
1523 x0 / self.config.width as f32 * 2.0 - 1.0,
1524 1.0 - (y0 / self.config.height as f32 * 2.0),
1525 ],
1526 size: [
1527 (x1 - x0) / self.config.width as f32 * 2.0,
1528 (y1 - y0) / self.config.height as f32 * 2.0,
1529 ],
1530 color: bg_color,
1531 };
1532 bg_index += 1;
1533 }
1534 col += 1;
1535 continue;
1536 }
1537
1538 let start_col = col;
1540 let run_color = cell.bg_color;
1541 col += 1;
1542 while col < row_cells.len() {
1543 let next_cell = &row_cells[col];
1544 let next_has_cursor = cursor_pos.is_some_and(|(cx, cy)| cx == col && cy == row)
1545 && cursor_opacity > 0.0;
1546 if next_cell.bg_color != run_color || next_has_cursor {
1547 break;
1548 }
1549 col += 1;
1550 }
1551 let run_length = col - start_col;
1552
1553 let x0 = content_x + start_col as f32 * self.cell_width;
1555 let x1 = content_x + (start_col + run_length) as f32 * self.cell_width;
1556 let y0 = content_y + row as f32 * self.cell_height;
1557 let y1 = y0 + self.cell_height;
1558
1559 if bg_index < self.max_bg_instances {
1560 self.bg_instances[bg_index] = BackgroundInstance {
1561 position: [
1562 x0 / self.config.width as f32 * 2.0 - 1.0,
1563 1.0 - (y0 / self.config.height as f32 * 2.0),
1564 ],
1565 size: [
1566 (x1 - x0) / self.config.width as f32 * 2.0,
1567 (y1 - y0) / self.config.height as f32 * 2.0,
1568 ],
1569 color: bg_color,
1570 };
1571 bg_index += 1;
1572 }
1573 }
1574
1575 let natural_line_height = self.font_ascent + self.font_descent + self.font_leading;
1577 let vertical_padding = (self.cell_height - natural_line_height).max(0.0) / 2.0;
1578 let baseline_y =
1579 content_y + (row as f32 * self.cell_height) + vertical_padding + self.font_ascent;
1580
1581 let text_alpha = if self.keep_text_opaque {
1583 opacity_multiplier } else {
1585 self.window_opacity * opacity_multiplier
1586 };
1587
1588 for (col_idx, cell) in row_cells.iter().enumerate() {
1589 if cell.wide_char_spacer || cell.grapheme == " " {
1590 continue;
1591 }
1592
1593 let chars: Vec<char> = cell.grapheme.chars().collect();
1594 if chars.is_empty() {
1595 continue;
1596 }
1597
1598 let ch = chars[0];
1599
1600 let char_type = block_chars::classify_char(ch);
1602 if chars.len() == 1 && block_chars::should_render_geometrically(char_type) {
1603 let char_w = if cell.wide_char {
1604 self.cell_width * 2.0
1605 } else {
1606 self.cell_width
1607 };
1608 let x0 = content_x + col_idx as f32 * self.cell_width;
1609 let y0 = content_y + row as f32 * self.cell_height;
1610
1611 let fg_color = [
1612 cell.fg_color[0] as f32 / 255.0,
1613 cell.fg_color[1] as f32 / 255.0,
1614 cell.fg_color[2] as f32 / 255.0,
1615 text_alpha,
1616 ];
1617
1618 let aspect_ratio = self.cell_height / char_w;
1620 if let Some(box_geo) = block_chars::get_box_drawing_geometry(ch, aspect_ratio) {
1621 for segment in &box_geo.segments {
1622 let rect = segment.to_pixel_rect(x0, y0, char_w, self.cell_height);
1623
1624 let extension = 1.0;
1626 let ext_x = if segment.x <= 0.01 { extension } else { 0.0 };
1627 let ext_y = if segment.y <= 0.01 { extension } else { 0.0 };
1628 let ext_w = if segment.x + segment.width >= 0.99 {
1629 extension
1630 } else {
1631 0.0
1632 };
1633 let ext_h = if segment.y + segment.height >= 0.99 {
1634 extension
1635 } else {
1636 0.0
1637 };
1638
1639 let final_x = rect.x - ext_x;
1640 let final_y = rect.y - ext_y;
1641 let final_w = rect.width + ext_x + ext_w;
1642 let final_h = rect.height + ext_y + ext_h;
1643
1644 if text_index < self.max_text_instances {
1645 self.text_instances[text_index] = TextInstance {
1646 position: [
1647 final_x / self.config.width as f32 * 2.0 - 1.0,
1648 1.0 - (final_y / self.config.height as f32 * 2.0),
1649 ],
1650 size: [
1651 final_w / self.config.width as f32 * 2.0,
1652 final_h / self.config.height as f32 * 2.0,
1653 ],
1654 tex_offset: [
1655 self.solid_pixel_offset.0 as f32 / 2048.0,
1656 self.solid_pixel_offset.1 as f32 / 2048.0,
1657 ],
1658 tex_size: [1.0 / 2048.0, 1.0 / 2048.0],
1659 color: fg_color,
1660 is_colored: 0,
1661 };
1662 text_index += 1;
1663 }
1664 }
1665 continue;
1666 }
1667
1668 if let Some(geo_block) = block_chars::get_geometric_block(ch) {
1670 let rect = geo_block.to_pixel_rect(x0, y0, char_w, self.cell_height);
1671
1672 let extension = 1.0;
1674 let ext_x = if geo_block.x == 0.0 { extension } else { 0.0 };
1675 let ext_y = if geo_block.y == 0.0 { extension } else { 0.0 };
1676 let ext_w = if geo_block.x + geo_block.width >= 1.0 {
1677 extension
1678 } else {
1679 0.0
1680 };
1681 let ext_h = if geo_block.y + geo_block.height >= 1.0 {
1682 extension
1683 } else {
1684 0.0
1685 };
1686
1687 let final_x = rect.x - ext_x;
1688 let final_y = rect.y - ext_y;
1689 let final_w = rect.width + ext_x + ext_w;
1690 let final_h = rect.height + ext_y + ext_h;
1691
1692 if text_index < self.max_text_instances {
1693 self.text_instances[text_index] = TextInstance {
1694 position: [
1695 final_x / self.config.width as f32 * 2.0 - 1.0,
1696 1.0 - (final_y / self.config.height as f32 * 2.0),
1697 ],
1698 size: [
1699 final_w / self.config.width as f32 * 2.0,
1700 final_h / self.config.height as f32 * 2.0,
1701 ],
1702 tex_offset: [
1703 self.solid_pixel_offset.0 as f32 / 2048.0,
1704 self.solid_pixel_offset.1 as f32 / 2048.0,
1705 ],
1706 tex_size: [1.0 / 2048.0, 1.0 / 2048.0],
1707 color: fg_color,
1708 is_colored: 0,
1709 };
1710 text_index += 1;
1711 }
1712 continue;
1713 }
1714 }
1715
1716 let glyph_result = if chars.len() > 1 {
1718 self.font_manager
1719 .find_grapheme_glyph(&cell.grapheme, cell.bold, cell.italic)
1720 } else {
1721 self.font_manager.find_glyph(ch, cell.bold, cell.italic)
1722 };
1723
1724 if let Some((font_idx, glyph_id)) = glyph_result {
1725 let cache_key = ((font_idx as u64) << 32) | (glyph_id as u64);
1726 let info = if self.glyph_cache.contains_key(&cache_key) {
1727 self.lru_remove(cache_key);
1728 self.lru_push_front(cache_key);
1729 self.glyph_cache.get(&cache_key).unwrap().clone()
1730 } else if let Some(raster) = self.rasterize_glyph(font_idx, glyph_id) {
1731 let info = self.upload_glyph(cache_key, &raster);
1732 self.glyph_cache.insert(cache_key, info.clone());
1733 self.lru_push_front(cache_key);
1734 info
1735 } else {
1736 continue;
1737 };
1738
1739 let char_w = if cell.wide_char {
1740 self.cell_width * 2.0
1741 } else {
1742 self.cell_width
1743 };
1744 let x0 = content_x + col_idx as f32 * self.cell_width;
1745 let y0 = content_y + row as f32 * self.cell_height;
1746 let x1 = x0 + char_w;
1747 let y1 = y0 + self.cell_height;
1748
1749 let cell_w = x1 - x0;
1750 let cell_h = y1 - y0;
1751 let scale_x = cell_w / char_w;
1752 let scale_y = cell_h / self.cell_height;
1753
1754 let baseline_offset = baseline_y - (content_y + row as f32 * self.cell_height);
1755 let glyph_left = x0 + (info.bearing_x * scale_x).round();
1756 let baseline_in_cell = (baseline_offset * scale_y).round();
1757 let glyph_top = y0 + baseline_in_cell - info.bearing_y;
1758
1759 let render_w = info.width as f32 * scale_x;
1760 let render_h = info.height as f32 * scale_y;
1761
1762 let (final_left, final_top, final_w, final_h) =
1763 if chars.len() == 1 && block_chars::should_snap_to_boundaries(char_type) {
1764 block_chars::snap_glyph_to_cell(
1765 glyph_left, glyph_top, render_w, render_h, x0, y0, x1, y1, 3.0, 0.5,
1766 )
1767 } else {
1768 (glyph_left, glyph_top, render_w, render_h)
1769 };
1770
1771 let fg_color = [
1772 cell.fg_color[0] as f32 / 255.0,
1773 cell.fg_color[1] as f32 / 255.0,
1774 cell.fg_color[2] as f32 / 255.0,
1775 text_alpha,
1776 ];
1777
1778 if text_index < self.max_text_instances {
1779 self.text_instances[text_index] = TextInstance {
1780 position: [
1781 final_left / self.config.width as f32 * 2.0 - 1.0,
1782 1.0 - (final_top / self.config.height as f32 * 2.0),
1783 ],
1784 size: [
1785 final_w / self.config.width as f32 * 2.0,
1786 final_h / self.config.height as f32 * 2.0,
1787 ],
1788 tex_offset: [info.x as f32 / 2048.0, info.y as f32 / 2048.0],
1789 tex_size: [info.width as f32 / 2048.0, info.height as f32 / 2048.0],
1790 color: fg_color,
1791 is_colored: if info.is_colored { 1 } else { 0 },
1792 };
1793 text_index += 1;
1794 }
1795 }
1796 }
1797 }
1798
1799 if self.command_separator_enabled && !separator_marks.is_empty() {
1801 let width_f = self.config.width as f32;
1802 let height_f = self.config.height as f32;
1803 let opacity_multiplier = viewport.opacity;
1804 for &(screen_row, exit_code, custom_color) in separator_marks {
1805 if screen_row < rows && bg_index < self.max_bg_instances {
1806 let x0 = content_x;
1807 let x1 = content_x + cols as f32 * self.cell_width;
1808 let y0 = content_y + screen_row as f32 * self.cell_height;
1809 let color = self.separator_color(exit_code, custom_color, opacity_multiplier);
1810 self.bg_instances[bg_index] = BackgroundInstance {
1811 position: [x0 / width_f * 2.0 - 1.0, 1.0 - (y0 / height_f * 2.0)],
1812 size: [
1813 (x1 - x0) / width_f * 2.0,
1814 self.command_separator_thickness / height_f * 2.0,
1815 ],
1816 color,
1817 };
1818 bg_index += 1;
1819 }
1820 }
1821 }
1822 let _ = bg_index; self.queue.write_buffer(
1826 &self.bg_instance_buffer,
1827 0,
1828 bytemuck::cast_slice(&self.bg_instances),
1829 );
1830 self.queue.write_buffer(
1831 &self.text_instance_buffer,
1832 0,
1833 bytemuck::cast_slice(&self.text_instances),
1834 );
1835
1836 Ok(())
1837 }
1838}