1use std::fmt;
4
5#[derive(Debug, Clone, PartialEq, Eq, Hash)]
13pub struct AccessibleName(String);
14
15impl AccessibleName {
16 #[doc(hidden)]
19 #[must_use]
20 pub fn from_checked_literal(text: &'static str) -> Self {
21 let trimmed = text.trim();
22 Self(if trimmed.is_empty() { text } else { trimmed }.to_owned())
25 }
26 #[must_use]
28 pub fn as_str(&self) -> &str {
29 &self.0
30 }
31}
32impl TryFrom<&str> for AccessibleName {
33 type Error = EmptyName;
34 fn try_from(value: &str) -> Result<Self, Self::Error> {
37 let value = value.trim();
38 if value.is_empty() {
39 Err(EmptyName)
40 } else {
41 Ok(Self(value.to_owned()))
42 }
43 }
44}
45impl TryFrom<String> for AccessibleName {
46 type Error = EmptyName;
47 fn try_from(value: String) -> Result<Self, Self::Error> {
48 Self::try_from(value.as_str())
49 }
50}
51impl fmt::Display for AccessibleName {
52 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
53 f.write_str(&self.0)
54 }
55}
56
57#[derive(Debug, Clone, Copy, PartialEq, Eq)]
59pub struct EmptyName;
60impl fmt::Display for EmptyName {
61 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
62 f.write_str("accessible names must contain text")
63 }
64}
65impl std::error::Error for EmptyName {}
66
67#[derive(Debug, Clone, PartialEq, Eq, Hash)]
69pub enum Alt {
70 Text(AccessibleName),
72 Decorative,
74}
75
76#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
78#[non_exhaustive]
79pub enum ClipMode {
80 Full,
82 Lines(u8),
84 Chars(usize),
86}
87
88#[derive(Debug, Clone, PartialEq, Eq, Hash)]
99pub struct Clipped {
100 full: String,
101 mode: ClipMode,
102}
103impl Clipped {
104 #[must_use]
106 pub fn full(text: impl Into<String>) -> Self {
107 Self {
108 full: text.into(),
109 mode: ClipMode::Full,
110 }
111 }
112 #[must_use]
115 pub fn lines(text: impl Into<String>, lines: u8) -> Self {
116 Self {
117 full: text.into(),
118 mode: ClipMode::Lines(lines),
119 }
120 }
121 #[must_use]
123 pub fn chars(text: impl Into<String>, chars: usize) -> Self {
124 Self {
125 full: text.into(),
126 mode: ClipMode::Chars(chars),
127 }
128 }
129 #[must_use]
131 pub fn full_text(&self) -> &str {
132 &self.full
133 }
134 #[must_use]
136 pub fn shown(&self) -> String {
137 match self.mode {
138 ClipMode::Chars(limit) if self.full.chars().count() > limit => {
139 let mut shown: String = self.full.chars().take(limit).collect();
140 shown.push('…');
141 shown
142 }
143 _ => self.full.clone(),
144 }
145 }
146 #[must_use]
148 pub fn is_truncated(&self) -> bool {
149 match self.mode {
150 ClipMode::Full => false,
151 ClipMode::Lines(_) => true,
152 ClipMode::Chars(limit) => self.full.chars().count() > limit,
153 }
154 }
155}
156
157#[doc(hidden)]
159#[must_use]
160pub const fn is_blank(text: &str) -> bool {
161 let mut rest = text.as_bytes();
162 while let [first, tail @ ..] = rest {
163 if !first.is_ascii_whitespace() {
164 return false;
165 }
166 rest = tail;
167 }
168 true
169}
170
171#[macro_export]
185macro_rules! name {
186 ($text:literal) => {{
187 const _: () = assert!(
188 !$crate::core::text::is_blank($text),
189 "accessible names must contain text"
190 );
191 $crate::core::text::AccessibleName::from_checked_literal($text)
192 }};
193}