use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct AccessibleName(String);
impl AccessibleName {
#[doc(hidden)]
#[must_use]
pub fn from_checked_literal(text: &'static str) -> Self {
let trimmed = text.trim();
Self(if trimmed.is_empty() { text } else { trimmed }.to_owned())
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl TryFrom<&str> for AccessibleName {
type Error = EmptyName;
fn try_from(value: &str) -> Result<Self, Self::Error> {
let value = value.trim();
if value.is_empty() {
Err(EmptyName)
} else {
Ok(Self(value.to_owned()))
}
}
}
impl TryFrom<String> for AccessibleName {
type Error = EmptyName;
fn try_from(value: String) -> Result<Self, Self::Error> {
Self::try_from(value.as_str())
}
}
impl fmt::Display for AccessibleName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct EmptyName;
impl fmt::Display for EmptyName {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str("accessible names must contain text")
}
}
impl std::error::Error for EmptyName {}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub enum Alt {
Text(AccessibleName),
Decorative,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[non_exhaustive]
pub enum ClipMode {
Full,
Lines(u8),
Chars(usize),
}
#[derive(Debug, Clone, PartialEq, Eq, Hash)]
pub struct Clipped {
full: String,
mode: ClipMode,
}
impl Clipped {
#[must_use]
pub fn full(text: impl Into<String>) -> Self {
Self {
full: text.into(),
mode: ClipMode::Full,
}
}
#[must_use]
pub fn lines(text: impl Into<String>, lines: u8) -> Self {
Self {
full: text.into(),
mode: ClipMode::Lines(lines),
}
}
#[must_use]
pub fn chars(text: impl Into<String>, chars: usize) -> Self {
Self {
full: text.into(),
mode: ClipMode::Chars(chars),
}
}
#[must_use]
pub fn full_text(&self) -> &str {
&self.full
}
#[must_use]
pub fn shown(&self) -> String {
match self.mode {
ClipMode::Chars(limit) if self.full.chars().count() > limit => {
let mut shown: String = self.full.chars().take(limit).collect();
shown.push('…');
shown
}
_ => self.full.clone(),
}
}
#[must_use]
pub fn is_truncated(&self) -> bool {
match self.mode {
ClipMode::Full => false,
ClipMode::Lines(_) => true,
ClipMode::Chars(limit) => self.full.chars().count() > limit,
}
}
}
#[doc(hidden)]
#[must_use]
pub const fn is_blank(text: &str) -> bool {
let mut rest = text.as_bytes();
while let [first, tail @ ..] = rest {
if !first.is_ascii_whitespace() {
return false;
}
rest = tail;
}
true
}
#[macro_export]
macro_rules! name {
($text:literal) => {{
const _: () = assert!(
!$crate::core::text::is_blank($text),
"accessible names must contain text"
);
$crate::core::text::AccessibleName::from_checked_literal($text)
}};
}