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 multiview_mask: None,
295 });
296
297 render_pass.set_pipeline(&self.cell_renderer.pipelines.bg_pipeline);
298 render_pass.set_vertex_buffer(0, self.cell_renderer.buffers.vertex_buffer.slice(..));
299 render_pass
300 .set_vertex_buffer(1, self.cell_renderer.buffers.bg_instance_buffer.slice(..));
301 render_pass.draw(0..4, 0..instances.len() as u32);
302 }
303
304 self.cell_renderer
305 .queue()
306 .submit(std::iter::once(encoder.finish()));
307
308 instances.clear();
310 self.scratch_divider_instances = instances;
311
312 Ok(())
313 }
314
315 pub fn render_focus_indicator(
324 &mut self,
325 surface_view: &wgpu::TextureView,
326 viewport: &PaneViewport,
327 settings: &PaneDividerSettings,
328 ) -> Result<()> {
329 if !settings.show_focus_indicator {
330 return Ok(());
331 }
332
333 let border_w = settings.focus_width;
334 let color = [
335 settings.focus_color[0],
336 settings.focus_color[1],
337 settings.focus_color[2],
338 1.0,
339 ];
340
341 let instances = vec![
343 crate::cell_renderer::types::BackgroundInstance {
345 position: [
346 viewport.x / self.size.width as f32 * 2.0 - 1.0,
347 1.0 - (viewport.y / self.size.height as f32 * 2.0),
348 ],
349 size: [
350 viewport.width / self.size.width as f32 * 2.0,
351 border_w / self.size.height as f32 * 2.0,
352 ],
353 color,
354 },
355 crate::cell_renderer::types::BackgroundInstance {
357 position: [
358 viewport.x / self.size.width as f32 * 2.0 - 1.0,
359 1.0 - ((viewport.y + viewport.height - border_w) / self.size.height as f32
360 * 2.0),
361 ],
362 size: [
363 viewport.width / self.size.width as f32 * 2.0,
364 border_w / self.size.height as f32 * 2.0,
365 ],
366 color,
367 },
368 crate::cell_renderer::types::BackgroundInstance {
370 position: [
371 viewport.x / self.size.width as f32 * 2.0 - 1.0,
372 1.0 - ((viewport.y + border_w) / self.size.height as f32 * 2.0),
373 ],
374 size: [
375 border_w / self.size.width as f32 * 2.0,
376 (viewport.height - border_w * 2.0) / self.size.height as f32 * 2.0,
377 ],
378 color,
379 },
380 crate::cell_renderer::types::BackgroundInstance {
382 position: [
383 (viewport.x + viewport.width - border_w) / self.size.width as f32 * 2.0 - 1.0,
384 1.0 - ((viewport.y + border_w) / self.size.height as f32 * 2.0),
385 ],
386 size: [
387 border_w / self.size.width as f32 * 2.0,
388 (viewport.height - border_w * 2.0) / self.size.height as f32 * 2.0,
389 ],
390 color,
391 },
392 ];
393
394 self.cell_renderer.queue().write_buffer(
396 &self.cell_renderer.buffers.bg_instance_buffer,
397 0,
398 bytemuck::cast_slice(&instances),
399 );
400
401 let mut encoder =
403 self.cell_renderer
404 .device()
405 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
406 label: Some("focus indicator encoder"),
407 });
408
409 {
410 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
411 label: Some("focus indicator pass"),
412 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
413 view: surface_view,
414 resolve_target: None,
415 ops: wgpu::Operations {
416 load: wgpu::LoadOp::Load, store: wgpu::StoreOp::Store,
418 },
419 depth_slice: None,
420 })],
421 depth_stencil_attachment: None,
422 timestamp_writes: None,
423 occlusion_query_set: None,
424 multiview_mask: None,
425 });
426
427 render_pass.set_pipeline(&self.cell_renderer.pipelines.bg_pipeline);
428 render_pass.set_vertex_buffer(0, self.cell_renderer.buffers.vertex_buffer.slice(..));
429 render_pass
430 .set_vertex_buffer(1, self.cell_renderer.buffers.bg_instance_buffer.slice(..));
431 render_pass.draw(0..4, 0..instances.len() as u32);
432 }
433
434 self.cell_renderer
435 .queue()
436 .submit(std::iter::once(encoder.finish()));
437 Ok(())
438 }
439
440 pub fn render_pane_titles(
445 &mut self,
446 surface_view: &wgpu::TextureView,
447 titles: &[PaneTitleInfo],
448 ) -> Result<()> {
449 if titles.is_empty() {
450 return Ok(());
451 }
452
453 let width = self.size.width as f32;
454 let height = self.size.height as f32;
455
456 let mut bg_instances = Vec::with_capacity(titles.len());
458 for title in titles {
459 let x_ndc = title.x / width * 2.0 - 1.0;
460 let y_ndc = 1.0 - (title.y / height * 2.0);
461 let w_ndc = title.width / width * 2.0;
462 let h_ndc = title.height / height * 2.0;
463
464 let brightness = if title.focused { 1.0 } else { 0.7 };
467
468 bg_instances.push(crate::cell_renderer::types::BackgroundInstance {
469 position: [x_ndc, y_ndc],
470 size: [w_ndc, h_ndc],
471 color: [
472 title.bg_color[0] * brightness,
473 title.bg_color[1] * brightness,
474 title.bg_color[2] * brightness,
475 1.0, ],
477 });
478 }
479
480 self.cell_renderer.queue().write_buffer(
482 &self.cell_renderer.buffers.bg_instance_buffer,
483 0,
484 bytemuck::cast_slice(&bg_instances),
485 );
486
487 let mut encoder =
489 self.cell_renderer
490 .device()
491 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
492 label: Some("pane title bg encoder"),
493 });
494
495 {
496 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
497 label: Some("pane title bg pass"),
498 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
499 view: surface_view,
500 resolve_target: None,
501 ops: wgpu::Operations {
502 load: wgpu::LoadOp::Load,
503 store: wgpu::StoreOp::Store,
504 },
505 depth_slice: None,
506 })],
507 depth_stencil_attachment: None,
508 timestamp_writes: None,
509 occlusion_query_set: None,
510 multiview_mask: None,
511 });
512
513 render_pass.set_pipeline(&self.cell_renderer.pipelines.bg_pipeline);
514 render_pass.set_vertex_buffer(0, self.cell_renderer.buffers.vertex_buffer.slice(..));
515 render_pass
516 .set_vertex_buffer(1, self.cell_renderer.buffers.bg_instance_buffer.slice(..));
517 render_pass.draw(0..4, 0..bg_instances.len() as u32);
518 }
519
520 self.cell_renderer
521 .queue()
522 .submit(std::iter::once(encoder.finish()));
523
524 let mut text_instances = Vec::new();
526 let baseline_y = self.cell_renderer.font.font_ascent;
527
528 for title in titles {
529 let title_text = &title.title;
530 if title_text.is_empty() {
531 continue;
532 }
533
534 let padding_x = 8.0;
536 let mut x_pos = title.x + padding_x;
537 let y_base = title.y + (title.height - self.cell_renderer.grid.cell_height) / 2.0;
538
539 let text_color = [
540 title.text_color[0],
541 title.text_color[1],
542 title.text_color[2],
543 if title.focused { 1.0 } else { 0.8 },
544 ];
545
546 let max_chars =
548 ((title.width - padding_x * 2.0) / self.cell_renderer.grid.cell_width) as usize;
549 let display_text: String = if title_text.len() > max_chars && max_chars > 3 {
550 let truncated: String = title_text.chars().take(max_chars - 1).collect();
551 format!("{}\u{2026}", truncated) } else {
553 title_text.clone()
554 };
555
556 for ch in display_text.chars() {
557 if x_pos >= title.x + title.width - padding_x {
558 break;
559 }
560
561 if let Some((font_idx, glyph_id)) =
562 self.cell_renderer.font_manager.find_glyph(ch, false, false)
563 {
564 let cache_key = ((font_idx as u64) << 32) | (glyph_id as u64);
565 let force_monochrome = crate::cell_renderer::atlas::should_render_as_symbol(ch);
567 let info = if self
568 .cell_renderer
569 .atlas
570 .glyph_cache
571 .contains_key(&cache_key)
572 {
573 self.cell_renderer.lru_remove(cache_key);
574 self.cell_renderer.lru_push_front(cache_key);
575 self.cell_renderer
576 .atlas
577 .glyph_cache
578 .get(&cache_key)
579 .expect("Glyph cache entry must exist after contains_key check")
580 .clone()
581 } else if let Some(raster) =
582 self.cell_renderer
583 .rasterize_glyph(font_idx, glyph_id, force_monochrome)
584 {
585 let info = self.cell_renderer.upload_glyph(cache_key, &raster);
586 self.cell_renderer
587 .atlas
588 .glyph_cache
589 .insert(cache_key, info.clone());
590 self.cell_renderer.lru_push_front(cache_key);
591 info
592 } else {
593 x_pos += self.cell_renderer.grid.cell_width;
594 continue;
595 };
596
597 let glyph_left = x_pos + info.bearing_x;
598 let glyph_top = y_base + (baseline_y - info.bearing_y);
599
600 text_instances.push(crate::cell_renderer::types::TextInstance {
601 position: [
602 glyph_left / width * 2.0 - 1.0,
603 1.0 - (glyph_top / height * 2.0),
604 ],
605 size: [
606 info.width as f32 / width * 2.0,
607 info.height as f32 / height * 2.0,
608 ],
609 tex_offset: [info.x as f32 / ATLAS_SIZE, info.y as f32 / ATLAS_SIZE],
610 tex_size: [
611 info.width as f32 / ATLAS_SIZE,
612 info.height as f32 / ATLAS_SIZE,
613 ],
614 color: text_color,
615 is_colored: if info.is_colored { 1 } else { 0 },
616 });
617 }
618
619 x_pos += self.cell_renderer.grid.cell_width;
620 }
621 }
622
623 if text_instances.is_empty() {
624 return Ok(());
625 }
626
627 self.cell_renderer.queue().write_buffer(
629 &self.cell_renderer.buffers.text_instance_buffer,
630 0,
631 bytemuck::cast_slice(&text_instances),
632 );
633
634 let mut encoder =
636 self.cell_renderer
637 .device()
638 .create_command_encoder(&wgpu::CommandEncoderDescriptor {
639 label: Some("pane title text encoder"),
640 });
641
642 {
643 let mut render_pass = encoder.begin_render_pass(&wgpu::RenderPassDescriptor {
644 label: Some("pane title text pass"),
645 color_attachments: &[Some(wgpu::RenderPassColorAttachment {
646 view: surface_view,
647 resolve_target: None,
648 ops: wgpu::Operations {
649 load: wgpu::LoadOp::Load,
650 store: wgpu::StoreOp::Store,
651 },
652 depth_slice: None,
653 })],
654 depth_stencil_attachment: None,
655 timestamp_writes: None,
656 occlusion_query_set: None,
657 multiview_mask: None,
658 });
659
660 render_pass.set_pipeline(&self.cell_renderer.pipelines.text_pipeline);
661 render_pass.set_bind_group(0, &self.cell_renderer.pipelines.text_bind_group, &[]);
662 render_pass.set_vertex_buffer(0, self.cell_renderer.buffers.vertex_buffer.slice(..));
663 render_pass
664 .set_vertex_buffer(1, self.cell_renderer.buffers.text_instance_buffer.slice(..));
665 render_pass.draw(0..4, 0..text_instances.len() as u32);
666 }
667
668 self.cell_renderer
669 .queue()
670 .submit(std::iter::once(encoder.finish()));
671
672 Ok(())
673 }
674}