i_slint_core/
string.rs

1// Copyright © SixtyFPS GmbH <info@slint.dev>
2// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
3
4//! module for the SharedString and related things
5
6#![allow(unsafe_code)]
7#![warn(missing_docs)]
8
9use crate::SharedVector;
10use alloc::string::String;
11use core::fmt::{Debug, Display, Write};
12use core::ops::Deref;
13#[cfg(not(feature = "std"))]
14#[allow(unused)]
15use num_traits::Float;
16
17/// This macro is the same as [`std::format!`], but it returns a [`SharedString`] instead.
18///
19/// ### Example
20/// ```rust
21/// let s : slint::SharedString = slint::format!("Hello {}", "world");
22/// assert_eq!(s, slint::SharedString::from("Hello world"));
23/// ```
24#[macro_export]
25macro_rules! format {
26    ($($arg:tt)*) => {{
27        $crate::string::format(core::format_args!($($arg)*))
28    }}
29}
30
31/// A string type used by the Slint run-time.
32///
33/// SharedString uses implicit data sharing to make it efficient to pass around copies. When
34/// cloning, a reference to the data is cloned, not the data itself. The data itself is only copied
35/// when modifying it, for example using [push_str](SharedString::push_str). This is also called copy-on-write.
36///
37/// Under the hood the string data is UTF-8 encoded and it is always terminated with a null character.
38///
39/// `SharedString` implements [`Deref<Target=str>`] so it can be easily passed to any function taking a `&str`.
40/// It also implement `From` such that it an easily be converted to and from the typical rust String type with `.into()`
41#[derive(Clone, Default)]
42#[repr(C)]
43pub struct SharedString {
44    // Invariant: valid utf-8, `\0` terminated
45    inner: SharedVector<u8>,
46}
47
48impl SharedString {
49    /// Creates a new empty string
50    ///
51    /// Same as `SharedString::default()`
52    pub fn new() -> Self {
53        Self::default()
54    }
55
56    fn as_ptr(&self) -> *const u8 {
57        self.inner.as_ptr()
58    }
59
60    /// Size of the string, in bytes. This excludes the terminating null character.
61    pub fn len(&self) -> usize {
62        self.inner.len().saturating_sub(1)
63    }
64
65    /// Return true if the String is empty
66    pub fn is_empty(&self) -> bool {
67        self.len() == 0
68    }
69
70    /// Return a slice to the string
71    pub fn as_str(&self) -> &str {
72        // Safety: self.as_ptr is a pointer from the inner which has utf-8
73        unsafe {
74            core::str::from_utf8_unchecked(core::slice::from_raw_parts(self.as_ptr(), self.len()))
75        }
76    }
77
78    /// Append a string to this string
79    ///
80    /// ```
81    /// # use i_slint_core::SharedString;
82    /// let mut hello = SharedString::from("Hello");
83    /// hello.push_str(", ");
84    /// hello.push_str("World");
85    /// hello.push_str("!");
86    /// assert_eq!(hello, "Hello, World!");
87    /// ```
88    pub fn push_str(&mut self, x: &str) {
89        let mut iter = x.as_bytes().iter().copied();
90        if self.inner.is_empty() {
91            self.inner.extend(iter.chain(core::iter::once(0)));
92        } else if let Some(first) = iter.next() {
93            // We skip the `first` from `iter` because we will write it at the
94            // location of the previous `\0`, after extend did the re-alloc of the
95            // right size
96            let prev_len = self.len();
97            self.inner.extend(iter.chain(core::iter::once(0)));
98            self.inner.make_mut_slice()[prev_len] = first;
99        }
100    }
101}
102
103impl Deref for SharedString {
104    type Target = str;
105    fn deref(&self) -> &Self::Target {
106        self.as_str()
107    }
108}
109
110impl From<&str> for SharedString {
111    fn from(value: &str) -> Self {
112        SharedString {
113            inner: SharedVector::from_iter(
114                value.as_bytes().iter().cloned().chain(core::iter::once(0)),
115            ),
116        }
117    }
118}
119
120impl Debug for SharedString {
121    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
122        Debug::fmt(self.as_str(), f)
123    }
124}
125
126impl Display for SharedString {
127    fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
128        Display::fmt(self.as_str(), f)
129    }
130}
131
132impl AsRef<str> for SharedString {
133    #[inline]
134    fn as_ref(&self) -> &str {
135        self.as_str()
136    }
137}
138
139#[cfg(feature = "serde")]
140impl serde::Serialize for SharedString {
141    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
142    where
143        S: serde::Serializer,
144    {
145        let string = self.as_str();
146        serializer.serialize_str(string)
147    }
148}
149
150#[cfg(feature = "serde")]
151impl<'de> serde::Deserialize<'de> for SharedString {
152    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
153    where
154        D: serde::Deserializer<'de>,
155    {
156        let string = String::deserialize(deserializer)?;
157        Ok(SharedString::from(string))
158    }
159}
160
161#[cfg(feature = "std")]
162impl AsRef<std::ffi::CStr> for SharedString {
163    #[inline]
164    fn as_ref(&self) -> &std::ffi::CStr {
165        if self.inner.is_empty() {
166            return Default::default();
167        }
168        // Safety: we ensure that there is always a terminated \0
169        debug_assert_eq!(self.inner.as_slice()[self.inner.len() - 1], 0);
170        unsafe { std::ffi::CStr::from_bytes_with_nul_unchecked(self.inner.as_slice()) }
171    }
172}
173
174#[cfg(feature = "std")]
175impl AsRef<std::path::Path> for SharedString {
176    #[inline]
177    fn as_ref(&self) -> &std::path::Path {
178        self.as_str().as_ref()
179    }
180}
181
182#[cfg(feature = "std")]
183impl AsRef<std::ffi::OsStr> for SharedString {
184    #[inline]
185    fn as_ref(&self) -> &std::ffi::OsStr {
186        self.as_str().as_ref()
187    }
188}
189
190impl AsRef<[u8]> for SharedString {
191    #[inline]
192    fn as_ref(&self) -> &[u8] {
193        self.as_str().as_bytes()
194    }
195}
196
197impl<T> PartialEq<T> for SharedString
198where
199    T: ?Sized + AsRef<str>,
200{
201    fn eq(&self, other: &T) -> bool {
202        self.as_str() == other.as_ref()
203    }
204}
205impl Eq for SharedString {}
206
207impl<T> PartialOrd<T> for SharedString
208where
209    T: ?Sized + AsRef<str>,
210{
211    fn partial_cmp(&self, other: &T) -> Option<core::cmp::Ordering> {
212        PartialOrd::partial_cmp(self.as_str(), other.as_ref())
213    }
214}
215impl Ord for SharedString {
216    fn cmp(&self, other: &Self) -> core::cmp::Ordering {
217        Ord::cmp(self.as_str(), other.as_str())
218    }
219}
220
221impl From<String> for SharedString {
222    fn from(s: String) -> Self {
223        s.as_str().into()
224    }
225}
226
227impl From<&String> for SharedString {
228    fn from(s: &String) -> Self {
229        s.as_str().into()
230    }
231}
232
233impl From<char> for SharedString {
234    fn from(c: char) -> Self {
235        SharedString::from(c.encode_utf8(&mut [0; 6]) as &str)
236    }
237}
238
239impl From<SharedString> for String {
240    fn from(s: SharedString) -> String {
241        s.as_str().into()
242    }
243}
244
245impl From<&SharedString> for String {
246    fn from(s: &SharedString) -> String {
247        s.as_str().into()
248    }
249}
250
251impl core::ops::AddAssign<&str> for SharedString {
252    fn add_assign(&mut self, other: &str) {
253        self.push_str(other);
254    }
255}
256
257impl core::ops::Add<&str> for SharedString {
258    type Output = SharedString;
259    fn add(mut self, other: &str) -> SharedString {
260        self.push_str(other);
261        self
262    }
263}
264
265impl core::hash::Hash for SharedString {
266    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
267        self.as_str().hash(state)
268    }
269}
270
271impl Write for SharedString {
272    fn write_str(&mut self, s: &str) -> core::fmt::Result {
273        self.push_str(s);
274        Ok(())
275    }
276}
277
278impl core::borrow::Borrow<str> for SharedString {
279    fn borrow(&self) -> &str {
280        self.as_str()
281    }
282}
283
284impl Extend<char> for SharedString {
285    fn extend<X: IntoIterator<Item = char>>(&mut self, iter: X) {
286        let iter = iter.into_iter();
287        self.inner.reserve(iter.size_hint().0);
288        let mut buf = [0; 4];
289        for ch in iter {
290            let utf8 = ch.encode_utf8(&mut buf);
291            self.push_str(utf8);
292        }
293    }
294}
295
296impl FromIterator<char> for SharedString {
297    fn from_iter<T: IntoIterator<Item = char>>(iter: T) -> Self {
298        let mut str = Self::new();
299        str.extend(iter);
300        str
301    }
302}
303
304/// Same as [`std::fmt::format()`], but return a [`SharedString`] instead
305pub fn format(args: core::fmt::Arguments<'_>) -> SharedString {
306    // unfortunately, the estimated_capacity is unstable
307    //let capacity = args.estimated_capacity();
308    let mut output = SharedString::default();
309    output.write_fmt(args).unwrap();
310    output
311}
312
313/// A trait for converting a value to a [`SharedString`].
314///
315/// This trait is automatically implemented for any type which implements the [`Display`] trait as long as the trait is in scope.
316/// As such, `ToSharedString` shouldn’t be implemented directly: [`Display`] should be implemented instead, and you get the `ToSharedString` implementation for free.
317pub trait ToSharedString {
318    /// Converts the given value to a [`SharedString`].
319    fn to_shared_string(&self) -> SharedString;
320}
321
322impl<T> ToSharedString for T
323where
324    T: Display + ?Sized,
325{
326    fn to_shared_string(&self) -> SharedString {
327        format!("{}", self)
328    }
329}
330
331/// Convert a f62 to a SharedString
332pub fn shared_string_from_number(n: f64) -> SharedString {
333    // Number from which the increment of f32 is 1, so that we print enough precision to be able to represent all integers
334    if n < 16777216. {
335        crate::format!("{}", n as f32)
336    } else {
337        crate::format!("{}", n)
338    }
339}
340
341/// Convert a f64 to a SharedString with a fixed number of digits after the decimal point
342pub fn shared_string_from_number_fixed(n: f64, digits: usize) -> SharedString {
343    crate::format!("{number:.digits$}", number = n, digits = digits)
344}
345
346/// Convert a f64 to a SharedString following a similar logic as JavaScript's Number.toPrecision()
347pub fn shared_string_from_number_precision(n: f64, precision: usize) -> SharedString {
348    let exponent = f64::log10(n.abs()).floor() as isize;
349    if precision == 0 {
350        shared_string_from_number(n)
351    } else if exponent < -6 || (exponent >= 0 && exponent as usize >= precision) {
352        crate::format!(
353            "{number:.digits$e}",
354            number = n,
355            digits = precision.saturating_add_signed(-1)
356        )
357    } else {
358        shared_string_from_number_fixed(n, precision.saturating_add_signed(-(exponent + 1)))
359    }
360}
361
362#[test]
363fn simple_test() {
364    use std::string::ToString;
365    let x = SharedString::from("hello world!");
366    assert_eq!(x, "hello world!");
367    assert_ne!(x, "hello world?");
368    assert_eq!(x, x.clone());
369    assert_eq!("hello world!", x.as_str());
370    let string = String::from("hello world!");
371    assert_eq!(x, string);
372    assert_eq!(x.to_string(), string);
373    let def = SharedString::default();
374    assert_eq!(def, SharedString::default());
375    assert_eq!(def, SharedString::new());
376    assert_ne!(def, x);
377    assert_eq!(
378        (&x as &dyn AsRef<std::ffi::CStr>).as_ref(),
379        &*std::ffi::CString::new("hello world!").unwrap()
380    );
381    assert_eq!(SharedString::from('h'), "h");
382    assert_eq!(SharedString::from('😎'), "😎");
383}
384
385#[test]
386fn threading() {
387    let shared_cst = SharedString::from("Hello there!");
388    let shared_mtx = std::sync::Arc::new(std::sync::Mutex::new(SharedString::from("Shared:")));
389    let mut handles = std::vec![];
390    for _ in 0..20 {
391        let cst = shared_cst.clone();
392        let mtx = shared_mtx.clone();
393        handles.push(std::thread::spawn(move || {
394            assert_eq!(cst, "Hello there!");
395            let mut cst2 = cst.clone();
396            cst2.push_str(" ... or not?");
397            assert_eq!(cst2, "Hello there! ... or not?");
398            assert_eq!(cst.clone(), "Hello there!");
399
400            let shared = {
401                let mut lock = mtx.lock().unwrap();
402                assert!(lock.starts_with("Shared:"));
403                lock.push_str("!");
404                lock.clone()
405            };
406            assert!(shared.clone().starts_with("Shared:"));
407        }));
408    }
409    for j in handles {
410        j.join().unwrap();
411    }
412    assert_eq!(shared_cst.clone(), "Hello there!");
413    assert_eq!(shared_mtx.lock().unwrap().as_str(), "Shared:!!!!!!!!!!!!!!!!!!!!");
414    // 20x"!"
415}
416
417#[test]
418fn to_shared_string() {
419    let i = 5.1;
420    let five = SharedString::from("5.1");
421
422    assert_eq!(five, i.to_shared_string());
423}
424
425#[cfg(feature = "ffi")]
426pub(crate) mod ffi {
427    use super::*;
428
429    /// for cbindgen.
430    #[allow(non_camel_case_types)]
431    type c_char = u8;
432
433    #[unsafe(no_mangle)]
434    /// Returns a nul-terminated pointer for this string.
435    /// The returned value is owned by the string, and should not be used after any
436    /// mutable function have been called on the string, and must not be freed.
437    pub extern "C" fn slint_shared_string_bytes(ss: &SharedString) -> *const c_char {
438        if ss.is_empty() {
439            "\0".as_ptr()
440        } else {
441            ss.as_ptr()
442        }
443    }
444
445    #[unsafe(no_mangle)]
446    /// Destroy the shared string
447    pub unsafe extern "C" fn slint_shared_string_drop(ss: *const SharedString) {
448        core::ptr::read(ss);
449    }
450
451    #[unsafe(no_mangle)]
452    /// Increment the reference count of the string.
453    /// The resulting structure must be passed to slint_shared_string_drop
454    pub unsafe extern "C" fn slint_shared_string_clone(out: *mut SharedString, ss: &SharedString) {
455        core::ptr::write(out, ss.clone())
456    }
457
458    #[unsafe(no_mangle)]
459    /// Safety: bytes must be a valid utf-8 string of size len without null inside.
460    /// The resulting structure must be passed to slint_shared_string_drop
461    pub unsafe extern "C" fn slint_shared_string_from_bytes(
462        out: *mut SharedString,
463        bytes: *const c_char,
464        len: usize,
465    ) {
466        let str = core::str::from_utf8(core::slice::from_raw_parts(bytes, len)).unwrap();
467        core::ptr::write(out, SharedString::from(str));
468    }
469
470    /// Create a string from a number.
471    /// The resulting structure must be passed to slint_shared_string_drop
472    #[unsafe(no_mangle)]
473    pub unsafe extern "C" fn slint_shared_string_from_number(out: *mut SharedString, n: f64) {
474        let str = shared_string_from_number(n);
475        core::ptr::write(out, str);
476    }
477
478    #[test]
479    fn test_slint_shared_string_from_number() {
480        unsafe {
481            let mut s = core::mem::MaybeUninit::uninit();
482            slint_shared_string_from_number(s.as_mut_ptr(), 45.);
483            assert_eq!(s.assume_init(), "45");
484
485            let mut s = core::mem::MaybeUninit::uninit();
486            slint_shared_string_from_number(s.as_mut_ptr(), 45.12);
487            assert_eq!(s.assume_init(), "45.12");
488
489            let mut s = core::mem::MaybeUninit::uninit();
490            slint_shared_string_from_number(s.as_mut_ptr(), -1325466.);
491            assert_eq!(s.assume_init(), "-1325466");
492
493            let mut s = core::mem::MaybeUninit::uninit();
494            slint_shared_string_from_number(s.as_mut_ptr(), 0.);
495            assert_eq!(s.assume_init(), "0");
496
497            let mut s = core::mem::MaybeUninit::uninit();
498            slint_shared_string_from_number(
499                s.as_mut_ptr(),
500                ((1235.82756f32 * 1000f32).round() / 1000f32) as _,
501            );
502            assert_eq!(s.assume_init(), "1235.828");
503        }
504    }
505
506    #[unsafe(no_mangle)]
507    pub extern "C" fn slint_shared_string_from_number_fixed(
508        out: &mut SharedString,
509        n: f64,
510        digits: usize,
511    ) {
512        *out = shared_string_from_number_fixed(n, digits);
513    }
514
515    #[test]
516    fn test_slint_shared_string_from_number_fixed() {
517        let mut s = SharedString::default();
518
519        let num = 12345.6789;
520
521        slint_shared_string_from_number_fixed(&mut s, num, 0);
522        assert_eq!(s.as_str(), "12346");
523
524        slint_shared_string_from_number_fixed(&mut s, num, 1);
525        assert_eq!(s.as_str(), "12345.7");
526
527        slint_shared_string_from_number_fixed(&mut s, num, 6);
528        assert_eq!(s.as_str(), "12345.678900");
529
530        let num = -12345.6789;
531
532        slint_shared_string_from_number_fixed(&mut s, num, 0);
533        assert_eq!(s.as_str(), "-12346");
534
535        slint_shared_string_from_number_fixed(&mut s, num, 1);
536        assert_eq!(s.as_str(), "-12345.7");
537
538        slint_shared_string_from_number_fixed(&mut s, num, 6);
539        assert_eq!(s.as_str(), "-12345.678900");
540
541        slint_shared_string_from_number_fixed(&mut s, 1.23E+20_f64, 2);
542        assert_eq!(s.as_str(), "123000000000000000000.00");
543
544        slint_shared_string_from_number_fixed(&mut s, 1.23E-10_f64, 2);
545        assert_eq!(s.as_str(), "0.00");
546
547        slint_shared_string_from_number_fixed(&mut s, 2.34, 1);
548        assert_eq!(s.as_str(), "2.3");
549
550        slint_shared_string_from_number_fixed(&mut s, 2.35, 1);
551        assert_eq!(s.as_str(), "2.4");
552
553        slint_shared_string_from_number_fixed(&mut s, 2.55, 1);
554        assert_eq!(s.as_str(), "2.5");
555    }
556
557    #[unsafe(no_mangle)]
558    pub extern "C" fn slint_shared_string_from_number_precision(
559        out: &mut SharedString,
560        n: f64,
561        precision: usize,
562    ) {
563        *out = shared_string_from_number_precision(n, precision);
564    }
565
566    #[test]
567    fn test_slint_shared_string_from_number_precision() {
568        let mut s = SharedString::default();
569
570        let num = 5.123456;
571
572        slint_shared_string_from_number_precision(&mut s, num, 0);
573        assert_eq!(s.as_str(), "5.123456");
574
575        slint_shared_string_from_number_precision(&mut s, num, 5);
576        assert_eq!(s.as_str(), "5.1235");
577
578        slint_shared_string_from_number_precision(&mut s, num, 2);
579        assert_eq!(s.as_str(), "5.1");
580
581        slint_shared_string_from_number_precision(&mut s, num, 1);
582        assert_eq!(s.as_str(), "5");
583
584        let num = 0.000123;
585
586        slint_shared_string_from_number_precision(&mut s, num, 0);
587        assert_eq!(s.as_str(), "0.000123");
588
589        slint_shared_string_from_number_precision(&mut s, num, 5);
590        assert_eq!(s.as_str(), "0.00012300");
591
592        slint_shared_string_from_number_precision(&mut s, num, 2);
593        assert_eq!(s.as_str(), "0.00012");
594
595        slint_shared_string_from_number_precision(&mut s, num, 1);
596        assert_eq!(s.as_str(), "0.0001");
597
598        let num = 1234.5;
599
600        slint_shared_string_from_number_precision(&mut s, num, 1);
601        assert_eq!(s.as_str(), "1e3");
602
603        slint_shared_string_from_number_precision(&mut s, num, 2);
604        assert_eq!(s.as_str(), "1.2e3");
605
606        slint_shared_string_from_number_precision(&mut s, num, 6);
607        assert_eq!(s.as_str(), "1234.50");
608
609        let num = -1234.5;
610
611        slint_shared_string_from_number_precision(&mut s, num, 1);
612        assert_eq!(s.as_str(), "-1e3");
613
614        slint_shared_string_from_number_precision(&mut s, num, 2);
615        assert_eq!(s.as_str(), "-1.2e3");
616
617        slint_shared_string_from_number_precision(&mut s, num, 6);
618        assert_eq!(s.as_str(), "-1234.50");
619
620        let num = 0.00000012345;
621
622        slint_shared_string_from_number_precision(&mut s, num, 1);
623        assert_eq!(s.as_str(), "1e-7");
624
625        slint_shared_string_from_number_precision(&mut s, num, 10);
626        assert_eq!(s.as_str(), "1.234500000e-7");
627    }
628
629    /// Append some bytes to an existing shared string
630    ///
631    /// bytes must be a valid utf8 array of size `len`, without null bytes inside
632    #[unsafe(no_mangle)]
633    pub unsafe extern "C" fn slint_shared_string_append(
634        self_: &mut SharedString,
635        bytes: *const c_char,
636        len: usize,
637    ) {
638        let str = core::str::from_utf8(core::slice::from_raw_parts(bytes, len)).unwrap();
639        self_.push_str(str);
640    }
641    #[test]
642    fn test_slint_shared_string_append() {
643        let mut s = SharedString::default();
644        let mut append = |x: &str| unsafe {
645            slint_shared_string_append(&mut s, x.as_bytes().as_ptr(), x.len());
646        };
647        append("Hello");
648        append(", ");
649        append("world");
650        append("");
651        append("!");
652        assert_eq!(s.as_str(), "Hello, world!");
653    }
654
655    #[unsafe(no_mangle)]
656    pub unsafe extern "C" fn slint_shared_string_to_lowercase(
657        out: &mut SharedString,
658        ss: &SharedString,
659    ) {
660        *out = SharedString::from(ss.to_lowercase());
661    }
662    #[test]
663    fn test_slint_shared_string_to_lowercase() {
664        let s = SharedString::from("Hello");
665        let mut out = SharedString::default();
666
667        unsafe {
668            slint_shared_string_to_lowercase(&mut out, &s);
669        }
670        assert_eq!(out.as_str(), "hello");
671    }
672
673    #[unsafe(no_mangle)]
674    pub unsafe extern "C" fn slint_shared_string_to_uppercase(
675        out: &mut SharedString,
676        ss: &SharedString,
677    ) {
678        *out = SharedString::from(ss.to_uppercase());
679    }
680    #[test]
681    fn test_slint_shared_string_to_uppercase() {
682        let s = SharedString::from("Hello");
683        let mut out = SharedString::default();
684
685        unsafe {
686            slint_shared_string_to_uppercase(&mut out, &s);
687        }
688        assert_eq!(out.as_str(), "HELLO");
689    }
690}
691
692#[cfg(feature = "serde")]
693#[test]
694fn test_serialize_deserialize_sharedstring() {
695    let v = SharedString::from("data");
696    let serialized = serde_json::to_string(&v).unwrap();
697    let deserialized: SharedString = serde_json::from_str(&serialized).unwrap();
698    assert_eq!(v, deserialized);
699}
700
701#[test]
702fn test_extend_from_chars() {
703    let mut s = SharedString::from("x");
704    s.extend(core::iter::repeat('a').take(4).chain(core::iter::once('🍌')));
705    assert_eq!(s.as_str(), "xaaaa🍌");
706}
707
708#[test]
709fn test_collect_from_chars() {
710    let s: SharedString = core::iter::repeat('a').take(4).chain(core::iter::once('🍌')).collect();
711    assert_eq!(s.as_str(), "aaaa🍌");
712}