tui_piechart/title.rs
1//! Title positioning, alignment, and styling configuration for block wrappers.
2//!
3//! This module provides types and functionality for controlling where and how
4//! block titles are positioned, aligned, and styled with different Unicode fonts.
5//!
6//! # Examples
7//!
8//! ## Using the unified Title API
9//!
10//! ```
11//! use tui_piechart::title::{Title, TitleStyle};
12//! use tui_piechart::border_style::BorderStyle;
13//!
14//! // Create a positioned title using fluent API (preserves your text styling)
15//! let styled_text = TitleStyle::Bold.apply("My Chart");
16//! let block = BorderStyle::Rounded.block()
17//! .title(
18//! Title::new(styled_text)
19//! .center()
20//! .bottom()
21//! );
22//! ```
23//!
24//! ## Using individual components (legacy)
25//!
26//! ```
27//! use tui_piechart::title::{TitleAlignment, TitlePosition, TitleStyle, BlockExt};
28//! use tui_piechart::border_style::BorderStyle;
29//!
30//! let title = TitleStyle::Bold.apply("My Chart");
31//! let block = BorderStyle::Rounded.block()
32//! .title(title)
33//! .title_alignment_horizontal(TitleAlignment::Center)
34//! .title_vertical_position(TitlePosition::Bottom);
35//! ```
36
37use ratatui::layout::Alignment;
38use ratatui::text::Line;
39use ratatui::widgets::Block;
40
41/// A builder for positioning block titles with horizontal alignment and vertical placement.
42///
43/// This struct handles only the positioning of titles (alignment and position),
44/// preserving any text styling you've already applied. You can style your text
45/// separately using `TitleStyle` or ratatui's styling features.
46///
47/// # Examples
48///
49/// ```
50/// use tui_piechart::title::{Title, TitleStyle};
51/// use tui_piechart::border_style::BorderStyle;
52///
53/// // Simple title with defaults (center top)
54/// let simple = Title::new("Statistics");
55///
56/// // Style text first, then position it
57/// let styled_text = TitleStyle::Bold.apply("Results");
58/// let title = Title::new(styled_text)
59/// .right()
60/// .bottom();
61///
62/// // Position plain text
63/// let positioned = Title::new("Dashboard")
64/// .center()
65/// .top();
66/// ```
67///
68/// # Method Chaining
69///
70/// All builder methods return `Self`, allowing for fluent method chaining:
71///
72/// ```
73/// use tui_piechart::title::Title;
74///
75/// let title = Title::new("My Chart")
76/// .center() // horizontal alignment
77/// .bottom(); // vertical position
78/// ```
79#[derive(Debug, Clone, PartialEq, Eq)]
80pub struct Title {
81 text: String,
82 alignment: TitleAlignment,
83 position: TitlePosition,
84}
85
86impl Title {
87 /// Creates a new title with the given text and default positioning.
88 ///
89 /// Defaults: Center alignment, Top position
90 ///
91 /// The text is preserved as-is, including any styling you've already applied.
92 #[must_use]
93 pub fn new(text: impl Into<String>) -> Self {
94 Self {
95 text: text.into(),
96 alignment: TitleAlignment::default(),
97 position: TitlePosition::default(),
98 }
99 }
100
101 /// Sets horizontal alignment to Start (left in LTR).
102 #[must_use]
103 pub fn left(mut self) -> Self {
104 self.alignment = TitleAlignment::Start;
105 self
106 }
107
108 /// Sets horizontal alignment to Center.
109 #[must_use]
110 pub fn center(mut self) -> Self {
111 self.alignment = TitleAlignment::Center;
112 self
113 }
114
115 /// Sets horizontal alignment to End (right in LTR).
116 #[must_use]
117 pub fn right(mut self) -> Self {
118 self.alignment = TitleAlignment::End;
119 self
120 }
121
122 /// Sets vertical position to Top.
123 #[must_use]
124 pub fn top(mut self) -> Self {
125 self.position = TitlePosition::Top;
126 self
127 }
128
129 /// Sets vertical position to Bottom.
130 #[must_use]
131 pub fn bottom(mut self) -> Self {
132 self.position = TitlePosition::Bottom;
133 self
134 }
135
136 /// Returns the text as a Line for rendering (preserves original styling).
137 #[must_use]
138 pub fn render(&self) -> Line<'static> {
139 Line::from(self.text.clone())
140 }
141
142 /// Gets the horizontal alignment.
143 #[must_use]
144 pub fn alignment(&self) -> TitleAlignment {
145 self.alignment
146 }
147
148 /// Gets the vertical position.
149 #[must_use]
150 pub fn position(&self) -> TitlePosition {
151 self.position
152 }
153}
154
155impl From<Title> for Line<'static> {
156 fn from(title: Title) -> Self {
157 title.render()
158 }
159}
160
161impl<T: Into<String>> From<T> for Title {
162 fn from(text: T) -> Self {
163 Title::new(text)
164 }
165}
166
167/// Horizontal alignment for block titles.
168///
169/// Controls how the title text is aligned horizontally within the block's top
170/// or bottom border. Supports start (left), center, and end (right) alignment.
171///
172/// # Examples
173///
174/// ```
175/// use tui_piechart::title::{TitleAlignment, BlockExt};
176/// use tui_piechart::border_style::BorderStyle;
177///
178/// let block = BorderStyle::Rounded.block()
179/// .title("Centered Title")
180/// .title_alignment_horizontal(TitleAlignment::Center);
181/// ```
182///
183/// # Text Direction
184///
185/// The alignment is logical rather than physical:
186/// - **Start**: Left in LTR languages, right in RTL languages
187/// - **Center**: Always centered
188/// - **End**: Right in LTR languages, left in RTL languages
189#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
190pub enum TitleAlignment {
191 /// Start-aligned title (left in LTR, right in RTL)
192 ///
193 /// The title appears at the start of the text direction. For left-to-right
194 /// languages (like English), this means left-aligned.
195 Start,
196
197 /// Center-aligned title (default)
198 ///
199 /// The title appears centered horizontally within the block border.
200 /// This is the default alignment.
201 #[default]
202 Center,
203
204 /// End-aligned title (right in LTR, left in RTL)
205 ///
206 /// The title appears at the end of the text direction. For left-to-right
207 /// languages (like English), this means right-aligned.
208 End,
209}
210
211impl From<TitleAlignment> for Alignment {
212 fn from(alignment: TitleAlignment) -> Self {
213 match alignment {
214 TitleAlignment::Start => Alignment::Left,
215 TitleAlignment::Center => Alignment::Center,
216 TitleAlignment::End => Alignment::Right,
217 }
218 }
219}
220
221/// Vertical position for block titles.
222///
223/// Controls whether the title appears at the top or bottom of the block border.
224///
225/// # Examples
226///
227/// ```
228/// use tui_piechart::title::{TitlePosition, BlockExt};
229/// use tui_piechart::border_style::BorderStyle;
230///
231/// let block = BorderStyle::Rounded.block()
232/// .title("Bottom Title")
233/// .title_vertical_position(TitlePosition::Bottom);
234/// ```
235///
236/// # Combinations
237///
238/// Title position can be combined with horizontal alignment to create
239/// 6 different title placements:
240/// - Top-Start, Top-Center, Top-End
241/// - Bottom-Start, Bottom-Center, Bottom-End
242#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
243pub enum TitlePosition {
244 /// Title at the top (default)
245 ///
246 /// The title appears in the top border of the block. This is the default
247 /// position and is the most common placement for block titles.
248 #[default]
249 Top,
250
251 /// Title at the bottom
252 ///
253 /// The title appears in the bottom border of the block. Useful when you
254 /// want to place other content at the top or when the title serves as
255 /// a caption rather than a header.
256 Bottom,
257}
258
259/// Font style for block titles using Unicode character variants.
260///
261/// Converts regular ASCII text to different Unicode character sets to achieve
262/// visual font styles in terminal user interfaces. Each style uses specific
263/// Unicode code points that represent the same letters in different typographic styles.
264///
265/// # Examples
266///
267/// ```
268/// use tui_piechart::title::TitleStyle;
269///
270/// let bold = TitleStyle::Bold.apply("Statistics");
271/// let italic = TitleStyle::Italic.apply("Results");
272/// let script = TitleStyle::Script.apply("Elegant");
273/// ```
274///
275/// # Limitations
276///
277/// - Only supports ASCII letters (a-z, A-Z), numbers (0-9), and spaces
278/// - Other characters (punctuation, special symbols) are passed through unchanged
279/// - Terminal font must support the Unicode characters (most modern terminals do)
280/// - Some styles may not render identically across different fonts
281#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
282pub enum TitleStyle {
283 /// Normal/regular text (default) - no transformation applied
284 #[default]
285 Normal,
286
287 /// Bold text using Unicode Mathematical Bold characters
288 ///
289 /// Converts text to bold Unicode variants. Example: "Hello" → "𝐇𝐞𝐥𝐥𝐨"
290 Bold,
291
292 /// Italic text using Unicode Mathematical Italic characters
293 ///
294 /// Converts text to italic Unicode variants. Example: "Hello" → "𝐻𝑒𝑙𝑙𝑜"
295 Italic,
296
297 /// Bold Italic text using Unicode Mathematical Bold Italic characters
298 ///
299 /// Combines bold and italic styling. Example: "Hello" → "𝑯𝒆𝒍𝒍𝒐"
300 BoldItalic,
301
302 /// Script/cursive text using Unicode Mathematical Script characters
303 ///
304 /// Converts text to flowing script style. Example: "Hello" → "𝐻ℯ𝓁𝓁ℴ"
305 Script,
306
307 /// Bold Script text using Unicode Mathematical Bold Script characters
308 ///
309 /// Script style with bold weight. Example: "Hello" → "𝓗𝓮𝓵𝓵𝓸"
310 BoldScript,
311
312 /// Sans-serif text using Unicode Mathematical Sans-Serif characters
313 ///
314 /// Clean sans-serif style. Example: "Hello" → "𝖧𝖾𝗅𝗅𝗈"
315 SansSerif,
316
317 /// Bold Sans-serif text using Unicode Mathematical Sans-Serif Bold characters
318 ///
319 /// Bold sans-serif style. Example: "Hello" → "𝗛𝗲𝗹𝗹𝗼"
320 BoldSansSerif,
321
322 /// Italic Sans-serif text using Unicode Mathematical Sans-Serif Italic characters
323 ///
324 /// Italic sans-serif style. Example: "Hello" → "𝘏𝘦𝘭𝘭𝘰"
325 ItalicSansSerif,
326
327 /// Monospace text using Unicode Monospace characters
328 ///
329 /// Fixed-width monospace style. Example: "Hello" → "𝙷𝚎𝚕𝚕𝚘"
330 Monospace,
331}
332
333impl TitleStyle {
334 /// Apply this style to the given text.
335 ///
336 /// Converts ASCII letters and numbers to their Unicode equivalents in the
337 /// selected style. Non-ASCII characters and unsupported characters are
338 /// passed through unchanged.
339 ///
340 /// # Examples
341 ///
342 /// ```
343 /// use tui_piechart::title::TitleStyle;
344 ///
345 /// let bold = TitleStyle::Bold.apply("Chart 2024");
346 /// let italic = TitleStyle::Italic.apply("Statistics");
347 /// let script = TitleStyle::Script.apply("Elegant Title");
348 /// ```
349 ///
350 /// # Character Support
351 ///
352 /// - **Letters**: Full support for a-z and A-Z
353 /// - **Numbers**: Support varies by style (most support 0-9)
354 /// - **Spaces**: Preserved as-is
355 /// - **Punctuation**: Passed through unchanged
356 #[must_use]
357 pub fn apply(&self, text: &str) -> String {
358 match self {
359 Self::Normal => text.to_string(),
360 Self::Bold => convert_to_bold(text),
361 Self::Italic => convert_to_italic(text),
362 Self::BoldItalic => convert_to_bold_italic(text),
363 Self::Script => convert_to_script(text),
364 Self::BoldScript => convert_to_bold_script(text),
365 Self::SansSerif => convert_to_sans_serif(text),
366 Self::BoldSansSerif => convert_to_bold_sans_serif(text),
367 Self::ItalicSansSerif => convert_to_italic_sans_serif(text),
368 Self::Monospace => convert_to_monospace(text),
369 }
370 }
371}
372
373// Unicode conversion functions - using macro to reduce code duplication
374
375/// Macro to generate Unicode conversion functions.
376///
377/// This macro generates functions that convert ASCII text to Unicode character variants.
378/// It reduces code duplication by handling the repetitive pattern of mapping character
379/// ranges to Unicode code points.
380///
381/// # Parameters
382/// - `$name`: Function name
383/// - `$upper`: Unicode base for uppercase letters (A-Z)
384/// - `$lower`: Unicode base for lowercase letters (a-z)
385/// - `$digit`: Optional Unicode base for digits (0-9)
386macro_rules! unicode_converter {
387 // Version with digit support
388 ($name:ident, $upper:expr, $lower:expr, $digit:expr) => {
389 fn $name(text: &str) -> String {
390 text.chars()
391 .map(|c| match c {
392 'A'..='Z' => char::from_u32($upper + (c as u32 - 'A' as u32)).unwrap(),
393 'a'..='z' => char::from_u32($lower + (c as u32 - 'a' as u32)).unwrap(),
394 '0'..='9' => char::from_u32($digit + (c as u32 - '0' as u32)).unwrap(),
395 _ => c,
396 })
397 .collect()
398 }
399 };
400 // Version without digit support
401 ($name:ident, $upper:expr, $lower:expr) => {
402 fn $name(text: &str) -> String {
403 text.chars()
404 .map(|c| match c {
405 'A'..='Z' => char::from_u32($upper + (c as u32 - 'A' as u32)).unwrap(),
406 'a'..='z' => char::from_u32($lower + (c as u32 - 'a' as u32)).unwrap(),
407 _ => c,
408 })
409 .collect()
410 }
411 };
412}
413
414// Generate all Unicode conversion functions using the macro
415unicode_converter!(convert_to_bold, 0x1D400, 0x1D41A, 0x1D7CE);
416unicode_converter!(convert_to_italic, 0x1D434, 0x1D44E);
417unicode_converter!(convert_to_bold_italic, 0x1D468, 0x1D482);
418unicode_converter!(convert_to_script, 0x1D49C, 0x1D4B6);
419unicode_converter!(convert_to_bold_script, 0x1D4D0, 0x1D4EA);
420unicode_converter!(convert_to_sans_serif, 0x1D5A0, 0x1D5BA, 0x1D7E2);
421unicode_converter!(convert_to_bold_sans_serif, 0x1D5D4, 0x1D5EE, 0x1D7EC);
422unicode_converter!(convert_to_italic_sans_serif, 0x1D608, 0x1D622);
423unicode_converter!(convert_to_monospace, 0x1D670, 0x1D68A, 0x1D7F6);
424
425/// Extension trait for adding title positioning helpers to Block.
426///
427/// This trait provides ergonomic methods for setting title alignment and position
428/// on Ratatui's `Block` type. It allows for method chaining and uses semantic
429/// types instead of raw alignment values.
430///
431/// # Examples
432///
433/// ## Using the unified Title API (recommended)
434///
435/// ```
436/// use tui_piechart::title::{Title, TitleStyle};
437/// use ratatui::widgets::Block;
438///
439/// let styled = TitleStyle::Bold.apply("My Chart");
440/// let block = Block::bordered()
441/// .title(Title::new(styled).center().bottom());
442/// ```
443///
444/// ## Using individual positioning methods (legacy)
445///
446/// ```
447/// use tui_piechart::title::{TitleAlignment, TitlePosition, BlockExt};
448/// use ratatui::widgets::Block;
449///
450/// let block = Block::bordered()
451/// .title("My Chart")
452/// .title_alignment_horizontal(TitleAlignment::Center)
453/// .title_vertical_position(TitlePosition::Bottom);
454/// ```
455pub trait BlockExt<'a>: Sized {
456 /// Apply a unified Title with styling and positioning.
457 ///
458 /// This is the recommended way to add styled and positioned titles.
459 ///
460 /// # Examples
461 ///
462 /// ```
463 /// use tui_piechart::title::{Title, TitleStyle, BlockExt};
464 /// use ratatui::widgets::Block;
465 ///
466 /// let styled = TitleStyle::Bold.apply("Stats");
467 /// let block = Block::bordered()
468 /// .apply_title(Title::new(styled).center().bottom());
469 /// ```
470 #[must_use]
471 fn apply_title(self, title: Title) -> Self;
472 /// Sets the horizontal alignment of the title.
473 ///
474 /// Controls whether the title appears at the start (left), center, or end (right)
475 /// of the block border.
476 ///
477 /// # Examples
478 ///
479 /// ```
480 /// use tui_piechart::title::{TitleAlignment, BlockExt};
481 /// use ratatui::widgets::Block;
482 ///
483 /// let block = Block::bordered()
484 /// .title("My Chart")
485 /// .title_alignment_horizontal(TitleAlignment::Center);
486 /// ```
487 #[must_use]
488 fn title_alignment_horizontal(self, alignment: TitleAlignment) -> Self;
489
490 /// Sets the vertical position of the title.
491 ///
492 /// Controls whether the title appears at the top or bottom of the block border.
493 ///
494 /// # Examples
495 ///
496 /// ```
497 /// use tui_piechart::title::{TitlePosition, BlockExt};
498 /// use ratatui::widgets::Block;
499 ///
500 /// let block = Block::bordered()
501 /// .title("My Chart")
502 /// .title_vertical_position(TitlePosition::Bottom);
503 /// ```
504 #[must_use]
505 fn title_vertical_position(self, position: TitlePosition) -> Self;
506}
507
508impl<'a> BlockExt<'a> for Block<'a> {
509 fn apply_title(self, title: Title) -> Self {
510 let styled_text = title.render();
511 let alignment = title.alignment();
512 let position = title.position();
513
514 let block = match position {
515 TitlePosition::Top => self.title(styled_text),
516 TitlePosition::Bottom => self.title_bottom(styled_text),
517 };
518
519 block.title_alignment(alignment.into())
520 }
521
522 fn title_alignment_horizontal(self, alignment: TitleAlignment) -> Self {
523 self.title_alignment(alignment.into())
524 }
525
526 fn title_vertical_position(self, position: TitlePosition) -> Self {
527 use ratatui::widgets::block::Position as RatatuiPosition;
528 match position {
529 TitlePosition::Top => self.title_position(RatatuiPosition::Top),
530 TitlePosition::Bottom => self.title_position(RatatuiPosition::Bottom),
531 }
532 }
533}
534
535#[cfg(test)]
536mod tests {
537 use super::*;
538
539 #[test]
540 fn title_new() {
541 let title = Title::new("Test");
542 assert_eq!(title.text, "Test");
543 assert_eq!(title.alignment, TitleAlignment::Center);
544 assert_eq!(title.position, TitlePosition::Top);
545 }
546
547 #[test]
548 fn title_builder_alignment() {
549 let title = Title::new("Test").left();
550 assert_eq!(title.alignment, TitleAlignment::Start);
551 }
552
553 #[test]
554 fn title_builder_position() {
555 let title = Title::new("Test").bottom();
556 assert_eq!(title.position, TitlePosition::Bottom);
557 }
558
559 #[test]
560 fn title_builder_chaining() {
561 let title = Title::new("Test").center().bottom();
562 assert_eq!(title.alignment, TitleAlignment::Center);
563 assert_eq!(title.position, TitlePosition::Bottom);
564 }
565
566 #[test]
567 fn title_from_string() {
568 let title: Title = "Test".into();
569 assert_eq!(title.text, "Test");
570 }
571
572 #[test]
573 fn title_alignment_default() {
574 assert_eq!(TitleAlignment::default(), TitleAlignment::Center);
575 }
576
577 #[test]
578 fn title_position_default() {
579 assert_eq!(TitlePosition::default(), TitlePosition::Top);
580 }
581
582 #[test]
583 fn title_style_default() {
584 assert_eq!(TitleStyle::default(), TitleStyle::Normal);
585 }
586
587 #[test]
588 fn title_alignment_to_ratatui_alignment() {
589 assert_eq!(Alignment::from(TitleAlignment::Start), Alignment::Left);
590 assert_eq!(Alignment::from(TitleAlignment::Center), Alignment::Center);
591 assert_eq!(Alignment::from(TitleAlignment::End), Alignment::Right);
592 }
593
594 #[test]
595 fn title_alignment_clone() {
596 let align = TitleAlignment::End;
597 let cloned = align;
598 assert_eq!(align, cloned);
599 }
600
601 #[test]
602 fn title_position_clone() {
603 let pos = TitlePosition::Bottom;
604 let cloned = pos;
605 assert_eq!(pos, cloned);
606 }
607
608 #[test]
609 fn title_style_clone() {
610 let style = TitleStyle::Bold;
611 let cloned = style;
612 assert_eq!(style, cloned);
613 }
614
615 #[test]
616 fn title_alignment_debug() {
617 let align = TitleAlignment::Start;
618 let debug = format!("{align:?}");
619 assert_eq!(debug, "Start");
620 }
621
622 #[test]
623 fn title_position_debug() {
624 let pos = TitlePosition::Bottom;
625 let debug = format!("{pos:?}");
626 assert_eq!(debug, "Bottom");
627 }
628
629 #[test]
630 fn title_style_debug() {
631 let style = TitleStyle::Bold;
632 let debug = format!("{style:?}");
633 assert_eq!(debug, "Bold");
634 }
635
636 #[test]
637 fn block_ext_title_alignment() {
638 let block = Block::bordered()
639 .title("Test")
640 .title_alignment_horizontal(TitleAlignment::Center);
641 // If this compiles and doesn't panic, the trait is working
642 assert!(format!("{block:?}").contains("Test"));
643 }
644
645 #[test]
646 fn block_ext_title_position() {
647 let block = Block::bordered()
648 .title("Test")
649 .title_vertical_position(TitlePosition::Bottom);
650 // If this compiles and doesn't panic, the trait is working
651 assert!(format!("{block:?}").contains("Test"));
652 }
653
654 #[test]
655 fn block_ext_method_chaining() {
656 let block = Block::bordered()
657 .title("Test")
658 .title_alignment_horizontal(TitleAlignment::End)
659 .title_vertical_position(TitlePosition::Bottom);
660 // If this compiles and doesn't panic, method chaining works
661 assert!(format!("{block:?}").contains("Test"));
662 }
663
664 #[test]
665 fn title_style_normal() {
666 let text = "Hello World";
667 assert_eq!(TitleStyle::Normal.apply(text), "Hello World");
668 }
669
670 #[test]
671 fn title_style_bold_letters() {
672 let result = TitleStyle::Bold.apply("Hello");
673 assert_ne!(result, "Hello");
674 assert_eq!(result.chars().count(), 5); // Same length
675 }
676
677 #[test]
678 fn title_style_bold_with_numbers() {
679 let result = TitleStyle::Bold.apply("Chart 2024");
680 assert!(result.chars().count() >= 10); // At least same length
681 }
682
683 #[test]
684 fn title_style_italic_letters() {
685 let result = TitleStyle::Italic.apply("Statistics");
686 assert_ne!(result, "Statistics");
687 }
688
689 #[test]
690 fn title_style_preserves_spaces() {
691 let result = TitleStyle::Bold.apply("Hello World");
692 assert!(result.contains(' '));
693 }
694
695 #[test]
696 fn title_style_preserves_punctuation() {
697 let result = TitleStyle::Bold.apply("Hello!");
698 assert!(result.ends_with('!'));
699 }
700
701 #[test]
702 fn title_style_script() {
703 let result = TitleStyle::Script.apply("Test");
704 assert_ne!(result, "Test");
705 }
706
707 #[test]
708 fn title_style_monospace() {
709 let result = TitleStyle::Monospace.apply("Code");
710 assert_ne!(result, "Code");
711 }
712
713 #[test]
714 fn title_style_sans_serif() {
715 let result = TitleStyle::SansSerif.apply("Modern");
716 assert_ne!(result, "Modern");
717 }
718
719 #[test]
720 fn title_style_empty_string() {
721 assert_eq!(TitleStyle::Bold.apply(""), "");
722 assert_eq!(TitleStyle::Italic.apply(""), "");
723 }
724
725 #[test]
726 fn title_style_mixed_case() {
727 let result = TitleStyle::Bold.apply("TeSt");
728 assert_ne!(result, "TeSt");
729 assert_eq!(result.chars().count(), 4);
730 }
731}