Preserve grapheme clusters when shortening desktop labels

This commit is contained in:
2026-09-11 13:48:21 -07:00
parent 108b1dd208
commit cb91525cfd
3 changed files with 61 additions and 2 deletions
Generated
+1
View File
@@ -2176,6 +2176,7 @@ dependencies = [
"serde_json",
"tempfile",
"tokio",
"unicode-segmentation",
]
[[package]]
+1
View File
@@ -18,6 +18,7 @@ serde_json.workspace = true
reqwest = { version = "0.12", default-features = false, features = ["json", "rustls-tls"] }
tokio = { version = "1", features = ["rt", "time"] }
tempfile = "3"
unicode-segmentation = "1.13.3"
[dev-dependencies]
iced_runtime = "0.14"
+59 -2
View File
@@ -12,6 +12,7 @@ use iced::{
},
};
use oa_core::*;
use unicode_segmentation::UnicodeSegmentation;
pub fn label<'a>(s: impl Into<String>) -> widget::Text<'a> {
text(s.into()).size(11).color(style::MUTED)
@@ -24,10 +25,12 @@ pub fn action<'a>(s: &str, m: Message) -> widget::Button<'a, Message> {
}
pub fn short(s: &str, limit: usize) -> String {
let s = s.split_whitespace().collect::<Vec<_>>().join(" ");
if s.chars().count() > limit {
if s.graphemes(true).count() > limit {
format!(
"{}…",
s.chars().take(limit.saturating_sub(1)).collect::<String>()
s.graphemes(true)
.take(limit.saturating_sub(1))
.collect::<String>()
)
} else {
s
@@ -1290,3 +1293,57 @@ fn checks(app: &App) -> View<'_, Message> {
}
scrollable(out.padding(24)).into()
}
#[cfg(test)]
mod tests {
use super::short;
#[test]
fn short_preserves_native_lifeline_clusters_at_cutoff() {
let oa = "abcdefghijklmnope\u{0301}尾";
assert_eq!(short(oa, 17), "abcdefghijklmnop…");
assert_eq!(short(oa, 18), oa);
assert_eq!(short(oa, 19), oa);
let sa = "abcdefghijklmnop👩\u{200d}🔬 tail";
assert_eq!(short(sa, 17), "abcdefghijklmnop…");
assert_eq!(short(sa, 18), "abcdefghijklmnop👩\u{200d}🔬…");
assert_eq!(short(sa, 19), "abcdefghijklmnop👩\u{200d}🔬 …");
assert_eq!(short(sa, 22), sa);
}
#[test]
fn short_never_splits_retained_extended_graphemes() {
for cluster in ["e\u{0301}", "👩\u{200d}🔬", "👍🏽", "🇫🇷"] {
let value = format!("A{cluster}BC");
assert_eq!(short(&value, 2), "A…");
assert_eq!(short(&value, 3), format!("A{cluster}…"));
assert_eq!(short(&value, 4), value);
}
}
#[test]
fn short_preserves_ascii_cjk_and_whitespace_policy() {
assert_eq!(short("abcd", 3), "ab…");
assert_eq!(short("abcd", 4), "abcd");
assert_eq!(short("abcd", 5), "abcd");
assert_eq!(short("甲乙丙丁", 3), "甲乙…");
assert_eq!(short("甲乙丙丁", 4), "甲乙丙丁");
assert_eq!(short(" \tA\r\n e\u{0301}\u{2003}B ", 5), "A e\u{0301} B");
assert_eq!(short(" \tA\r\n e\u{0301}\u{2003}B ", 4), "A e\u{0301}…");
}
#[test]
fn short_preserves_empty_and_small_limit_policy() {
for limit in [0, 1, 2] {
assert_eq!(short("", limit), "");
assert_eq!(short(" \t\r\n", limit), "");
}
assert_eq!(short("A", 0), "…");
assert_eq!(short("A", 1), "A");
assert_eq!(short("AB", 1), "…");
assert_eq!(short("e\u{0301}", 0), "…");
assert_eq!(short("e\u{0301}", 1), "e\u{0301}");
assert_eq!(short("e\u{0301}B", 1), "…");
}
}