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 force_monochrome =
871 chars.len() == 1 && super::atlas::should_render_as_symbol(*ch);
872 let info = if self.glyph_cache.contains_key(&cache_key) {
873 self.lru_remove(cache_key);
875 self.lru_push_front(cache_key);
876 self.glyph_cache.get(&cache_key).unwrap().clone()
877 } else if let Some(raster) =
878 self.rasterize_glyph(font_idx, glyph_id, force_monochrome)
879 {
880 let info = self.upload_glyph(cache_key, &raster);
881 self.glyph_cache.insert(cache_key, info.clone());
882 self.lru_push_front(cache_key);
883 info
884 } else {
885 x_offset += self.cell_width;
886 continue;
887 };
888
889 let char_w = if is_wide {
890 self.cell_width * 2.0
891 } else {
892 self.cell_width
893 };
894 let x0 =
895 (self.window_padding + self.content_offset_x + x_offset).round();
896 let x1 =
897 (self.window_padding + self.content_offset_x + x_offset + char_w)
898 .round();
899 let y0 = (self.window_padding
900 + self.content_offset_y
901 + row as f32 * self.cell_height)
902 .round();
903 let y1 = (self.window_padding
904 + self.content_offset_y
905 + (row + 1) as f32 * self.cell_height)
906 .round();
907
908 let cell_w = x1 - x0;
909 let cell_h = y1 - y0;
910
911 let scale_x = cell_w / char_w;
912 let scale_y = cell_h / self.cell_height;
913
914 let baseline_offset = baseline_y_unrounded
921 - (self.window_padding
922 + self.content_offset_y
923 + row as f32 * self.cell_height);
924 let glyph_left = x0 + (info.bearing_x * scale_x).round();
925 let baseline_in_cell = (baseline_offset * scale_y).round();
926 let glyph_top = y0 + baseline_in_cell - info.bearing_y;
927
928 let render_w = info.width as f32 * scale_x;
929 let render_h = info.height as f32 * scale_y;
930
931 let (final_left, final_top, final_w, final_h) = if chars.len() == 1
935 && block_chars::should_snap_to_boundaries(char_type)
936 {
937 block_chars::snap_glyph_to_cell(
939 glyph_left, glyph_top, render_w, render_h, x0, y0, x1, y1, 3.0,
940 0.5,
941 )
942 } else {
943 (glyph_left, glyph_top, render_w, render_h)
944 };
945
946 row_text.push(TextInstance {
947 position: [
948 final_left / self.config.width as f32 * 2.0 - 1.0,
949 1.0 - (final_top / self.config.height as f32 * 2.0),
950 ],
951 size: [
952 final_w / self.config.width as f32 * 2.0,
953 final_h / self.config.height as f32 * 2.0,
954 ],
955 tex_offset: [info.x as f32 / 2048.0, info.y as f32 / 2048.0],
956 tex_size: [info.width as f32 / 2048.0, info.height as f32 / 2048.0],
957 color: render_fg_color,
958 is_colored: if info.is_colored { 1 } else { 0 },
959 });
960 }
961 }
962 x_offset += self.cell_width;
963 current_col += 1;
964 }
965
966 let bg_start = row * self.cols;
968 self.bg_instances[bg_start..bg_start + self.cols].copy_from_slice(&row_bg);
969
970 let text_start = row * self.cols * 2;
971 for i in 0..(self.cols * 2) {
973 self.text_instances[text_start + i].size = [0.0, 0.0];
974 }
975 let text_count = row_text.len().min(self.cols * 2);
977 self.text_instances[text_start..text_start + text_count]
978 .copy_from_slice(&row_text[..text_count]);
979
980 self.queue.write_buffer(
982 &self.bg_instance_buffer,
983 (bg_start * std::mem::size_of::<BackgroundInstance>()) as u64,
984 bytemuck::cast_slice(&row_bg),
985 );
986 self.queue.write_buffer(
987 &self.text_instance_buffer,
988 (text_start * std::mem::size_of::<TextInstance>()) as u64,
989 bytemuck::cast_slice(
990 &self.text_instances[text_start..text_start + self.cols * 2],
991 ),
992 );
993
994 self.row_cache[row] = Some(RowCacheEntry {});
995 self.dirty_rows[row] = false;
996 }
997 }
998
999 let base_overlay_index = self.cols * self.rows;
1002 let mut overlay_instances = vec![
1003 BackgroundInstance {
1004 position: [0.0, 0.0],
1005 size: [0.0, 0.0],
1006 color: [0.0, 0.0, 0.0, 0.0],
1007 };
1008 10
1009 ];
1010
1011 let cursor_visible = self.cursor_opacity > 0.0
1013 && !self.cursor_hidden_for_shader
1014 && (self.is_focused
1015 || self.unfocused_cursor_style != par_term_config::UnfocusedCursorStyle::Hidden);
1016
1017 let cursor_col = self.cursor_pos.0;
1019 let cursor_row = self.cursor_pos.1;
1020 let cursor_x0 =
1021 self.window_padding + self.content_offset_x + cursor_col as f32 * self.cell_width;
1022 let cursor_x1 = cursor_x0 + self.cell_width;
1023 let cursor_y0 =
1024 self.window_padding + self.content_offset_y + cursor_row as f32 * self.cell_height;
1025 let cursor_y1 = cursor_y0 + self.cell_height;
1026
1027 overlay_instances[0] = self.cursor_overlay.unwrap_or(BackgroundInstance {
1029 position: [0.0, 0.0],
1030 size: [0.0, 0.0],
1031 color: [0.0, 0.0, 0.0, 0.0],
1032 });
1033
1034 if cursor_visible && self.cursor_guide_enabled {
1036 let guide_x0 = self.window_padding + self.content_offset_x;
1037 let guide_x1 =
1038 self.config.width as f32 - self.window_padding - self.content_inset_right;
1039 overlay_instances[1] = BackgroundInstance {
1040 position: [
1041 guide_x0 / self.config.width as f32 * 2.0 - 1.0,
1042 1.0 - (cursor_y0 / self.config.height as f32 * 2.0),
1043 ],
1044 size: [
1045 (guide_x1 - guide_x0) / self.config.width as f32 * 2.0,
1046 (cursor_y1 - cursor_y0) / self.config.height as f32 * 2.0,
1047 ],
1048 color: self.cursor_guide_color,
1049 };
1050 }
1051
1052 if cursor_visible && self.cursor_shadow_enabled {
1054 let shadow_x0 = cursor_x0 + self.cursor_shadow_offset[0];
1055 let shadow_y0 = cursor_y0 + self.cursor_shadow_offset[1];
1056 overlay_instances[2] = BackgroundInstance {
1057 position: [
1058 shadow_x0 / self.config.width as f32 * 2.0 - 1.0,
1059 1.0 - (shadow_y0 / self.config.height as f32 * 2.0),
1060 ],
1061 size: [
1062 self.cell_width / self.config.width as f32 * 2.0,
1063 self.cell_height / self.config.height as f32 * 2.0,
1064 ],
1065 color: self.cursor_shadow_color,
1066 };
1067 }
1068
1069 if cursor_visible && self.cursor_boost > 0.0 {
1071 let glow_expand = 4.0 * self.scale_factor * self.cursor_boost; let glow_x0 = cursor_x0 - glow_expand;
1073 let glow_y0 = cursor_y0 - glow_expand;
1074 let glow_w = self.cell_width + glow_expand * 2.0;
1075 let glow_h = self.cell_height + glow_expand * 2.0;
1076 overlay_instances[3] = BackgroundInstance {
1077 position: [
1078 glow_x0 / self.config.width as f32 * 2.0 - 1.0,
1079 1.0 - (glow_y0 / self.config.height as f32 * 2.0),
1080 ],
1081 size: [
1082 glow_w / self.config.width as f32 * 2.0,
1083 glow_h / self.config.height as f32 * 2.0,
1084 ],
1085 color: [
1086 self.cursor_boost_color[0],
1087 self.cursor_boost_color[1],
1088 self.cursor_boost_color[2],
1089 self.cursor_boost * 0.3 * self.cursor_opacity, ],
1091 };
1092 }
1093
1094 let render_hollow = cursor_visible
1097 && !self.is_focused
1098 && self.unfocused_cursor_style == par_term_config::UnfocusedCursorStyle::Hollow;
1099
1100 if render_hollow {
1101 use par_term_emu_core_rust::cursor::CursorStyle;
1102 let is_block = matches!(
1103 self.cursor_style,
1104 CursorStyle::SteadyBlock | CursorStyle::BlinkingBlock
1105 );
1106
1107 if is_block {
1108 let border_width = 2.0; let color = [
1110 self.cursor_color[0],
1111 self.cursor_color[1],
1112 self.cursor_color[2],
1113 self.cursor_opacity,
1114 ];
1115
1116 overlay_instances[4] = BackgroundInstance {
1118 position: [
1119 cursor_x0 / self.config.width as f32 * 2.0 - 1.0,
1120 1.0 - (cursor_y0 / self.config.height as f32 * 2.0),
1121 ],
1122 size: [
1123 self.cell_width / self.config.width as f32 * 2.0,
1124 border_width / self.config.height as f32 * 2.0,
1125 ],
1126 color,
1127 };
1128
1129 overlay_instances[5] = BackgroundInstance {
1131 position: [
1132 cursor_x0 / self.config.width as f32 * 2.0 - 1.0,
1133 1.0 - ((cursor_y1 - border_width) / self.config.height as f32 * 2.0),
1134 ],
1135 size: [
1136 self.cell_width / self.config.width as f32 * 2.0,
1137 border_width / self.config.height as f32 * 2.0,
1138 ],
1139 color,
1140 };
1141
1142 overlay_instances[6] = BackgroundInstance {
1144 position: [
1145 cursor_x0 / self.config.width as f32 * 2.0 - 1.0,
1146 1.0 - ((cursor_y0 + border_width) / self.config.height as f32 * 2.0),
1147 ],
1148 size: [
1149 border_width / self.config.width as f32 * 2.0,
1150 (self.cell_height - border_width * 2.0) / self.config.height as f32 * 2.0,
1151 ],
1152 color,
1153 };
1154
1155 overlay_instances[7] = BackgroundInstance {
1157 position: [
1158 (cursor_x1 - border_width) / self.config.width as f32 * 2.0 - 1.0,
1159 1.0 - ((cursor_y0 + border_width) / self.config.height as f32 * 2.0),
1160 ],
1161 size: [
1162 border_width / self.config.width as f32 * 2.0,
1163 (self.cell_height - border_width * 2.0) / self.config.height as f32 * 2.0,
1164 ],
1165 color,
1166 };
1167 }
1168 }
1169
1170 for (i, instance) in overlay_instances.iter().enumerate() {
1172 self.bg_instances[base_overlay_index + i] = *instance;
1173 }
1174 self.queue.write_buffer(
1175 &self.bg_instance_buffer,
1176 (base_overlay_index * std::mem::size_of::<BackgroundInstance>()) as u64,
1177 bytemuck::cast_slice(&overlay_instances),
1178 );
1179
1180 let separator_base = self.cols * self.rows + 10;
1182 let mut separator_instances = vec![
1183 BackgroundInstance {
1184 position: [0.0, 0.0],
1185 size: [0.0, 0.0],
1186 color: [0.0, 0.0, 0.0, 0.0],
1187 };
1188 self.rows
1189 ];
1190
1191 if self.command_separator_enabled {
1192 let width_f = self.config.width as f32;
1193 let height_f = self.config.height as f32;
1194 for &(screen_row, exit_code, custom_color) in &self.visible_separator_marks {
1195 if screen_row < self.rows {
1196 let x0 = self.window_padding + self.content_offset_x;
1197 let x1 = width_f - self.window_padding - self.content_inset_right;
1198 let y0 = self.window_padding
1199 + self.content_offset_y
1200 + screen_row as f32 * self.cell_height;
1201 let color = self.separator_color(exit_code, custom_color, 1.0);
1202 separator_instances[screen_row] = BackgroundInstance {
1203 position: [x0 / width_f * 2.0 - 1.0, 1.0 - (y0 / height_f * 2.0)],
1204 size: [
1205 (x1 - x0) / width_f * 2.0,
1206 self.command_separator_thickness / height_f * 2.0,
1207 ],
1208 color,
1209 };
1210 }
1211 }
1212 }
1213
1214 for (i, instance) in separator_instances.iter().enumerate() {
1215 if separator_base + i < self.max_bg_instances {
1216 self.bg_instances[separator_base + i] = *instance;
1217 }
1218 }
1219 let separator_byte_offset = separator_base * std::mem::size_of::<BackgroundInstance>();
1220 let separator_byte_count =
1221 separator_instances.len() * std::mem::size_of::<BackgroundInstance>();
1222 if separator_byte_offset + separator_byte_count
1223 <= self.max_bg_instances * std::mem::size_of::<BackgroundInstance>()
1224 {
1225 self.queue.write_buffer(
1226 &self.bg_instance_buffer,
1227 separator_byte_offset as u64,
1228 bytemuck::cast_slice(&separator_instances),
1229 );
1230 }
1231
1232 Ok(())
1233 }
1234
1235 #[allow(dead_code, clippy::too_many_arguments)]
1253 pub fn render_pane_to_view(
1254 &mut self,
1255 surface_view: &wgpu::TextureView,
1256 viewport: &PaneViewport,
1257 cells: &[Cell],
1258 cols: usize,
1259 rows: usize,
1260 cursor_pos: Option<(usize, usize)>,
1261 cursor_opacity: f32,
1262 show_scrollbar: bool,
1263 clear_first: bool,
1264 skip_background_image: bool,
1265 separator_marks: &[SeparatorMark],
1266 pane_background: Option<&par_term_config::PaneBackground>,
1267 ) -> Result<()> {
1268 self.build_pane_instance_buffers(
1271 viewport,
1272 cells,
1273 cols,
1274 rows,
1275 cursor_pos,
1276 cursor_opacity,
1277 skip_background_image,
1278 separator_marks,
1279 )?;
1280
1281 let pane_bg_resources = if let Some(pane_bg) = pane_background
1285 && let Some(ref path) = pane_bg.image_path
1286 {
1287 self.pane_bg_cache.get(path.as_str()).map(|entry| {
1288 self.create_pane_bg_bind_group(
1289 entry,
1290 viewport.x,
1291 viewport.y,
1292 viewport.width,
1293 viewport.height,
1294 pane_bg.mode,
1295 pane_bg.opacity,
1296 )
1297 })
1298 } else {
1299 None
1300 };
1301
1302 let mut encoder = self
1303 .device
1304 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
1305 label: Some("pane render encoder"),
1306 });
1307
1308 let load_op = if clear_first {
1310 let clear_color = if self.bg_is_solid_color {
1311 wgpu::Color {
1312 r: self.solid_bg_color[0] as f64
1313 * self.window_opacity as f64
1314 * viewport.opacity as f64,
1315 g: self.solid_bg_color[1] as f64
1316 * self.window_opacity as f64
1317 * viewport.opacity as f64,
1318 b: self.solid_bg_color[2] as f64
1319 * self.window_opacity as f64
1320 * viewport.opacity as f64,
1321 a: self.window_opacity as f64 * viewport.opacity as f64,
1322 }
1323 } else {
1324 wgpu::Color {
1325 r: self.background_color[0] as f64
1326 * self.window_opacity as f64
1327 * viewport.opacity as f64,
1328 g: self.background_color[1] as f64
1329 * self.window_opacity as f64
1330 * viewport.opacity as f64,
1331 b: self.background_color[2] as f64
1332 * self.window_opacity as f64
1333 * viewport.opacity as f64,
1334 a: self.window_opacity as f64 * viewport.opacity as f64,
1335 }
1336 };
1337 wgpu::LoadOp::Clear(clear_color)
1338 } else {
1339 wgpu::LoadOp::Load
1340 };
1341
1342 {
1343 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
1344 label: Some("pane render pass"),
1345 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
1346 view: surface_view,
1347 resolve_target: None,
1348 ops: wgpu::Operations {
1349 load: load_op,
1350 store: wgpu::StoreOp::Store,
1351 },
1352 depth_slice: None,
1353 })],
1354 depth_stencil_attachment: None,
1355 timestamp_writes: None,
1356 occlusion_query_set: None,
1357 });
1358
1359 let (sx, sy, sw, sh) = viewport.to_scissor_rect();
1361 render_pass.set_scissor_rect(sx, sy, sw, sh);
1362
1363 if let Some((ref bind_group, ref _buf)) = pane_bg_resources {
1367 render_pass.set_pipeline(&self.bg_image_pipeline);
1368 render_pass.set_bind_group(0, bind_group, &[]);
1369 render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
1370 render_pass.draw(0..4, 0..1);
1371 }
1372
1373 render_pass.set_pipeline(&self.bg_pipeline);
1375 render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
1376 render_pass.set_vertex_buffer(1, self.bg_instance_buffer.slice(..));
1377 render_pass.draw(0..4, 0..self.max_bg_instances as u32);
1378
1379 render_pass.set_pipeline(&self.text_pipeline);
1381 render_pass.set_bind_group(0, &self.text_bind_group, &[]);
1382 render_pass.set_vertex_buffer(0, self.vertex_buffer.slice(..));
1383 render_pass.set_vertex_buffer(1, self.text_instance_buffer.slice(..));
1384 render_pass.draw(0..4, 0..self.max_text_instances as u32);
1385
1386 if show_scrollbar {
1388 render_pass.set_scissor_rect(0, 0, self.config.width, self.config.height);
1390 self.scrollbar.render(&mut render_pass);
1391 }
1392 }
1393
1394 self.queue.submit(std::iter::once(encoder.finish()));
1395 Ok(())
1396 }
1397
1398 #[allow(clippy::too_many_arguments)]
1407 fn build_pane_instance_buffers(
1408 &mut self,
1409 viewport: &PaneViewport,
1410 cells: &[Cell],
1411 cols: usize,
1412 rows: usize,
1413 cursor_pos: Option<(usize, usize)>,
1414 cursor_opacity: f32,
1415 skip_solid_background: bool,
1416 separator_marks: &[SeparatorMark],
1417 ) -> Result<()> {
1418 let _shaping_options = ShapingOptions {
1419 enable_ligatures: self.enable_ligatures,
1420 enable_kerning: self.enable_kerning,
1421 ..Default::default()
1422 };
1423
1424 for instance in &mut self.bg_instances {
1426 instance.size = [0.0, 0.0];
1427 instance.color = [0.0, 0.0, 0.0, 0.0];
1428 }
1429
1430 let bg_start_index = if !skip_solid_background && !self.bg_instances.is_empty() {
1434 let bg_color = self.background_color;
1435 let opacity = self.window_opacity * viewport.opacity;
1436 self.bg_instances[0] = super::types::BackgroundInstance {
1437 position: [viewport.x, viewport.y],
1438 size: [viewport.width, viewport.height],
1439 color: [
1440 bg_color[0] * opacity,
1441 bg_color[1] * opacity,
1442 bg_color[2] * opacity,
1443 opacity,
1444 ],
1445 };
1446 1 } else {
1448 0 };
1450
1451 for instance in &mut self.text_instances {
1452 instance.size = [0.0, 0.0];
1453 }
1454
1455 let mut bg_index = bg_start_index;
1457 let mut text_index = 0;
1458
1459 let (content_x, content_y) = viewport.content_origin();
1461 let opacity_multiplier = viewport.opacity;
1462
1463 for row in 0..rows {
1464 let row_start = row * cols;
1465 let row_end = (row + 1) * cols;
1466 if row_start >= cells.len() {
1467 break;
1468 }
1469 let row_cells = &cells[row_start..row_end.min(cells.len())];
1470
1471 let mut col = 0;
1473 while col < row_cells.len() {
1474 let cell = &row_cells[col];
1475 let is_default_bg = (cell.bg_color[0] as f32 / 255.0 - self.background_color[0])
1476 .abs()
1477 < 0.001
1478 && (cell.bg_color[1] as f32 / 255.0 - self.background_color[1]).abs() < 0.001
1479 && (cell.bg_color[2] as f32 / 255.0 - self.background_color[2]).abs() < 0.001;
1480
1481 let has_cursor = cursor_pos.is_some_and(|(cx, cy)| cx == col && cy == row)
1483 && cursor_opacity > 0.0
1484 && !self.cursor_hidden_for_shader;
1485
1486 if is_default_bg && !has_cursor {
1487 col += 1;
1488 continue;
1489 }
1490
1491 let bg_alpha =
1493 if self.transparency_affects_only_default_background && !is_default_bg {
1494 1.0
1495 } else {
1496 self.window_opacity
1497 };
1498 let pane_alpha = bg_alpha * opacity_multiplier;
1499 let mut bg_color = [
1500 cell.bg_color[0] as f32 / 255.0,
1501 cell.bg_color[1] as f32 / 255.0,
1502 cell.bg_color[2] as f32 / 255.0,
1503 pane_alpha,
1504 ];
1505
1506 if has_cursor {
1508 use par_term_emu_core_rust::cursor::CursorStyle;
1509 match self.cursor_style {
1510 CursorStyle::SteadyBlock | CursorStyle::BlinkingBlock => {
1511 for (bg, &cursor) in bg_color.iter_mut().take(3).zip(&self.cursor_color)
1512 {
1513 *bg = *bg * (1.0 - cursor_opacity) + cursor * cursor_opacity;
1514 }
1515 bg_color[3] = bg_color[3].max(cursor_opacity * opacity_multiplier);
1516 }
1517 _ => {}
1518 }
1519
1520 let x0 = content_x + col as f32 * self.cell_width;
1522 let y0 = content_y + row as f32 * self.cell_height;
1523 let x1 = x0 + self.cell_width;
1524 let y1 = y0 + self.cell_height;
1525
1526 if bg_index < self.max_bg_instances {
1527 self.bg_instances[bg_index] = BackgroundInstance {
1528 position: [
1529 x0 / self.config.width as f32 * 2.0 - 1.0,
1530 1.0 - (y0 / self.config.height as f32 * 2.0),
1531 ],
1532 size: [
1533 (x1 - x0) / self.config.width as f32 * 2.0,
1534 (y1 - y0) / self.config.height as f32 * 2.0,
1535 ],
1536 color: bg_color,
1537 };
1538 bg_index += 1;
1539 }
1540 col += 1;
1541 continue;
1542 }
1543
1544 let start_col = col;
1546 let run_color = cell.bg_color;
1547 col += 1;
1548 while col < row_cells.len() {
1549 let next_cell = &row_cells[col];
1550 let next_has_cursor = cursor_pos.is_some_and(|(cx, cy)| cx == col && cy == row)
1551 && cursor_opacity > 0.0;
1552 if next_cell.bg_color != run_color || next_has_cursor {
1553 break;
1554 }
1555 col += 1;
1556 }
1557 let run_length = col - start_col;
1558
1559 let x0 = content_x + start_col as f32 * self.cell_width;
1561 let x1 = content_x + (start_col + run_length) as f32 * self.cell_width;
1562 let y0 = content_y + row as f32 * self.cell_height;
1563 let y1 = y0 + self.cell_height;
1564
1565 if bg_index < self.max_bg_instances {
1566 self.bg_instances[bg_index] = BackgroundInstance {
1567 position: [
1568 x0 / self.config.width as f32 * 2.0 - 1.0,
1569 1.0 - (y0 / self.config.height as f32 * 2.0),
1570 ],
1571 size: [
1572 (x1 - x0) / self.config.width as f32 * 2.0,
1573 (y1 - y0) / self.config.height as f32 * 2.0,
1574 ],
1575 color: bg_color,
1576 };
1577 bg_index += 1;
1578 }
1579 }
1580
1581 let natural_line_height = self.font_ascent + self.font_descent + self.font_leading;
1583 let vertical_padding = (self.cell_height - natural_line_height).max(0.0) / 2.0;
1584 let baseline_y =
1585 content_y + (row as f32 * self.cell_height) + vertical_padding + self.font_ascent;
1586
1587 let text_alpha = if self.keep_text_opaque {
1589 opacity_multiplier } else {
1591 self.window_opacity * opacity_multiplier
1592 };
1593
1594 for (col_idx, cell) in row_cells.iter().enumerate() {
1595 if cell.wide_char_spacer || cell.grapheme == " " {
1596 continue;
1597 }
1598
1599 let chars: Vec<char> = cell.grapheme.chars().collect();
1600 if chars.is_empty() {
1601 continue;
1602 }
1603
1604 let ch = chars[0];
1605
1606 let char_type = block_chars::classify_char(ch);
1608 if chars.len() == 1 && block_chars::should_render_geometrically(char_type) {
1609 let char_w = if cell.wide_char {
1610 self.cell_width * 2.0
1611 } else {
1612 self.cell_width
1613 };
1614 let x0 = content_x + col_idx as f32 * self.cell_width;
1615 let y0 = content_y + row as f32 * self.cell_height;
1616
1617 let fg_color = [
1618 cell.fg_color[0] as f32 / 255.0,
1619 cell.fg_color[1] as f32 / 255.0,
1620 cell.fg_color[2] as f32 / 255.0,
1621 text_alpha,
1622 ];
1623
1624 let aspect_ratio = self.cell_height / char_w;
1626 if let Some(box_geo) = block_chars::get_box_drawing_geometry(ch, aspect_ratio) {
1627 for segment in &box_geo.segments {
1628 let rect = segment.to_pixel_rect(x0, y0, char_w, self.cell_height);
1629
1630 let extension = 1.0;
1632 let ext_x = if segment.x <= 0.01 { extension } else { 0.0 };
1633 let ext_y = if segment.y <= 0.01 { extension } else { 0.0 };
1634 let ext_w = if segment.x + segment.width >= 0.99 {
1635 extension
1636 } else {
1637 0.0
1638 };
1639 let ext_h = if segment.y + segment.height >= 0.99 {
1640 extension
1641 } else {
1642 0.0
1643 };
1644
1645 let final_x = rect.x - ext_x;
1646 let final_y = rect.y - ext_y;
1647 let final_w = rect.width + ext_x + ext_w;
1648 let final_h = rect.height + ext_y + ext_h;
1649
1650 if text_index < self.max_text_instances {
1651 self.text_instances[text_index] = TextInstance {
1652 position: [
1653 final_x / self.config.width as f32 * 2.0 - 1.0,
1654 1.0 - (final_y / self.config.height as f32 * 2.0),
1655 ],
1656 size: [
1657 final_w / self.config.width as f32 * 2.0,
1658 final_h / self.config.height as f32 * 2.0,
1659 ],
1660 tex_offset: [
1661 self.solid_pixel_offset.0 as f32 / 2048.0,
1662 self.solid_pixel_offset.1 as f32 / 2048.0,
1663 ],
1664 tex_size: [1.0 / 2048.0, 1.0 / 2048.0],
1665 color: fg_color,
1666 is_colored: 0,
1667 };
1668 text_index += 1;
1669 }
1670 }
1671 continue;
1672 }
1673
1674 if let Some(geo_block) = block_chars::get_geometric_block(ch) {
1676 let rect = geo_block.to_pixel_rect(x0, y0, char_w, self.cell_height);
1677
1678 let extension = 1.0;
1680 let ext_x = if geo_block.x == 0.0 { extension } else { 0.0 };
1681 let ext_y = if geo_block.y == 0.0 { extension } else { 0.0 };
1682 let ext_w = if geo_block.x + geo_block.width >= 1.0 {
1683 extension
1684 } else {
1685 0.0
1686 };
1687 let ext_h = if geo_block.y + geo_block.height >= 1.0 {
1688 extension
1689 } else {
1690 0.0
1691 };
1692
1693 let final_x = rect.x - ext_x;
1694 let final_y = rect.y - ext_y;
1695 let final_w = rect.width + ext_x + ext_w;
1696 let final_h = rect.height + ext_y + ext_h;
1697
1698 if text_index < self.max_text_instances {
1699 self.text_instances[text_index] = TextInstance {
1700 position: [
1701 final_x / self.config.width as f32 * 2.0 - 1.0,
1702 1.0 - (final_y / self.config.height as f32 * 2.0),
1703 ],
1704 size: [
1705 final_w / self.config.width as f32 * 2.0,
1706 final_h / self.config.height as f32 * 2.0,
1707 ],
1708 tex_offset: [
1709 self.solid_pixel_offset.0 as f32 / 2048.0,
1710 self.solid_pixel_offset.1 as f32 / 2048.0,
1711 ],
1712 tex_size: [1.0 / 2048.0, 1.0 / 2048.0],
1713 color: fg_color,
1714 is_colored: 0,
1715 };
1716 text_index += 1;
1717 }
1718 continue;
1719 }
1720 }
1721
1722 let glyph_result = if chars.len() > 1 {
1724 self.font_manager
1725 .find_grapheme_glyph(&cell.grapheme, cell.bold, cell.italic)
1726 } else {
1727 self.font_manager.find_glyph(ch, cell.bold, cell.italic)
1728 };
1729
1730 if let Some((font_idx, glyph_id)) = glyph_result {
1731 let cache_key = ((font_idx as u64) << 32) | (glyph_id as u64);
1732 let force_monochrome =
1735 chars.len() == 1 && super::atlas::should_render_as_symbol(ch);
1736 let info = if self.glyph_cache.contains_key(&cache_key) {
1737 self.lru_remove(cache_key);
1738 self.lru_push_front(cache_key);
1739 self.glyph_cache.get(&cache_key).unwrap().clone()
1740 } else if let Some(raster) =
1741 self.rasterize_glyph(font_idx, glyph_id, force_monochrome)
1742 {
1743 let info = self.upload_glyph(cache_key, &raster);
1744 self.glyph_cache.insert(cache_key, info.clone());
1745 self.lru_push_front(cache_key);
1746 info
1747 } else {
1748 continue;
1749 };
1750
1751 let char_w = if cell.wide_char {
1752 self.cell_width * 2.0
1753 } else {
1754 self.cell_width
1755 };
1756 let x0 = content_x + col_idx as f32 * self.cell_width;
1757 let y0 = content_y + row as f32 * self.cell_height;
1758 let x1 = x0 + char_w;
1759 let y1 = y0 + self.cell_height;
1760
1761 let cell_w = x1 - x0;
1762 let cell_h = y1 - y0;
1763 let scale_x = cell_w / char_w;
1764 let scale_y = cell_h / self.cell_height;
1765
1766 let baseline_offset = baseline_y - (content_y + row as f32 * self.cell_height);
1767 let glyph_left = x0 + (info.bearing_x * scale_x).round();
1768 let baseline_in_cell = (baseline_offset * scale_y).round();
1769 let glyph_top = y0 + baseline_in_cell - info.bearing_y;
1770
1771 let render_w = info.width as f32 * scale_x;
1772 let render_h = info.height as f32 * scale_y;
1773
1774 let (final_left, final_top, final_w, final_h) =
1775 if chars.len() == 1 && block_chars::should_snap_to_boundaries(char_type) {
1776 block_chars::snap_glyph_to_cell(
1777 glyph_left, glyph_top, render_w, render_h, x0, y0, x1, y1, 3.0, 0.5,
1778 )
1779 } else {
1780 (glyph_left, glyph_top, render_w, render_h)
1781 };
1782
1783 let fg_color = [
1784 cell.fg_color[0] as f32 / 255.0,
1785 cell.fg_color[1] as f32 / 255.0,
1786 cell.fg_color[2] as f32 / 255.0,
1787 text_alpha,
1788 ];
1789
1790 if text_index < self.max_text_instances {
1791 self.text_instances[text_index] = TextInstance {
1792 position: [
1793 final_left / self.config.width as f32 * 2.0 - 1.0,
1794 1.0 - (final_top / self.config.height as f32 * 2.0),
1795 ],
1796 size: [
1797 final_w / self.config.width as f32 * 2.0,
1798 final_h / self.config.height as f32 * 2.0,
1799 ],
1800 tex_offset: [info.x as f32 / 2048.0, info.y as f32 / 2048.0],
1801 tex_size: [info.width as f32 / 2048.0, info.height as f32 / 2048.0],
1802 color: fg_color,
1803 is_colored: if info.is_colored { 1 } else { 0 },
1804 };
1805 text_index += 1;
1806 }
1807 }
1808 }
1809 }
1810
1811 if self.command_separator_enabled && !separator_marks.is_empty() {
1813 let width_f = self.config.width as f32;
1814 let height_f = self.config.height as f32;
1815 let opacity_multiplier = viewport.opacity;
1816 for &(screen_row, exit_code, custom_color) in separator_marks {
1817 if screen_row < rows && bg_index < self.max_bg_instances {
1818 let x0 = content_x;
1819 let x1 = content_x + cols as f32 * self.cell_width;
1820 let y0 = content_y + screen_row as f32 * self.cell_height;
1821 let color = self.separator_color(exit_code, custom_color, opacity_multiplier);
1822 self.bg_instances[bg_index] = BackgroundInstance {
1823 position: [x0 / width_f * 2.0 - 1.0, 1.0 - (y0 / height_f * 2.0)],
1824 size: [
1825 (x1 - x0) / width_f * 2.0,
1826 self.command_separator_thickness / height_f * 2.0,
1827 ],
1828 color,
1829 };
1830 bg_index += 1;
1831 }
1832 }
1833 }
1834 let _ = bg_index; self.queue.write_buffer(
1838 &self.bg_instance_buffer,
1839 0,
1840 bytemuck::cast_slice(&self.bg_instances),
1841 );
1842 self.queue.write_buffer(
1843 &self.text_instance_buffer,
1844 0,
1845 bytemuck::cast_slice(&self.text_instances),
1846 );
1847
1848 Ok(())
1849 }
1850}