oxiui_text/label.rs
1//! Static text display widget state.
2//!
3//! [`Label`] tracks the displayed text string and layout constraints. Pixel-
4//! accurate truncation requires a `TextPipeline` and is handled by adapters
5//! calling `crate::truncation::truncate`; the `is_truncated` flag lets callers
6//! communicate the result back to this state struct.
7
8// ── Label ─────────────────────────────────────────────────────────────────────
9
10/// State for a static text label widget.
11///
12/// This is a pure data structure. Rendering and actual pixel-accurate
13/// truncation are handled by the caller (adapter layer) using the functions in
14/// `crate::truncation`.
15#[derive(Debug, Clone)]
16pub struct Label {
17 text: String,
18 /// Maximum number of lines to display before clipping or truncating.
19 max_lines: Option<usize>,
20 /// Set by the adapter when the text had to be truncated to fit.
21 truncated: bool,
22}
23
24impl Label {
25 /// Create a new `Label` with the given text and no line limit.
26 pub fn new(text: impl Into<String>) -> Self {
27 Self {
28 text: text.into(),
29 max_lines: None,
30 truncated: false,
31 }
32 }
33
34 /// Limit the label to `n` visible lines.
35 ///
36 /// When `n` is `1`, adapters should apply single-line ellipsis truncation
37 /// via `crate::truncation::truncate`.
38 pub fn with_max_lines(mut self, n: usize) -> Self {
39 self.max_lines = Some(n);
40 self
41 }
42
43 /// The raw text stored in this label.
44 pub fn text(&self) -> &str {
45 &self.text
46 }
47
48 /// The optional maximum line count.
49 pub fn max_lines(&self) -> Option<usize> {
50 self.max_lines
51 }
52
53 /// Returns `true` when an adapter reported that truncation occurred during
54 /// the last layout pass.
55 pub fn is_truncated(&self) -> bool {
56 self.truncated
57 }
58
59 /// Called by adapters after each layout pass to record whether truncation
60 /// occurred.
61 pub fn set_truncated(&mut self, truncated: bool) {
62 self.truncated = truncated;
63 }
64
65 /// Returns the text that should be displayed.
66 ///
67 /// For single-line labels (`max_lines == Some(1)`), pixel-accurate
68 /// truncation requires a `TextPipeline`; call `crate::truncation::truncate`
69 /// from the adapter and pass `set_truncated(true)` when the result
70 /// differs from the raw text. This method returns the full raw text so the
71 /// adapter can measure and decide.
72 pub fn display_text(&self) -> &str {
73 &self.text
74 }
75}
76
77impl Default for Label {
78 fn default() -> Self {
79 Self::new("")
80 }
81}
82
83// ── Tests ─────────────────────────────────────────────────────────────────────
84
85#[cfg(test)]
86mod tests {
87 use super::*;
88
89 #[test]
90 fn label_new() {
91 let label = Label::new("hello");
92 assert_eq!(label.text(), "hello");
93 assert!(label.max_lines().is_none());
94 }
95
96 #[test]
97 fn label_is_truncated_false_initially() {
98 let label = Label::new("hello world");
99 assert!(
100 !label.is_truncated(),
101 "newly created label must not be truncated"
102 );
103 }
104
105 #[test]
106 fn label_set_truncated() {
107 let mut label = Label::new("a very long text");
108 assert!(!label.is_truncated());
109 label.set_truncated(true);
110 assert!(label.is_truncated());
111 }
112
113 #[test]
114 fn label_with_max_lines() {
115 let label = Label::new("text").with_max_lines(1);
116 assert_eq!(label.max_lines(), Some(1));
117 }
118
119 #[test]
120 fn label_display_text_matches_raw() {
121 let label = Label::new("hello");
122 assert_eq!(label.display_text(), "hello");
123 }
124
125 #[test]
126 fn label_default() {
127 let label = Label::default();
128 assert_eq!(label.text(), "");
129 assert!(!label.is_truncated());
130 }
131}