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}