beyonder_gpu/block_renderers/
mod.rs1pub mod agent_message;
2pub mod approval;
3pub mod shell_block;
4
5use crate::pipeline::RectInstance;
6use beyonder_core::{Block, BlockContent};
7
8pub trait BlockRenderer {
10 fn measure_height(&self, block: &Block, width: f32, font_size: f32) -> f32;
12
13 fn render_rects(
15 &self,
16 block: &Block,
17 x: f32,
18 y: f32,
19 width: f32,
20 rects: &mut Vec<RectInstance>,
21 );
22}
23
24pub fn measure_block_height(block: &Block, width: f32, font_size: f32) -> f32 {
27 let cmd_bar_h = font_size * 2.8; let inner_gap = font_size * 0.4; let header_h = font_size * 1.8; let line_h = font_size * 1.45;
31 let v_pad = font_size * 0.6; match &block.content {
33 BlockContent::ShellCommand { output, .. } => {
34 let last_content = output
37 .rows
38 .iter()
39 .rposition(|row| {
40 row.cells.iter().any(|c| {
41 let fc = c.grapheme.chars().next().unwrap_or('\0');
42 fc != ' ' && fc != '\0'
43 })
44 })
45 .map(|i| i + 1)
46 .unwrap_or(0);
47 if last_content == 0 {
48 cmd_bar_h
50 } else {
51 cmd_bar_h + inner_gap + last_content as f32 * line_h + v_pad
52 }
53 }
54 BlockContent::AgentMessage { content_blocks, .. } => {
55 let effective_w = (width - font_size * 1.0).max(1.0);
58 let chars_per_line = ((effective_w / (font_size * 0.6)).floor() as usize).max(1);
59
60 let visual_lines: f32 = content_blocks
61 .iter()
62 .map(|cb| match cb {
63 beyonder_core::ContentBlock::Text { text } => text
64 .lines()
65 .map(|line| {
66 let stripped = strip_md_markers(line);
67 let chars = stripped.chars().count().max(1);
68 chars.div_ceil(chars_per_line) as f32
69 })
70 .sum::<f32>()
71 .max(1.0),
72 beyonder_core::ContentBlock::Code { code, .. } => {
73 code.lines()
75 .map(|line| {
76 let chars = line.chars().count().max(1);
77 chars.div_ceil(chars_per_line) as f32
78 })
79 .sum::<f32>()
80 .max(1.0)
81 + 1.0
82 }
83 _ => 1.0,
84 })
85 .sum::<f32>()
86 .max(1.0);
87 let extra = if matches!(block.status, beyonder_core::BlockStatus::Running) {
89 line_h * 1.5
90 } else {
91 0.0
92 };
93 v_pad + visual_lines * line_h + v_pad + extra
95 }
96 BlockContent::ApprovalRequest { .. } => font_size * 10.0,
97 BlockContent::ToolCall { output, error, .. } => {
98 let text = output.as_deref().or(error.as_deref()).unwrap_or("");
99 let lines = if text.is_empty() {
100 1.0
101 } else {
102 text.lines().count() as f32
103 };
104 header_h + lines * line_h + v_pad
105 }
106 BlockContent::Text { text } => {
107 let lines = text.lines().count().max(1) as f32;
108 v_pad + lines * line_h + v_pad
109 }
110 _ => font_size * 6.0,
111 }
112}
113
114fn strip_md_markers(line: &str) -> String {
116 let s = line.trim_start_matches('#').trim_start();
117 let s = if s.starts_with("- ") || s.starts_with("* ") {
118 &s[2..]
119 } else {
120 s
121 };
122 s.replace("**", "")
124 .replace('*', "")
125 .replace("__", "")
126 .replace('`', "")
127}
128
129pub fn render_block_background(
131 _block: &Block,
132 x: f32,
133 y: f32,
134 width: f32,
135 height: f32,
136 rects: &mut Vec<RectInstance>,
137) {
138 rects.push(
140 RectInstance::filled(x, y, width, height, [0.118, 0.118, 0.180, 1.0]).with_radius(3.0),
141 );
142}