gallery/vendor/i-slint-core/string.rs
Greg Pomerantz 2f4768f354 Fix 120Hz scroll jank end-to-end: render-ahead present + full-cache eviction off the UI thread
Three root causes, all fixed and verified on the Pixel 9 Pro:

1. Present phase (backend + renderer): VSYNC_LOCKED_RENDER defers the
   render to the vsync wake; RENDER_AHEAD presents the previous frame at
   the start of render() (phase = wake + ~1.1 ms) and holds the new
   frame one slot. Crash fix: a held wgpu frame must be PRESENTED, never
   merely dropped, before the next acquire (AlreadyAcquired panic at
   launch otherwise); discard_pending_frame presents too.

2. Full-res prefetch churn (src/lib.rs): the 8-slot full cache cycled
   through a ~20-cell viewport forever (137 decodes/20 s) and each
   eviction free()'d a 50 MB Vec on the UI thread (20-52 ms CPU spikes,
   ~7/s = the residual SLOWEV NonMain jank). Fix: 60 s tombstone
   cooldown on evicted paths (idle prefetch skips, urgent tap bypasses)
   + off-thread reaper for the freed blobs (insert: 21 ms -> 4 us).

3. (earlier) Skia final flush + Mailbox present mode (vendor, M3).

Instrumentation (all behind --features perf): OTHR/INVK/INVK_TAG/FLCK
lines for cross-thread closure timing and push-site tagging.

Warm measured swipe, 8580-file grid: cadence median 8.33 ms (exact
120 Hz), p99 15.5 ms, 5/552 gaps >16.7 ms (0.9%, max 28.3 ms, zero
>33 ms); PRES phase median ~994 us, zero late frames. Docs updated
(docs/BUGS/scroll-jank-120hz.md, full scorecard + line reference).

Also: vendored i-slint-core + wgpu-hal (vkshim interposition patch),
vkshim profiling tooling, backend made a non-optional dep, debuggable
flag for simpleperf (REMOVE before shipping).
2026-10-30 02:49:23 -04:00

877 lines
26 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

