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//! ```
9//! use tui_piechart::title::{TitleAlignment, TitlePosition, TitleStyle, BlockExt};
10//! use tui_piechart::border_style::BorderStyle;
11//!
12//! // Create a block with centered bold title at the bottom
13//! let title = TitleStyle::Bold.apply("My Chart");
14//! let block = BorderStyle::Rounded.block()
15//!     .title(title)
16//!     .title_alignment_horizontal(TitleAlignment::Center)
17//!     .title_vertical_position(TitlePosition::Bottom);
18//! ```
19
20use ratatui::layout::Alignment;
21use ratatui::widgets::Block;
22
23/// Horizontal alignment for block titles.
24///
25/// Controls how the title text is aligned horizontally within the block's top
26/// or bottom border. Supports start (left), center, and end (right) alignment.
27///
28/// # Examples
29///
30/// ```
31/// use tui_piechart::title::{TitleAlignment, BlockExt};
32/// use tui_piechart::border_style::BorderStyle;
33///
34/// let block = BorderStyle::Rounded.block()
35///     .title("Centered Title")
36///     .title_alignment_horizontal(TitleAlignment::Center);
37/// ```
38///
39/// # Text Direction
40///
41/// The alignment is logical rather than physical:
42/// - **Start**: Left in LTR languages, right in RTL languages
43/// - **Center**: Always centered
44/// - **End**: Right in LTR languages, left in RTL languages
45#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
46pub enum TitleAlignment {
47    /// Start-aligned title (left in LTR, right in RTL)
48    ///
49    /// The title appears at the start of the text direction. For left-to-right
50    /// languages (like English), this means left-aligned.
51    Start,
52
53    /// Center-aligned title (default)
54    ///
55    /// The title appears centered horizontally within the block border.
56    /// This is the default alignment.
57    #[default]
58    Center,
59
60    /// End-aligned title (right in LTR, left in RTL)
61    ///
62    /// The title appears at the end of the text direction. For left-to-right
63    /// languages (like English), this means right-aligned.
64    End,
65}
66
67impl From<TitleAlignment> for Alignment {
68    fn from(alignment: TitleAlignment) -> Self {
69        match alignment {
70            TitleAlignment::Start => Alignment::Left,
71            TitleAlignment::Center => Alignment::Center,
72            TitleAlignment::End => Alignment::Right,
73        }
74    }
75}
76
77/// Vertical position for block titles.
78///
79/// Controls whether the title appears at the top or bottom of the block border.
80///
81/// # Examples
82///
83/// ```
84/// use tui_piechart::title::{TitlePosition, BlockExt};
85/// use tui_piechart::border_style::BorderStyle;
86///
87/// let block = BorderStyle::Rounded.block()
88///     .title("Bottom Title")
89///     .title_vertical_position(TitlePosition::Bottom);
90/// ```
91///
92/// # Combinations
93///
94/// Title position can be combined with horizontal alignment to create
95/// 6 different title placements:
96/// - Top-Start, Top-Center, Top-End
97/// - Bottom-Start, Bottom-Center, Bottom-End
98#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
99pub enum TitlePosition {
100    /// Title at the top (default)
101    ///
102    /// The title appears in the top border of the block. This is the default
103    /// position and is the most common placement for block titles.
104    #[default]
105    Top,
106
107    /// Title at the bottom
108    ///
109    /// The title appears in the bottom border of the block. Useful when you
110    /// want to place other content at the top or when the title serves as
111    /// a caption rather than a header.
112    Bottom,
113}
114
115/// Font style for block titles using Unicode character variants.
116///
117/// Converts regular ASCII text to different Unicode character sets to achieve
118/// visual font styles in terminal user interfaces. Each style uses specific
119/// Unicode code points that represent the same letters in different typographic styles.
120///
121/// # Examples
122///
123/// ```
124/// use tui_piechart::title::TitleStyle;
125///
126/// let bold = TitleStyle::Bold.apply("Statistics");
127/// let italic = TitleStyle::Italic.apply("Results");
128/// let script = TitleStyle::Script.apply("Elegant");
129/// ```
130///
131/// # Limitations
132///
133/// - Only supports ASCII letters (a-z, A-Z), numbers (0-9), and spaces
134/// - Other characters (punctuation, special symbols) are passed through unchanged
135/// - Terminal font must support the Unicode characters (most modern terminals do)
136/// - Some styles may not render identically across different fonts
137#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
138pub enum TitleStyle {
139    /// Normal/regular text (default) - no transformation applied
140    #[default]
141    Normal,
142
143    /// Bold text using Unicode Mathematical Bold characters
144    ///
145    /// Converts text to bold Unicode variants. Example: "Hello" → "𝐇𝐞𝐥𝐥𝐨"
146    Bold,
147
148    /// Italic text using Unicode Mathematical Italic characters
149    ///
150    /// Converts text to italic Unicode variants. Example: "Hello" → "𝐻𝑒𝑙𝑙𝑜"
151    Italic,
152
153    /// Bold Italic text using Unicode Mathematical Bold Italic characters
154    ///
155    /// Combines bold and italic styling. Example: "Hello" → "𝑯𝒆𝒍𝒍𝒐"
156    BoldItalic,
157
158    /// Script/cursive text using Unicode Mathematical Script characters
159    ///
160    /// Converts text to flowing script style. Example: "Hello" → "𝐻ℯ𝓁𝓁ℴ"
161    Script,
162
163    /// Bold Script text using Unicode Mathematical Bold Script characters
164    ///
165    /// Script style with bold weight. Example: "Hello" → "𝓗𝓮𝓵𝓵𝓸"
166    BoldScript,
167
168    /// Sans-serif text using Unicode Mathematical Sans-Serif characters
169    ///
170    /// Clean sans-serif style. Example: "Hello" → "𝖧𝖾𝗅𝗅𝗈"
171    SansSerif,
172
173    /// Bold Sans-serif text using Unicode Mathematical Sans-Serif Bold characters
174    ///
175    /// Bold sans-serif style. Example: "Hello" → "𝗛𝗲𝗹𝗹𝗼"
176    BoldSansSerif,
177
178    /// Italic Sans-serif text using Unicode Mathematical Sans-Serif Italic characters
179    ///
180    /// Italic sans-serif style. Example: "Hello" → "𝘏𝘦𝘭𝘭𝘰"
181    ItalicSansSerif,
182
183    /// Monospace text using Unicode Monospace characters
184    ///
185    /// Fixed-width monospace style. Example: "Hello" → "𝙷𝚎𝚕𝚕𝚘"
186    Monospace,
187}
188
189impl TitleStyle {
190    /// Apply this style to the given text.
191    ///
192    /// Converts ASCII letters and numbers to their Unicode equivalents in the
193    /// selected style. Non-ASCII characters and unsupported characters are
194    /// passed through unchanged.
195    ///
196    /// # Examples
197    ///
198    /// ```
199    /// use tui_piechart::title::TitleStyle;
200    ///
201    /// let bold = TitleStyle::Bold.apply("Chart 2024");
202    /// let italic = TitleStyle::Italic.apply("Statistics");
203    /// let script = TitleStyle::Script.apply("Elegant Title");
204    /// ```
205    ///
206    /// # Character Support
207    ///
208    /// - **Letters**: Full support for a-z and A-Z
209    /// - **Numbers**: Support varies by style (most support 0-9)
210    /// - **Spaces**: Preserved as-is
211    /// - **Punctuation**: Passed through unchanged
212    #[must_use]
213    pub fn apply(&self, text: &str) -> String {
214        match self {
215            Self::Normal => text.to_string(),
216            Self::Bold => convert_to_bold(text),
217            Self::Italic => convert_to_italic(text),
218            Self::BoldItalic => convert_to_bold_italic(text),
219            Self::Script => convert_to_script(text),
220            Self::BoldScript => convert_to_bold_script(text),
221            Self::SansSerif => convert_to_sans_serif(text),
222            Self::BoldSansSerif => convert_to_bold_sans_serif(text),
223            Self::ItalicSansSerif => convert_to_italic_sans_serif(text),
224            Self::Monospace => convert_to_monospace(text),
225        }
226    }
227}
228
229// Unicode conversion functions - using macro to reduce code duplication
230
231/// Macro to generate Unicode conversion functions.
232///
233/// This macro generates functions that convert ASCII text to Unicode character variants.
234/// It reduces code duplication by handling the repetitive pattern of mapping character
235/// ranges to Unicode code points.
236///
237/// # Parameters
238/// - `$name`: Function name
239/// - `$upper`: Unicode base for uppercase letters (A-Z)
240/// - `$lower`: Unicode base for lowercase letters (a-z)
241/// - `$digit`: Optional Unicode base for digits (0-9)
242macro_rules! unicode_converter {
243    // Version with digit support
244    ($name:ident, $upper:expr, $lower:expr, $digit:expr) => {
245        fn $name(text: &str) -> String {
246            text.chars()
247                .map(|c| match c {
248                    'A'..='Z' => char::from_u32($upper + (c as u32 - 'A' as u32)).unwrap(),
249                    'a'..='z' => char::from_u32($lower + (c as u32 - 'a' as u32)).unwrap(),
250                    '0'..='9' => char::from_u32($digit + (c as u32 - '0' as u32)).unwrap(),
251                    _ => c,
252                })
253                .collect()
254        }
255    };
256    // Version without digit support
257    ($name:ident, $upper:expr, $lower:expr) => {
258        fn $name(text: &str) -> String {
259            text.chars()
260                .map(|c| match c {
261                    'A'..='Z' => char::from_u32($upper + (c as u32 - 'A' as u32)).unwrap(),
262                    'a'..='z' => char::from_u32($lower + (c as u32 - 'a' as u32)).unwrap(),
263                    _ => c,
264                })
265                .collect()
266        }
267    };
268}
269
270// Generate all Unicode conversion functions using the macro
271unicode_converter!(convert_to_bold, 0x1D400, 0x1D41A, 0x1D7CE);
272unicode_converter!(convert_to_italic, 0x1D434, 0x1D44E);
273unicode_converter!(convert_to_bold_italic, 0x1D468, 0x1D482);
274unicode_converter!(convert_to_script, 0x1D49C, 0x1D4B6);
275unicode_converter!(convert_to_bold_script, 0x1D4D0, 0x1D4EA);
276unicode_converter!(convert_to_sans_serif, 0x1D5A0, 0x1D5BA, 0x1D7E2);
277unicode_converter!(convert_to_bold_sans_serif, 0x1D5D4, 0x1D5EE, 0x1D7EC);
278unicode_converter!(convert_to_italic_sans_serif, 0x1D608, 0x1D622);
279unicode_converter!(convert_to_monospace, 0x1D670, 0x1D68A, 0x1D7F6);
280
281/// Extension trait for adding title positioning helpers to Block.
282///
283/// This trait provides ergonomic methods for setting title alignment and position
284/// on Ratatui's `Block` type. It allows for method chaining and uses semantic
285/// types instead of raw alignment values.
286///
287/// # Examples
288///
289/// ```
290/// use tui_piechart::title::{TitleAlignment, TitlePosition, BlockExt};
291/// use ratatui::widgets::Block;
292///
293/// let block = Block::bordered()
294///     .title("My Chart")
295///     .title_alignment_horizontal(TitleAlignment::Center)
296///     .title_vertical_position(TitlePosition::Bottom);
297/// ```
298///
299/// # Method Chaining
300///
301/// All methods return `Self`, allowing for fluent method chaining:
302///
303/// ```
304/// use tui_piechart::title::{TitleAlignment, TitlePosition, BlockExt};
305/// use tui_piechart::border_style::BorderStyle;
306///
307/// let block = BorderStyle::Rounded.block()
308///     .title("Statistics")
309///     .title_alignment_horizontal(TitleAlignment::End)
310///     .title_vertical_position(TitlePosition::Bottom);
311/// ```
312pub trait BlockExt<'a> {
313    /// Sets the horizontal alignment of the title.
314    ///
315    /// Controls whether the title appears at the start (left), center, or end (right)
316    /// of the block border.
317    ///
318    /// # Examples
319    ///
320    /// ```
321    /// use tui_piechart::title::{TitleAlignment, BlockExt};
322    /// use ratatui::widgets::Block;
323    ///
324    /// let block = Block::bordered()
325    ///     .title("My Chart")
326    ///     .title_alignment_horizontal(TitleAlignment::Center);
327    /// ```
328    #[must_use]
329    fn title_alignment_horizontal(self, alignment: TitleAlignment) -> Self;
330
331    /// Sets the vertical position of the title.
332    ///
333    /// Controls whether the title appears at the top or bottom of the block border.
334    ///
335    /// # Examples
336    ///
337    /// ```
338    /// use tui_piechart::title::{TitlePosition, BlockExt};
339    /// use ratatui::widgets::Block;
340    ///
341    /// let block = Block::bordered()
342    ///     .title("My Chart")
343    ///     .title_vertical_position(TitlePosition::Bottom);
344    /// ```
345    #[must_use]
346    fn title_vertical_position(self, position: TitlePosition) -> Self;
347}
348
349impl<'a> BlockExt<'a> for Block<'a> {
350    fn title_alignment_horizontal(self, alignment: TitleAlignment) -> Self {
351        self.title_alignment(alignment.into())
352    }
353
354    fn title_vertical_position(self, position: TitlePosition) -> Self {
355        use ratatui::widgets::block::Position as RatatuiPosition;
356        match position {
357            TitlePosition::Top => self.title_position(RatatuiPosition::Top),
358            TitlePosition::Bottom => self.title_position(RatatuiPosition::Bottom),
359        }
360    }
361}
362
363#[cfg(test)]
364mod tests {
365    use super::*;
366
367    #[test]
368    fn title_alignment_default() {
369        assert_eq!(TitleAlignment::default(), TitleAlignment::Center);
370    }
371
372    #[test]
373    fn title_position_default() {
374        assert_eq!(TitlePosition::default(), TitlePosition::Top);
375    }
376
377    #[test]
378    fn title_style_default() {
379        assert_eq!(TitleStyle::default(), TitleStyle::Normal);
380    }
381
382    #[test]
383    fn title_alignment_to_ratatui_alignment() {
384        assert_eq!(Alignment::from(TitleAlignment::Start), Alignment::Left);
385        assert_eq!(Alignment::from(TitleAlignment::Center), Alignment::Center);
386        assert_eq!(Alignment::from(TitleAlignment::End), Alignment::Right);
387    }
388
389    #[test]
390    fn title_alignment_clone() {
391        let align = TitleAlignment::End;
392        let cloned = align;
393        assert_eq!(align, cloned);
394    }
395
396    #[test]
397    fn title_position_clone() {
398        let pos = TitlePosition::Bottom;
399        let cloned = pos;
400        assert_eq!(pos, cloned);
401    }
402
403    #[test]
404    fn title_style_clone() {
405        let style = TitleStyle::Bold;
406        let cloned = style;
407        assert_eq!(style, cloned);
408    }
409
410    #[test]
411    fn title_alignment_debug() {
412        let align = TitleAlignment::Start;
413        let debug = format!("{:?}", align);
414        assert_eq!(debug, "Start");
415    }
416
417    #[test]
418    fn title_position_debug() {
419        let pos = TitlePosition::Bottom;
420        let debug = format!("{:?}", pos);
421        assert_eq!(debug, "Bottom");
422    }
423
424    #[test]
425    fn title_style_debug() {
426        let style = TitleStyle::Bold;
427        let debug = format!("{:?}", style);
428        assert_eq!(debug, "Bold");
429    }
430
431    #[test]
432    fn block_ext_title_alignment() {
433        let block = Block::bordered()
434            .title("Test")
435            .title_alignment_horizontal(TitleAlignment::Center);
436        // If this compiles and doesn't panic, the trait is working
437        assert!(format!("{:?}", block).contains("Test"));
438    }
439
440    #[test]
441    fn block_ext_title_position() {
442        let block = Block::bordered()
443            .title("Test")
444            .title_vertical_position(TitlePosition::Bottom);
445        // If this compiles and doesn't panic, the trait is working
446        assert!(format!("{:?}", block).contains("Test"));
447    }
448
449    #[test]
450    fn block_ext_method_chaining() {
451        let block = Block::bordered()
452            .title("Test")
453            .title_alignment_horizontal(TitleAlignment::End)
454            .title_vertical_position(TitlePosition::Bottom);
455        // If this compiles and doesn't panic, method chaining works
456        assert!(format!("{:?}", block).contains("Test"));
457    }
458
459    #[test]
460    fn title_style_normal() {
461        let text = "Hello World";
462        assert_eq!(TitleStyle::Normal.apply(text), "Hello World");
463    }
464
465    #[test]
466    fn title_style_bold_letters() {
467        let result = TitleStyle::Bold.apply("Hello");
468        assert_ne!(result, "Hello");
469        assert_eq!(result.chars().count(), 5); // Same length
470    }
471
472    #[test]
473    fn title_style_bold_with_numbers() {
474        let result = TitleStyle::Bold.apply("Chart 2024");
475        assert!(result.chars().count() >= 10); // At least same length
476    }
477
478    #[test]
479    fn title_style_italic_letters() {
480        let result = TitleStyle::Italic.apply("Statistics");
481        assert_ne!(result, "Statistics");
482    }
483
484    #[test]
485    fn title_style_preserves_spaces() {
486        let result = TitleStyle::Bold.apply("Hello World");
487        assert!(result.contains(' '));
488    }
489
490    #[test]
491    fn title_style_preserves_punctuation() {
492        let result = TitleStyle::Bold.apply("Hello!");
493        assert!(result.ends_with('!'));
494    }
495
496    #[test]
497    fn title_style_script() {
498        let result = TitleStyle::Script.apply("Test");
499        assert_ne!(result, "Test");
500    }
501
502    #[test]
503    fn title_style_monospace() {
504        let result = TitleStyle::Monospace.apply("Code");
505        assert_ne!(result, "Code");
506    }
507
508    #[test]
509    fn title_style_sans_serif() {
510        let result = TitleStyle::SansSerif.apply("Modern");
511        assert_ne!(result, "Modern");
512    }
513
514    #[test]
515    fn title_style_empty_string() {
516        assert_eq!(TitleStyle::Bold.apply(""), "");
517        assert_eq!(TitleStyle::Italic.apply(""), "");
518    }
519
520    #[test]
521    fn title_style_mixed_case() {
522        let result = TitleStyle::Bold.apply("TeSt");
523        assert_ne!(result, "TeSt");
524        assert_eq!(result.chars().count(), 4);
525    }
526}