Skip to main content

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