1use super::block_chars;
2use super::instance_buffers::{
3 CURSOR_BRIGHTNESS_THRESHOLD, STIPPLE_OFF_PX, STIPPLE_ON_PX, UNDERLINE_HEIGHT_RATIO,
4};
5use super::{BackgroundInstance, Cell, CellRenderer, PaneViewport, TextInstance};
6use anyhow::Result;
7use par_term_config::{SeparatorMark, color_u8x4_rgb_to_f32, color_u8x4_rgb_to_f32_a};
8use par_term_fonts::text_shaper::ShapingOptions;
9
10mod cursor_overlays;
11mod separators;
12
13use cursor_overlays::CursorOverlayParams;
14
15pub struct PaneRenderViewParams<'a> {
17 pub viewport: &'a PaneViewport,
18 pub cells: &'a [Cell],
19 pub cols: usize,
20 pub rows: usize,
21 pub cursor_pos: Option<(usize, usize)>,
22 pub cursor_opacity: f32,
23 pub show_scrollbar: bool,
24 pub clear_first: bool,
25 pub skip_background_image: bool,
26 pub separator_marks: &'a [SeparatorMark],
27 pub pane_background: Option<&'a par_term_config::PaneBackground>,
28}
29
30pub(super) struct PaneInstanceBuildParams<'a> {
32 pub viewport: &'a PaneViewport,
33 pub cells: &'a [Cell],
34 pub cols: usize,
35 pub rows: usize,
36 pub cursor_pos: Option<(usize, usize)>,
37 pub cursor_opacity: f32,
38 pub skip_solid_background: bool,
39 pub separator_marks: &'a [SeparatorMark],
40}
41
42impl CellRenderer {
43 pub fn render_pane_to_view(
61 &mut self,
62 surface_view: &wgpu::TextureView,
63 p: PaneRenderViewParams<'_>,
64 ) -> Result<()> {
65 let PaneRenderViewParams {
66 viewport,
67 cells,
68 cols,
69 rows,
70 cursor_pos,
71 cursor_opacity,
72 show_scrollbar,
73 clear_first,
74 skip_background_image,
75 separator_marks,
76 pane_background,
77 } = p;
78 let cursor_overlay_start = self.build_pane_instance_buffers(PaneInstanceBuildParams {
82 viewport,
83 cells,
84 cols,
85 rows,
86 cursor_pos,
87 cursor_opacity,
88 skip_solid_background: skip_background_image,
89 separator_marks,
90 })?;
91
92 let has_pane_bg = if let Some(pane_bg) = pane_background
97 && let Some(ref path) = pane_bg.image_path
98 && self.bg_state.pane_bg_cache.contains_key(path.as_str())
99 {
100 self.prepare_pane_bg_bind_group(
101 path.as_str(),
102 super::background::PaneBgBindGroupParams {
103 pane_x: viewport.x,
104 pane_y: viewport.y,
105 pane_width: viewport.width,
106 pane_height: viewport.height,
107 mode: pane_bg.mode,
108 opacity: pane_bg.opacity,
109 darken: pane_bg.darken,
110 },
111 );
112 true
113 } else {
114 false
115 };
116
117 let pane_bg_path: Option<String> = if has_pane_bg {
119 pane_background
120 .and_then(|pb| pb.image_path.as_ref())
121 .map(|p| p.to_string())
122 } else {
123 None
124 };
125
126 let mut encoder = self
127 .device
128 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
129 label: Some("pane render encoder"),
130 });
131
132 let load_op = if clear_first {
134 let clear_color = if self.bg_state.bg_is_solid_color {
135 wgpu::Color {
136 r: self.bg_state.solid_bg_color[0] as f64
137 * self.window_opacity as f64
138 * viewport.opacity as f64,
139 g: self.bg_state.solid_bg_color[1] as f64
140 * self.window_opacity as f64
141 * viewport.opacity as f64,
142 b: self.bg_state.solid_bg_color[2] as f64
143 * self.window_opacity as f64
144 * viewport.opacity as f64,
145 a: self.window_opacity as f64 * viewport.opacity as f64,
146 }
147 } else {
148 wgpu::Color {
149 r: self.background_color[0] as f64
150 * self.window_opacity as f64
151 * viewport.opacity as f64,
152 g: self.background_color[1] as f64
153 * self.window_opacity as f64
154 * viewport.opacity as f64,
155 b: self.background_color[2] as f64
156 * self.window_opacity as f64
157 * viewport.opacity as f64,
158 a: self.window_opacity as f64 * viewport.opacity as f64,
159 }
160 };
161 wgpu::LoadOp::Clear(clear_color)
162 } else {
163 wgpu::LoadOp::Load
164 };
165
166 {
167 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
168 label: Some("pane render pass"),
169 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
170 view: surface_view,
171 resolve_target: None,
172 ops: wgpu::Operations {
173 load: load_op,
174 store: wgpu::StoreOp::Store,
175 },
176 depth_slice: None,
177 })],
178 depth_stencil_attachment: None,
179 timestamp_writes: None,
180 occlusion_query_set: None,
181 });
182
183 let (sx, sy, sw, sh) = viewport.to_scissor_rect();
185 render_pass.set_scissor_rect(sx, sy, sw, sh);
186
187 if let Some(ref path) = pane_bg_path
191 && let Some(cached) = self.bg_state.pane_bg_uniform_cache.get(path.as_str())
192 {
193 render_pass.set_pipeline(&self.pipelines.bg_image_pipeline);
194 render_pass.set_bind_group(0, &cached.bind_group, &[]);
195 render_pass.set_vertex_buffer(0, self.buffers.vertex_buffer.slice(..));
196 render_pass.draw(0..4, 0..1);
197 }
198
199 self.emit_three_phase_draw_calls(
200 &mut render_pass,
201 cursor_overlay_start as u32,
202 self.buffers.actual_bg_instances as u32,
203 );
204
205 if show_scrollbar {
207 render_pass.set_scissor_rect(0, 0, self.config.width, self.config.height);
209 self.scrollbar.render(&mut render_pass);
210 }
211 }
212
213 self.queue.submit(std::iter::once(encoder.finish()));
214 Ok(())
215 }
216
217 fn build_pane_instance_buffers(&mut self, p: PaneInstanceBuildParams<'_>) -> Result<usize> {
229 let PaneInstanceBuildParams {
230 viewport,
231 cells,
232 cols,
233 rows,
234 cursor_pos,
235 cursor_opacity,
236 skip_solid_background,
237 separator_marks,
238 } = p;
239 let _shaping_options = ShapingOptions {
240 enable_ligatures: self.font.enable_ligatures,
241 enable_kerning: self.font.enable_kerning,
242 ..Default::default()
243 };
244
245 for instance in &mut self.bg_instances {
247 instance.size = [0.0, 0.0];
248 instance.color = [0.0, 0.0, 0.0, 0.0];
249 }
250
251 let bg_start_index = if !skip_solid_background && !self.bg_instances.is_empty() {
255 let bg_color = self.background_color;
256 let opacity = self.window_opacity * viewport.opacity;
257 let width_f = self.config.width as f32;
258 let height_f = self.config.height as f32;
259 self.bg_instances[0] = super::types::BackgroundInstance {
260 position: [
261 viewport.x / width_f * 2.0 - 1.0,
262 1.0 - (viewport.y / height_f * 2.0),
263 ],
264 size: [
265 viewport.width / width_f * 2.0,
266 viewport.height / height_f * 2.0,
267 ],
268 color: [
269 bg_color[0] * opacity,
270 bg_color[1] * opacity,
271 bg_color[2] * opacity,
272 opacity,
273 ],
274 };
275 1 } else {
277 0 };
279
280 for instance in &mut self.text_instances {
281 instance.size = [0.0, 0.0];
282 }
283
284 let mut bg_index = bg_start_index;
286 let mut text_index = 0;
287
288 let (content_x, content_y) = viewport.content_origin();
290 let opacity_multiplier = viewport.opacity;
291
292 for row in 0..rows {
293 let row_start = row * cols;
294 let row_end = (row + 1) * cols;
295 if row_start >= cells.len() {
296 break;
297 }
298 let row_cells = &cells[row_start..row_end.min(cells.len())];
299
300 let mut col = 0;
302 while col < row_cells.len() {
303 let cell = &row_cells[col];
304 let bg_f = color_u8x4_rgb_to_f32(cell.bg_color);
305 let is_default_bg = (bg_f[0] - self.background_color[0]).abs() < 0.001
306 && (bg_f[1] - self.background_color[1]).abs() < 0.001
307 && (bg_f[2] - self.background_color[2]).abs() < 0.001;
308
309 let cursor_at_cell = cursor_pos.is_some_and(|(cx, cy)| cx == col && cy == row)
311 && !self.cursor.hidden_for_shader;
312 let render_hollow_here = cursor_at_cell
314 && !self.is_focused
315 && self.cursor.unfocused_style == par_term_config::UnfocusedCursorStyle::Hollow;
316 let has_cursor = (cursor_at_cell && cursor_opacity > 0.0) || render_hollow_here;
317
318 let is_half_block = {
322 let mut chars = cell.grapheme.chars();
323 matches!(chars.next(), Some('\u{2580}' | '\u{2584}')) && chars.next().is_none()
324 };
325
326 if is_half_block || (is_default_bg && !has_cursor) {
327 col += 1;
328 continue;
329 }
330
331 let bg_alpha =
333 if self.transparency_affects_only_default_background && !is_default_bg {
334 1.0
335 } else {
336 self.window_opacity
337 };
338 let pane_alpha = bg_alpha * opacity_multiplier;
339 let mut bg_color = color_u8x4_rgb_to_f32_a(cell.bg_color, pane_alpha);
340
341 if has_cursor {
343 use par_term_emu_core_rust::cursor::CursorStyle;
344 match self.cursor.style {
345 CursorStyle::SteadyBlock | CursorStyle::BlinkingBlock => {
346 if !render_hollow_here {
347 for (bg, &cursor) in
349 bg_color.iter_mut().take(3).zip(&self.cursor.color)
350 {
351 *bg = *bg * (1.0 - cursor_opacity) + cursor * cursor_opacity;
352 }
353 bg_color[3] = bg_color[3].max(cursor_opacity * opacity_multiplier);
354 }
355 }
357 _ => {}
358 }
359
360 let x0 = content_x + col as f32 * self.grid.cell_width;
362 let x1 = x0 + self.grid.cell_width;
363 let y0 = (content_y + row as f32 * self.grid.cell_height).round();
365 let y1 = (content_y + (row + 1) as f32 * self.grid.cell_height).round();
366
367 if bg_index < self.buffers.max_bg_instances {
368 self.bg_instances[bg_index] = BackgroundInstance {
369 position: [
370 x0 / self.config.width as f32 * 2.0 - 1.0,
371 1.0 - (y0 / self.config.height as f32 * 2.0),
372 ],
373 size: [
374 (x1 - x0) / self.config.width as f32 * 2.0,
375 (y1 - y0) / self.config.height as f32 * 2.0,
376 ],
377 color: bg_color,
378 };
379 bg_index += 1;
380 }
381 col += 1;
382 continue;
383 }
384
385 let start_col = col;
387 let run_color = cell.bg_color;
388 col += 1;
389 while col < row_cells.len() {
390 let next_cell = &row_cells[col];
391 let next_cursor_at_cell = cursor_pos
392 .is_some_and(|(cx, cy)| cx == col && cy == row)
393 && !self.cursor.hidden_for_shader;
394 let next_hollow = next_cursor_at_cell
395 && !self.is_focused
396 && self.cursor.unfocused_style
397 == par_term_config::UnfocusedCursorStyle::Hollow;
398 let next_has_cursor =
399 (next_cursor_at_cell && cursor_opacity > 0.0) || next_hollow;
400 let next_is_half_block = {
401 let mut chars = next_cell.grapheme.chars();
402 matches!(chars.next(), Some('\u{2580}' | '\u{2584}'))
403 && chars.next().is_none()
404 };
405 if next_cell.bg_color != run_color || next_has_cursor || next_is_half_block {
406 break;
407 }
408 col += 1;
409 }
410 let run_length = col - start_col;
411
412 let x0 = content_x + start_col as f32 * self.grid.cell_width;
414 let x1 = content_x + (start_col + run_length) as f32 * self.grid.cell_width;
415 let y0 = (content_y + row as f32 * self.grid.cell_height).round();
417 let y1 = (content_y + (row + 1) as f32 * self.grid.cell_height).round();
418
419 if bg_index < self.buffers.max_bg_instances {
420 self.bg_instances[bg_index] = BackgroundInstance {
421 position: [
422 x0 / self.config.width as f32 * 2.0 - 1.0,
423 1.0 - (y0 / self.config.height as f32 * 2.0),
424 ],
425 size: [
426 (x1 - x0) / self.config.width as f32 * 2.0,
427 (y1 - y0) / self.config.height as f32 * 2.0,
428 ],
429 color: bg_color,
430 };
431 bg_index += 1;
432 }
433 }
434
435 let natural_line_height =
437 self.font.font_ascent + self.font.font_descent + self.font.font_leading;
438 let vertical_padding = (self.grid.cell_height - natural_line_height).max(0.0) / 2.0;
439 let baseline_y = content_y
440 + (row as f32 * self.grid.cell_height)
441 + vertical_padding
442 + self.font.font_ascent;
443
444 let text_alpha = if self.keep_text_opaque {
446 opacity_multiplier } else {
448 self.window_opacity * opacity_multiplier
449 };
450
451 let cursor_is_block_on_this_row = {
454 use par_term_emu_core_rust::cursor::CursorStyle;
455 cursor_pos.is_some_and(|(_, cy)| cy == row)
456 && cursor_opacity > 0.0
457 && !self.cursor.hidden_for_shader
458 && matches!(
459 self.cursor.style,
460 CursorStyle::SteadyBlock | CursorStyle::BlinkingBlock
461 )
462 && (self.is_focused
463 || self.cursor.unfocused_style
464 == par_term_config::UnfocusedCursorStyle::Same)
465 };
466
467 for (col_idx, cell) in row_cells.iter().enumerate() {
468 if cell.wide_char_spacer || cell.grapheme == " " {
469 continue;
470 }
471
472 let chars: Vec<char> = cell.grapheme.chars().collect();
473 if chars.is_empty() {
474 continue;
475 }
476
477 let ch = chars[0];
478
479 let render_fg_color: [f32; 4] = if cursor_is_block_on_this_row
482 && cursor_pos.is_some_and(|(cx, _)| cx == col_idx)
483 {
484 if let Some(cursor_text) = self.cursor.text_color {
485 [cursor_text[0], cursor_text[1], cursor_text[2], text_alpha]
486 } else {
487 let cursor_brightness =
488 (self.cursor.color[0] + self.cursor.color[1] + self.cursor.color[2])
489 / 3.0;
490 if cursor_brightness > CURSOR_BRIGHTNESS_THRESHOLD {
491 [0.0, 0.0, 0.0, text_alpha]
492 } else {
493 [1.0, 1.0, 1.0, text_alpha]
494 }
495 }
496 } else {
497 color_u8x4_rgb_to_f32_a(cell.fg_color, text_alpha)
498 };
499
500 let char_type = block_chars::classify_char(ch);
502 if chars.len() == 1 && block_chars::should_render_geometrically(char_type) {
503 let char_w = if cell.wide_char {
504 self.grid.cell_width * 2.0
505 } else {
506 self.grid.cell_width
507 };
508 let x0 = (content_x + col_idx as f32 * self.grid.cell_width).round();
509 let y0 = (content_y + row as f32 * self.grid.cell_height).round();
510 let y1 = (content_y + (row + 1) as f32 * self.grid.cell_height).round();
511 let snapped_cell_height = y1 - y0;
512
513 let aspect_ratio = snapped_cell_height / char_w;
515 if let Some(box_geo) = block_chars::get_box_drawing_geometry(ch, aspect_ratio) {
516 for segment in &box_geo.segments {
517 let rect = segment
518 .to_pixel_rect(x0, y0, char_w, snapped_cell_height)
519 .snap_to_pixels();
520
521 let extension = 1.0;
523 let ext_x = if segment.x <= 0.01 { extension } else { 0.0 };
524 let ext_y = if segment.y <= 0.01 { extension } else { 0.0 };
525 let ext_w = if segment.x + segment.width >= 0.99 {
526 extension
527 } else {
528 0.0
529 };
530 let ext_h = if segment.y + segment.height >= 0.99 {
531 extension
532 } else {
533 0.0
534 };
535
536 let final_x = rect.x - ext_x;
537 let final_y = rect.y - ext_y;
538 let final_w = rect.width + ext_x + ext_w;
539 let final_h = rect.height + ext_y + ext_h;
540
541 if text_index < self.buffers.max_text_instances {
542 self.text_instances[text_index] = TextInstance {
543 position: [
544 final_x / self.config.width as f32 * 2.0 - 1.0,
545 1.0 - (final_y / self.config.height as f32 * 2.0),
546 ],
547 size: [
548 final_w / self.config.width as f32 * 2.0,
549 final_h / self.config.height as f32 * 2.0,
550 ],
551 tex_offset: [
552 self.atlas.solid_pixel_offset.0 as f32 / 2048.0,
553 self.atlas.solid_pixel_offset.1 as f32 / 2048.0,
554 ],
555 tex_size: [1.0 / 2048.0, 1.0 / 2048.0],
556 color: render_fg_color,
557 is_colored: 0,
558 };
559 text_index += 1;
560 }
561 }
562 continue;
563 }
564
565 if ch == '\u{2584}' || ch == '\u{2580}' {
569 let x1 = (content_x + (col_idx + 1) as f32 * self.grid.cell_width).round();
570 let cell_w = x1 - x0;
571 let y_mid = y0 + self.grid.cell_height / 2.0;
572
573 let bg_half_color = color_u8x4_rgb_to_f32_a(cell.bg_color, text_alpha);
574 let (top_color, bottom_color) = if ch == '\u{2584}' {
575 (bg_half_color, render_fg_color) } else {
577 (render_fg_color, bg_half_color) };
579
580 let tex_offset = [
581 self.atlas.solid_pixel_offset.0 as f32 / 2048.0,
582 self.atlas.solid_pixel_offset.1 as f32 / 2048.0,
583 ];
584 let tex_size = [1.0 / 2048.0, 1.0 / 2048.0];
585
586 if text_index < self.buffers.max_text_instances {
588 self.text_instances[text_index] = TextInstance {
589 position: [
590 x0 / self.config.width as f32 * 2.0 - 1.0,
591 1.0 - (y0 / self.config.height as f32 * 2.0),
592 ],
593 size: [
594 cell_w / self.config.width as f32 * 2.0,
595 (y_mid - y0) / self.config.height as f32 * 2.0,
596 ],
597 tex_offset,
598 tex_size,
599 color: top_color,
600 is_colored: 0,
601 };
602 text_index += 1;
603 }
604
605 if text_index < self.buffers.max_text_instances {
607 self.text_instances[text_index] = TextInstance {
608 position: [
609 x0 / self.config.width as f32 * 2.0 - 1.0,
610 1.0 - (y_mid / self.config.height as f32 * 2.0),
611 ],
612 size: [
613 cell_w / self.config.width as f32 * 2.0,
614 (y1 - y_mid) / self.config.height as f32 * 2.0,
615 ],
616 tex_offset,
617 tex_size,
618 color: bottom_color,
619 is_colored: 0,
620 };
621 text_index += 1;
622 }
623 continue;
624 }
625
626 if let Some(geo_block) = block_chars::get_geometric_block(ch) {
628 let rect = geo_block.to_pixel_rect(x0, y0, char_w, self.grid.cell_height);
629
630 let extension = 1.0;
632 let ext_x = if geo_block.x == 0.0 { extension } else { 0.0 };
633 let ext_y = if geo_block.y == 0.0 { extension } else { 0.0 };
634 let ext_w = if geo_block.x + geo_block.width >= 1.0 {
635 extension
636 } else {
637 0.0
638 };
639 let ext_h = if geo_block.y + geo_block.height >= 1.0 {
640 extension
641 } else {
642 0.0
643 };
644
645 let final_x = rect.x - ext_x;
646 let final_y = rect.y - ext_y;
647 let final_w = rect.width + ext_x + ext_w;
648 let final_h = rect.height + ext_y + ext_h;
649
650 if text_index < self.buffers.max_text_instances {
651 self.text_instances[text_index] = TextInstance {
652 position: [
653 final_x / self.config.width as f32 * 2.0 - 1.0,
654 1.0 - (final_y / self.config.height as f32 * 2.0),
655 ],
656 size: [
657 final_w / self.config.width as f32 * 2.0,
658 final_h / self.config.height as f32 * 2.0,
659 ],
660 tex_offset: [
661 self.atlas.solid_pixel_offset.0 as f32 / 2048.0,
662 self.atlas.solid_pixel_offset.1 as f32 / 2048.0,
663 ],
664 tex_size: [1.0 / 2048.0, 1.0 / 2048.0],
665 color: render_fg_color,
666 is_colored: 0,
667 };
668 text_index += 1;
669 }
670 continue;
671 }
672 }
673
674 let (force_monochrome, base_char) = if chars.len() == 1 {
677 (super::atlas::should_render_as_symbol(ch), ch)
678 } else if chars.len() == 2
679 && chars[1] == '\u{FE0F}'
680 && super::atlas::should_render_as_symbol(chars[0])
681 {
682 (true, chars[0])
683 } else {
684 (false, ch)
685 };
686
687 let mut glyph_result = if force_monochrome || chars.len() == 1 {
690 self.font_manager
691 .find_glyph(base_char, cell.bold, cell.italic)
692 } else {
693 self.font_manager
694 .find_grapheme_glyph(&cell.grapheme, cell.bold, cell.italic)
695 };
696
697 let mut excluded_fonts: Vec<usize> = Vec::new();
699 let resolved_info = loop {
700 match glyph_result {
701 Some((font_idx, glyph_id)) => {
702 let cache_key = ((font_idx as u64) << 32) | (glyph_id as u64);
703 if self.atlas.glyph_cache.contains_key(&cache_key) {
704 self.lru_remove(cache_key);
705 self.lru_push_front(cache_key);
706 break Some(
707 self.atlas
708 .glyph_cache
709 .get(&cache_key)
710 .expect(
711 "Glyph cache entry must exist after contains_key check",
712 )
713 .clone(),
714 );
715 } else if let Some(raster) =
716 self.rasterize_glyph(font_idx, glyph_id, force_monochrome)
717 {
718 let info = self.upload_glyph(cache_key, &raster);
719 self.atlas.glyph_cache.insert(cache_key, info.clone());
720 self.lru_push_front(cache_key);
721 break Some(info);
722 } else {
723 excluded_fonts.push(font_idx);
725 glyph_result = self.font_manager.find_glyph_excluding(
726 base_char,
727 cell.bold,
728 cell.italic,
729 &excluded_fonts,
730 );
731 continue;
732 }
733 }
734 None => break None,
735 }
736 };
737
738 let resolved_info = if resolved_info.is_none() && force_monochrome {
740 let mut glyph_result2 =
741 self.font_manager
742 .find_glyph(base_char, cell.bold, cell.italic);
743 loop {
744 match glyph_result2 {
745 Some((font_idx, glyph_id)) => {
746 let cache_key =
747 ((font_idx as u64) << 32) | (glyph_id as u64) | (1u64 << 63);
748 if let Some(raster) =
749 self.rasterize_glyph(font_idx, glyph_id, false)
750 {
751 let info = self.upload_glyph(cache_key, &raster);
752 self.atlas.glyph_cache.insert(cache_key, info.clone());
753 self.lru_push_front(cache_key);
754 break Some(info);
755 } else {
756 glyph_result2 = self.font_manager.find_glyph_excluding(
757 base_char,
758 cell.bold,
759 cell.italic,
760 &[font_idx],
761 );
762 continue;
763 }
764 }
765 None => break None,
766 }
767 }
768 } else {
769 resolved_info
770 };
771
772 if let Some(info) = resolved_info {
773 let char_w = if cell.wide_char {
774 self.grid.cell_width * 2.0
775 } else {
776 self.grid.cell_width
777 };
778 let x0 = content_x + col_idx as f32 * self.grid.cell_width;
779 let y0 = content_y + row as f32 * self.grid.cell_height;
780 let x1 = x0 + char_w;
781 let y1 = y0 + self.grid.cell_height;
782
783 let cell_w = x1 - x0;
784 let cell_h = y1 - y0;
785 let scale_x = cell_w / char_w;
786 let scale_y = cell_h / self.grid.cell_height;
787
788 let baseline_offset =
789 baseline_y - (content_y + row as f32 * self.grid.cell_height);
790 let glyph_left = x0 + (info.bearing_x * scale_x).round();
791 let baseline_in_cell = (baseline_offset * scale_y).round();
792 let glyph_top = y0 + baseline_in_cell - info.bearing_y;
793
794 let render_w = info.width as f32 * scale_x;
795 let render_h = info.height as f32 * scale_y;
796
797 let (final_left, final_top, final_w, final_h) =
798 if chars.len() == 1 && block_chars::should_snap_to_boundaries(char_type) {
799 block_chars::snap_glyph_to_cell(block_chars::SnapGlyphParams {
800 glyph_left,
801 glyph_top,
802 render_w,
803 render_h,
804 cell_x0: x0,
805 cell_y0: y0,
806 cell_x1: x1,
807 cell_y1: y1,
808 snap_threshold: 3.0,
809 extension: 0.5,
810 })
811 } else {
812 (glyph_left, glyph_top, render_w, render_h)
813 };
814
815 if text_index < self.buffers.max_text_instances {
816 self.text_instances[text_index] = TextInstance {
817 position: [
818 final_left / self.config.width as f32 * 2.0 - 1.0,
819 1.0 - (final_top / self.config.height as f32 * 2.0),
820 ],
821 size: [
822 final_w / self.config.width as f32 * 2.0,
823 final_h / self.config.height as f32 * 2.0,
824 ],
825 tex_offset: [info.x as f32 / 2048.0, info.y as f32 / 2048.0],
826 tex_size: [info.width as f32 / 2048.0, info.height as f32 / 2048.0],
827 color: render_fg_color,
828 is_colored: if info.is_colored { 1 } else { 0 },
829 };
830 text_index += 1;
831 }
832 }
833 }
834
835 {
838 let underline_thickness = (self.grid.cell_height * UNDERLINE_HEIGHT_RATIO)
839 .max(1.0)
840 .round();
841 let tex_offset = [
842 self.atlas.solid_pixel_offset.0 as f32 / 2048.0,
843 self.atlas.solid_pixel_offset.1 as f32 / 2048.0,
844 ];
845 let tex_size = [1.0 / 2048.0, 1.0 / 2048.0];
846 let y0 = content_y + (row + 1) as f32 * self.grid.cell_height - underline_thickness;
847 let ndc_y = 1.0 - (y0 / self.config.height as f32 * 2.0);
848 let ndc_h = underline_thickness / self.config.height as f32 * 2.0;
849 let is_stipple =
850 self.link_underline_style == par_term_config::LinkUnderlineStyle::Stipple;
851 let stipple_period = STIPPLE_ON_PX + STIPPLE_OFF_PX;
852
853 for col_idx in 0..cols {
854 if row_start + col_idx >= cells.len() {
855 break;
856 }
857 let cell = &cells[row_start + col_idx];
858 if !cell.underline {
859 continue;
860 }
861 let fg = color_u8x4_rgb_to_f32_a(cell.fg_color, text_alpha);
862 let cell_x0 = content_x + col_idx as f32 * self.grid.cell_width;
863
864 if is_stipple {
865 let mut px = 0.0;
866 while px < self.grid.cell_width
867 && text_index < self.buffers.max_text_instances
868 {
869 let seg_w = STIPPLE_ON_PX.min(self.grid.cell_width - px);
870 let x = cell_x0 + px;
871 self.text_instances[text_index] = TextInstance {
872 position: [x / self.config.width as f32 * 2.0 - 1.0, ndc_y],
873 size: [seg_w / self.config.width as f32 * 2.0, ndc_h],
874 tex_offset,
875 tex_size,
876 color: fg,
877 is_colored: 0,
878 };
879 text_index += 1;
880 px += stipple_period;
881 }
882 } else if text_index < self.buffers.max_text_instances {
883 self.text_instances[text_index] = TextInstance {
884 position: [cell_x0 / self.config.width as f32 * 2.0 - 1.0, ndc_y],
885 size: [self.grid.cell_width / self.config.width as f32 * 2.0, ndc_h],
886 tex_offset,
887 tex_size,
888 color: fg,
889 is_colored: 0,
890 };
891 text_index += 1;
892 }
893 }
894 }
895 }
896
897 bg_index = self.emit_separator_instances(
899 separator_marks,
900 cols,
901 rows,
902 content_x,
903 content_y,
904 opacity_multiplier,
905 bg_index,
906 );
907
908 let cursor_overlay_start = bg_index;
912
913 if let Some((cursor_col, cursor_row)) = cursor_pos {
914 let cursor_x0 = content_x + cursor_col as f32 * self.grid.cell_width;
915 let cursor_x1 = cursor_x0 + self.grid.cell_width;
916 let cursor_y0 = (content_y + cursor_row as f32 * self.grid.cell_height).round();
917 let cursor_y1 = (content_y + (cursor_row + 1) as f32 * self.grid.cell_height).round();
918
919 bg_index = self.emit_cursor_overlays(
921 CursorOverlayParams {
922 cursor_x0,
923 cursor_x1,
924 cursor_y0,
925 cursor_y1,
926 cols,
927 content_x,
928 cursor_opacity,
929 },
930 bg_index,
931 );
932 }
933
934 self.buffers.actual_bg_instances = bg_index;
936 self.buffers.actual_text_instances = text_index;
937
938 self.queue.write_buffer(
940 &self.buffers.bg_instance_buffer,
941 0,
942 bytemuck::cast_slice(&self.bg_instances),
943 );
944 self.queue.write_buffer(
945 &self.buffers.text_instance_buffer,
946 0,
947 bytemuck::cast_slice(&self.text_instances),
948 );
949
950 Ok(cursor_overlay_start)
951 }
952}