diff --git a/bin/assets/ui/breeze.css b/bin/assets/ui/breeze.css index a8f070d19..44e086765 100644 --- a/bin/assets/ui/breeze.css +++ b/bin/assets/ui/breeze.css @@ -185,7 +185,8 @@ SelectButton:selected:pressed { } pushbutton:disabled, -selectbutton:disabled { +selectbutton:disabled, +textinput:disabled { color: var(--disabled-color); border-color: var(--disabled-border); } diff --git a/include/eepp/scene/node.hpp b/include/eepp/scene/node.hpp index f619a0036..777faffa6 100644 --- a/include/eepp/scene/node.hpp +++ b/include/eepp/scene/node.hpp @@ -355,7 +355,7 @@ class EE_API Node : public Transformable { bool hasFocusWithin() const; - virtual void setFocus(); + virtual Node* setFocus(); Node* getFirstWidget() const; diff --git a/include/eepp/ui.hpp b/include/eepp/ui.hpp index 46eb641d2..9b15b04ff 100644 --- a/include/eepp/ui.hpp +++ b/include/eepp/ui.hpp @@ -87,4 +87,6 @@ #include +#include + #endif diff --git a/include/eepp/ui/uiapplication.hpp b/include/eepp/ui/uiapplication.hpp new file mode 100644 index 000000000..3f6216ece --- /dev/null +++ b/include/eepp/ui/uiapplication.hpp @@ -0,0 +1,32 @@ +#ifndef EE_UI_UIAPPLICATION +#define EE_UI_UIAPPLICATION + +#include +using namespace EE::Window; + +namespace EE { namespace UI { + +class UISceneNode; + +class UIApplication { + public: + UIApplication( const WindowSettings& windowSettings, bool loadBaseResources = true, + const ContextSettings& contextSettings = ContextSettings() ); + + virtual ~UIApplication(); + + EE::Window::Window* getWindow() const; + + UISceneNode* getUI() const; + + int run(); + + protected: + UISceneNode* mUISceneNode{ nullptr }; + EE::Window::Window* mWindow{ nullptr }; + bool mDidRun{ false }; +}; + +}} // namespace EE::UI + +#endif diff --git a/include/eepp/ui/uinode.hpp b/include/eepp/ui/uinode.hpp index 06b54dae9..55dc7d235 100644 --- a/include/eepp/ui/uinode.hpp +++ b/include/eepp/ui/uinode.hpp @@ -235,7 +235,7 @@ class EE_API UINode : public Node { const Uint32& getDragButton() const; - virtual void setFocus(); + virtual Node* setFocus(); Float getPropertyRelativeTargetContainerLength( const CSS::PropertyRelativeTarget& relativeTarget, diff --git a/include/eepp/ui/uiscenenode.hpp b/include/eepp/ui/uiscenenode.hpp index cc928ae88..0f6272b85 100644 --- a/include/eepp/ui/uiscenenode.hpp +++ b/include/eepp/ui/uiscenenode.hpp @@ -204,7 +204,7 @@ class EE_API UISceneNode : public SceneNode { virtual void onDrawDebugDataChange(); - virtual void setFocus(); + virtual Node* setFocus(); virtual void onParentChange(); diff --git a/include/eepp/window/window.hpp b/include/eepp/window/window.hpp index 75fdbd270..e922afc00 100644 --- a/include/eepp/window/window.hpp +++ b/include/eepp/window/window.hpp @@ -462,7 +462,7 @@ class EE_API Window { /** Runs the main loop function passed as parameter ** @param func The main loop function ** @param fps The desired FPS ( 0 = infinite ) */ - void runMainLoop( void ( *func )(), int fps = 0 ); + void runMainLoop( std::function func, int fps = 0 ); /** @return The current display index. */ virtual int getCurrentDisplayIndex(); @@ -504,6 +504,8 @@ class EE_API Window { InputMethod& getIME(); + const std::function& getMainLoop() { return mMainLoop; } + protected: friend class Engine; friend class Input; @@ -520,6 +522,7 @@ class EE_API Window { WindowQuitCallback mQuitCallback; Sizei mLastWindowedSize; InputMethod mIME; + std::function mMainLoop; class FrameData { public: diff --git a/premake4.lua b/premake4.lua index 9ef6d6a1f..5e4b5a910 100644 --- a/premake4.lua +++ b/premake4.lua @@ -1343,9 +1343,26 @@ solution "eepp" set_kind() language "C++" files { "src/examples/ui_hello_world/*.cpp" } - includedirs { "src/thirdparty" } build_link_configuration( "eepp-ui-hello-world", true ) + project "eepp-7guis-counter" + set_kind() + language "C++" + files { "src/examples/7guis/counter/*.cpp" } + build_link_configuration( "eepp-7guis-counter", true ) + + project "eepp-7guis-temperature-converter" + set_kind() + language "C++" + files { "src/examples/7guis/temperature_converter/*.cpp" } + build_link_configuration( "eepp-7guis-temperature-converter", true ) + + project "eepp-7guis-flight-booker" + set_kind() + language "C++" + files { "src/examples/7guis/flight_booker/*.cpp" } + build_link_configuration( "eepp-7guis-flight-booker", true ) + -- Tools project "eepp-textureatlaseditor" set_kind() diff --git a/premake5.lua b/premake5.lua index 5079f4d4d..3ca9876d8 100644 --- a/premake5.lua +++ b/premake5.lua @@ -1100,7 +1100,6 @@ workspace "eepp" kind "ConsoleApp" language "C++" files { "src/examples/http_request/*.cpp" } - incdirs { "src/thirdparty" } build_link_configuration( "eepp-http-request", true ) project "eepp-ui-hello-world" @@ -1110,6 +1109,24 @@ workspace "eepp" incdirs { "src/thirdparty" } build_link_configuration( "eepp-ui-hello-world", true ) + project "eepp-7guis-counter" + set_kind() + language "C++" + files { "src/examples/7guis/counter/*.cpp" } + build_link_configuration( "eepp-7guis-counter", true ) + + project "eepp-7guis-temperature-converter" + set_kind() + language "C++" + files { "src/examples/7guis/temperature_converter/*.cpp" } + build_link_configuration( "eepp-7guis-temperature-converter", true ) + + project "eepp-7guis-flight-booker" + set_kind() + language "C++" + files { "src/examples/7guis/flight_booker/*.cpp" } + build_link_configuration( "eepp-7guis-flight-booker", true ) + -- Tools project "eepp-textureatlaseditor" set_kind() diff --git a/src/eepp/core/string.cpp b/src/eepp/core/string.cpp index bd778ed8d..d379a350a 100644 --- a/src/eepp/core/string.cpp +++ b/src/eepp/core/string.cpp @@ -892,14 +892,25 @@ std::string String::randString( size_t len, std::string dictionary ) { return dictionary.substr( 0, len ); } +void numberClean( std::string& strNumber ) { + while ( strNumber.back() == '0' ) + strNumber.pop_back(); + if ( strNumber.back() == '.' ) + strNumber.pop_back(); +} + std::string String::fromFloat( const Float& value, const std::string& append, const std::string& prepend ) { - return prepend + toString( value ) + append; + std::string val( toString( value ) ); + numberClean( val ); + return prepend + val + append; } std::string String::fromDouble( const double& value, const std::string& append, const std::string& prepend ) { - return prepend + toString( value ) + append; + std::string val( toString( value ) ); + numberClean( val ); + return prepend + val + append; } void String::insertChar( String& str, const unsigned int& pos, const StringBaseType& tchar ) { diff --git a/src/eepp/scene/node.cpp b/src/eepp/scene/node.cpp index 0555eed9e..e9b03d65a 100644 --- a/src/eepp/scene/node.cpp +++ b/src/eepp/scene/node.cpp @@ -1657,7 +1657,9 @@ bool Node::hasFocusWithin() const { return hasFocus() || inParentTreeOf( getEventDispatcher()->getFocusNode() ); } -void Node::setFocus() {} +Node* Node::setFocus() { + return this; +} Node* Node::getFirstWidget() const { Node* child = mChild; diff --git a/src/eepp/ui/uiapplication.cpp b/src/eepp/ui/uiapplication.cpp new file mode 100644 index 000000000..aa409e1d8 --- /dev/null +++ b/src/eepp/ui/uiapplication.cpp @@ -0,0 +1,94 @@ +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +using namespace EE::Graphics; +using namespace EE::System; +using namespace EE::Scene; + +namespace EE { namespace UI { + +UIApplication::UIApplication( const WindowSettings& windowSettings, bool loadBaseResources, + const ContextSettings& contextSettings ) { + DisplayManager* displayManager = Engine::instance()->getDisplayManager(); + displayManager->enableScreenSaver(); + displayManager->enableMouseFocusClickThrough(); + displayManager->disableBypassCompositor(); + + Display* currentDisplay = displayManager->getDisplayIndex( 0 ); + Float pixelDensity = currentDisplay->getPixelDensity(); + + mWindow = Engine::instance()->createWindow( windowSettings, contextSettings ); + + if ( !mWindow->isOpen() ) + return; + + mDidRun = true; + + PixelDensity::setPixelDensity( eemax( mWindow->getScale(), pixelDensity ) ); + + if ( !loadBaseResources ) + return; + + FileSystem::changeWorkingDirectory( Sys::getProcessPath() ); + + FontTrueType* font = + FontTrueType::New( "NotoSans-Regular", "assets/fonts/NotoSans-Regular.ttf" ); + + mUISceneNode = UISceneNode::New(); + mUISceneNode->getUIThemeManager()->setDefaultFont( font ); + SceneManager::instance()->add( mUISceneNode ); + + mUISceneNode->getRoot()->addClass( "appbackground" ); + + UITheme* theme = UITheme::load( "uitheme", "uitheme", "", font, "assets/ui/breeze.css" ); + mUISceneNode->setStyleSheet( theme->getStyleSheet() ); + mUISceneNode->getUIThemeManager() + ->setDefaultEffectsEnabled( true ) + ->setDefaultTheme( theme ) + ->setDefaultFont( font ) + ->add( theme ); +} + +UIApplication::~UIApplication() { + Engine::destroySingleton(); + MemoryManager::showResults(); +} + +EE::Window::Window* UIApplication::getWindow() const { + return mWindow; +} + +UISceneNode* UIApplication::getUI() const { + return mUISceneNode; +} + +int UIApplication::run() { + mWindow->runMainLoop( [this]() { + mWindow->getInput()->update(); + SceneManager::instance()->update(); + + if ( mUISceneNode->invalidated() ) { + mWindow->clear(); + + SceneManager::instance()->draw(); + + mWindow->display(); + } else { +#if EE_PLATFORM != EE_PLATFORM_EMSCRIPTEN + mWindow->getInput()->waitEvent( Milliseconds( mWindow->hasFocus() ? 16 : 100 ) ); +#endif + } + } ); + + return mDidRun ? EXIT_SUCCESS : EXIT_FAILURE; +} + +}} // namespace EE::UI diff --git a/src/eepp/ui/uinode.cpp b/src/eepp/ui/uinode.cpp index 7e571eee1..846cafe15 100644 --- a/src/eepp/ui/uinode.cpp +++ b/src/eepp/ui/uinode.cpp @@ -1557,9 +1557,10 @@ void UINode::onWidgetFocusLoss() { invalidateDraw(); } -void UINode::setFocus() { +Node* UINode::setFocus() { if ( NULL != getEventDispatcher() ) getEventDispatcher()->setFocusNode( this ); + return this; } Float UINode::getPropertyRelativeTargetContainerLength( diff --git a/src/eepp/ui/uiscenenode.cpp b/src/eepp/ui/uiscenenode.cpp index 1f113b3e3..1e23ad446 100644 --- a/src/eepp/ui/uiscenenode.cpp +++ b/src/eepp/ui/uiscenenode.cpp @@ -107,9 +107,10 @@ void UISceneNode::onDrawDebugDataChange() { } } -void UISceneNode::setFocus() { +Node* UISceneNode::setFocus() { if ( NULL != getEventDispatcher() ) getEventDispatcher()->setFocusNode( mRoot ); + return this; } void UISceneNode::nodeToWorldTranslation( Vector2f& Pos ) const { diff --git a/src/eepp/ui/uispinbox.cpp b/src/eepp/ui/uispinbox.cpp index ac35e5a33..70a52c9ea 100644 --- a/src/eepp/ui/uispinbox.cpp +++ b/src/eepp/ui/uispinbox.cpp @@ -41,7 +41,7 @@ UISpinBox::UISpinBox() : mInput->setAllowOnlyNumbers( true, false ); mInput->addEventListener( Event::OnBufferChange, - cb::Make1( this, &UISpinBox::onBufferChange ) ); + [this]( auto event ) { onBufferChange( event ); } ); double val = mValue; mValue += 1; setValue( val ); diff --git a/src/eepp/ui/uitextinput.cpp b/src/eepp/ui/uitextinput.cpp index ba3ef93cb..db1c0454e 100644 --- a/src/eepp/ui/uitextinput.cpp +++ b/src/eepp/ui/uitextinput.cpp @@ -804,7 +804,10 @@ Uint32 UITextInput::onTextInput( const TextInputEvent& event ) { return 0; if ( mOnlyNumbers && ( ( mAllowFloat && text[i] == '.' && mDoc.find( "." ).isValid() ) || !String::isNumber( text[i], mAllowFloat ) ) ) { - return 0; + if ( !( i == 0 && mDoc.getSelection( true ).start().column() == 0 && + ( text[i] == '-' || text[i] == '+' ) ) ) { + return 0; + } } } diff --git a/src/eepp/window/window.cpp b/src/eepp/window/window.cpp index 8afc6b474..2d6374a41 100644 --- a/src/eepp/window/window.cpp +++ b/src/eepp/window/window.cpp @@ -595,14 +595,22 @@ void Window::setGLContextThread() {} void Window::unsetGLContextThread() {} -void Window::runMainLoop( void ( *func )(), int fps ) { #if EE_PLATFORM == EE_PLATFORM_EMSCRIPTEN - emscripten_set_main_loop( func, fps, 1 ); +static void eepp_mainloop() { + Engine::instance()->getCurrentWindow()->getMainLoop()(); +} +#endif + +void Window::runMainLoop( std::function func, int fps ) { + mMainLoop = std::move(func); + +#if EE_PLATFORM == EE_PLATFORM_EMSCRIPTEN + emscripten_set_main_loop( eepp_mainloop, fps, 1 ); #else setFrameRateLimit( fps ); while ( isRunning() ) { - func(); + mMainLoop(); } #endif } diff --git a/src/examples/7guis/counter/counter.cpp b/src/examples/7guis/counter/counter.cpp new file mode 100644 index 000000000..d3e510c51 --- /dev/null +++ b/src/examples/7guis/counter/counter.cpp @@ -0,0 +1,17 @@ +#include + +EE_MAIN_FUNC int main( int, char** ) { + UIApplication app( { 380, 64, "eepp - 7GUIs - Counter" } ); + UIWidget* hbox = app.getUI()->loadLayoutFromString( R"xml( + + + + + )xml" ); + int count = 0; + auto view = hbox->find( "count_view" ); + hbox->find( "count_click" )->onClick( [&count, view]( auto ) { + view->setText( String::toString( ++count ) ); + } ); + return app.run(); +} diff --git a/src/examples/7guis/flight_booker/flight_booker.cpp b/src/examples/7guis/flight_booker/flight_booker.cpp new file mode 100644 index 000000000..4b632c6ce --- /dev/null +++ b/src/examples/7guis/flight_booker/flight_booker.cpp @@ -0,0 +1,74 @@ +#include +#include + +EE_MAIN_FUNC int main( int, char** ) { + UIApplication app( { 440, 240, "eepp - 7GUIs - Flight Booker" } ); + UIWidget* hbox = app.getUI()->loadLayoutFromString( R"xml( + + + + one-way flight + return flight + + + + + + )xml" ); + auto ddlType = hbox->find( "type" ); + auto dateFrom = hbox->find( "date_from" ); + auto dateTo = hbox->find( "date_to" ); + auto bookBut = hbox->find( "book" ); + const auto covertDate = []( const String& dateStr ) -> std::optional { + if ( std::count( dateStr.begin(), dateStr.end(), '.' ) != 2 ) + return {}; + std::tm time = {}; + std::istringstream ss( dateStr ); + ss >> std::get_time( &time, "%d.%m.%Y" ); + if ( ss.fail() ) + return {}; + return std::mktime( &time ); + }; + const auto getCurrentDate = []() { + std::time_t now = std::time( nullptr ); + std::tm* ltm = std::localtime( &now ); + std::stringstream ss; + ss << std::put_time( ltm, "%d.%m.%Y" ); + return ss.str(); + }; + const auto updateDateInput = [&]( UITextInput* input, + const std::optional& date ) -> bool { + if ( !input->isEnabled() || date ) { + input->removeClass( "error_input" ); + return true; + } else + input->addClass( "error_input" ); + return false; + }; + const auto update = [&]() { + std::optional fromDate = covertDate( dateFrom->getText() ); + std::optional toDate = covertDate( dateTo->getText() ); + dateTo->setEnabled( ddlType->getListBox()->getItemSelectedIndex() != 0 ); + bookBut->setEnabled( updateDateInput( dateFrom, fromDate ) && + updateDateInput( dateTo, toDate ) && + ( ddlType->getListBox()->getItemSelectedIndex() == 0 || + ( fromDate && toDate && *fromDate < *toDate ) ) ); + }; + ddlType->on( Event::OnItemSelected, [&update]( auto ) { update(); } ); + dateFrom->setText( getCurrentDate() )->on( Event::OnValueChange, [&update]( auto ) { + update(); + } ); + dateTo->on( Event::OnValueChange, [&update]( auto ) { update(); } ); + bookBut->setFocus()->onClick( [&]( auto ) { + String msg( String::format( "You just booked a %s on %s", + ddlType->getListBox()->getItemSelectedText().toUtf8(), + dateFrom->getText().toUtf8() ) ); + UIMessageBox::New( UIMessageBox::OK, msg )->showWhenReady(); + } ); + update(); + return app.run(); +} diff --git a/src/examples/7guis/temperature_converter/temperature_converter.cpp b/src/examples/7guis/temperature_converter/temperature_converter.cpp new file mode 100644 index 000000000..c38bbc591 --- /dev/null +++ b/src/examples/7guis/temperature_converter/temperature_converter.cpp @@ -0,0 +1,35 @@ +#include + +EE_MAIN_FUNC int main( int, char** ) { + UIApplication app( { 490, 64, "eepp - 7GUIs - Temperature Converter" } ); + UIWidget* hbox = app.getUI()->loadLayoutFromString( R"xml( + + + + + + + )xml" ); + UITextInput* celsiusInput = hbox->find( "celsius_input" ); + UITextInput* fahrenheitInput = hbox->find( "fahrenheit_input" ); + const auto f2c = []( double f ) { return ( f - 32 ) * 5 / 9; }; + const auto c2f = []( double c ) { return c * 9 / 5 + 32; }; + bool converting = false; + const auto convert = [&]( bool fromCelsius ) { + if ( converting ) // Only process input value change and skip value change from setText + return; + BoolScopedOp op( converting, true ); + auto sourceInput = fromCelsius ? celsiusInput : fahrenheitInput; + double val; + if ( !String::fromString( val, sourceInput->getText() ) ) + return; + auto str( String::fromDouble( fromCelsius ? c2f( val ) : f2c( val ) ) ); + if ( fromCelsius ) + fahrenheitInput->setText( str ); + else + celsiusInput->setText( str ); + }; + celsiusInput->setFocus()->on( Event::OnValueChange, [&convert]( auto ) { convert( true ); } ); + fahrenheitInput->on( Event::OnValueChange, [&convert]( auto ) { convert( false ); } ); + return app.run(); +} diff --git a/src/examples/ui_hello_world/ui_hello_world.cpp b/src/examples/ui_hello_world/ui_hello_world.cpp index 7d469d2da..0528b6d90 100644 --- a/src/examples/ui_hello_world/ui_hello_world.cpp +++ b/src/examples/ui_hello_world/ui_hello_world.cpp @@ -1,8 +1,4 @@ #include -#include -#include -#include -using namespace EE::UI::Doc; EE::Window::Window* win = NULL; diff --git a/src/tools/ecode/ignorematcher.cpp b/src/tools/ecode/ignorematcher.cpp index 2fc37c6be..f01e42e0d 100644 --- a/src/tools/ecode/ignorematcher.cpp +++ b/src/tools/ecode/ignorematcher.cpp @@ -171,6 +171,8 @@ const std::string& GitIgnoreMatcher::getIgnoreFilePath() const { bool GitIgnoreMatcher::parse() { std::string patternFile; FileSystem::fileGet( mPath + mIgnoreFileName, patternFile ); + if ( FileSystem::fileExists( mPath + ".git" ) ) // Also ignore the .git folder + mPatterns.emplace_back( std::make_pair( "**/.git/**", false ) ); std::vector patterns = String::split( patternFile ); for ( auto& pattern : patterns ) { bool negates = false; @@ -187,8 +189,6 @@ bool GitIgnoreMatcher::parse() { pattern = String::rTrim( pattern, '/' ); mPatterns.emplace_back( std::make_pair( pattern, negates ) ); } - if ( FileSystem::fileExists( mPath + ".git" ) ) - mPatterns.emplace_back( std::make_pair( "/.git", false ) ); // Also ignore the .git folder return !mPatterns.empty(); } @@ -258,10 +258,25 @@ bool IgnoreMatcherManager::foundMatch() const { return !mMatchers.empty(); } +bool IgnoreMatcherManager::match( const FileInfo& file ) const { + eeASSERT( foundMatch() ); + auto dirPath( file.getDirectoryPath() ); + std::string localPath; + for ( const auto& matcher : mMatchers ) { + localPath.clear(); + if ( String::startsWith( dirPath, matcher->getPath() ) ) + localPath = dirPath.substr( matcher->getPath().size() ); + if ( matcher->match( localPath + file.getFileName() ) ) + return true; + } + return false; +} + bool IgnoreMatcherManager::match( const std::string& dir, const std::string& value ) const { eeASSERT( foundMatch() ); + std::string localPath; for ( const auto& matcher : mMatchers ) { - std::string localPath; + localPath.clear(); if ( String::startsWith( dir, matcher->getPath() ) ) localPath = dir.substr( matcher->getPath().size() ); if ( matcher->match( localPath + value ) ) diff --git a/src/tools/ecode/ignorematcher.hpp b/src/tools/ecode/ignorematcher.hpp index b74243d5b..e6786faea 100644 --- a/src/tools/ecode/ignorematcher.hpp +++ b/src/tools/ecode/ignorematcher.hpp @@ -2,7 +2,6 @@ #define ECODE_IGNOREMATCHER_HPP #include -#include #include #include @@ -69,6 +68,8 @@ class IgnoreMatcherManager { bool foundMatch() const; + bool match( const FileInfo& file ) const; + bool match( const std::string& dir, const std::string& value ) const; std::string findRepositoryRootPath() const; diff --git a/src/tools/ecode/projectdirectorytree.cpp b/src/tools/ecode/projectdirectorytree.cpp index bc9e14ebf..9a04edcfc 100644 --- a/src/tools/ecode/projectdirectorytree.cpp +++ b/src/tools/ecode/projectdirectorytree.cpp @@ -30,9 +30,7 @@ ProjectDirectoryTree::~ProjectDirectoryTree() { mRunning = false; Lock l( mFilesMutex ); } - { - Lock l( mDoneMutex ); - } + { Lock l( mDoneMutex ); } } void ProjectDirectoryTree::scan( const ProjectDirectoryTree::ScanCompleteEvent& scanComplete, @@ -320,8 +318,10 @@ void ProjectDirectoryTree::onChange( const ProjectDirectoryTree::Action& action, } void ProjectDirectoryTree::tryAddFile( const FileInfo& file ) { + if ( mIgnoreHidden && file.isHidden() ) + return; IgnoreMatcherManager matcher( getIgnoreMatcherFromPath( file.getFilepath() ) ); - if ( !matcher.foundMatch() || !matcher.match( file.getDirectoryPath(), file.getFilepath() ) ) { + if ( !matcher.foundMatch() || !matcher.match( file ) ) { bool foundPattern = mAcceptedPatterns.empty(); for ( auto& pattern : mAcceptedPatterns ) { if ( pattern.matches( file.getFilepath() ) ) { @@ -345,6 +345,8 @@ void ProjectDirectoryTree::addFile( const FileInfo& file ) { if ( file.isDirectory() ) { if ( !String::startsWith( file.getFilepath(), mPath ) || isDirInTree( file.getFilepath() ) ) return; + if ( mIgnoreHidden && file.isHidden() ) + return; Lock l( mFilesMutex ); std::vector files; std::vector names;