1use anyhow::Result;
2
3use super::{DividerRenderInfo, PaneDividerSettings, PaneTitleInfo, Renderer};
4use crate::cell_renderer::PaneViewport;
5use crate::cell_renderer::pane_render::ATLAS_SIZE;
6
7impl Renderer {
8 pub fn render_dividers(
17 &mut self,
18 surface_view: &wgpu::TextureView,
19 dividers: &[DividerRenderInfo],
20 settings: &PaneDividerSettings,
21 ) -> Result<()> {
22 if dividers.is_empty() {
23 return Ok(());
24 }
25
26 let mut instances = std::mem::take(&mut self.scratch_divider_instances);
30 instances.clear();
31
32 let w = self.size.width as f32;
33 let h = self.size.height as f32;
34
35 for divider in dividers {
36 let color = if divider.hovered {
37 settings.hover_color
38 } else {
39 settings.divider_color
40 };
41
42 use par_term_config::DividerStyle;
43 match settings.divider_style {
44 DividerStyle::Solid => {
45 let x_ndc = divider.x / w * 2.0 - 1.0;
46 let y_ndc = 1.0 - (divider.y / h * 2.0);
47 let w_ndc = divider.width / w * 2.0;
48 let h_ndc = divider.height / h * 2.0;
49
50 instances.push(crate::cell_renderer::types::BackgroundInstance {
51 position: [x_ndc, y_ndc],
52 size: [w_ndc, h_ndc],
53 color: [color[0], color[1], color[2], 1.0],
54 });
55 }
56 DividerStyle::Double => {
57 let is_horizontal = divider.width > divider.height;
59 let thickness = if is_horizontal {
60 divider.height
61 } else {
62 divider.width
63 };
64
65 if thickness >= 4.0 {
66 if is_horizontal {
68 instances.push(crate::cell_renderer::types::BackgroundInstance {
70 position: [divider.x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
71 size: [divider.width / w * 2.0, 1.0 / h * 2.0],
72 color: [color[0], color[1], color[2], 1.0],
73 });
74 let bottom_y = divider.y + divider.height - 1.0;
76 instances.push(crate::cell_renderer::types::BackgroundInstance {
77 position: [divider.x / w * 2.0 - 1.0, 1.0 - (bottom_y / h * 2.0)],
78 size: [divider.width / w * 2.0, 1.0 / h * 2.0],
79 color: [color[0], color[1], color[2], 1.0],
80 });
81 } else {
82 instances.push(crate::cell_renderer::types::BackgroundInstance {
84 position: [divider.x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
85 size: [1.0 / w * 2.0, divider.height / h * 2.0],
86 color: [color[0], color[1], color[2], 1.0],
87 });
88 let right_x = divider.x + divider.width - 1.0;
90 instances.push(crate::cell_renderer::types::BackgroundInstance {
91 position: [right_x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
92 size: [1.0 / w * 2.0, divider.height / h * 2.0],
93 color: [color[0], color[1], color[2], 1.0],
94 });
95 }
96 } else {
97 if is_horizontal {
100 let center_y = divider.y + (divider.height - 1.0) / 2.0;
101 instances.push(crate::cell_renderer::types::BackgroundInstance {
102 position: [divider.x / w * 2.0 - 1.0, 1.0 - (center_y / h * 2.0)],
103 size: [divider.width / w * 2.0, 1.0 / h * 2.0],
104 color: [color[0], color[1], color[2], 1.0],
105 });
106 } else {
107 let center_x = divider.x + (divider.width - 1.0) / 2.0;
108 instances.push(crate::cell_renderer::types::BackgroundInstance {
109 position: [center_x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
110 size: [1.0 / w * 2.0, divider.height / h * 2.0],
111 color: [color[0], color[1], color[2], 1.0],
112 });
113 }
114 }
115 }
116 DividerStyle::Dashed => {
117 let is_horizontal = divider.width > divider.height;
119 let dash_len: f32 = 6.0;
120 let gap_len: f32 = 4.0;
121
122 if is_horizontal {
123 let mut x = divider.x;
124 while x < divider.x + divider.width {
125 let seg_w = dash_len.min(divider.x + divider.width - x);
126 instances.push(crate::cell_renderer::types::BackgroundInstance {
127 position: [x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
128 size: [seg_w / w * 2.0, divider.height / h * 2.0],
129 color: [color[0], color[1], color[2], 1.0],
130 });
131 x += dash_len + gap_len;
132 }
133 } else {
134 let mut y = divider.y;
135 while y < divider.y + divider.height {
136 let seg_h = dash_len.min(divider.y + divider.height - y);
137 instances.push(crate::cell_renderer::types::BackgroundInstance {
138 position: [divider.x / w * 2.0 - 1.0, 1.0 - (y / h * 2.0)],
139 size: [divider.width / w * 2.0, seg_h / h * 2.0],
140 color: [color[0], color[1], color[2], 1.0],
141 });
142 y += dash_len + gap_len;
143 }
144 }
145 }
146 DividerStyle::Shadow => {
147 let is_horizontal = divider.width > divider.height;
150 let thickness = if is_horizontal {
151 divider.height
152 } else {
153 divider.width
154 };
155
156 let highlight = [
158 (color[0] + 0.3).min(1.0),
159 (color[1] + 0.3).min(1.0),
160 (color[2] + 0.3).min(1.0),
161 1.0,
162 ];
163 let shadow = [(color[0] * 0.3), (color[1] * 0.3), (color[2] * 0.3), 1.0];
165
166 if thickness >= 3.0 {
167 let edge = 1.0_f32;
169 if is_horizontal {
170 instances.push(crate::cell_renderer::types::BackgroundInstance {
172 position: [divider.x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
173 size: [divider.width / w * 2.0, edge / h * 2.0],
174 color: highlight,
175 });
176 let body_y = divider.y + edge;
178 let body_h = divider.height - edge * 2.0;
179 if body_h > 0.0 {
180 instances.push(crate::cell_renderer::types::BackgroundInstance {
181 position: [divider.x / w * 2.0 - 1.0, 1.0 - (body_y / h * 2.0)],
182 size: [divider.width / w * 2.0, body_h / h * 2.0],
183 color: [color[0], color[1], color[2], 1.0],
184 });
185 }
186 let shadow_y = divider.y + divider.height - edge;
188 instances.push(crate::cell_renderer::types::BackgroundInstance {
189 position: [divider.x / w * 2.0 - 1.0, 1.0 - (shadow_y / h * 2.0)],
190 size: [divider.width / w * 2.0, edge / h * 2.0],
191 color: shadow,
192 });
193 } else {
194 instances.push(crate::cell_renderer::types::BackgroundInstance {
196 position: [divider.x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
197 size: [edge / w * 2.0, divider.height / h * 2.0],
198 color: highlight,
199 });
200 let body_x = divider.x + edge;
202 let body_w = divider.width - edge * 2.0;
203 if body_w > 0.0 {
204 instances.push(crate::cell_renderer::types::BackgroundInstance {
205 position: [body_x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
206 size: [body_w / w * 2.0, divider.height / h * 2.0],
207 color: [color[0], color[1], color[2], 1.0],
208 });
209 }
210 let shadow_x = divider.x + divider.width - edge;
212 instances.push(crate::cell_renderer::types::BackgroundInstance {
213 position: [shadow_x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
214 size: [edge / w * 2.0, divider.height / h * 2.0],
215 color: shadow,
216 });
217 }
218 } else {
219 if is_horizontal {
221 let half = (divider.height / 2.0).max(1.0);
222 instances.push(crate::cell_renderer::types::BackgroundInstance {
223 position: [divider.x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
224 size: [divider.width / w * 2.0, half / h * 2.0],
225 color: highlight,
226 });
227 let bottom_y = divider.y + half;
228 let bottom_h = divider.height - half;
229 if bottom_h > 0.0 {
230 instances.push(crate::cell_renderer::types::BackgroundInstance {
231 position: [
232 divider.x / w * 2.0 - 1.0,
233 1.0 - (bottom_y / h * 2.0),
234 ],
235 size: [divider.width / w * 2.0, bottom_h / h * 2.0],
236 color: shadow,
237 });
238 }
239 } else {
240 let half = (divider.width / 2.0).max(1.0);
241 instances.push(crate::cell_renderer::types::BackgroundInstance {
242 position: [divider.x / w * 2.0 - 1.0, 1.0 - (divider.y / h * 2.0)],
243 size: [half / w * 2.0, divider.height / h * 2.0],
244 color: highlight,
245 });
246 let right_x = divider.x + half;
247 let right_w = divider.width - half;
248 if right_w > 0.0 {
249 instances.push(crate::cell_renderer::types::BackgroundInstance {
250 position: [
251 right_x / w * 2.0 - 1.0,
252 1.0 - (divider.y / h * 2.0),
253 ],
254 size: [right_w / w * 2.0, divider.height / h * 2.0],
255 color: shadow,
256 });
257 }
258 }
259 }
260 }
261 }
262 }
263
264 self.cell_renderer.queue().write_buffer(
266 &self.cell_renderer.buffers.bg_instance_buffer,
267 0,
268 bytemuck::cast_slice(&instances),
269 );
270
271 let mut encoder =
273 self.cell_renderer
274 .device()
275 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
276 label: Some("divider render encoder"),
277 });
278
279 {
280 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
281 label: Some("divider render pass"),
282 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
283 view: surface_view,
284 resolve_target: None,
285 ops: wgpu::Operations {
286 load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store,
288 },
289 depth_slice: None,
290 })],
291 depth_stencil_attachment: None,
292 timestamp_writes: None,
293 occlusion_query_set: None,
294 });
295
296 render_pass.set_pipeline(&self.cell_renderer.pipelines.bg_pipeline);
297 render_pass.set_vertex_buffer(0, self.cell_renderer.buffers.vertex_buffer.slice(..));
298 render_pass
299 .set_vertex_buffer(1, self.cell_renderer.buffers.bg_instance_buffer.slice(..));
300 render_pass.draw(0..4, 0..instances.len() as u32);
301 }
302
303 self.cell_renderer
304 .queue()
305 .submit(std::iter::once(encoder.finish()));
306
307 instances.clear();
309 self.scratch_divider_instances = instances;
310
311 Ok(())
312 }
313
314 pub fn render_focus_indicator(
323 &mut self,
324 surface_view: &wgpu::TextureView,
325 viewport: &PaneViewport,
326 settings: &PaneDividerSettings,
327 ) -> Result<()> {
328 if !settings.show_focus_indicator {
329 return Ok(());
330 }
331
332 let border_w = settings.focus_width;
333 let color = [
334 settings.focus_color[0],
335 settings.focus_color[1],
336 settings.focus_color[2],
337 1.0,
338 ];
339
340 let instances = vec![
342 crate::cell_renderer::types::BackgroundInstance {
344 position: [
345 viewport.x / self.size.width as f32 * 2.0 - 1.0,
346 1.0 - (viewport.y / self.size.height as f32 * 2.0),
347 ],
348 size: [
349 viewport.width / self.size.width as f32 * 2.0,
350 border_w / self.size.height as f32 * 2.0,
351 ],
352 color,
353 },
354 crate::cell_renderer::types::BackgroundInstance {
356 position: [
357 viewport.x / self.size.width as f32 * 2.0 - 1.0,
358 1.0 - ((viewport.y + viewport.height - border_w) / self.size.height as f32
359 * 2.0),
360 ],
361 size: [
362 viewport.width / self.size.width as f32 * 2.0,
363 border_w / self.size.height as f32 * 2.0,
364 ],
365 color,
366 },
367 crate::cell_renderer::types::BackgroundInstance {
369 position: [
370 viewport.x / self.size.width as f32 * 2.0 - 1.0,
371 1.0 - ((viewport.y + border_w) / self.size.height as f32 * 2.0),
372 ],
373 size: [
374 border_w / self.size.width as f32 * 2.0,
375 (viewport.height - border_w * 2.0) / self.size.height as f32 * 2.0,
376 ],
377 color,
378 },
379 crate::cell_renderer::types::BackgroundInstance {
381 position: [
382 (viewport.x + viewport.width - border_w) / self.size.width as f32 * 2.0 - 1.0,
383 1.0 - ((viewport.y + border_w) / self.size.height as f32 * 2.0),
384 ],
385 size: [
386 border_w / self.size.width as f32 * 2.0,
387 (viewport.height - border_w * 2.0) / self.size.height as f32 * 2.0,
388 ],
389 color,
390 },
391 ];
392
393 self.cell_renderer.queue().write_buffer(
395 &self.cell_renderer.buffers.bg_instance_buffer,
396 0,
397 bytemuck::cast_slice(&instances),
398 );
399
400 let mut encoder =
402 self.cell_renderer
403 .device()
404 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
405 label: Some("focus indicator encoder"),
406 });
407
408 {
409 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
410 label: Some("focus indicator pass"),
411 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
412 view: surface_view,
413 resolve_target: None,
414 ops: wgpu::Operations {
415 load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store,
417 },
418 depth_slice: None,
419 })],
420 depth_stencil_attachment: None,
421 timestamp_writes: None,
422 occlusion_query_set: None,
423 });
424
425 render_pass.set_pipeline(&self.cell_renderer.pipelines.bg_pipeline);
426 render_pass.set_vertex_buffer(0, self.cell_renderer.buffers.vertex_buffer.slice(..));
427 render_pass
428 .set_vertex_buffer(1, self.cell_renderer.buffers.bg_instance_buffer.slice(..));
429 render_pass.draw(0..4, 0..instances.len() as u32);
430 }
431
432 self.cell_renderer
433 .queue()
434 .submit(std::iter::once(encoder.finish()));
435 Ok(())
436 }
437
438 pub fn render_pane_titles(
443 &mut self,
444 surface_view: &wgpu::TextureView,
445 titles: &[PaneTitleInfo],
446 ) -> Result<()> {
447 if titles.is_empty() {
448 return Ok(());
449 }
450
451 let width = self.size.width as f32;
452 let height = self.size.height as f32;
453
454 let mut bg_instances = Vec::with_capacity(titles.len());
456 for title in titles {
457 let x_ndc = title.x / width * 2.0 - 1.0;
458 let y_ndc = 1.0 - (title.y / height * 2.0);
459 let w_ndc = title.width / width * 2.0;
460 let h_ndc = title.height / height * 2.0;
461
462 let brightness = if title.focused { 1.0 } else { 0.7 };
465
466 bg_instances.push(crate::cell_renderer::types::BackgroundInstance {
467 position: [x_ndc, y_ndc],
468 size: [w_ndc, h_ndc],
469 color: [
470 title.bg_color[0] * brightness,
471 title.bg_color[1] * brightness,
472 title.bg_color[2] * brightness,
473 1.0, ],
475 });
476 }
477
478 self.cell_renderer.queue().write_buffer(
480 &self.cell_renderer.buffers.bg_instance_buffer,
481 0,
482 bytemuck::cast_slice(&bg_instances),
483 );
484
485 let mut encoder =
487 self.cell_renderer
488 .device()
489 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
490 label: Some("pane title bg encoder"),
491 });
492
493 {
494 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
495 label: Some("pane title bg pass"),
496 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
497 view: surface_view,
498 resolve_target: None,
499 ops: wgpu::Operations {
500 load: wgpu::LoadOp::Load,
501 store: wgpu::StoreOp::Store,
502 },
503 depth_slice: None,
504 })],
505 depth_stencil_attachment: None,
506 timestamp_writes: None,
507 occlusion_query_set: None,
508 });
509
510 render_pass.set_pipeline(&self.cell_renderer.pipelines.bg_pipeline);
511 render_pass.set_vertex_buffer(0, self.cell_renderer.buffers.vertex_buffer.slice(..));
512 render_pass
513 .set_vertex_buffer(1, self.cell_renderer.buffers.bg_instance_buffer.slice(..));
514 render_pass.draw(0..4, 0..bg_instances.len() as u32);
515 }
516
517 self.cell_renderer
518 .queue()
519 .submit(std::iter::once(encoder.finish()));
520
521 let mut text_instances = Vec::new();
523 let baseline_y = self.cell_renderer.font.font_ascent;
524
525 for title in titles {
526 let title_text = &title.title;
527 if title_text.is_empty() {
528 continue;
529 }
530
531 let padding_x = 8.0;
533 let mut x_pos = title.x + padding_x;
534 let y_base = title.y + (title.height - self.cell_renderer.grid.cell_height) / 2.0;
535
536 let text_color = [
537 title.text_color[0],
538 title.text_color[1],
539 title.text_color[2],
540 if title.focused { 1.0 } else { 0.8 },
541 ];
542
543 let max_chars =
545 ((title.width - padding_x * 2.0) / self.cell_renderer.grid.cell_width) as usize;
546 let display_text: String = if title_text.len() > max_chars && max_chars > 3 {
547 let truncated: String = title_text.chars().take(max_chars - 1).collect();
548 format!("{}\u{2026}", truncated) } else {
550 title_text.clone()
551 };
552
553 for ch in display_text.chars() {
554 if x_pos >= title.x + title.width - padding_x {
555 break;
556 }
557
558 if let Some((font_idx, glyph_id)) =
559 self.cell_renderer.font_manager.find_glyph(ch, false, false)
560 {
561 let cache_key = ((font_idx as u64) << 32) | (glyph_id as u64);
562 let force_monochrome = crate::cell_renderer::atlas::should_render_as_symbol(ch);
564 let info = if self
565 .cell_renderer
566 .atlas
567 .glyph_cache
568 .contains_key(&cache_key)
569 {
570 self.cell_renderer.lru_remove(cache_key);
571 self.cell_renderer.lru_push_front(cache_key);
572 self.cell_renderer
573 .atlas
574 .glyph_cache
575 .get(&cache_key)
576 .expect("Glyph cache entry must exist after contains_key check")
577 .clone()
578 } else if let Some(raster) =
579 self.cell_renderer
580 .rasterize_glyph(font_idx, glyph_id, force_monochrome)
581 {
582 let info = self.cell_renderer.upload_glyph(cache_key, &raster);
583 self.cell_renderer
584 .atlas
585 .glyph_cache
586 .insert(cache_key, info.clone());
587 self.cell_renderer.lru_push_front(cache_key);
588 info
589 } else {
590 x_pos += self.cell_renderer.grid.cell_width;
591 continue;
592 };
593
594 let glyph_left = x_pos + info.bearing_x;
595 let glyph_top = y_base + (baseline_y - info.bearing_y);
596
597 text_instances.push(crate::cell_renderer::types::TextInstance {
598 position: [
599 glyph_left / width * 2.0 - 1.0,
600 1.0 - (glyph_top / height * 2.0),
601 ],
602 size: [
603 info.width as f32 / width * 2.0,
604 info.height as f32 / height * 2.0,
605 ],
606 tex_offset: [info.x as f32 / ATLAS_SIZE, info.y as f32 / ATLAS_SIZE],
607 tex_size: [
608 info.width as f32 / ATLAS_SIZE,
609 info.height as f32 / ATLAS_SIZE,
610 ],
611 color: text_color,
612 is_colored: if info.is_colored { 1 } else { 0 },
613 });
614 }
615
616 x_pos += self.cell_renderer.grid.cell_width;
617 }
618 }
619
620 if text_instances.is_empty() {
621 return Ok(());
622 }
623
624 self.cell_renderer.queue().write_buffer(
626 &self.cell_renderer.buffers.text_instance_buffer,
627 0,
628 bytemuck::cast_slice(&text_instances),
629 );
630
631 let mut encoder =
633 self.cell_renderer
634 .device()
635 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
636 label: Some("pane title text encoder"),
637 });
638
639 {
640 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
641 label: Some("pane title text pass"),
642 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
643 view: surface_view,
644 resolve_target: None,
645 ops: wgpu::Operations {
646 load: wgpu::LoadOp::Load,
647 store: wgpu::StoreOp::Store,
648 },
649 depth_slice: None,
650 })],
651 depth_stencil_attachment: None,
652 timestamp_writes: None,
653 occlusion_query_set: None,
654 });
655
656 render_pass.set_pipeline(&self.cell_renderer.pipelines.text_pipeline);
657 render_pass.set_bind_group(0, &self.cell_renderer.pipelines.text_bind_group, &[]);
658 render_pass.set_vertex_buffer(0, self.cell_renderer.buffers.vertex_buffer.slice(..));
659 render_pass
660 .set_vertex_buffer(1, self.cell_renderer.buffers.text_instance_buffer.slice(..));
661 render_pass.draw(0..4, 0..text_instances.len() as u32);
662 }
663
664 self.cell_renderer
665 .queue()
666 .submit(std::iter::once(encoder.finish()));
667
668 Ok(())
669 }
670}