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}