1#![doc(html_favicon_url = "https://zng-ui.github.io/res/zng-logo-icon.png")]
2#![doc(html_logo_url = "https://zng-ui.github.io/res/zng-logo.png")]
3#![doc = include_str!(concat!("../", std::env!("CARGO_PKG_README")))]
9#![warn(unused_extern_crates)]
10#![warn(missing_docs)]
11
12use std::{borrow::Cow, fmt, hash::Hash, mem, ops::Deref, sync::Arc};
13
14const INLINE_MAX: usize = (mem::size_of::<usize>() * 3) - 1;
15
16fn inline_to_str(d: &[u8; INLINE_MAX]) -> &str {
17 let utf8 = if let Some(i) = d.iter().position(|&b| b == b'\0') {
18 &d[..i]
19 } else {
20 &d[..]
21 };
22 std::str::from_utf8(utf8).unwrap()
23}
24fn str_to_inline(s: &str) -> [u8; INLINE_MAX] {
25 let mut inline = [b'\0'; INLINE_MAX];
26 inline[..s.len()].copy_from_slice(s.as_bytes());
27 inline
28}
29
30#[derive(Clone)]
31#[repr(u8)]
32enum TxtData {
33 Static(&'static str),
34 Inline([u8; INLINE_MAX]),
35 Arc(Arc<str>),
36 #[allow(clippy::box_collection)]
38 String(Box<String>),
39}
40impl fmt::Debug for TxtData {
41 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
42 if f.alternate() {
43 match self {
44 Self::Static(s) => write!(f, "Static({s:?})"),
45 Self::Inline(d) => write!(f, "Inline({:?})", inline_to_str(d)),
46 Self::String(s) => write!(f, "String({s:?})"),
47 Self::Arc(s) => write!(f, "Arc({s:?})"),
48 }
49 } else {
50 write!(f, "{:?}", self.deref())
51 }
52 }
53}
54impl fmt::Display for TxtData {
55 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
56 write!(f, "{}", self.deref())
57 }
58}
59impl PartialEq for TxtData {
60 fn eq(&self, other: &Self) -> bool {
61 self.deref() == other.deref()
62 }
63}
64impl Eq for TxtData {}
65impl Hash for TxtData {
66 fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
67 Hash::hash(&self.deref(), state)
68 }
69}
70impl Deref for TxtData {
71 type Target = str;
72
73 fn deref(&self) -> &str {
74 match self {
75 TxtData::Static(s) => s,
76 TxtData::Inline(d) => inline_to_str(d),
77 TxtData::String(s) => s.as_str(),
78 TxtData::Arc(s) => s,
79 }
80 }
81}
82
83#[derive(Debug, Clone, Copy, PartialEq, Eq)]
87pub enum TxtRepr {
88 Static,
90 Inline,
92 String,
94 Arc,
96}
97
98#[derive(PartialEq, Eq, Hash)]
108pub struct Txt(TxtData);
109impl Clone for Txt {
114 fn clone(&self) -> Self {
115 Self(match &self.0 {
116 TxtData::Static(s) => TxtData::Static(s),
117 TxtData::Inline(d) => TxtData::Inline(*d),
118 TxtData::String(s) => return Self::from_str(s),
119 TxtData::Arc(s) => TxtData::Arc(Arc::clone(s)),
120 })
121 }
122}
123impl Txt {
124 pub const fn from_static(s: &'static str) -> Txt {
126 Txt(TxtData::Static(s))
127 }
128
129 pub fn from_string(s: String) -> Txt {
135 Txt(TxtData::String(Box::new(s)))
136 }
137
138 #[expect(clippy::should_implement_trait)] pub fn from_str(s: &str) -> Txt {
146 if s.is_empty() {
147 Self::from_static("")
148 } else if s.len() <= INLINE_MAX && !s.contains('\0') {
149 Self(TxtData::Inline(str_to_inline(s)))
150 } else {
151 Self(TxtData::Arc(Arc::from(s)))
152 }
153 }
154
155 pub fn from_arc(s: Arc<str>) -> Txt {
163 if s.is_empty() {
164 Self::from_static("")
165 } else if s.len() <= INLINE_MAX && !s.contains('\0') {
166 Self(TxtData::Inline(str_to_inline(&s)))
167 } else {
168 Self(TxtData::Arc(s))
169 }
170 }
171
172 pub fn from_char(c: char) -> Txt {
174 #[allow(clippy::assertions_on_constants)]
175 const _: () = assert!(4 <= INLINE_MAX, "cannot inline char");
176
177 let mut buf = [0u8; 4];
178 let s = c.encode_utf8(&mut buf);
179
180 if s.contains('\0') {
181 return Txt(TxtData::Arc(Arc::from(&*s)));
182 }
183
184 Txt(TxtData::Inline(str_to_inline(s)))
185 }
186
187 pub fn from_fmt(args: std::fmt::Arguments) -> Txt {
189 if let Some(s) = args.as_str() {
190 Txt::from_static(s)
191 } else {
192 let mut r = Txt(TxtData::Inline([b'\0'; INLINE_MAX]));
193 std::fmt::write(&mut r, args).unwrap();
194 r
195 }
196 }
197
198 pub const fn repr(&self) -> TxtRepr {
200 match &self.0 {
201 TxtData::Static(_) => TxtRepr::Static,
202 TxtData::Inline(_) => TxtRepr::Inline,
203 TxtData::String(_) => TxtRepr::String,
204 TxtData::Arc(_) => TxtRepr::Arc,
205 }
206 }
207
208 pub fn to_mut(&mut self) -> &mut String {
215 self.0 = match mem::replace(&mut self.0, TxtData::Static("")) {
216 TxtData::String(s) => TxtData::String(s),
217 TxtData::Static(s) => TxtData::String(Box::new(s.to_owned())),
218 TxtData::Inline(d) => TxtData::String(Box::new(inline_to_str(&d).to_owned())),
219 TxtData::Arc(s) => TxtData::String(Box::new((*s).to_owned())),
220 };
221
222 if let TxtData::String(s) = &mut self.0 { s } else { unreachable!() }
223 }
224
225 pub fn end_mut(&mut self) {
228 match mem::replace(&mut self.0, TxtData::Static("")) {
229 TxtData::String(s) => {
230 *self = Self::from_str(&s);
231 }
232 already => self.0 = already,
233 }
234 }
235
236 pub fn into_owned(self) -> String {
240 match self.0 {
241 TxtData::String(s) => *s,
242 TxtData::Static(s) => s.to_owned(),
243 TxtData::Inline(d) => inline_to_str(&d).to_owned(),
244 TxtData::Arc(s) => (*s).to_owned(),
245 }
246 }
247
248 pub fn clear(&mut self) {
251 match &mut self.0 {
252 TxtData::String(s) => s.clear(),
253 d => *d = TxtData::Static(""),
254 }
255 }
256
257 pub fn pop(&mut self) -> Option<char> {
264 match &mut self.0 {
265 TxtData::String(s) => s.pop(),
266 TxtData::Static(s) => {
267 if let Some((i, c)) = s.char_indices().last() {
268 *s = &s[..i];
269 Some(c)
270 } else {
271 None
272 }
273 }
274 TxtData::Inline(d) => {
275 let s = inline_to_str(d);
276 if let Some((i, c)) = s.char_indices().last() {
277 if i > 0 {
278 *d = str_to_inline(&s[..i]);
279 } else {
280 self.0 = TxtData::Static("");
281 }
282 Some(c)
283 } else {
284 None
285 }
286 }
287 TxtData::Arc(_) => self.to_mut().pop(),
288 }
289 }
290
291 pub fn truncate(&mut self, new_len: usize) {
299 match &mut self.0 {
300 TxtData::String(s) => s.truncate(new_len),
301 TxtData::Static(s) => {
302 if new_len <= s.len() {
303 assert!(s.is_char_boundary(new_len));
304 *s = &s[..new_len];
305 }
306 }
307 TxtData::Inline(d) => {
308 if new_len == 0 {
309 self.0 = TxtData::Static("");
310 } else {
311 let s = inline_to_str(d);
312 if new_len < s.len() {
313 assert!(s.is_char_boundary(new_len));
314 d[new_len..].iter_mut().for_each(|b| *b = b'\0');
315 }
316 }
317 }
318 TxtData::Arc(_) => self.to_mut().truncate(new_len),
319 }
320 }
321
322 pub fn split_off(&mut self, at: usize) -> Txt {
331 match &mut self.0 {
332 TxtData::String(s) => Txt::from_string(s.split_off(at)),
333 TxtData::Static(s) => {
334 assert!(s.is_char_boundary(at));
335 let other = &s[at..];
336 *s = &s[..at];
337 Txt(TxtData::Static(other))
338 }
339 TxtData::Inline(d) => {
340 let s = inline_to_str(d);
341 assert!(s.is_char_boundary(at));
342 let a_len = at;
343 let b_len = s.len() - at;
344
345 let r = Txt(if b_len == 0 {
346 TxtData::Static("")
347 } else {
348 TxtData::Inline(str_to_inline(&s[at..]))
349 });
350
351 if a_len == 0 {
352 self.0 = TxtData::Static("");
353 } else {
354 *d = str_to_inline(&s[..at]);
355 }
356
357 r
358 }
359 TxtData::Arc(_) => Txt::from_string(self.to_mut().split_off(at)),
360 }
361 }
362
363 pub fn push(&mut self, c: char) {
368 match &mut self.0 {
369 TxtData::String(s) => s.push(c),
370 TxtData::Inline(inlined) => {
371 if let Some(len) = inlined.iter().position(|&c| c == b'\0') {
372 let c_len = c.len_utf8();
373 if len + c_len <= INLINE_MAX && c != '\0' {
374 let mut buf = [0u8; 4];
375 let s = c.encode_utf8(&mut buf);
376 inlined[len..len + c_len].copy_from_slice(s.as_bytes());
377 return;
378 }
379 }
380 self.to_mut().push(c)
381 }
382 _ => {
383 let len = self.len();
384 let c_len = c.len_utf8();
385 if len + c_len <= INLINE_MAX && c != '\0' {
386 let mut inlined = str_to_inline(self.as_str());
387 let mut buf = [0u8; 4];
388 let s = c.encode_utf8(&mut buf);
389 inlined[len..len + c_len].copy_from_slice(s.as_bytes());
390
391 self.0 = TxtData::Inline(inlined);
392 } else {
393 self.to_mut().push(c)
394 }
395 }
396 }
397 }
398
399 pub fn push_str(&mut self, s: &str) {
404 if s.is_empty() {
405 return;
406 }
407
408 match &mut self.0 {
409 TxtData::String(str) => str.push_str(s),
410 TxtData::Inline(inlined) => {
411 if let Some(len) = inlined.iter().position(|&c| c == b'\0')
412 && len + s.len() <= INLINE_MAX
413 && !s.contains('\0')
414 {
415 inlined[len..len + s.len()].copy_from_slice(s.as_bytes());
416 return;
417 }
418 self.to_mut().push_str(s)
419 }
420 _ => {
421 let len = self.len();
422 if len + s.len() <= INLINE_MAX && !s.contains('\0') {
423 let mut inlined = str_to_inline(self.as_str());
424 inlined[len..len + s.len()].copy_from_slice(s.as_bytes());
425
426 self.0 = TxtData::Inline(inlined);
427 } else {
428 self.to_mut().push_str(s)
429 }
430 }
431 }
432 }
433
434 pub fn as_str(&self) -> &str {
436 self.0.deref()
437 }
438
439 pub fn as_static_str(&self) -> Option<&'static str> {
441 match self.0 {
442 TxtData::Static(s) => Some(s),
443 _ => None,
444 }
445 }
446}
447impl fmt::Debug for Txt {
448 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
449 fmt::Debug::fmt(&self.0, f)
450 }
451}
452impl fmt::Display for Txt {
453 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
454 fmt::Display::fmt(&self.0, f)
455 }
456}
457impl Default for Txt {
458 fn default() -> Self {
460 Self::from_static("")
461 }
462}
463impl std::str::FromStr for Txt {
464 type Err = ();
465
466 fn from_str(s: &str) -> Result<Self, Self::Err> {
467 Ok(Txt::from_str(s))
468 }
469}
470impl From<&'static str> for Txt {
471 fn from(value: &'static str) -> Self {
472 Txt(TxtData::Static(value))
473 }
474}
475impl From<String> for Txt {
476 fn from(value: String) -> Self {
477 Txt(TxtData::String(Box::new(value)))
478 }
479}
480impl From<Cow<'static, str>> for Txt {
481 fn from(value: Cow<'static, str>) -> Self {
482 match value {
483 Cow::Borrowed(s) => Txt(TxtData::Static(s)),
484 Cow::Owned(s) => Txt(TxtData::String(Box::new(s))),
485 }
486 }
487}
488impl From<char> for Txt {
489 fn from(value: char) -> Self {
490 Txt::from_char(value)
491 }
492}
493impl From<Txt> for String {
494 fn from(value: Txt) -> Self {
495 value.into_owned()
496 }
497}
498impl From<Txt> for Cow<'static, str> {
499 fn from(value: Txt) -> Self {
500 match value.0 {
501 TxtData::Static(s) => Cow::Borrowed(s),
502 TxtData::String(s) => Cow::Owned(*s),
503 TxtData::Inline(d) => Cow::Owned(inline_to_str(&d).to_owned()),
504 TxtData::Arc(s) => Cow::Owned((*s).to_owned()),
505 }
506 }
507}
508impl From<Txt> for std::path::PathBuf {
509 fn from(value: Txt) -> Self {
510 value.into_owned().into()
511 }
512}
513impl From<Txt> for Box<dyn std::error::Error> {
514 fn from(err: Txt) -> Self {
515 err.into_owned().into()
516 }
517}
518impl From<Txt> for Box<dyn std::error::Error + Send + Sync> {
519 fn from(err: Txt) -> Self {
520 err.into_owned().into()
521 }
522}
523impl From<Txt> for std::ffi::OsString {
524 fn from(value: Txt) -> Self {
525 String::from(value).into()
526 }
527}
528impl std::ops::Deref for Txt {
529 type Target = str;
530
531 fn deref(&self) -> &Self::Target {
532 self.0.deref()
533 }
534}
535impl AsRef<str> for Txt {
536 fn as_ref(&self) -> &str {
537 self.0.as_ref()
538 }
539}
540impl AsRef<std::path::Path> for Txt {
541 fn as_ref(&self) -> &std::path::Path {
542 self.0.as_ref()
543 }
544}
545impl AsRef<std::ffi::OsStr> for Txt {
546 fn as_ref(&self) -> &std::ffi::OsStr {
547 self.0.as_ref()
548 }
549}
550impl std::borrow::Borrow<str> for Txt {
551 fn borrow(&self) -> &str {
552 self.as_str()
553 }
554}
555impl<'a> std::ops::Add<&'a str> for Txt {
556 type Output = Txt;
557
558 fn add(mut self, rhs: &'a str) -> Self::Output {
559 self += rhs;
560 self
561 }
562}
563impl std::ops::AddAssign<&str> for Txt {
564 fn add_assign(&mut self, rhs: &str) {
565 self.push_str(rhs);
566 }
567}
568impl PartialEq<&str> for Txt {
569 fn eq(&self, other: &&str) -> bool {
570 self.as_str().eq(*other)
571 }
572}
573impl PartialEq<str> for Txt {
574 fn eq(&self, other: &str) -> bool {
575 self.as_str().eq(other)
576 }
577}
578impl PartialEq<String> for Txt {
579 fn eq(&self, other: &String) -> bool {
580 self.as_str().eq(other)
581 }
582}
583impl PartialEq<Txt> for &str {
584 fn eq(&self, other: &Txt) -> bool {
585 other.as_str().eq(*self)
586 }
587}
588impl PartialEq<Txt> for str {
589 fn eq(&self, other: &Txt) -> bool {
590 other.as_str().eq(self)
591 }
592}
593impl PartialEq<Txt> for String {
594 fn eq(&self, other: &Txt) -> bool {
595 other.as_str().eq(self)
596 }
597}
598impl serde::Serialize for Txt {
599 fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
600 where
601 S: serde::Serializer,
602 {
603 serializer.serialize_str(self.as_str())
604 }
605}
606impl<'de> serde::Deserialize<'de> for Txt {
607 fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
608 where
609 D: serde::Deserializer<'de>,
610 {
611 String::deserialize(deserializer).map(Txt::from)
612 }
613}
614impl AsRef<[u8]> for Txt {
615 fn as_ref(&self) -> &[u8] {
616 self.as_str().as_ref()
617 }
618}
619impl std::fmt::Write for Txt {
620 fn write_str(&mut self, s: &str) -> fmt::Result {
621 self.push_str(s);
622 Ok(())
623 }
624}
625impl PartialOrd for Txt {
626 fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
627 Some(self.cmp(other))
628 }
629}
630impl Ord for Txt {
631 fn cmp(&self, other: &Self) -> std::cmp::Ordering {
632 self.as_str().cmp(other.as_str())
633 }
634}
635impl FromIterator<char> for Txt {
636 fn from_iter<I: IntoIterator<Item = char>>(iter: I) -> Txt {
637 String::from_iter(iter).into()
638 }
639}
640impl<'a> FromIterator<&'a char> for Txt {
641 fn from_iter<I: IntoIterator<Item = &'a char>>(iter: I) -> Txt {
642 String::from_iter(iter).into()
643 }
644}
645impl<'a> FromIterator<&'a str> for Txt {
646 fn from_iter<I: IntoIterator<Item = &'a str>>(iter: I) -> Txt {
647 String::from_iter(iter).into()
648 }
649}
650impl FromIterator<String> for Txt {
651 fn from_iter<I: IntoIterator<Item = String>>(iter: I) -> Txt {
652 String::from_iter(iter).into()
653 }
654}
655impl<'a> FromIterator<Cow<'a, str>> for Txt {
656 fn from_iter<I: IntoIterator<Item = Cow<'a, str>>>(iter: I) -> Txt {
657 String::from_iter(iter).into()
658 }
659}
660
661impl FromIterator<Txt> for Txt {
662 fn from_iter<I: IntoIterator<Item = Txt>>(iter: I) -> Txt {
663 let mut iterator = iter.into_iter();
664
665 match iterator.next() {
666 None => Txt::from_static(""),
667 Some(mut buf) => {
668 buf.extend(iterator);
669 buf
670 }
671 }
672 }
673}
674
675impl Extend<char> for Txt {
676 fn extend<T: IntoIterator<Item = char>>(&mut self, iter: T) {
677 if let TxtData::String(s) = &mut self.0 {
678 s.extend(iter);
679 } else {
680 let iter = iter.into_iter();
681 let (lower_bound, _) = iter.size_hint();
682
683 if self.len() + lower_bound < INLINE_MAX {
684 for c in iter {
686 self.push(c);
687 }
688 } else {
689 self.to_mut().extend(iter);
690 }
691 }
692 }
693}
694impl<'a> Extend<&'a char> for Txt {
695 fn extend<I: IntoIterator<Item = &'a char>>(&mut self, iter: I) {
696 self.extend(iter.into_iter().cloned());
697 }
698}
699impl<'a> Extend<&'a str> for Txt {
700 fn extend<I: IntoIterator<Item = &'a str>>(&mut self, iter: I) {
701 iter.into_iter().for_each(move |s| self.push_str(s));
702 }
703}
704impl Extend<String> for Txt {
705 fn extend<I: IntoIterator<Item = String>>(&mut self, iter: I) {
706 iter.into_iter().for_each(move |s| self.push_str(&s));
707 }
708}
709impl<'a> Extend<Cow<'a, str>> for Txt {
710 fn extend<I: IntoIterator<Item = Cow<'a, str>>>(&mut self, iter: I) {
711 iter.into_iter().for_each(move |s| self.push_str(&s));
712 }
713}
714
715impl Extend<Txt> for Txt {
716 fn extend<I: IntoIterator<Item = Txt>>(&mut self, iter: I) {
717 iter.into_iter().for_each(move |s| self.push_str(&s));
718 }
719}
720
721pub trait ToTxt {
727 fn to_txt(&self) -> Txt;
742}
743impl<T: ToString> ToTxt for T {
744 fn to_txt(&self) -> Txt {
745 self.to_string().into()
746 }
747}
748
749#[macro_export]
761macro_rules! formatx {
762 ($($tt:tt)*) => {
763 {
764 let res = $crate::Txt::from_fmt(format_args!($($tt)*));
765 res
766 }
767 };
768}