perf(ui): store Variant values inline

Replace Variant's heap-owned payloads with manually managed inline
  storage while preserving its return-by-value API.

  - inline std::string, String, DrawablePtr, Vector2f, and Rectf
  - add move construction and capacity-preserving assignment
  - add explicit borrowed std::string support and string views
  - avoid redundant conversions in table and tree hot paths
  - borrow stable process fields in eproc
  - eliminate per-frame eproc class allocation churn
  - focus the eproc search input on startup
  - add comprehensive Variant lifecycle and ownership tests
This commit is contained in:
Martín Lucas Golini
2026-09-21 02:37:11 -03:00
parent 51b88fa125
commit 53f1fe6045
10 changed files with 810 additions and 329 deletions

View File

@@ -762,6 +762,11 @@ class EE_API String {
**/
String( const String& str );
/** @brief Move constructor
** @param str Instance to move
**/
String( String&& str ) noexcept;
/** @brief Copy constructor
** @param str Instance to copy
**/

View File

@@ -1,12 +1,16 @@
#ifndef EE_UI_MODEL_VARIANT_HPP
#define EE_UI_MODEL_VARIANT_HPP
#include <cstring>
#include <eepp/core/core.hpp>
#include <eepp/graphics/drawable.hpp>
#include <eepp/graphics/drawableresource.hpp>
#include <eepp/math/rect.hpp>
#include <eepp/ui/uiicon.hpp>
#include <new>
#include <string>
#include <string_view>
#include <utility>
using namespace EE::Graphics;
using namespace EE::Math;
@@ -31,56 +35,407 @@ class EE_API Variant {
Vector2f,
Rectf,
cstr,
StringPtr
StringPtr,
StdStringPtr
};
Variant() : mType( Type::Invalid ) {}
explicit Variant( const std::string& string ) : mType( Type::StdString ) {
mValue.asStdString = eeNew( std::string, ( string ) );
Variant() = default;
explicit Variant( const std::string& string ) {
new ( &mValue.asStdString ) std::string( string );
mType = Type::StdString;
}
explicit Variant( const String& string ) : mType( Type::String ) {
mValue.asString = eeNew( String, ( string ) );
explicit Variant( std::string&& string ) {
new ( &mValue.asStdString ) std::string( std::move( string ) );
mType = Type::StdString;
}
explicit Variant( const std::string* string ) : mType( Type::StdStringPtr ) {
mValue.asStdStringPtr = string;
}
explicit Variant( const String& string ) {
new ( &mValue.asString ) String( string );
mType = Type::String;
}
explicit Variant( String&& string ) {
new ( &mValue.asString ) String( std::move( string ) );
mType = Type::String;
}
explicit Variant( const String* string ) : mType( Type::StringPtr ) {
mValue.asStringPtr = string;
}
Variant( DrawablePtr drawable ) : mDrawable( std::move( drawable ) ), mType( Type::Drawable ) {}
Variant( UIIcon* icon ) : mType( Type::Icon ) { mValue.asIcon = icon; }
Variant( const Vector2f& v ) : mType( Type::Vector2f ) {
mValue.asVector2f = eeNew( Vector2f, ( v ) );
}
Variant( void* data ) : mType( Type::DataPtr ) { mValue.asDataPtr = data; }
Variant( const Rectf& r ) : mType( Type::Rectf ) { mValue.asRectf = eeNew( Rectf, ( r ) ); }
Variant( bool val ) : mType( Type::Bool ) { mValue.asBool = val; }
Variant( const Float& val ) : mType( Type::Float ) { mValue.asFloat = val; }
Variant( const int& val ) : mType( Type::Int ) { mValue.asInt = val; }
Variant( const unsigned int& val ) : mType( Type::Uint ) { mValue.asUint = val; }
Variant( const Int64& val ) : mType( Type::Int64 ) { mValue.asInt64 = val; }
Variant( const Uint64& val ) : mType( Type::Uint64 ) { mValue.asUint64 = val; }
explicit Variant( const char* data ) : mType( Type::cstr ) { mValue.asCStr = data; }
~Variant() { reset(); }
Variant( const Variant& other ) : mType( Type::Invalid ) {
switch ( other.mType ) {
Variant( DrawablePtr drawable ) {
new ( &mValue.asDrawable ) DrawablePtr( std::move( drawable ) );
mType = Type::Drawable;
}
Variant( UIIcon* icon ) : mType( Type::Icon ) { mValue.asIcon = icon; }
Variant( const Vector2f& v ) {
new ( &mValue.asVector2f ) Vector2f( v );
mType = Type::Vector2f;
}
Variant( void* data ) : mType( Type::DataPtr ) { mValue.asDataPtr = data; }
Variant( const Rectf& r ) {
new ( &mValue.asRectf ) Rectf( r );
mType = Type::Rectf;
}
Variant( bool val ) : mType( Type::Bool ) { mValue.asBool = val; }
Variant( const Float& val ) : mType( Type::Float ) { mValue.asFloat = val; }
Variant( const int& val ) : mType( Type::Int ) { mValue.asInt = val; }
Variant( const unsigned int& val ) : mType( Type::Uint ) { mValue.asUint = val; }
Variant( const Int64& val ) : mType( Type::Int64 ) { mValue.asInt64 = val; }
Variant( const Uint64& val ) : mType( Type::Uint64 ) { mValue.asUint64 = val; }
explicit Variant( const char* data ) : mType( Type::cstr ) { mValue.asCStr = data; }
~Variant() { destroy(); }
Variant( const Variant& other ) { copyConstruct( other ); }
Variant( Variant&& other ) { moveConstruct( std::move( other ) ); }
Variant& operator=( const Variant& other ) {
if ( this == &other )
return *this;
if ( mType == other.mType ) {
copyAssignSameType( other );
} else {
destroy();
copyConstruct( other );
}
return *this;
}
Variant& operator=( Variant&& other ) {
if ( this == &other )
return *this;
if ( mType == other.mType ) {
moveAssignSameType( std::move( other ) );
} else {
destroy();
moveConstruct( std::move( other ) );
}
return *this;
}
static Variant fromRef( const std::string& string ) { return Variant( &string ); }
const std::string& asStdString() const { return mValue.asStdString; }
const std::string& asStdStringPtr() const { return *mValue.asStdStringPtr; }
const String& asString() const { return mValue.asString; }
const String& asStringPtr() const { return *mValue.asStringPtr; }
const DrawablePtr& asDrawable() const { return mValue.asDrawable; }
const bool& asBool() const { return mValue.asBool; }
const Float& asFloat() const { return mValue.asFloat; }
const int& asInt() const { return mValue.asInt; }
const unsigned int& asUint() const { return mValue.asUint; }
const Int64& asInt64() const { return mValue.asInt64; }
const Uint64& asUint64() const { return mValue.asUint64; }
const Vector2f& asVector2f() const { return mValue.asVector2f; }
const Rectf& asRectf() const { return mValue.asRectf; }
const char* asCStr() const { return mValue.asCStr; }
UIIcon* asIcon() const { return mValue.asIcon; }
void* asDataPtr() const { return mValue.asDataPtr; }
Type getType() const { return mType; }
bool is( const Type& type ) const { return type == mType; }
bool isString() const {
return mType == Type::StdString || mType == Type::StdStringPtr || mType == Type::cstr ||
mType == Type::String || mType == Type::StringPtr;
}
bool isStdStringLike() const {
return mType == Type::StdString || mType == Type::StdStringPtr || mType == Type::cstr;
}
std::string_view asStdStringView() const {
switch ( mType ) {
case Type::StdString:
mValue.asStdString = eeNew( std::string, ( *other.mValue.asStdString ) );
return asStdString();
case Type::StdStringPtr:
return asStdStringPtr();
case Type::cstr:
return asCStr();
default:
return {};
}
}
void reset() { destroy(); }
bool isValid() const { return mType != Type::Invalid; }
std::string toString() const {
switch ( mType ) {
case Type::Bool:
return asBool() ? "true" : "false";
case Type::Int:
return String::toString( asInt() );
case Type::Uint:
return String::toString( asUint() );
case Type::Int64:
return String::toString( asInt64() );
case Type::Uint64:
return String::toString( asUint64() );
case Type::Float:
return String::toString( asFloat() );
case Type::StdString:
return asStdString();
case Type::StdStringPtr:
return asStdStringPtr();
case Type::String:
return asString();
case Type::StringPtr:
return asStringPtr();
case Type::Drawable:
return asDrawable()->isDrawableResource()
? static_cast<DrawableResource*>( asDrawable().get() )->getName()
: "Drawable";
case Type::Icon:
return asIcon()->getName();
case Type::DataPtr:
return String::format( "%p", asDataPtr() );
case Type::Vector2f:
return String::format( "%.2f-%.2f", asVector2f().x, asVector2f().y );
case Type::Rectf:
return String::format( "%.2f-%.2f-%.2f-%.2f", asRectf().Top, asRectf().Right,
asRectf().Bottom, asRectf().Left );
case Type::cstr:
return asCStr();
case Type::Invalid:
break;
}
return "";
}
bool operator<( const Variant& other ) const {
if ( mType != other.mType ) {
if ( isStdStringLike() && other.isStdStringLike() )
return asStdStringView() < other.asStdStringView();
return toString() < other.toString();
}
switch ( mType ) {
case Type::Bool:
return asBool() < other.asBool();
case Type::Int:
return asInt() < other.asInt();
case Type::Uint:
return asUint() < other.asUint();
case Type::Int64:
return asInt64() < other.asInt64();
case Type::Uint64:
return asUint64() < other.asUint64();
case Type::Float:
return asFloat() < other.asFloat();
case Type::StdString:
return asStdString() < other.asStdString();
case Type::StdStringPtr:
return asStdStringPtr() < other.asStdStringPtr();
case Type::String:
return asString() < other.asString();
case Type::StringPtr:
return asStringPtr() < other.asStringPtr();
case Type::Drawable:
return asDrawable() < other.asDrawable();
case Type::Icon:
return asIcon() < other.asIcon();
case Type::DataPtr:
return asDataPtr() < other.asDataPtr();
case Type::Vector2f:
return asVector2f() < other.asVector2f();
case Type::Rectf:
return asRectf().getSize() < other.asRectf().getSize();
case Type::cstr:
return std::strcmp( asCStr(), other.asCStr() ) < 0;
case Type::Invalid:
break;
}
return false;
}
bool operator==( const Variant& other ) const {
if ( mType != other.mType ) {
if ( isStdStringLike() && other.isStdStringLike() )
return asStdStringView() == other.asStdStringView();
return toString() == other.toString();
}
switch ( mType ) {
case Type::Bool:
return asBool() == other.asBool();
case Type::Int:
return asInt() == other.asInt();
case Type::Uint:
return asUint() == other.asUint();
case Type::Int64:
return asInt64() == other.asInt64();
case Type::Uint64:
return asUint64() == other.asUint64();
case Type::Float:
return asFloat() == other.asFloat();
case Type::StdString:
return asStdString() == other.asStdString();
case Type::StdStringPtr:
return asStdStringPtr() == other.asStdStringPtr();
case Type::String:
return asString() == other.asString();
case Type::StringPtr:
return asStringPtr() == other.asStringPtr();
case Type::Drawable:
return asDrawable() == other.asDrawable();
case Type::Icon:
return asIcon() == other.asIcon();
case Type::DataPtr:
return asDataPtr() == other.asDataPtr();
case Type::Vector2f:
return asVector2f() == other.asVector2f();
case Type::Rectf:
return asRectf().getSize() == other.asRectf().getSize();
case Type::cstr:
return std::strcmp( asCStr(), other.asCStr() ) == 0;
case Type::Invalid:
break;
}
return false;
}
size_t size() const {
switch ( mType ) {
case Type::Bool:
return 1;
case Type::Int:
return sizeof( int );
case Type::Uint:
return sizeof( unsigned int );
case Type::Int64:
return sizeof( Int64 );
case Type::Uint64:
return sizeof( Uint64 );
case Type::Float:
return sizeof( Float );
case Type::StdString:
return asStdString().size();
case Type::StdStringPtr:
return asStdStringPtr().size();
case Type::String:
return asString().size();
case Type::StringPtr:
return asStringPtr().size();
case Type::Drawable:
return sizeof( mValue.asDrawable );
case Type::Icon:
return asIcon()->getName().size();
case Type::DataPtr:
return sizeof( mValue.asDataPtr );
case Type::Vector2f:
return sizeof( mValue.asVector2f );
case Type::Rectf:
return sizeof( mValue.asRectf );
case Type::cstr:
return std::strlen( asCStr() );
case Type::Invalid:
break;
}
return 0;
}
private:
union Storage {
void* asDataPtr;
UIIcon* asIcon;
std::string asStdString;
const std::string* asStdStringPtr;
String asString;
const String* asStringPtr;
DrawablePtr asDrawable;
bool asBool;
Float asFloat;
int asInt;
unsigned int asUint;
Int64 asInt64;
Uint64 asUint64;
Vector2f asVector2f;
Rectf asRectf;
const char* asCStr;
Storage() {}
~Storage() {}
} mValue;
Type mType{ Type::Invalid };
void destroy() {
switch ( mType ) {
case Type::StdString:
mValue.asStdString.~basic_string();
break;
case Type::String:
mValue.asString = eeNew( String, ( *other.mValue.asString ) );
mValue.asString.~String();
break;
case Type::Drawable:
mValue.asDrawable.~DrawablePtr();
break;
default:
break;
}
mType = Type::Invalid;
}
void copyConstruct( const Variant& other ) {
switch ( other.mType ) {
case Type::StdString:
new ( &mValue.asStdString ) std::string( other.mValue.asStdString );
break;
case Type::StdStringPtr:
mValue.asStdStringPtr = other.mValue.asStdStringPtr;
break;
case Type::String:
new ( &mValue.asString ) String( other.mValue.asString );
break;
case Type::StringPtr:
mValue.asStringPtr = other.mValue.asStringPtr;
break;
case Type::Drawable:
mDrawable = other.mDrawable;
new ( &mValue.asDrawable ) DrawablePtr( other.mValue.asDrawable );
break;
case Type::Icon:
mValue.asIcon = other.mValue.asIcon;
break;
case Type::Vector2f:
mValue.asVector2f = eeNew( Vector2f, ( *other.mValue.asVector2f ) );
new ( &mValue.asVector2f ) Vector2f( other.mValue.asVector2f );
break;
case Type::Rectf:
mValue.asRectf = eeNew( Rectf, ( *other.mValue.asRectf ) );
new ( &mValue.asRectf ) Rectf( other.mValue.asRectf );
break;
case Type::Bool:
mValue.asBool = other.mValue.asBool;
@@ -112,254 +467,101 @@ class EE_API Variant {
mType = other.mType;
}
Variant( Variant&& other ) noexcept :
mValue( other.mValue ), mDrawable( std::move( other.mDrawable ) ), mType( other.mType ) {
other.mType = Type::Invalid;
other.mValue = {};
}
Variant& operator=( const Variant& other ) {
if ( this != &other ) {
reset();
new ( this ) Variant( other ); // reuse copy constructor
}
return *this;
}
Variant& operator=( Variant&& other ) noexcept {
if ( this != &other ) {
reset();
mType = other.mType;
mDrawable = std::move( other.mDrawable );
mValue = other.mValue;
other.mType = Type::Invalid;
other.mValue = {};
}
return *this;
}
const std::string& asStdString() const { return *mValue.asStdString; }
const String& asString() const { return *mValue.asString; }
const String& asStringPtr() const { return *mValue.asStringPtr; }
const DrawablePtr& asDrawable() const { return mDrawable; }
const bool& asBool() const { return mValue.asBool; }
const Float& asFloat() const { return mValue.asFloat; }
const int& asInt() const { return mValue.asInt; }
const unsigned int& asUint() const { return mValue.asUint; }
const Int64& asInt64() const { return mValue.asInt64; }
const Uint64& asUint64() const { return mValue.asUint64; }
const Vector2f& asVector2f() const { return *mValue.asVector2f; }
const Rectf& asRectf() const { return *mValue.asRectf; }
const char* asCStr() const { return mValue.asCStr; }
UIIcon* asIcon() const { return mValue.asIcon; }
void* asDataPtr() const { return mValue.asDataPtr; }
bool is( const Type& type ) const { return type == mType; }
bool isString() const {
return mType == Type::StdString || mType == Type::cstr || mType == Type::String ||
mType == Type::StringPtr;
}
void reset() {
switch ( mType ) {
void moveConstruct( Variant&& other ) {
switch ( other.mType ) {
case Type::StdString:
eeSAFE_DELETE( mValue.asStdString );
new ( &mValue.asStdString ) std::string( std::move( other.mValue.asStdString ) );
break;
case Type::StdStringPtr:
mValue.asStdStringPtr = other.mValue.asStdStringPtr;
break;
case Type::String:
eeSAFE_DELETE( mValue.asString );
new ( &mValue.asString ) String( std::move( other.mValue.asString ) );
break;
case Type::StringPtr:
mValue.asStringPtr = other.mValue.asStringPtr;
break;
case Type::Drawable:
mDrawable.reset();
new ( &mValue.asDrawable ) DrawablePtr( std::move( other.mValue.asDrawable ) );
break;
case Type::Icon:
mValue.asIcon = other.mValue.asIcon;
break;
case Type::Vector2f:
eeSAFE_DELETE( mValue.asVector2f );
new ( &mValue.asVector2f ) Vector2f( std::move( other.mValue.asVector2f ) );
break;
case Type::Rectf:
eeSAFE_DELETE( mValue.asRectf );
new ( &mValue.asRectf ) Rectf( std::move( other.mValue.asRectf ) );
break;
case Type::Bool:
mValue.asBool = other.mValue.asBool;
break;
case Type::Float:
mValue.asFloat = other.mValue.asFloat;
break;
case Type::Int:
mValue.asInt = other.mValue.asInt;
break;
case Type::Uint:
mValue.asUint = other.mValue.asUint;
break;
case Type::Int64:
mValue.asInt64 = other.mValue.asInt64;
break;
case Type::Uint64:
mValue.asUint64 = other.mValue.asUint64;
break;
case Type::cstr:
mValue.asCStr = other.mValue.asCStr;
break;
case Type::DataPtr:
mValue.asDataPtr = other.mValue.asDataPtr;
break;
case Type::Invalid:
break;
}
mType = other.mType;
other.destroy();
}
void copyAssignSameType( const Variant& other ) {
switch ( mType ) {
case Type::StdString:
mValue.asStdString = other.mValue.asStdString;
break;
case Type::String:
mValue.asString = other.mValue.asString;
break;
case Type::Drawable:
mValue.asDrawable = other.mValue.asDrawable;
break;
default:
destroy();
copyConstruct( other );
break;
}
mType = Type::Invalid;
}
bool isValid() const { return mType != Type::Invalid; }
std::string toString() const {
void moveAssignSameType( Variant&& other ) {
switch ( mType ) {
case Type::Bool:
return asBool() ? "true" : "false";
case Type::Int:
return String::toString( asInt() );
case Type::Uint:
return String::toString( asUint() );
case Type::Int64:
return String::toString( asInt64() );
case Type::Uint64:
return String::toString( asUint64() );
case Type::Float:
return String::toString( asFloat() );
case Type::StdString:
return asStdString();
mValue.asStdString = std::move( other.mValue.asStdString );
other.destroy();
break;
case Type::String:
return asString();
case Type::StringPtr:
return asStringPtr();
mValue.asString = std::move( other.mValue.asString );
other.destroy();
break;
case Type::Drawable:
return asDrawable()->isDrawableResource()
? static_cast<DrawableResource*>( asDrawable().get() )->getName()
: "Drawable";
case Type::Icon:
return asIcon()->getName();
case Type::DataPtr:
return String::format( "%p", asDataPtr() );
case Type::Vector2f:
return String::format( "%.2f-%.2f", asVector2f().x, asVector2f().y );
case Type::Rectf:
return String::format( "%.2f-%.2f-%.2f-%.2f", asRectf().Top, asRectf().Right,
asRectf().Bottom, asRectf().Left );
case Type::cstr:
return asCStr();
case Type::Invalid:
mValue.asDrawable = std::move( other.mValue.asDrawable );
other.destroy();
break;
default:
destroy();
moveConstruct( std::move( other ) );
break;
}
return "";
}
bool operator<( const Variant& other ) const {
if ( mType != other.mType )
return toString() < other.toString();
switch ( mType ) {
case Type::Bool:
return asBool() < other.asBool();
case Type::Int:
return asInt() < other.asInt();
case Type::Uint:
return asUint() < other.asUint();
case Type::Int64:
return asInt64() < other.asInt64();
case Type::Uint64:
return asUint64() < other.asUint64();
case Type::Float:
return asFloat() < other.asFloat();
case Type::StdString:
return asStdString() < other.asStdString();
case Type::String:
return asString() < other.asString();
case Type::StringPtr:
return asStringPtr() < other.asStringPtr();
case Type::Drawable:
return asDrawable() < other.asDrawable();
case Type::Icon:
return asIcon() < other.asIcon();
case Type::DataPtr:
return asDataPtr() < other.asDataPtr();
case Type::Vector2f:
return asVector2f() < other.asVector2f();
case Type::Rectf:
return asRectf().getSize() < other.asRectf().getSize();
case Type::cstr:
return strcmp( asCStr(), other.asCStr() ) < 0;
case Type::Invalid:
break;
}
return false;
}
bool operator==( const Variant& other ) const {
if ( mType != other.mType )
return toString() == other.toString();
switch ( mType ) {
case Type::Bool:
return asBool() == other.asBool();
case Type::Int:
return asInt() == other.asInt();
case Type::Uint:
return asUint() == other.asUint();
case Type::Int64:
return asInt64() == other.asInt64();
case Type::Uint64:
return asUint64() == other.asUint64();
case Type::Float:
return asFloat() == other.asFloat();
case Type::StdString:
return asStdString() == other.asStdString();
case Type::String:
return asString() == other.asString();
case Type::StringPtr:
return asStringPtr() == other.asStringPtr();
case Type::Drawable:
return asDrawable() == other.asDrawable();
case Type::Icon:
return asIcon() == other.asIcon();
case Type::DataPtr:
return asDataPtr() == other.asDataPtr();
case Type::Vector2f:
return asVector2f() == other.asVector2f();
case Type::Rectf:
return asRectf().getSize() == other.asRectf().getSize();
case Type::cstr:
return strcmp( asCStr(), other.asCStr() ) == 0;
case Type::Invalid:
break;
}
return false;
}
size_t size() const {
switch ( mType ) {
case Type::Bool:
return 1;
case Type::Int:
return sizeof( int );
case Type::Uint:
return sizeof( unsigned int );
case Type::Int64:
return sizeof( Int64 );
case Type::Uint64:
return sizeof( Uint64 );
case Type::Float:
return sizeof( Float );
case Type::StdString:
return asStdString().size();
case Type::String:
return asString().size();
case Type::StringPtr:
return asStringPtr().size();
case Type::Drawable:
return sizeof( mDrawable );
case Type::Icon:
return asIcon()->getName().size();
case Type::DataPtr:
return sizeof( mValue.asDataPtr );
case Type::Vector2f:
return sizeof( mValue.asVector2f );
case Type::Rectf:
return sizeof( mValue.asRectf );
case Type::cstr:
return strlen( asCStr() );
case Type::Invalid:
break;
}
return 0;
}
private:
union {
void* asDataPtr{ nullptr };
UIIcon* asIcon;
std::string* asStdString;
String* asString;
const String* asStringPtr;
bool asBool;
Float asFloat;
int asInt;
unsigned int asUint;
Int64 asInt64;
Uint64 asUint64;
Vector2f* asVector2f;
Rectf* asRectf;
const char* asCStr;
} mValue;
DrawablePtr mDrawable;
Type mType;
};
}}} // namespace EE::UI::Models

View File

@@ -2081,6 +2081,8 @@ String::String( const StringType& utf32String ) : mString( utf32String ) {}
String::String( const String& str ) : mString( str.mString ) {}
String::String( String&& str ) noexcept : mString( std::move( str.mString ) ) {}
String::String( const String::View& utf32String ) : mString( utf32String ) {}
String String::fromUtf16( const char* utf16String, const size_t& utf16StringSize,

View File

@@ -15,6 +15,29 @@ namespace EE { namespace UI { namespace Abstract {
static constexpr String::HashType onModelUpdateTag = String::hash( "onModelUpdate" );
template <typename Callback> static void consumeVariantText( const Variant& value, Callback&& cb ) {
switch ( value.getType() ) {
case Variant::Type::String:
cb( value.asString() );
break;
case Variant::Type::StringPtr:
cb( value.asStringPtr() );
break;
case Variant::Type::StdString:
cb( value.asStdString() );
break;
case Variant::Type::StdStringPtr:
cb( value.asStdStringPtr() );
break;
case Variant::Type::cstr:
cb( value.asCStr() );
break;
default:
cb( value.toString() );
break;
}
}
UIAbstractTableView::UIAbstractTableView( const std::string& tag ) :
UIAbstractView( tag ),
mDragBorderDistance( PixelDensity::dpToPx( 4 ) ),
@@ -1009,19 +1032,26 @@ void UIAbstractTableView::updateTableCellData( UITableCell* cell, const ModelInd
Variant cls( getModel()->data( index, ModelRole::Class ) );
cell->setLoadingState( true );
if ( cls.isValid() ) {
bool hasClass = false;
hasClass =
( cls.is( Variant::Type::cstr ) &&
cell->hasClass( std::string_view{ cls.asCStr() } ) ) ||
( cls.is( Variant::Type::StdString ) && cell->hasClass( cls.asStdString() ) ) ||
cell->hasClass( cls.toString() );
const bool isStdStringLike = cls.isStdStringLike();
std::string convertedClass;
const std::string_view className =
isStdStringLike ? cls.asStdStringView()
: std::string_view{ convertedClass = cls.toString() };
const bool hasClass = cell->hasClass( className );
needsReloadStyle =
cell->getClasses().empty() || cell->getClasses().size() != 1 || !hasClass;
if ( !hasClass )
cell->setClass( cls.toString() );
if ( !hasClass ) {
if ( cls.is( Variant::Type::StdString ) )
cell->setClass( cls.asStdString() );
else if ( cls.is( Variant::Type::StdStringPtr ) )
cell->setClass( cls.asStdStringPtr() );
else if ( isStdStringLike )
cell->setClass( std::string{ className } );
else
cell->setClass( std::move( convertedClass ) );
}
} else {
needsReloadStyle = !cell->getClasses().empty();
cell->resetClass();
@@ -1033,25 +1063,14 @@ void UIAbstractTableView::updateTableCellData( UITableCell* cell, const ModelInd
if ( getModel()->tooltipModelRoleEnabled() ) {
Variant tooltip( getModel()->data( index, ModelRole::Tooltip ) );
if ( tooltip.isValid() ) {
if ( tooltip.is( Variant::Type::String ) )
cell->setTooltipText( tooltip.asString() );
else if ( tooltip.is( Variant::Type::StringPtr ) )
cell->setTooltipText( tooltip.asStringPtr() );
else
cell->setTooltipText( tooltip.toString() );
}
if ( tooltip.isValid() )
consumeVariantText( tooltip,
[cell]( const auto& text ) { cell->setTooltipText( text ); } );
}
Variant txt( getModel()->data( index, ModelRole::Display ) );
if ( txt.isValid() ) {
if ( txt.is( Variant::Type::String ) )
cell->setText( txt.asString() );
else if ( txt.is( Variant::Type::StringPtr ) )
cell->setText( txt.asStringPtr() );
else
cell->setText( txt.toString() );
}
if ( txt.isValid() )
consumeVariantText( txt, [cell]( const auto& text ) { cell->setText( text ); } );
}
void UIAbstractTableView::moveSelection( int steps ) {

View File

@@ -218,13 +218,13 @@ bool SortingProxyModel::lessThan( const ModelIndex& index1, const ModelIndex& in
auto data1 = mSource->data( index1, mSortRole );
auto data2 = mSource->data( index2, mSortRole );
if ( data1.isString() && data2.isString() ) {
if ( data1.is( Variant::Type::StdString ) && data2.is( Variant::Type::StdString ) )
return String::toLower( data1.asStdString() ) < String::toLower( data2.asStdString() );
if ( data1.isStdStringLike() && data2.isStdStringLike() )
return String::toLower( std::string{ data1.asStdStringView() } ) <
String::toLower( std::string{ data2.asStdStringView() } );
if ( data1.is( Variant::Type::String ) && data2.is( Variant::Type::String ) )
return String::toLower( data1.asString() ) < String::toLower( data2.asString() );
if ( data1.is( Variant::Type::cstr ) && data2.is( Variant::Type::cstr ) )
return String::toLower( std::string( data1.asCStr() ) ) <
String::toLower( std::string( data2.asCStr() ) );
if ( data1.is( Variant::Type::StringPtr ) && data2.is( Variant::Type::StringPtr ) )
return String::toLower( data1.asStringPtr() ) < String::toLower( data2.asStringPtr() );
}
return data1 < data2;
}

View File

@@ -6,6 +6,7 @@
#include <eepp/ui/uitableview.hpp>
#include <cmath>
#include <string_view>
namespace EE { namespace UI {
@@ -392,19 +393,22 @@ ModelIndex UITableView::findRowWithText( const std::string& text, const bool& ca
: ( model->treeColumn() >= 0 ? model->treeColumn() : 0 ) );
Variant var = model->data( index );
if ( var.isValid() ) {
std::string convertedValue;
const std::string_view value =
var.isStdStringLike() ? var.asStdStringView()
: std::string_view{ convertedValue = var.toString() };
bool matches = false;
switch ( matchKind ) {
case Abstract::UIAbstractView::FindRowWithTextMatchKind::Equals:
matches = var.toString() == text;
matches = value == text;
break;
case Abstract::UIAbstractView::FindRowWithTextMatchKind::StartsWith:
matches = String::startsWith( caseSensitive ? var.toString()
: String::toLower( var.toString() ),
caseSensitive ? text : String::toLower( text ) );
matches = caseSensitive ? String::startsWith( value, text )
: String::istartsWith( value, text );
break;
case Abstract::UIAbstractView::FindRowWithTextMatchKind::Contains:
matches = caseSensitive ? String::contains( var.toString(), text )
: String::icontains( var.toString(), text );
matches = caseSensitive ? String::contains( value, text )
: String::icontains( value, text );
break;
}

View File

@@ -11,6 +11,7 @@
#include <eepp/ui/uitooltip.hpp>
#include <eepp/ui/uitreeview.hpp>
#include <stack>
#include <string_view>
namespace EE { namespace UI {
@@ -811,19 +812,22 @@ ModelIndex UITreeView::findRowWithText( const std::string& text, const bool& cas
traverseTree( [&]( const int&, const ModelIndex& index, const size_t&, const Float& ) {
Variant var = model->data( index );
if ( var.isValid() ) {
std::string convertedValue;
const std::string_view value =
var.isStdStringLike() ? var.asStdStringView()
: std::string_view{ convertedValue = var.toString() };
bool matches = false;
switch ( matchKind ) {
case Abstract::UIAbstractView::FindRowWithTextMatchKind::Equals:
matches = var.toString() == text;
matches = value == text;
break;
case Abstract::UIAbstractView::FindRowWithTextMatchKind::StartsWith:
matches = String::startsWith( caseSensitive ? var.toString()
: String::toLower( var.toString() ),
caseSensitive ? text : String::toLower( text ) );
matches = caseSensitive ? String::startsWith( value, text )
: String::istartsWith( value, text );
break;
case Abstract::UIAbstractView::FindRowWithTextMatchKind::Contains:
matches = caseSensitive ? String::contains( var.toString(), text )
: String::icontains( var.toString(), text );
matches = caseSensitive ? String::contains( value, text )
: String::icontains( value, text );
break;
}
@@ -847,18 +851,21 @@ ModelIndex UITreeView::openRowWithPath( const std::vector<std::string>& pathTree
ModelIndex foundIndex = {};
const auto& part = pathTree[i];
traverseTree(
[&model, &foundIndex, &part, &parentIndex,
i]( const int&, const ModelIndex& index, const size_t& indentLevel, const Float& ) {
Variant var = model->data( index );
if ( i == indentLevel && var.isValid() && var.toString() == part ) {
if ( !parentIndex.isValid() || parentIndex == index.parent() ) {
foundIndex = index;
return IterationDecision::Stop;
}
traverseTree( [&model, &foundIndex, &part, &parentIndex,
i]( const int&, const ModelIndex& index, const size_t& indentLevel,
const Float& ) {
Variant var = model->data( index );
const bool matches =
i == indentLevel && var.isValid() &&
( var.isStdStringLike() ? var.asStdStringView() == part : var.toString() == part );
if ( matches ) {
if ( !parentIndex.isValid() || parentIndex == index.parent() ) {
foundIndex = index;
return IterationDecision::Stop;
}
return IterationDecision::Continue;
} );
}
return IterationDecision::Continue;
} );
if ( foundIndex == ModelIndex() )
break;

View File

@@ -0,0 +1,229 @@
#include "utest.hpp"
#include <eepp/graphics/rectangledrawable.hpp>
#include <eepp/ui/models/variant.hpp>
#include <limits>
#include <type_traits>
using namespace EE;
using namespace EE::Graphics;
using namespace EE::Math;
using namespace EE::UI;
using namespace EE::UI::Models;
static_assert( static_cast<int>( Variant::Type::Invalid ) == 0 );
static_assert( static_cast<int>( Variant::Type::StringPtr ) == 15 );
static_assert( static_cast<int>( Variant::Type::StdStringPtr ) == 16 );
static_assert( std::is_nothrow_move_constructible_v<std::string> );
static_assert( std::is_nothrow_move_constructible_v<String> );
static_assert( std::is_nothrow_move_constructible_v<DrawablePtr> );
UTEST( Variant, ownedStdStringLifecycle ) {
const std::string shortString = "ecode";
const std::string longString( 256, 'x' );
Variant shortValue( shortString );
EXPECT_TRUE( shortValue.is( Variant::Type::StdString ) );
EXPECT_TRUE( shortValue.isString() );
EXPECT_TRUE( shortValue.isStdStringLike() );
EXPECT_TRUE( shortValue.asStdString() == shortString );
EXPECT_TRUE( shortValue.asStdStringView() == shortString );
EXPECT_EQ( shortValue.size(), shortString.size() );
Variant copied( shortValue );
EXPECT_TRUE( copied == shortValue );
Variant longValue( longString );
const char* longData = longValue.asStdString().data();
Variant moved( std::move( longValue ) );
EXPECT_TRUE( !longValue.isValid() );
EXPECT_TRUE( moved.asStdString() == longString );
EXPECT_EQ( moved.asStdString().data(), longData );
Variant copyAssigned( std::string( 512, 'z' ) );
copyAssigned = moved;
EXPECT_TRUE( copyAssigned.asStdString() == longString );
Variant moveAssigned( std::string( 128, 'q' ) );
const char* movedData = moved.asStdString().data();
moveAssigned = std::move( moved );
EXPECT_TRUE( !moved.isValid() );
EXPECT_EQ( moveAssigned.asStdString().data(), movedData );
EXPECT_TRUE( moveAssigned.asStdString() == longString );
copyAssigned = copyAssigned;
EXPECT_TRUE( copyAssigned.asStdString() == longString );
Variant* self = &moveAssigned;
moveAssigned = std::move( *self );
EXPECT_TRUE( moveAssigned.asStdString() == longString );
Variant empty( std::string{} );
EXPECT_TRUE( empty.asStdString().empty() );
empty.reset();
EXPECT_TRUE( !empty.isValid() );
}
UTEST( Variant, ownedStringLifecycle ) {
String source( "héllo" );
Variant value( source );
EXPECT_TRUE( value.is( Variant::Type::String ) );
EXPECT_TRUE( value.asString() == source );
Variant copied( value );
EXPECT_TRUE( copied.asString() == source );
String movedSource( "moved" );
Variant movedValue( std::move( movedSource ) );
EXPECT_TRUE( movedValue.asString() == String( "moved" ) );
Variant moved( std::move( value ) );
EXPECT_TRUE( !value.isValid() );
EXPECT_TRUE( moved.asString() == source );
Variant copyAssigned( String( "capacity" ) );
copyAssigned = copied;
EXPECT_TRUE( copyAssigned.asString() == source );
Variant moveAssigned( String( "old" ) );
moveAssigned = std::move( copied );
EXPECT_TRUE( !copied.isValid() );
EXPECT_TRUE( moveAssigned.asString() == source );
moveAssigned.reset();
EXPECT_TRUE( !moveAssigned.isValid() );
}
UTEST( Variant, borrowedStringsRemainBorrowed ) {
std::string stdString = "borrowed std::string";
String string = "borrowed String";
Variant stdPtr = Variant::fromRef( stdString );
EXPECT_TRUE( stdPtr.is( Variant::Type::StdStringPtr ) );
EXPECT_EQ( &stdPtr.asStdStringPtr(), &stdString );
EXPECT_TRUE( stdPtr.asStdStringView() == stdString );
EXPECT_TRUE( stdPtr.toString() == stdString );
EXPECT_EQ( stdPtr.size(), stdString.size() );
Variant stdPtrCopy( stdPtr );
Variant stdPtrMove( std::move( stdPtrCopy ) );
EXPECT_TRUE( !stdPtrCopy.isValid() );
EXPECT_EQ( &stdPtrMove.asStdStringPtr(), &stdString );
stdPtrMove.reset();
EXPECT_TRUE( stdString == "borrowed std::string" );
Variant stringPtr( &string );
EXPECT_TRUE( stringPtr.is( Variant::Type::StringPtr ) );
EXPECT_EQ( &stringPtr.asStringPtr(), &string );
EXPECT_TRUE( stringPtr.toString() == string.toUtf8() );
stringPtr.reset();
EXPECT_TRUE( string == String( "borrowed String" ) );
Variant cstr( "borrowed cstr" );
EXPECT_TRUE( cstr.is( Variant::Type::cstr ) );
EXPECT_TRUE( cstr.asStdStringView() == "borrowed cstr" );
EXPECT_EQ( cstr.size(), 13u );
}
UTEST( Variant, stringComparisonAndOrdering ) {
std::string borrowed = "beta";
Variant owned( std::string( "beta" ) );
Variant pointer = Variant::fromRef( borrowed );
Variant cstr( "beta" );
Variant alpha( std::string( "alpha" ) );
EXPECT_TRUE( owned == pointer );
EXPECT_TRUE( pointer == cstr );
EXPECT_TRUE( alpha < pointer );
EXPECT_TRUE( owned.toString() == "beta" );
}
UTEST( Variant, valueAndPointerAlternatives ) {
int pointedValue = 42;
void* data = &pointedValue;
auto* icon = reinterpret_cast<UIIcon*>( &pointedValue );
const Vector2f vector( 10.f, 20.f );
const Rectf rect( 1.f, 2.f, 3.f, 4.f );
Variant boolValue( true );
Variant floatValue( 1.5f );
Variant intValue( -42 );
Variant uintValue( std::numeric_limits<unsigned int>::max() );
Variant int64Value( Int64( -9000000000 ) );
Variant uint64Value( Uint64( 18000000000ULL ) );
Variant dataValue( data );
Variant iconValue( icon );
Variant vectorValue( vector );
Variant rectValue( rect );
EXPECT_TRUE( boolValue.asBool() );
EXPECT_EQ( floatValue.asFloat(), 1.5f );
EXPECT_EQ( intValue.asInt(), -42 );
EXPECT_EQ( uintValue.asUint(), std::numeric_limits<unsigned int>::max() );
EXPECT_EQ( int64Value.asInt64(), Int64( -9000000000 ) );
EXPECT_EQ( uint64Value.asUint64(), Uint64( 18000000000ULL ) );
EXPECT_EQ( dataValue.asDataPtr(), data );
EXPECT_EQ( iconValue.asIcon(), icon );
EXPECT_TRUE( vectorValue.asVector2f() == vector );
EXPECT_TRUE( rectValue.asRectf() == rect );
EXPECT_EQ( vectorValue.size(), sizeof( Vector2f ) );
EXPECT_EQ( rectValue.size(), sizeof( Rectf ) );
Variant copied( vectorValue );
EXPECT_TRUE( copied.asVector2f() == vector );
Variant moved( std::move( copied ) );
EXPECT_TRUE( !copied.isValid() );
EXPECT_TRUE( moved.asVector2f() == vector );
moved = rectValue;
EXPECT_TRUE( moved.is( Variant::Type::Rectf ) );
EXPECT_TRUE( moved.asRectf() == rect );
}
UTEST( Variant, typeTransitions ) {
Variant value( std::string( "text" ) );
value = Variant( 42 );
EXPECT_TRUE( value.is( Variant::Type::Int ) );
EXPECT_EQ( value.asInt(), 42 );
value = Variant( String( "unicode" ) );
EXPECT_TRUE( value.is( Variant::Type::String ) );
value = Variant( DrawablePtr{} );
EXPECT_TRUE( value.is( Variant::Type::Drawable ) );
value.reset();
EXPECT_TRUE( !value.isValid() );
value = Variant( Vector2f( 2.f, 3.f ) );
value = Variant( std::string( "again" ) );
EXPECT_TRUE( value.is( Variant::Type::StdString ) );
EXPECT_TRUE( value.asStdString() == "again" );
}
UTEST( Variant, drawableOwnershipLifecycle ) {
auto drawable = makeResource<RectangleDrawable>( Vector2f( 1.f, 2.f ), Sizef( 3.f, 4.f ) );
ASSERT_TRUE( drawable != nullptr );
const auto initialRefs = drawable.use_count();
Variant original( drawable );
EXPECT_EQ( drawable.use_count(), initialRefs + 1 );
{
Variant copied( original );
EXPECT_EQ( drawable.use_count(), initialRefs + 2 );
Variant moved( std::move( copied ) );
EXPECT_TRUE( !copied.isValid() );
EXPECT_EQ( drawable.use_count(), initialRefs + 2 );
Variant moveAssigned( DrawablePtr{} );
moveAssigned = std::move( moved );
EXPECT_TRUE( !moved.isValid() );
EXPECT_EQ( drawable.use_count(), initialRefs + 2 );
Variant assigned( DrawablePtr{} );
assigned = original;
EXPECT_EQ( drawable.use_count(), initialRefs + 3 );
assigned = Variant( 7 );
EXPECT_EQ( drawable.use_count(), initialRefs + 2 );
moveAssigned.reset();
EXPECT_EQ( drawable.use_count(), initialRefs + 1 );
}
original.reset();
EXPECT_EQ( drawable.use_count(), initialRefs );
}

View File

@@ -7,6 +7,7 @@
#include <array>
#include <cmath>
#include <string_view>
#if EE_PLATFORM == EE_PLATFORM_LINUX || EE_PLATFORM == EE_PLATFORM_MACOS || \
EE_PLATFORM == EE_PLATFORM_BSD
#include <unistd.h>
@@ -153,6 +154,21 @@ const char* usernameClass( const ProcessInfo& process ) {
return "eproc-process-username-other";
}
void setCellClassEnabled( UITableCell& cell, std::string_view className, bool enabled ) {
const bool hasClass = cell.hasClass( className );
if ( enabled == hasClass )
return;
if ( enabled )
cell.addClass( std::string{ className } );
else
cell.removeClass( std::string{ className } );
}
constexpr std::array<std::string_view, 5> kUsernameClasses = {
"eproc-process-username-ended", "eproc-process-username-traced", "eproc-process-username-own",
"eproc-process-username-system", "eproc-process-username-other",
};
} // namespace
App::App() {
@@ -174,7 +190,7 @@ App::App() {
mApp = std::make_unique<UIApplication>( ws );
if ( mApp->getUI() && mApp->getWindow() )
mApp->getWindow()->setTitle(
mApp->getUI()->i18n( "eproc_window_title", "System Monitor" ) );
mApp->getUI()->i18n( "eproc_window_title", "eproc - System Monitor" ) );
}
App::~App() {}
@@ -504,6 +520,8 @@ void App::setupUI() {
UIWidgetInspector::create( mApp->getUI() );
}
} );
mSearchInput->setFocus();
}
void App::setupProcessTable() {
@@ -515,23 +533,20 @@ void App::setupProcessTable() {
mTableView->setColumnsHidden(
std::vector<size_t>( kOptionalProcessColumns.begin(), kOptionalProcessColumns.end() ),
true );
mTableView->setOnUpdateCellCb( [this]( UITableCell* cell, Model* model ) {
if ( !cell || !model )
mTableView->setOnUpdateCellCb( [this]( UITableCell* cell, Model* ) {
if ( !cell )
return;
const ModelIndex index = cell->getCurIndex();
const ProcessInfo* process = processForProxyIndex( index );
const Variant columnClass = model->data( index, ModelRole::Class );
std::vector<std::string> classes;
if ( columnClass.isValid() )
classes.emplace_back( columnClass.toString() );
if ( process ) {
if ( process->status == ProcessStatus::Ended )
classes.emplace_back( "eproc-process-ended" );
if ( index.column() == ProcessModel::ColUsername )
classes.emplace_back( usernameClass( *process ) );
}
cell->setClasses( classes );
setCellClassEnabled( *cell, "eproc-process-ended",
process && process->status == ProcessStatus::Ended );
const std::string_view desiredUsernameClass =
process && index.column() == ProcessModel::ColUsername ? usernameClass( *process )
: std::string_view{};
for ( const auto className : kUsernameClasses )
setCellClassEnabled( *cell, className, className == desiredUsernameClass );
} );
mTableView->setRowHeight( 28 );
// The flexible column is Name; the icon column is fixed so every row lines up.

View File

@@ -193,15 +193,13 @@ Variant ProcessModel::data( const ModelIndex& index, ModelRole role ) const {
switch ( index.column() ) {
case ColName:
return Variant( proc->name );
return Variant::fromRef( proc->name );
case ColPid:
return Variant( String::toString( static_cast<Int64>( proc->pid ) ) );
case ColUsername:
return Variant( proc->username );
case ColCpu: {
std::string cpu = proc->formatCpu();
return Variant( cpu );
}
return Variant::fromRef( proc->username );
case ColCpu:
return Variant( proc->formatCpu() );
case ColMemory:
return Variant( proc->formatMemory() );
case ColSharedMem:
@@ -215,7 +213,7 @@ Variant ProcessModel::data( const ModelIndex& index, ModelRole role ) const {
case ColUpload:
return Variant( proc->formatUpload() );
case ColCommand:
return Variant( proc->command );
return Variant::fromRef( proc->command );
case ColTotalMemory:
return Variant( formatKiB( proc->vmRSS ) );
case ColVirtualSize:
@@ -228,7 +226,7 @@ Variant ProcessModel::data( const ModelIndex& index, ModelRole role ) const {
return Variant( proc->formatRelativeStartTime( mSystemInfo.uptimeSeconds,
mSystemInfo.clockTicksPerSecond ) );
case ColTty:
return Variant( proc->tty );
return Variant::fromRef( proc->tty );
case ColIoRead:
return Variant( formatBytes( proc->ioReadBytes ) );
case ColIoWrite: