batuta/oracle/svg/renderers/
text_heavy.rs1use crate::oracle::svg::builder::SvgBuilder;
6use crate::oracle::svg::grid_protocol::LayoutTemplate;
7use crate::oracle::svg::layout::{Viewport, GRID_SIZE};
8use crate::oracle::svg::palette::SovereignPalette;
9#[allow(unused_imports)]
11use crate::oracle::svg::shapes::Point;
12#[allow(unused_imports)]
14use crate::oracle::svg::typography::{FontWeight, TextAlign, TextStyle};
15
16#[derive(Debug)]
18pub struct TextHeavyRenderer {
19 builder: SvgBuilder,
21 palette: SovereignPalette,
23 current_y: f32,
25 line_height: f32,
27 margin_left: f32,
29}
30
31impl TextHeavyRenderer {
32 pub fn new() -> Self {
34 let viewport = Viewport::document();
35 Self {
36 builder: SvgBuilder::new().viewport(viewport),
37 palette: SovereignPalette::light(),
38 current_y: viewport.padding + GRID_SIZE * 4.0,
39 line_height: GRID_SIZE * 3.0, margin_left: viewport.padding + GRID_SIZE * 2.0,
41 }
42 }
43
44 pub fn viewport(mut self, viewport: Viewport) -> Self {
46 self.current_y = viewport.padding + GRID_SIZE * 4.0;
47 self.margin_left = viewport.padding + GRID_SIZE * 2.0;
48 self.builder = self.builder.viewport(viewport);
49 self
50 }
51
52 pub fn dark_mode(mut self) -> Self {
54 self.palette = SovereignPalette::dark();
55 self.builder = self.builder.dark_mode();
56 self
57 }
58
59 pub fn line_height(mut self, height: f32) -> Self {
61 self.line_height = height;
62 self
63 }
64
65 pub fn title(mut self, text: &str) -> Self {
67 self.builder = self.builder.title(text);
68
69 let style = self
70 .builder
71 .get_typography()
72 .headline_large
73 .clone()
74 .with_color(self.palette.material.on_background);
75
76 self.builder = self.builder.text_styled(self.margin_left, self.current_y, text, style);
77 self.current_y += GRID_SIZE * 6.0; self
80 }
81
82 pub fn heading(mut self, text: &str) -> Self {
84 self.current_y += GRID_SIZE * 2.0; let style = self
87 .builder
88 .get_typography()
89 .headline_small
90 .clone()
91 .with_color(self.palette.material.on_background);
92
93 self.builder = self.builder.text_styled(self.margin_left, self.current_y, text, style);
94 self.current_y += GRID_SIZE * 4.0;
95
96 self
97 }
98
99 pub fn paragraph(mut self, text: &str) -> Self {
101 let style = self
102 .builder
103 .get_typography()
104 .body_medium
105 .clone()
106 .with_color(self.palette.material.on_surface);
107
108 let max_width = 600.0; let words: Vec<&str> = text.split_whitespace().collect();
111 let mut current_line = String::new();
112 let char_width = 8.0; for word in words {
115 let test_line = if current_line.is_empty() {
116 word.to_string()
117 } else {
118 format!("{} {}", current_line, word)
119 };
120
121 if test_line.len() as f32 * char_width > max_width && !current_line.is_empty() {
122 self.builder = self.builder.text_styled(
124 self.margin_left,
125 self.current_y,
126 ¤t_line,
127 style.clone(),
128 );
129 self.current_y += self.line_height;
130 current_line = word.to_string();
131 } else {
132 current_line = test_line;
133 }
134 }
135
136 if !current_line.is_empty() {
138 self.builder =
139 self.builder.text_styled(self.margin_left, self.current_y, ¤t_line, style);
140 self.current_y += self.line_height;
141 }
142
143 self.current_y += GRID_SIZE; self
146 }
147
148 pub fn bullet(mut self, text: &str) -> Self {
150 let style = self
151 .builder
152 .get_typography()
153 .body_medium
154 .clone()
155 .with_color(self.palette.material.on_surface);
156
157 self.builder =
159 self.builder.text_styled(self.margin_left, self.current_y, "•", style.clone());
160
161 self.builder = self.builder.text_styled(
163 self.margin_left + GRID_SIZE * 2.0,
164 self.current_y,
165 text,
166 style,
167 );
168
169 self.current_y += self.line_height;
170
171 self
172 }
173
174 pub fn numbered(mut self, number: u32, text: &str) -> Self {
176 let style = self
177 .builder
178 .get_typography()
179 .body_medium
180 .clone()
181 .with_color(self.palette.material.on_surface);
182
183 self.builder = self.builder.text_styled(
185 self.margin_left,
186 self.current_y,
187 &format!("{}.", number),
188 style.clone(),
189 );
190
191 self.builder = self.builder.text_styled(
193 self.margin_left + GRID_SIZE * 3.0,
194 self.current_y,
195 text,
196 style,
197 );
198
199 self.current_y += self.line_height;
200
201 self
202 }
203
204 pub fn code(mut self, code: &str) -> Self {
206 self.current_y += GRID_SIZE;
207
208 let bg_color = self.palette.material.surface_variant;
210 let lines: Vec<&str> = code.lines().collect();
211 let code_height = (lines.len() as f32 * self.line_height) + GRID_SIZE * 2.0;
212
213 self.builder = self.builder.rect_styled(
214 "_code_bg",
215 self.margin_left,
216 self.current_y,
217 500.0,
218 code_height,
219 bg_color,
220 None,
221 GRID_SIZE / 2.0,
222 );
223
224 self.current_y += GRID_SIZE;
225
226 let style =
227 self.builder.get_typography().code.clone().with_color(self.palette.material.on_surface);
228
229 for line in lines {
230 self.builder = self.builder.text_styled(
231 self.margin_left + GRID_SIZE,
232 self.current_y,
233 line,
234 style.clone(),
235 );
236 self.current_y += self.line_height;
237 }
238
239 self.current_y += GRID_SIZE * 2.0; self
242 }
243
244 pub fn labeled_box(mut self, label: &str, value: &str) -> Self {
246 let label_style = self
247 .builder
248 .get_typography()
249 .label_medium
250 .clone()
251 .with_color(self.palette.material.on_surface_variant);
252
253 let value_style = self
254 .builder
255 .get_typography()
256 .body_medium
257 .clone()
258 .with_color(self.palette.material.on_surface);
259
260 self.builder =
262 self.builder.text_styled(self.margin_left, self.current_y, label, label_style);
263
264 self.builder = self.builder.text_styled(
266 self.margin_left + GRID_SIZE * 15.0,
267 self.current_y,
268 value,
269 value_style,
270 );
271
272 self.current_y += self.line_height;
273
274 self
275 }
276
277 pub fn divider(mut self) -> Self {
279 self.current_y += GRID_SIZE;
280
281 self.builder = self.builder.line_styled(
282 self.margin_left,
283 self.current_y,
284 self.margin_left + 600.0,
285 self.current_y,
286 self.palette.material.outline_variant,
287 1.0,
288 );
289
290 self.current_y += GRID_SIZE * 2.0;
291
292 self
293 }
294
295 pub fn space(mut self, lines: u32) -> Self {
297 self.current_y += self.line_height * lines as f32;
298 self
299 }
300
301 pub fn grid_protocol(mut self) -> Self {
303 self.builder = self.builder.grid_protocol().video_styles();
304 self.palette = SovereignPalette::dark();
305 self
306 }
307
308 pub fn template(mut self, template: LayoutTemplate) -> Self {
310 if !self.builder.is_grid_mode() {
311 self = self.grid_protocol();
312 }
313
314 let allocations = template.allocations();
315 for (name, span) in allocations {
316 let _ = self.builder.allocate(name, span);
317 }
318
319 self
320 }
321
322 pub fn build(self) -> String {
324 self.builder.build()
325 }
326}
327
328impl Default for TextHeavyRenderer {
329 fn default() -> Self {
330 Self::new()
331 }
332}
333
334#[cfg(test)]
335mod tests {
336 use super::*;
337
338 #[test]
339 fn test_text_heavy_renderer_creation() {
340 let renderer = TextHeavyRenderer::new();
341 assert_eq!(renderer.line_height, 24.0);
342 }
343
344 #[test]
345 fn test_text_heavy_title() {
346 let svg = TextHeavyRenderer::new().title("Documentation").build();
347
348 assert!(svg.contains("<title>Documentation</title>"));
349 assert!(svg.contains("Documentation"));
350 }
351
352 #[test]
353 fn test_text_heavy_heading() {
354 let svg = TextHeavyRenderer::new().title("Doc").heading("Section 1").build();
355
356 assert!(svg.contains("Section 1"));
357 }
358
359 #[test]
360 fn test_text_heavy_paragraph() {
361 let svg = TextHeavyRenderer::new()
362 .paragraph("This is a test paragraph with some text content.")
363 .build();
364
365 assert!(svg.contains("test paragraph"));
366 }
367
368 #[test]
369 fn test_text_heavy_bullets() {
370 let svg = TextHeavyRenderer::new().bullet("First item").bullet("Second item").build();
371
372 assert!(svg.contains("First item"));
373 assert!(svg.contains("Second item"));
374 assert!(svg.contains("•"));
375 }
376
377 #[test]
378 fn test_text_heavy_numbered() {
379 let svg =
380 TextHeavyRenderer::new().numbered(1, "First step").numbered(2, "Second step").build();
381
382 assert!(svg.contains("1."));
383 assert!(svg.contains("First step"));
384 }
385
386 #[test]
387 fn test_text_heavy_code() {
388 let svg = TextHeavyRenderer::new().code("let x = 42;\nprintln!(\"{}\", x);").build();
389
390 assert!(svg.contains("let x = 42"));
391 }
392
393 #[test]
394 fn test_text_heavy_labeled_box() {
395 let svg = TextHeavyRenderer::new()
396 .labeled_box("Version:", "1.0.0")
397 .labeled_box("Author:", "John Doe")
398 .build();
399
400 assert!(svg.contains("Version:"));
401 assert!(svg.contains("1.0.0"));
402 }
403
404 #[test]
405 fn test_text_heavy_divider() {
406 let svg = TextHeavyRenderer::new().divider().build();
407
408 assert!(svg.contains("<line"));
409 }
410
411 #[test]
412 fn test_text_heavy_dark_mode() {
413 let renderer = TextHeavyRenderer::new().dark_mode();
414 let _svg = renderer.build();
415 }
417
418 #[test]
419 fn test_text_heavy_complete_document() {
420 let svg = TextHeavyRenderer::new()
421 .title("API Documentation")
422 .paragraph("This document describes the API.")
423 .heading("Endpoints")
424 .bullet("GET /api/users")
425 .bullet("POST /api/users")
426 .divider()
427 .heading("Examples")
428 .code("curl https://api.example.com/users")
429 .build();
430
431 assert!(svg.contains("API Documentation"));
432 assert!(svg.contains("Endpoints"));
433 assert!(svg.contains("GET /api/users"));
434 }
435
436 #[test]
437 fn test_text_heavy_default() {
438 let renderer = TextHeavyRenderer::default();
439 assert_eq!(renderer.line_height, 24.0);
440 }
441
442 #[test]
443 fn test_text_heavy_viewport() {
444 let viewport = Viewport::new(800.0, 600.0);
445 let renderer = TextHeavyRenderer::new().viewport(viewport);
446 let svg = renderer.build();
447 assert!(svg.contains("<svg"));
449 }
450
451 #[test]
452 fn test_text_heavy_line_height() {
453 let renderer = TextHeavyRenderer::new().line_height(32.0);
454 assert_eq!(renderer.line_height, 32.0);
455 }
456
457 #[test]
458 fn test_text_heavy_space() {
459 let initial_y = TextHeavyRenderer::new().current_y;
460 let renderer = TextHeavyRenderer::new().space(2);
461 assert!(renderer.current_y > initial_y);
463 }
464
465 #[test]
466 fn test_text_heavy_long_paragraph() {
467 let long_text = "This is a very long paragraph that should trigger word wrapping because it exceeds the maximum width allowed for a single line in the text renderer.";
469 let svg = TextHeavyRenderer::new().paragraph(long_text).build();
470 assert!(svg.contains("paragraph"));
472 }
473
474 #[test]
475 fn test_text_heavy_empty_paragraph() {
476 let svg = TextHeavyRenderer::new().paragraph("").build();
477 assert!(svg.contains("<svg"));
479 }
480
481 #[test]
482 fn test_text_heavy_multiline_code() {
483 let code = "line 1\nline 2\nline 3";
484 let svg = TextHeavyRenderer::new().code(code).build();
485 assert!(svg.contains("line 1"));
486 assert!(svg.contains("line 2"));
487 assert!(svg.contains("line 3"));
488 }
489
490 #[test]
491 fn test_text_heavy_chain_methods() {
492 let svg = TextHeavyRenderer::new()
493 .line_height(28.0)
494 .title("Test")
495 .space(1)
496 .paragraph("Content")
497 .divider()
498 .bullet("Item")
499 .numbered(1, "Step")
500 .code("code")
501 .labeled_box("Key", "Value")
502 .build();
503
504 assert!(svg.contains("Test"));
505 assert!(svg.contains("Content"));
506 assert!(svg.contains("Item"));
507 }
508
509 #[test]
510 fn test_text_heavy_viewport_updates_margins() {
511 let viewport = Viewport::new(1024.0, 768.0);
512 let renderer = TextHeavyRenderer::new().viewport(viewport);
513 assert!(renderer.margin_left > 0.0);
515 }
516
517 #[test]
518 fn test_text_heavy_viewport_document() {
519 let viewport = Viewport::document();
520 let renderer = TextHeavyRenderer::new().viewport(viewport);
521 let svg = renderer.build();
522 assert!(svg.contains("<svg"));
523 }
524
525 #[test]
526 fn test_text_heavy_heading_increases_y() {
527 let initial = TextHeavyRenderer::new();
528 let after_heading = TextHeavyRenderer::new().heading("Section");
529 assert!(after_heading.current_y > initial.current_y);
531 }
532
533 #[test]
534 fn test_text_heavy_bullet_increases_y() {
535 let initial = TextHeavyRenderer::new();
536 let after_bullet = TextHeavyRenderer::new().bullet("Item");
537 assert!(after_bullet.current_y > initial.current_y);
538 }
539
540 #[test]
541 fn test_text_heavy_numbered_increases_y() {
542 let initial = TextHeavyRenderer::new();
543 let after_numbered = TextHeavyRenderer::new().numbered(1, "Step");
544 assert!(after_numbered.current_y > initial.current_y);
545 }
546
547 #[test]
550 fn test_text_heavy_grid_protocol() {
551 let svg = TextHeavyRenderer::new().grid_protocol().title("Grid Doc").build();
552
553 assert!(svg.contains("viewBox=\"0 0 1920 1080\""));
554 assert!(svg.contains("GRID PROTOCOL MANIFEST"));
555 }
556
557 #[test]
558 fn test_text_heavy_template() {
559 let svg = TextHeavyRenderer::new().template(LayoutTemplate::TwoColumn).build();
560
561 assert!(svg.contains("GRID PROTOCOL MANIFEST"));
562 assert!(svg.contains("\"header\""));
563 assert!(svg.contains("\"left\""));
564 assert!(svg.contains("\"right\""));
565 }
566
567 #[test]
568 fn test_text_heavy_template_auto_enables_grid() {
569 let svg = TextHeavyRenderer::new().template(LayoutTemplate::ReflectionReadings).build();
570
571 assert!(svg.contains("viewBox=\"0 0 1920 1080\""));
572 }
573}