// Copyright © SixtyFPS GmbH <info@slint.dev>
// SPDX-License-Identifier: GPL-3.0-only OR LicenseRef-Slint-Royalty-free-2.0 OR LicenseRef-Slint-Software-3.0
// cSpell:ignore xaaaa
//! module for the SharedString and related things
#![allow(unsafe_code)]
#![warn(missing_docs)]
use crate::SharedVector;
use alloc::string::String;
use core::fmt::{Debug, Display, Write};
use core::ops::Deref;
#[cfg(not(feature = "std"))]
#[allow(unused)]
use num_traits::Float;
/// This macro is the same as [`std::format!`], but it returns a [`SharedString`] instead.
///
/// ### Example
/// ```rust
/// let s : slint::SharedString = slint::format!("Hello {}", "world");
/// assert_eq!(s, slint::SharedString::from("Hello world"));
/// ```
#[macro_export]
macro_rules! format {
($($arg:tt)*) => {{
$crate::string::format(core::format_args!($($arg)*))
}}
}
/// A string type used by the Slint run-time.
///
/// SharedString uses implicit data sharing to make it efficient to pass around copies. When
/// cloning, a reference to the data is cloned, not the data itself. The data itself is only copied
/// when modifying it, for example using [push_str](SharedString::push_str). This is also called copy-on-write.
///
/// Under the hood the string data is UTF-8 encoded and it is always terminated with a null character.
///
/// `SharedString` implements [`Deref<Target=str>`] so it can be easily passed to any function taking a `&str`.
/// It also implement `From` such that it an easily be converted to and from the typical rust String type with `.into()`
#[derive(Clone, Default)]
#[repr(C)]
pub struct SharedString {
// Invariant: valid utf-8, `\0` terminated
inner: SharedVector<u8>,
}
impl SharedString {
/// Creates a new empty string
///
/// Same as `SharedString::default()`
pub fn new() -> Self {
Self::default()
}
fn as_ptr(&self) -> *const u8 {
self.inner.as_ptr()
}
/// Size of the string, in bytes. This excludes the terminating null character.
pub fn len(&self) -> usize {
self.inner.len().saturating_sub(1)
}
/// Return true if the String is empty
pub fn is_empty(&self) -> bool {
self.len() == 0
}
/// Return a slice to the string
pub fn as_str(&self) -> &str {
// Safety: self.as_ptr is a pointer from the inner which has utf-8
unsafe {
core::str::from_utf8_unchecked(core::slice::from_raw_parts(self.as_ptr(), self.len()))
}
}
/// Replace in this string characters equal to `from` with the character `to` `count` times
pub(crate) fn replace_characters(&mut self, from: char, to: char, count: usize) {
let mut from_buffer = [0u8; 4];
let mut to_buffer = [0u8; 4];
self.inner.replace_range(
from.encode_utf8(&mut from_buffer).as_bytes(),
to.encode_utf8(&mut to_buffer).as_bytes(),
count,
);
}
/// Append a string to this string
///
/// ```
/// # use i_slint_core::SharedString;
/// let mut hello = SharedString::from("Hello");
/// hello.push_str(", ");
/// hello.push_str("World");
/// hello.push_str("!");
/// assert_eq!(hello, "Hello, World!");
/// ```
pub fn push_str(&mut self, x: &str) {
let mut iter = x.as_bytes().iter().copied();
if self.inner.is_empty() {
self.inner.extend(iter.chain(core::iter::once(0)));
} else if let Some(first) = iter.next() {
// We skip the `first` from `iter` because we will write it at the
// location of the previous `\0`, after extend did the re-alloc of the
// right size
let prev_len = self.len();
self.inner.extend(iter.chain(core::iter::once(0)));
self.inner.make_mut_slice()[prev_len] = first;
}
}
}
impl Deref for SharedString {
type Target = str;
fn deref(&self) -> &Self::Target {
self.as_str()
}
}
impl From<&str> for SharedString {
fn from(value: &str) -> Self {
SharedString {
inner: SharedVector::from_iter(
value.as_bytes().iter().cloned().chain(core::iter::once(0)),
),
}
}
}
impl Debug for SharedString {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
Debug::fmt(self.as_str(), f)
}
}
impl Display for SharedString {
fn fmt(&self, f: &mut core::fmt::Formatter<'_>) -> core::fmt::Result {
Display::fmt(self.as_str(), f)
}
}
impl AsRef<str> for SharedString {
#[inline]
fn as_ref(&self) -> &str {
self.as_str()
}
}
#[cfg(feature = "serde")]
impl serde::Serialize for SharedString {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: serde::Serializer,
{
let string = self.as_str();
serializer.serialize_str(string)
}
}
#[cfg(feature = "serde")]
impl<'de> serde::Deserialize<'de> for SharedString {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
struct SharedStringVisitor;
impl<'de> serde::de::Visitor<'de> for SharedStringVisitor {
type Value = SharedString;
fn expecting(&self, formatter: &mut std::fmt::Formatter) -> std::fmt::Result {
formatter.write_str("a borrowed or owned string")
}
fn visit_str<E>(self, v: &str) -> Result<Self::Value, E>
where
E: serde::de::Error,
{
Ok(SharedString::from(v))
}
}
deserializer.deserialize_str(SharedStringVisitor)
}
}
#[cfg(feature = "std")]
impl AsRef<std::ffi::CStr> for SharedString {
#[inline]
fn as_ref(&self) -> &std::ffi::CStr {
if self.inner.is_empty() {
return Default::default();
}
// Safety: we ensure that there is always a terminated \0
debug_assert_eq!(self.inner.as_slice()[self.inner.len() - 1], 0);
unsafe { std::ffi::CStr::from_bytes_with_nul_unchecked(self.inner.as_slice()) }
}
}
#[cfg(feature = "std")]
impl AsRef<std::path::Path> for SharedString {
#[inline]
fn as_ref(&self) -> &std::path::Path {
self.as_str().as_ref()
}
}
#[cfg(feature = "std")]
impl AsRef<std::ffi::OsStr> for SharedString {
#[inline]
fn as_ref(&self) -> &std::ffi::OsStr {
self.as_str().as_ref()
}
}
impl AsRef<[u8]> for SharedString {
#[inline]
fn as_ref(&self) -> &[u8] {
self.as_str().as_bytes()
}
}
impl<T> PartialEq<T> for SharedString
where
T: ?Sized + AsRef<str>,
{
fn eq(&self, other: &T) -> bool {
self.as_str() == other.as_ref()
}
}
impl Eq for SharedString {}
impl<T> PartialOrd<T> for SharedString
where
T: ?Sized + AsRef<str>,
{
fn partial_cmp(&self, other: &T) -> Option<core::cmp::Ordering> {
PartialOrd::partial_cmp(self.as_str(), other.as_ref())
}
}
impl Ord for SharedString {
fn cmp(&self, other: &Self) -> core::cmp::Ordering {
Ord::cmp(self.as_str(), other.as_str())
}
}
impl From<String> for SharedString {
fn from(s: String) -> Self {
s.as_str().into()
}
}
impl From<&String> for SharedString {
fn from(s: &String) -> Self {
s.as_str().into()
}
}
impl From<char> for SharedString {
fn from(c: char) -> Self {
SharedString::from(c.encode_utf8(&mut [0; 6]) as &str)
}
}
impl From<SharedString> for String {
fn from(s: SharedString) -> String {
s.as_str().into()
}
}
impl From<&SharedString> for String {
fn from(s: &SharedString) -> String {
s.as_str().into()
}
}
impl core::ops::AddAssign<&str> for SharedString {
fn add_assign(&mut self, other: &str) {
self.push_str(other);
}
}
impl core::ops::Add<&str> for SharedString {
type Output = SharedString;
fn add(mut self, other: &str) -> SharedString {
self.push_str(other);
self
}
}
impl core::hash::Hash for SharedString {
fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
self.as_str().hash(state)
}
}
impl Write for SharedString {
fn write_str(&mut self, s: &str) -> core::fmt::Result {
self.push_str(s);
Ok(())
}
}
impl core::borrow::Borrow<str> for SharedString {
fn borrow(&self) -> &str {
self.as_str()
}
}
impl Extend<char> for SharedString {
fn extend<X: IntoIterator<Item = char>>(&mut self, iter: X) {
let iter = iter.into_iter();
self.inner.reserve(iter.size_hint().0);
let mut buf = [0; 4];
for ch in iter {
let utf8 = ch.encode_utf8(&mut buf);
self.push_str(utf8);
}
}
}
impl FromIterator<char> for SharedString {
fn from_iter<T: IntoIterator<Item = char>>(iter: T) -> Self {
let mut str = Self::new();
str.extend(iter);
str
}
}
/// Same as [`std::fmt::format()`], but return a [`SharedString`] instead
pub fn format(args: core::fmt::Arguments<'_>) -> SharedString {
// unfortunately, the estimated_capacity is unstable
//let capacity = args.estimated_capacity();
let mut output = SharedString::default();
output.write_fmt(args).unwrap();
output
}
/// A trait for converting a value to a [`SharedString`].
///
/// This trait is automatically implemented for any type which implements the [`Display`] trait as long as the trait is in scope.
/// As such, `ToSharedString` shouldn’t be implemented directly: [`Display`] should be implemented instead, and you get the `ToSharedString` implementation for free.
pub trait ToSharedString {
/// Converts the given value to a [`SharedString`].
fn to_shared_string(&self) -> SharedString;
}
impl<T> ToSharedString for T
where
T: Display + ?Sized,
{
fn to_shared_string(&self) -> SharedString {
format!("{}", self)
}
}
/// Convert a f64 to a SharedString but unlocalized with "." as decimal separator
#[inline]
pub fn shared_string_from_number_unlocalized(n: f64) -> SharedString {
crate::format!("{}", i_slint_common::FormattedNumber(n))
}
/// Convert a f64 to a SharedString
pub fn shared_string_from_number(n: f64) -> SharedString {
crate::context::GLOBAL_CONTEXT.with(|ctx| {
let mut result = shared_string_from_number_unlocalized(n);
if let Some(ctx) = ctx.get() {
let pinned = ctx.0.as_ref().project_ref();
let decimal_separator = pinned.locale_decimal_separator.get();
if decimal_separator != i_slint_common::DEFAULT_DECIMAL_SEPARATOR {
result.replace_characters('.', decimal_separator, 1);
}
}
result
})
}
/// Convert a f64 to a SharedString with a fixed number of digits after the decimal point
pub fn shared_string_from_number_fixed(n: f64, digits: usize) -> SharedString {
crate::context::GLOBAL_CONTEXT.with(|ctx| {
let mut result = crate::format!("{number:.digits$}", number = n, digits = digits);
if let Some(ctx) = ctx.get() {
let pinned = ctx.0.as_ref().project_ref();
let decimal_separator = pinned.locale_decimal_separator.get();
if decimal_separator != i_slint_common::DEFAULT_DECIMAL_SEPARATOR {
result.replace_characters('.', decimal_separator, 1);
}
}
result
})
}
/// Convert a f64 to a SharedString following a similar logic as JavaScript's Number.toPrecision()
pub fn shared_string_from_number_precision(n: f64, precision: usize) -> SharedString {
let exponent = f64::log10(n.abs()).floor() as isize;
if precision == 0 {
shared_string_from_number(n)
} else if exponent < -6 || (exponent >= 0 && exponent as usize >= precision) {
crate::format!(
"{number:.digits$e}",
number = n,
digits = precision.saturating_add_signed(-1)
)
} else {
shared_string_from_number_fixed(n, precision.saturating_add_signed(-(exponent + 1)))
}
}
/// Replaces all matches of `from` with `to` in `s` and returns the result as a new
/// `SharedString`.
pub fn shared_string_replace_all(s: &SharedString, from: &str, to: &str) -> SharedString {
let mut matches = s.match_indices(from);
let Some((first, _)) = matches.next() else {
return s.clone();
};
let mut result = SharedString::from(&s[..first]);
result.push_str(to);
let mut last_end = first + from.len();
for (start, _) in matches {
result.push_str(&s[last_end..start]);
result.push_str(to);
last_end = start + from.len();
}
result.push_str(&s[last_end..]);
result
}
/// Convert a string to a float
pub fn string_to_float(string: &str) -> Option<f32> {
crate::context::GLOBAL_CONTEXT.with(|ctx| {
let sep = ctx.get().map(|ctx| ctx.locale_decimal_separator()).unwrap_or('.');
if sep == '.' {
string.parse::<f32>().ok()
} else {
if string.contains('.') {
return None;
}
// Normalize locale separator to '.' because f64::parse only accepts '.'
string.replace(sep, ".").parse::<f32>().ok()
}
})
}
#[test]
fn test_string_to_float() {
const TEST: &[(&str, Option<f32>)] = &[
("-", None),
(".", None),
("-.", None),
("-.5", Some(-0.5)),
("--", None),
("..", None),
("5.5.", None),
("231.435", Some(231.435)),
("-0.007", Some(-0.007)),
("10e6", Some(10e6)),
];
for (test_string, result) in TEST {
assert_eq!(string_to_float(test_string), *result);
}
}
#[test]
fn simple_test() {
use std::string::ToString;
let x = SharedString::from("hello world!");
assert_eq!(x, "hello world!");
assert_ne!(x, "hello world?");
assert_eq!(x, x.clone());
assert_eq!("hello world!", x.as_str());
let string = String::from("hello world!");
assert_eq!(x, string);
assert_eq!(x.to_string(), string);
let def = SharedString::default();
assert_eq!(def, SharedString::default());
assert_eq!(def, SharedString::new());
assert_ne!(def, x);
assert_eq!(
(&x as &dyn AsRef<std::ffi::CStr>).as_ref(),
&*std::ffi::CString::new("hello world!").unwrap()
);
assert_eq!(SharedString::from('h'), "h");
assert_eq!(SharedString::from('😎'), "😎");
}
#[test]
fn threading() {
let shared_cst = SharedString::from("Hello there!");
let shared_mtx = std::sync::Arc::new(std::sync::Mutex::new(SharedString::from("Shared:")));
let mut handles = std::vec::Vec::new();
for _ in 0..20 {
let cst = shared_cst.clone();
let mtx = shared_mtx.clone();
handles.push(std::thread::spawn(move || {
assert_eq!(cst, "Hello there!");
let mut cst2 = cst.clone();
cst2.push_str(" ... or not?");
assert_eq!(cst2, "Hello there! ... or not?");
assert_eq!(cst.clone(), "Hello there!");
let shared = {
let mut lock = mtx.lock().unwrap();
assert!(lock.starts_with("Shared:"));
lock.push_str("!");
lock.clone()
};
assert!(shared.clone().starts_with("Shared:"));
}));
}
for j in handles {
j.join().unwrap();
}
assert_eq!(shared_cst.clone(), "Hello there!");
assert_eq!(shared_mtx.lock().unwrap().as_str(), "Shared:!!!!!!!!!!!!!!!!!!!!");
// 20x"!"
}
#[test]
fn to_shared_string() {
let i = 5.1;
let five = SharedString::from("5.1");
assert_eq!(five, i.to_shared_string());
}
#[test]
fn test_replace_characters() {
let mut value = SharedString::from("5.1");
value.replace_characters('.', ',', 1);
assert_eq!(value, "5,1".to_shared_string());
}
#[cfg(feature = "ffi")]
pub(crate) mod ffi {
use super::*;
/// for cbindgen.
#[allow(non_camel_case_types)]
type c_char = u8;
#[unsafe(no_mangle)]
/// Returns a nul-terminated pointer for this string.
/// The returned value is owned by the string, and should not be used after any
/// mutable function have been called on the string, and must not be freed.
pub extern "C" fn slint_shared_string_bytes(ss: &SharedString) -> *const c_char {
if ss.is_empty() { c"".as_ptr().cast() } else { ss.as_ptr() }
}
#[unsafe(no_mangle)]
/// Destroy the shared string
pub unsafe extern "C" fn slint_shared_string_drop(ss: *const SharedString) {
unsafe {
core::ptr::read(ss);
}
}
#[unsafe(no_mangle)]
/// Increment the reference count of the string.
/// The resulting structure must be passed to slint_shared_string_drop
pub unsafe extern "C" fn slint_shared_string_clone(out: *mut SharedString, ss: &SharedString) {
unsafe { core::ptr::write(out, ss.clone()) }
}
#[unsafe(no_mangle)]
/// Safety: bytes must be a valid utf-8 string of size len without null inside.
/// The resulting structure must be passed to slint_shared_string_drop
pub unsafe extern "C" fn slint_shared_string_from_bytes(
out: *mut SharedString,
bytes: *const c_char,
len: usize,
) {
unsafe {
let str = core::str::from_utf8(core::slice::from_raw_parts(bytes, len)).unwrap();
core::ptr::write(out, SharedString::from(str));
}
}
/// Create a string from a number but unlocalized.
/// The resulting structure must be passed to slint_shared_string_drop
#[unsafe(no_mangle)]
pub unsafe extern "C" fn slint_shared_string_from_number_unlocalized(
out: *mut SharedString,
n: f64,
) {
let str = shared_string_from_number_unlocalized(n);
unsafe { core::ptr::write(out, str) };
}
/// Create a string from a number.
/// The resulting structure must be passed to slint_shared_string_drop
#[unsafe(no_mangle)]
pub unsafe extern "C" fn slint_shared_string_from_number(out: *mut SharedString, n: f64) {
let str = shared_string_from_number(n);
unsafe { core::ptr::write(out, str) };
}
#[test]
fn test_slint_shared_string_from_number() {
unsafe {
let mut s = core::mem::MaybeUninit::uninit();
slint_shared_string_from_number(s.as_mut_ptr(), 45.);
assert_eq!(s.assume_init(), "45");
let mut s = core::mem::MaybeUninit::uninit();
slint_shared_string_from_number(s.as_mut_ptr(), 45.12);
assert_eq!(s.assume_init(), "45.12");
let mut s = core::mem::MaybeUninit::uninit();
slint_shared_string_from_number(s.as_mut_ptr(), -1325466.);
assert_eq!(s.assume_init(), "-1325466");
let mut s = core::mem::MaybeUninit::uninit();
slint_shared_string_from_number(s.as_mut_ptr(), 0.);
assert_eq!(s.assume_init(), "0");
let mut s = core::mem::MaybeUninit::uninit();
slint_shared_string_from_number(
s.as_mut_ptr(),
((1235.82756f32 * 1000f32).round() / 1000f32) as _,
);
assert_eq!(s.assume_init(), "1235.828");
}
}
#[unsafe(no_mangle)]
pub extern "C" fn slint_shared_string_from_number_fixed(
out: &mut SharedString,
n: f64,
digits: usize,
) {
*out = shared_string_from_number_fixed(n, digits);
}
#[test]
fn test_slint_shared_string_from_number_fixed() {
let mut s = SharedString::default();
let num = 12345.6789;
slint_shared_string_from_number_fixed(&mut s, num, 0);
assert_eq!(s.as_str(), "12346");
slint_shared_string_from_number_fixed(&mut s, num, 1);
assert_eq!(s.as_str(), "12345.7");
slint_shared_string_from_number_fixed(&mut s, num, 6);
assert_eq!(s.as_str(), "12345.678900");
let num = -12345.6789;
slint_shared_string_from_number_fixed(&mut s, num, 0);
assert_eq!(s.as_str(), "-12346");
slint_shared_string_from_number_fixed(&mut s, num, 1);
assert_eq!(s.as_str(), "-12345.7");
slint_shared_string_from_number_fixed(&mut s, num, 6);
assert_eq!(s.as_str(), "-12345.678900");
slint_shared_string_from_number_fixed(&mut s, 1.23E+20_f64, 2);
assert_eq!(s.as_str(), "123000000000000000000.00");
slint_shared_string_from_number_fixed(&mut s, 1.23E-10_f64, 2);
assert_eq!(s.as_str(), "0.00");
slint_shared_string_from_number_fixed(&mut s, 2.34, 1);
assert_eq!(s.as_str(), "2.3");
slint_shared_string_from_number_fixed(&mut s, 2.35, 1);
assert_eq!(s.as_str(), "2.4");
slint_shared_string_from_number_fixed(&mut s, 2.55, 1);
assert_eq!(s.as_str(), "2.5");
}
#[unsafe(no_mangle)]
pub extern "C" fn slint_shared_string_from_number_precision(
out: &mut SharedString,
n: f64,
precision: usize,
) {
*out = shared_string_from_number_precision(n, precision);
}
#[test]
fn test_slint_shared_string_from_number_precision() {
let mut s = SharedString::default();
let num = 5.123456;
slint_shared_string_from_number_precision(&mut s, num, 0);
assert_eq!(s.as_str(), "5.123456");
slint_shared_string_from_number_precision(&mut s, num, 5);
assert_eq!(s.as_str(), "5.1235");
slint_shared_string_from_number_precision(&mut s, num, 2);
assert_eq!(s.as_str(), "5.1");
slint_shared_string_from_number_precision(&mut s, num, 1);
assert_eq!(s.as_str(), "5");
let num = 0.000123;
slint_shared_string_from_number_precision(&mut s, num, 0);
assert_eq!(s.as_str(), "0.000123");
slint_shared_string_from_number_precision(&mut s, num, 5);
assert_eq!(s.as_str(), "0.00012300");
slint_shared_string_from_number_precision(&mut s, num, 2);
assert_eq!(s.as_str(), "0.00012");
slint_shared_string_from_number_precision(&mut s, num, 1);
assert_eq!(s.as_str(), "0.0001");
let num = 1234.5;
slint_shared_string_from_number_precision(&mut s, num, 1);
assert_eq!(s.as_str(), "1e3");
slint_shared_string_from_number_precision(&mut s, num, 2);
assert_eq!(s.as_str(), "1.2e3");
slint_shared_string_from_number_precision(&mut s, num, 6);
assert_eq!(s.as_str(), "1234.50");
let num = -1234.5;
slint_shared_string_from_number_precision(&mut s, num, 1);
assert_eq!(s.as_str(), "-1e3");
slint_shared_string_from_number_precision(&mut s, num, 2);
assert_eq!(s.as_str(), "-1.2e3");
slint_shared_string_from_number_precision(&mut s, num, 6);
assert_eq!(s.as_str(), "-1234.50");
let num = 0.00000012345;
slint_shared_string_from_number_precision(&mut s, num, 1);
assert_eq!(s.as_str(), "1e-7");
slint_shared_string_from_number_precision(&mut s, num, 10);
assert_eq!(s.as_str(), "1.234500000e-7");
}
/// Append some bytes to an existing shared string
///
/// bytes must be a valid utf8 array of size `len`, without null bytes inside
#[unsafe(no_mangle)]
pub unsafe extern "C" fn slint_shared_string_append(
self_: &mut SharedString,
bytes: *const c_char,
len: usize,
) {
let str = core::str::from_utf8(unsafe { core::slice::from_raw_parts(bytes, len) }).unwrap();
self_.push_str(str);
}
#[test]
fn test_slint_shared_string_append() {
let mut s = SharedString::default();
let mut append = |x: &str| unsafe {
slint_shared_string_append(&mut s, x.as_bytes().as_ptr(), x.len());
};
append("Hello");
append(", ");
append("world");
append("");
append("!");
assert_eq!(s.as_str(), "Hello, world!");
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn slint_shared_string_to_lowercase(
out: &mut SharedString,
ss: &SharedString,
) {
*out = SharedString::from(ss.to_lowercase());
}
#[test]
fn test_slint_shared_string_to_lowercase() {
let s = SharedString::from("Hello");
let mut out = SharedString::default();
unsafe {
slint_shared_string_to_lowercase(&mut out, &s);
}
assert_eq!(out.as_str(), "hello");
}
#[unsafe(no_mangle)]
pub unsafe extern "C" fn slint_shared_string_to_uppercase(
out: &mut SharedString,
ss: &SharedString,
) {
*out = SharedString::from(ss.to_uppercase());
}
#[test]
fn test_slint_shared_string_to_uppercase() {
let s = SharedString::from("Hello");
let mut out = SharedString::default();
unsafe {
slint_shared_string_to_uppercase(&mut out, &s);
}
assert_eq!(out.as_str(), "HELLO");
}
#[unsafe(no_mangle)]
pub extern "C" fn slint_shared_string_replace_all(
out: &mut SharedString,
ss: &SharedString,
from: crate::slice::Slice<u8>,
to: crate::slice::Slice<u8>,
) {
// Safety: the caller must pass valid utf-8 slices.
let from = unsafe { core::str::from_utf8_unchecked(from.as_slice()) };
let to = unsafe { core::str::from_utf8_unchecked(to.as_slice()) };
*out = super::shared_string_replace_all(ss, from, to);
}
#[test]
fn test_slint_shared_string_replace_all() {
let s = SharedString::from("Hello");
let from = SharedString::from("l");
let to = SharedString::from("L");
let mut out = SharedString::default();
slint_shared_string_replace_all(
&mut out,
&s,
crate::slice::Slice::from_slice(from.as_bytes()),
crate::slice::Slice::from_slice(to.as_bytes()),
);
assert_eq!(out.as_str(), "HeLLo");
}
}
#[cfg(feature = "serde")]
#[test]
fn test_serialize_deserialize_sharedstring() {
let v = SharedString::from("data");
let serialized = serde_json::to_string(&v).unwrap();
let deserialized: SharedString = serde_json::from_str(&serialized).unwrap();
assert_eq!(v, deserialized);
}
#[cfg(feature = "serde")]
#[test]
fn test_serialize_deserialize_sharedstring_from_reader() {
let v = SharedString::from("data");
let serialized = serde_json::to_string(&v).unwrap();
let deserialized: SharedString = serde_json::from_reader(serialized.as_bytes()).unwrap();
assert_eq!(v, deserialized);
}
#[test]
fn test_extend_from_chars() {
let mut s = SharedString::from("x");
s.extend(core::iter::repeat_n('a', 4).chain(core::iter::once('🍌')));
assert_eq!(s.as_str(), "xaaaa🍌");
}
#[test]
fn test_collect_from_chars() {
let s: SharedString = core::iter::repeat_n('a', 4).chain(core::iter::once('🍌')).collect();
assert_eq!(s.as_str(), "aaaa🍌");
}