Refactored `Txt` to represent text as an `Arc<str>` too. Fixed `Txt` that contains a null character.

This commit is contained in:
Samuel Guerra 2023-05-27 22:58:59 -03:00
parent fb485f6eba
commit 418c2e6301
11 changed files with 251 additions and 59 deletions

View File

@ -17,8 +17,8 @@ fn main() {
fn app_main() {
App::default().run_window(async {
let shortcut_text = var(Txt::empty());
let keypress_text = var(Txt::empty());
let shortcut_text = var(Txt::from_str(""));
let keypress_text = var(Txt::from_str(""));
let shortcut_error = var(false);
// examples_util::trace_var!(ctx, ?shortcut_text);

View File

@ -269,7 +269,7 @@ fn defaults() -> impl UiNode {
)
.map(|f| match f.done() {
Some(f) => f.best().family_name().to_text(),
None => Txt::empty(),
None => Txt::from_str(""),
});
Stack! {

View File

@ -959,7 +959,7 @@ pub trait CommandInfoExt {
static COMMAND_INFO_ID: StaticCommandMetaVarId<Txt> = StaticCommandMetaVarId::new_unique();
impl CommandInfoExt for Command {
fn info(self) -> CommandMetaVar<Txt> {
self.with_meta(|m| m.get_var_or_insert(&COMMAND_INFO_ID, Txt::empty))
self.with_meta(|m| m.get_var_or_insert(&COMMAND_INFO_ID, Txt::default))
}
fn init_info(self, info: impl Into<Txt>) -> Self {

View File

@ -327,7 +327,7 @@ impl ImagesService {
proxies: vec![],
loading: vec![],
decoding: vec![],
download_accept: Txt::empty(),
download_accept: Txt::from_static(""),
cache: IdMap::new(),
not_cached: vec![],
render: render::ImagesRender::default(),

View File

@ -299,7 +299,7 @@ mod print_fmt {
Fmt {
depth: 0,
output,
property_group: Txt::empty(),
property_group: Txt::from_str(""),
}
}
@ -442,7 +442,7 @@ mod print_fmt {
pub fn close_widget(&mut self, name: &str) {
self.depth -= 1;
self.property_group = Txt::empty();
self.property_group = Txt::from_str("");
self.write_tabs();
self.write("} ".bold());
self.write_comment_after(format_args!("{name}!"));

View File

@ -71,7 +71,7 @@ impl L10nService {
Ok(lang) => {
// and it is named correctly.
set.get_exact_or_insert(lang, Default::default)
.insert(Txt::empty(), dir.as_ref().unwrap().join(name_and_ext));
.insert(Txt::from_str(""), dir.as_ref().unwrap().join(name_and_ext));
}
Err(e) => {
errors.push(Arc::new(e));
@ -580,7 +580,7 @@ fn format_message(bundle: &ArcFluentBundle, id: &str, attribute: &str, args: &[(
txt.to_text()
} else {
tracing::error!("found `{:?}/{id}`, but not value", &bundle.locales[0]);
Txt::empty()
Txt::from_str("")
}
} else {
match msg.get_attribute(attribute) {
@ -597,7 +597,7 @@ fn format_message(bundle: &ArcFluentBundle, id: &str, attribute: &str, args: &[(
}
None => {
tracing::error!("found `{:?}/{id}`, but not attribute `{attribute}`", &bundle.locales[0]);
Txt::empty()
Txt::from_str("")
}
}
}

View File

@ -316,8 +316,8 @@ impl TextTransformFn {
pub fn transform(&self, text: Txt) -> Txt {
match self {
TextTransformFn::None => text,
TextTransformFn::Uppercase => Txt::owned(text.to_uppercase()),
TextTransformFn::Lowercase => Txt::owned(text.to_lowercase()),
TextTransformFn::Uppercase => Txt::from_string(text.to_uppercase()),
TextTransformFn::Lowercase => Txt::from_string(text.to_lowercase()),
TextTransformFn::Custom(fn_) => fn_(text),
}
}
@ -862,7 +862,8 @@ fn str_to_inline(s: &str) -> [u8; INLINE_MAX] {
enum TxtData {
Static(&'static str),
Inline([u8; INLINE_MAX]),
Owned(String),
String(String),
Arc(Arc<str>),
}
impl fmt::Debug for TxtData {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
@ -870,7 +871,8 @@ impl fmt::Debug for TxtData {
match self {
Self::Static(s) => write!(f, "Static({s:?})"),
Self::Inline(d) => write!(f, "Inline({:?})", inline_to_str(d)),
Self::Owned(s) => write!(f, "Owned({s:?})"),
Self::String(s) => write!(f, "String({s:?})"),
Self::Arc(s) => write!(f, "Arc({s:?})"),
}
} else {
write!(f, "{:?}", self.deref())
@ -900,30 +902,99 @@ impl Deref for TxtData {
match self {
TxtData::Static(s) => s,
TxtData::Inline(d) => inline_to_str(d),
TxtData::Owned(s) => s,
TxtData::String(s) => s,
TxtData::Arc(s) => s,
}
}
}
/// Text string type, can be owned, static or inlined.
/// Identifies how a [`Txt`] is currently storing the string data.
///
/// Note that this type dereferences to [`str`] so you can use all methods
/// of that type also. For mutation you can call [`to_mut`]
/// to access all mutating methods of [`String`]. The mutations that can be
/// implemented using only a borrowed `str` are provided as methods in this type.
/// Use [`Txt::repr`] to retrieve.
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TxtRepr {
/// Text data is stored as a `&'static str`.
Static,
/// Text data is a small string stored as a null terminated `[u8; {size_of::<usize>() * 3}]`.
Inline,
/// Text data is stored as a `String`.
String,
/// Text data is stored as an `Arc<str>`.
Arc,
}
/// Text string type, can be one of multiple internal representations, mostly optimized for sharing and one for editing.
///
/// [`to_mut`]: Txt::to_mut
#[derive(Clone, dm::Display, PartialEq, Eq, Hash)]
/// This type dereferences to [`str`] so you can use all methods of that type.
///
/// For editing some mutable methods are provided, you can also call [`Txt::to_mut`]
/// to access all mutating methods of [`String`]. After editing you can call [`Txt::end_mut`] to convert
/// back to an inner representation optimized for sharing.
///
/// See [`Txt::repr`] for more details about the inner representations.
#[derive(dm::Display, PartialEq, Eq, Hash)]
pub struct Txt(TxtData);
/// Clones the text.
///
/// If the inner representation is [`TxtRepr::String`] the returned value is in a representation optimized
/// for sharing, either a static empty, an inlined short or an `Arc<str>` long string.
impl Clone for Txt {
fn clone(&self) -> Self {
Self(match &self.0 {
TxtData::Static(s) => TxtData::Static(s),
TxtData::Inline(d) => TxtData::Inline(*d),
TxtData::String(s) => return Self::from_str(s),
TxtData::Arc(s) => TxtData::Arc(Arc::clone(s)),
})
}
}
impl Txt {
/// New text that is a `&'static str`.
pub const fn from_static(s: &'static str) -> Txt {
Txt(TxtData::Static(s))
}
/// New text that is an owned [`String`].
pub const fn owned(s: String) -> Txt {
Txt(TxtData::Owned(s))
/// New text from a [`String`] optimized for editing.
///
/// If you don't plan to edit the text after this call consider using [`from_str`] instead.
///
/// [`from_string`]: Self::from_string
pub const fn from_string(s: String) -> Txt {
Txt(TxtData::String(s))
}
/// New cloned from `s`.
///
/// The text will be internally optimized for sharing, if you plan to edit the text after this call
/// consider using [`from_string`] instead.
///
/// [`from_string`]: Self::from_string
#[allow(clippy::should_implement_trait)] // have implemented trait, this one is infallible.
pub fn from_str(s: &str) -> Txt {
if s.is_empty() {
Self::from_static("")
} else if s.len() <= INLINE_MAX && !s.contains('\0') {
Self(TxtData::Inline(str_to_inline(s)))
} else {
Self(TxtData::Arc(Arc::from(s)))
}
}
/// New from a shared arc str.
///
/// Note that the text can outlive the `Arc`, by cloning the string data when modified or
/// to use a more optimal representation, you cannot use the reference count of `s` to track
/// the lifetime of the text.
///
/// [`from_string`]: Self::from_string
pub fn from_arc(s: Arc<str>) -> Txt {
if s.is_empty() {
Self::from_static("")
} else if s.len() <= INLINE_MAX && !s.contains('\0') {
Self(TxtData::Inline(str_to_inline(&s)))
} else {
Self(TxtData::Arc(s))
}
}
/// New text that is an inlined `char`.
@ -934,43 +1005,75 @@ impl Txt {
let mut buf = [0u8; 4];
let s = c.encode_utf8(&mut buf);
if s.contains('\0') {
return Txt(TxtData::Arc(Arc::from(&*s)));
}
Txt(TxtData::Inline(str_to_inline(s)))
}
/// New empty text.
pub const fn empty() -> Txt {
Self::from_static("")
/// New text from [`format_args!`], avoids allocation if the text is static (no args) or can fit the inlined representation.
pub fn from_fmt(args: std::fmt::Arguments) -> Txt {
if let Some(s) = args.as_str() {
Txt::from_static(s)
} else {
let mut r = Txt(TxtData::Inline([b'\0'; INLINE_MAX]));
std::fmt::write(&mut r, args).unwrap();
r
}
}
/// If the text is an owned [`String`].
pub const fn is_owned(&self) -> bool {
matches!(&self.0, TxtData::Owned(_))
/// Identifies how the text is currently stored.
pub const fn repr(&self) -> TxtRepr {
match &self.0 {
TxtData::Static(_) => TxtRepr::Static,
TxtData::Inline(_) => TxtRepr::Inline,
TxtData::String(_) => TxtRepr::String,
TxtData::Arc(_) => TxtRepr::Arc,
}
}
/// Acquires a mutable reference to a [`String`] buffer.
///
/// Turns the text to owned if it was borrowed.
/// Converts the text to an internal representation optimized for editing, you can call [`end_mut`] after
/// editing to re-optimize the text for sharing.
///
/// [`end_mut`]: Self::end_mut
pub fn to_mut(&mut self) -> &mut String {
self.0 = match mem::replace(&mut self.0, TxtData::Static("")) {
TxtData::Owned(s) => TxtData::Owned(s),
TxtData::Static(s) => TxtData::Owned(s.to_owned()),
TxtData::Inline(d) => TxtData::Owned(inline_to_str(&d).to_owned()),
TxtData::String(s) => TxtData::String(s),
TxtData::Static(s) => TxtData::String(s.to_owned()),
TxtData::Inline(d) => TxtData::String(inline_to_str(&d).to_owned()),
TxtData::Arc(s) => TxtData::String((*s).to_owned()),
};
if let TxtData::Owned(s) = &mut self.0 {
if let TxtData::String(s) = &mut self.0 {
s
} else {
unreachable!()
}
}
/// Convert the inner representation of the string to not be [`String`]. After
/// this call the text can be cheaply cloned.
pub fn end_mut(&mut self) {
match mem::replace(&mut self.0, TxtData::Static("")) {
TxtData::String(s) => {
*self = Self::from_str(&s);
}
already => self.0 = already,
}
}
/// Extracts the owned string.
///
/// Turns the text to owned if it was borrowed.
pub fn into_owned(self) -> String {
match self.0 {
TxtData::Owned(s) => s,
TxtData::String(s) => s,
TxtData::Static(s) => s.to_owned(),
TxtData::Inline(d) => inline_to_str(&d).to_owned(),
TxtData::Arc(s) => (*s).to_owned(),
}
}
@ -978,7 +1081,7 @@ impl Txt {
/// replaces `self` with an empty str (`""`).
pub fn clear(&mut self) {
match &mut self.0 {
TxtData::Owned(s) => s.clear(),
TxtData::String(s) => s.clear(),
d => *d = TxtData::Static(""),
}
}
@ -987,11 +1090,11 @@ impl Txt {
///
/// Returns None if this `Txt` is empty.
///
/// This method calls [`String::pop`] if the text is owned, otherwise
/// reborrows a slice of the `str` without the last character.
/// This method only converts to [`TxtRepr::String`] if the
/// internal representation is [`TxtRepr::Arc`], other representations are reborrowed.
pub fn pop(&mut self) -> Option<char> {
match &mut self.0 {
TxtData::Owned(s) => s.pop(),
TxtData::String(s) => s.pop(),
TxtData::Static(s) => {
if let Some((i, c)) = s.char_indices().last() {
*s = &s[..i];
@ -1013,6 +1116,7 @@ impl Txt {
None
}
}
TxtData::Arc(_) => self.to_mut().pop(),
}
}
@ -1021,11 +1125,11 @@ impl Txt {
/// If `new_len` is greater than the text's current length, this has no
/// effect.
///
/// This method calls [`String::truncate`] if the text is owned, otherwise
/// reborrows a slice of the text.
/// This method only converts to [`TxtRepr::String`] if the
/// internal representation is [`TxtRepr::Arc`], other representations are reborrowed.
pub fn truncate(&mut self, new_len: usize) {
match &mut self.0 {
TxtData::Owned(s) => s.truncate(new_len),
TxtData::String(s) => s.truncate(new_len),
TxtData::Static(s) => {
if new_len <= s.len() {
assert!(s.is_char_boundary(new_len));
@ -1043,6 +1147,7 @@ impl Txt {
}
}
}
TxtData::Arc(_) => self.to_mut().truncate(new_len),
}
}
@ -1052,11 +1157,11 @@ impl Txt {
/// the returned `Txt` contains bytes `[at, len)`. `at` must be on the
/// boundary of a UTF-8 code point.
///
/// This method calls [`String::split_off`] if the text is owned, otherwise
/// reborrows slices of the text.
/// This method only converts to [`TxtRepr::String`] if the
/// internal representation is [`TxtRepr::Arc`], other representations are reborrowed.
pub fn split_off(&mut self, at: usize) -> Txt {
match &mut self.0 {
TxtData::Owned(s) => Txt::owned(s.split_off(at)),
TxtData::String(s) => Txt::from_string(s.split_off(at)),
TxtData::Static(s) => {
assert!(s.is_char_boundary(at));
let other = &s[at..];
@ -1083,6 +1188,75 @@ impl Txt {
r
}
TxtData::Arc(_) => Txt::from_string(self.to_mut().split_off(at)),
}
}
/// Push the character to the end of the text.
///
/// This method avoids converting to [`TxtRepr::String`] when the current text
/// plus char can fit inlined.
pub fn push(&mut self, c: char) {
match &mut self.0 {
TxtData::String(s) => s.push(c),
TxtData::Inline(inlined) => {
if let Some(len) = inlined.iter().position(|&c| c != b'\0') {
if len + 4 <= INLINE_MAX && c != '\0' {
let mut buf = [0u8; 4];
let s = c.encode_utf8(&mut buf);
inlined[len..len + 4].copy_from_slice(s.as_bytes());
return;
}
}
self.to_mut().push(c)
}
_ => {
let len = self.len();
if len + 4 <= INLINE_MAX && c != '\0' {
let mut inlined = str_to_inline(self.as_str());
let mut buf = [0u8; 4];
let s = c.encode_utf8(&mut buf);
inlined[len..len + 4].copy_from_slice(s.as_bytes());
self.0 = TxtData::Inline(inlined);
} else {
self.to_mut().push(c)
}
}
}
}
/// Push the string to the end of the text.
///
/// This method avoids converting to [`TxtRepr::String`] when the current text
/// plus char can fit inlined.
pub fn push_str(&mut self, s: &str) {
if s.is_empty() {
return;
}
match &mut self.0 {
TxtData::String(str) => str.push_str(s),
TxtData::Inline(inlined) => {
if let Some(len) = inlined.iter().position(|&c| c != b'\0') {
if len + s.len() <= INLINE_MAX && !s.contains('\0') {
inlined[len..len + s.len()].copy_from_slice(s.as_bytes());
return;
}
}
self.to_mut().push_str(s)
}
_ => {
let len = self.len();
if len + s.len() <= INLINE_MAX && !s.contains('\0') {
let mut inlined = str_to_inline(self.as_str());
inlined[len..len + s.len()].copy_from_slice(s.as_bytes());
self.0 = TxtData::Inline(inlined);
} else {
self.to_mut().push_str(s)
}
}
}
}
@ -1107,7 +1281,14 @@ impl fmt::Debug for Txt {
impl Default for Txt {
/// Empty.
fn default() -> Self {
Self::empty()
Self::from_static("")
}
}
impl std::str::FromStr for Txt {
type Err = ();
fn from_str(s: &str) -> Result<Self, Self::Err> {
Ok(Txt::from_str(s))
}
}
impl_from_and_into_var! {
@ -1115,12 +1296,12 @@ impl_from_and_into_var! {
Txt(TxtData::Static(s))
}
fn from(s: String) -> Txt {
Txt(TxtData::Owned(s))
Txt(TxtData::String(s))
}
fn from(s: Cow<'static, str>) -> Txt {
match s {
Cow::Borrowed(s) => Txt(TxtData::Static(s)),
Cow::Owned(s) => Txt(TxtData::Owned(s))
Cow::Owned(s) => Txt(TxtData::String(s))
}
}
fn from(c: char) -> Txt {
@ -1132,8 +1313,9 @@ impl_from_and_into_var! {
fn from(t: Txt) -> Cow<'static, str> {
match t.0 {
TxtData::Static(s) => Cow::Borrowed(s),
TxtData::Owned(s) => Cow::Owned(s),
TxtData::String(s) => Cow::Owned(s),
TxtData::Inline(d) => Cow::Owned(inline_to_str(&d).to_owned()),
TxtData::Arc(s) => Cow::Owned((*s).to_owned())
}
}
fn from(t: Txt) -> std::path::PathBuf {
@ -1177,7 +1359,7 @@ impl<'a> std::ops::Add<&'a str> for Txt {
}
impl std::ops::AddAssign<&str> for Txt {
fn add_assign(&mut self, rhs: &str) {
self.to_mut().push_str(rhs);
self.push_str(rhs);
}
}
impl PartialEq<&str> for Txt {
@ -1231,6 +1413,12 @@ impl AsRef<[u8]> for Txt {
self.as_str().as_ref()
}
}
impl std::fmt::Write for Txt {
fn write_str(&mut self, s: &str) -> fmt::Result {
self.push_str(s);
Ok(())
}
}
/// A trait for converting a value to a [`Txt`].
///
@ -1347,8 +1535,10 @@ pub enum UnderlinePosition {
Descent,
}
///<span data-del-macro-root></span> Creates a [`Txt`](crate::text::Txt) by calling the `format!` macro and
/// wrapping the result in a `Cow::Owned`.
///<span data-del-macro-root></span> Creates a [`Txt`] by formatting using the [`format_args!`] syntax.
///
/// Note that this behaves like a [`format!`] for [`Txt`], but it can be more performant because the
/// text type can represent `&'static str` and can i
///
/// # Examples
///
@ -1356,10 +1546,12 @@ pub enum UnderlinePosition {
/// # use zero_ui_core::text::formatx;
/// let text = formatx!("Hello {}", "World!");
/// ```
///
/// [`Txt`]: crate::text::Txt
#[macro_export]
macro_rules! formatx {
($($tt:tt)*) => {
$crate::text::Txt::owned(format!($($tt)*))
$crate::text::Txt::from_fmt(format_args!($($tt)*))
};
}
#[doc(inline)]

View File

@ -427,7 +427,7 @@ mod context {
}
app_local! {
static PROBE_ID: Txt = const { Txt::empty() };
static PROBE_ID: Txt = const { Txt::from_static("") };
}
#[property(CONTEXT, default(TEST_VAR))]

View File

@ -345,7 +345,7 @@ pub struct ImgErrorArgs {
#[property(EVENT, widget_impl(Image))]
pub fn on_error(child: impl UiNode, handler: impl WidgetHandler<ImgErrorArgs>) -> impl UiNode {
let mut handler = handler.cfg_boxed();
let mut error = Txt::empty();
let mut error = Txt::from_str("");
match_node(child, move |_, op| match op {
UiNodeOp::Init => {

View File

@ -265,7 +265,7 @@ fn markdown_view_fn(md: &str) -> impl UiNode {
}
blocks.push(code_block_view(CodeBlockFnArgs {
lang: match kind {
CodeBlockKind::Indented => Txt::empty(),
CodeBlockKind::Indented => Txt::from_str(""),
CodeBlockKind::Fenced(l) => l.to_text(),
},
txt: txt.into(),

View File

@ -28,7 +28,7 @@ pub(super) fn inspect_node(
let mut inspector_state = WriteTreeState::new();
let inspector = WindowId::new_unique();
let inspector_text = var(Txt::empty());
let inspector_text = var(Txt::from_str(""));
let can_inspect = can_inspect.into_var();