diff --git a/.agent/rules/build-project.md b/.agent/rules/build-project.md index 5e8a8901d..ca840e6c3 100644 --- a/.agent/rules/build-project.md +++ b/.agent/rules/build-project.md @@ -35,12 +35,26 @@ Always regenerate the project files before compiling or running tests after maki * **Tool:** Use `premake4` if installed; otherwise, fallback to `premake5` (the parameters are identical). * **Linker Flag (`--with-mold-linker`):** This flag is conditional. If the `mold` linker is installed on the system, you **must** include it to speed up linking. If `mold` is not installed, omit the flag. -**Command (if `mold` is installed):** +Choose the generator command from the configuration that will be built. Debug builds use AddressSanitizer; release builds do not. + +**Release command (if `mold` is installed):** +`premake4 --disable-static-build --with-mold-linker --with-debug-symbols gmake` + +**Release command (if `mold` is NOT installed):** +`premake4 --disable-static-build --with-debug-symbols gmake` + +**Debug command (if `mold` is installed):** `premake4 --disable-static-build --with-mold-linker --with-debug-symbols --address-sanitizer gmake` -**Command (if `mold` is NOT installed):** +**Debug command (if `mold` is NOT installed):** `premake4 --disable-static-build --with-debug-symbols --address-sanitizer gmake` +Never add `--address-sanitizer` when generating project files for a release build, and never omit it +when generating project files for a debug build. Always regenerate immediately before building so +the generated files match the intended configuration. Generator options are not tracked as object +dependencies, so when switching sanitizer mode, clean the affected configuration before rebuilding +to avoid mixing instrumented and uninstrumented C++ objects. + ## Step 1a: Format Changed Files After editing any C or C++ source file (`.c`, `.cpp`, `.h`, `.hpp`), you **must** run `clang-format` on all modified files to ensure consistent formatting with the project's style (defined in `.clang-format` at the repository root). @@ -60,6 +74,12 @@ Always use all processors reported by the platform when selecting the parallel j and other systems with `nproc`, use `-j$(nproc)` exactly; do not substitute an arbitrary fixed value such as `-j4`. Use the platform-equivalent processor-count command where `nproc` is unavailable. +On macOS in a managed or sandboxed environment, request elevated permission before invoking +`sysctl` or running any command that needs access to host display services. Read and validate +`sysctl -n hw.ncpu` separately before starting the build, then pass the verified positive value as a +literal `-jN` argument. If the query fails, returns zero, or returns an empty value, stop and request +permission; never allow an empty command substitution to turn `-j` into unbounded parallelism. + The valid OS directory names are: `windows`, `macosx`, `linux`, `bsd`, `haiku`. Run the following command, replacing `` with the correct environment: @@ -67,7 +87,8 @@ Run the following command, replacing `` with the correct environment: **Examples:** * Linux: `make -C make/linux -j$(nproc)` -* macOS: `make -C make/macosx -j$(sysctl -n hw.ncpu)` +* macOS: after an approved `sysctl -n hw.ncpu` returns a value such as `10`, run + `make -C make/macosx -j10` using that exact verified value. * Windows: `make -C make/windows -j%NUMBER_OF_PROCESSORS%` ## Running GUI Examples Under Xvfb diff --git a/TODO.md b/TODO.md index 317048634..304c826df 100644 --- a/TODO.md +++ b/TODO.md @@ -4,5 +4,3 @@ ## UI Module * Implement support for very simple state-changes from the XML file (ex: onclick="toggleclass(x)"). - -* Implement smooth-scrolling for macOS diff --git a/bin/assets/i18n/de.xml b/bin/assets/i18n/de.xml index d5861e19e..03431ac23 100644 --- a/bin/assets/i18n/de.xml +++ b/bin/assets/i18n/de.xml @@ -1146,6 +1146,8 @@ Für sichtbare Änderung ecode neu starten. Bildlaufleistentyp Die Bildlaufleiste hat eigenen Platz und verdeckt nie den Inhalt. Die Bildlaufleiste wird über dem Inhalt angezeigt. + Sanftes Scrollen + Animiert das Scrollen mit Mausrad und Trackpad. Speicherpfad für Bildschirmfotos Leicht Statischer Balken diff --git a/bin/assets/i18n/en.xml b/bin/assets/i18n/en.xml index a2b7d6e32..5ef9d1e30 100644 --- a/bin/assets/i18n/en.xml +++ b/bin/assets/i18n/en.xml @@ -1131,6 +1131,8 @@ Restart ecode to see the changes. Scrollbar Type The scrollbar has its own space and never covers content. The scrollbar appears over content. + Smooth Scrolling + Animate scrolling from mouse wheels and trackpads. Screenshot Save Path Slight Steady Bar diff --git a/bin/assets/i18n/fr.xml b/bin/assets/i18n/fr.xml index e68a81114..f6e850348 100644 --- a/bin/assets/i18n/fr.xml +++ b/bin/assets/i18n/fr.xml @@ -1130,6 +1130,8 @@ Redémarrer ecode pour voir les changements. Type de barre de défilement La barre de défilement occupe son propre espace et ne recouvre jamais le contenu. La barre de défilement apparaît par-dessus le contenu. + Défilement fluide + Anime le défilement avec la molette de la souris et le pavé tactile. Chemin d'enregistrement des captures d'écran Léger Barre fixe diff --git a/bin/assets/i18n/zh.xml b/bin/assets/i18n/zh.xml index 684bd63bb..d91d42f44 100644 --- a/bin/assets/i18n/zh.xml +++ b/bin/assets/i18n/zh.xml @@ -924,6 +924,8 @@ file in the directory tree. 滚动条类型 滚动条占用独立空间,不会覆盖内容。 滚动条叠加显示在内容上方。 + 平滑滚动 + 为鼠标滚轮和触控板触发的滚动添加动画。 会话快照与定期备份 保留会话之间未保存的文档更改。 截图保存路径 diff --git a/include/eepp/scene/node.hpp b/include/eepp/scene/node.hpp index 5b6822251..e64554e43 100644 --- a/include/eepp/scene/node.hpp +++ b/include/eepp/scene/node.hpp @@ -2124,8 +2124,8 @@ class EE_API Node : public Transformable { /** * @brief Handles mouse wheel scroll events. * - * Called when the mouse wheel is scrolled. Default implementation returns 1 - * to stop propagation but does not forward the event. + * Called when the mouse wheel is scrolled. Default implementation returns 0 so the event can + * bubble to an ancestor that implements scrolling. * * @param offset Scroll offset vector. * @param flipped Whether the scroll direction is flipped (e.g., on Mac). diff --git a/include/eepp/ui.hpp b/include/eepp/ui.hpp index 9ad7b75f0..94f255dd9 100644 --- a/include/eepp/ui.hpp +++ b/include/eepp/ui.hpp @@ -160,6 +160,7 @@ #include #include #include +#include #include #include #include diff --git a/include/eepp/ui/css/propertyids.hpp b/include/eepp/ui/css/propertyids.hpp index 6ff243953..82765d61a 100644 --- a/include/eepp/ui/css/propertyids.hpp +++ b/include/eepp/ui/css/propertyids.hpp @@ -162,6 +162,7 @@ enum class PropertyId : Uint16 { AllowFloat, TouchDrag, TouchDragDeceleration, + ScrollBehavior, WindowTitle, WindowOpacity, WindowButtonsOffset, diff --git a/include/eepp/ui/uicodeeditor.hpp b/include/eepp/ui/uicodeeditor.hpp index e6d582796..01ad23032 100644 --- a/include/eepp/ui/uicodeeditor.hpp +++ b/include/eepp/ui/uicodeeditor.hpp @@ -11,7 +11,7 @@ #include #include #include -#include +#include #include #include @@ -197,7 +197,7 @@ class EE_API DocSyntaxDefEvent : public DocEvent { std::string newLang; }; -class EE_API UICodeEditor : public UIWidget, public TextDocument::Client { +class EE_API UICodeEditor : public UITouchDraggableWidget, public TextDocument::Client { public: struct MinimapConfig { Float width{ 100 }; // dp width @@ -1087,6 +1087,8 @@ class EE_API UICodeEditor : public UIWidget, public TextDocument::Client { virtual Uint32 onMouseUp( const Vector2i& position, const Uint32& flags ); + virtual Uint32 onMouseWheel( const Vector2f& offset, bool flipped ); + virtual Uint32 onMouseClick( const Vector2i& position, const Uint32& flags ); virtual Uint32 onMouseDoubleClick( const Vector2i& position, const Uint32& flags ); @@ -1187,6 +1189,14 @@ class EE_API UICodeEditor : public UIWidget, public TextDocument::Client { virtual Uint32 onMessage( const NodeMessage* msg ); + virtual bool supportsScrollController() const; + + virtual Vector2f getScrollControllerPosition() const; + + virtual Vector2f getScrollControllerMaxPosition() const; + + virtual void setScrollControllerPosition( const Vector2f& position ); + void checkMouseOverColor( const Vector2i& position ); String checkMouseOverLink( const Vector2i& position, bool checkModifiers = true ); diff --git a/include/eepp/ui/uiscenenode.hpp b/include/eepp/ui/uiscenenode.hpp index 548806ae2..10a53d93a 100644 --- a/include/eepp/ui/uiscenenode.hpp +++ b/include/eepp/ui/uiscenenode.hpp @@ -775,6 +775,18 @@ class EE_API UISceneNode : public SceneNode { Uint32 getDefaultTextHints() const; + /** + * @brief Sets the default smooth-scrolling policy for this UI scene hierarchy. + * + * Embedded UI scenes inherit the value from their root host scene. New scrollable widgets + * snapshot this value when they are created and can override it individually. When applyNow is + * true, all existing scrollable widgets in the hierarchy are updated too. + */ + UISceneNode* setSmoothScrollEnabled( bool enabled, bool applyNow = false ); + + /** @return The smooth-scrolling default inherited from the root host UI scene. */ + bool isSmoothScrollEnabled() const; + static Uint32 resolveTextHints( Uint32 defaultHints, Uint32 overrideValue, Uint32 overrideMask ); @@ -1034,6 +1046,7 @@ class EE_API UISceneNode : public SceneNode { Sizef mLayoutViewportPixelsSize; bool mHasLayoutViewportPixelsSize{ false }; bool mFollowParentSize{ true }; + bool mSmoothScrollEnabled{ false }; bool mOwnsEventDispatcher{ true }; std::shared_ptr mThreadPool; URI mURI; diff --git a/include/eepp/ui/uiscrollablewidget.hpp b/include/eepp/ui/uiscrollablewidget.hpp index c53566a98..c31ae7520 100644 --- a/include/eepp/ui/uiscrollablewidget.hpp +++ b/include/eepp/ui/uiscrollablewidget.hpp @@ -7,7 +7,7 @@ namespace EE { namespace UI { class UIScrollBar; -class EE_API UIScrollableWidget : public UIWidget { +class EE_API UIScrollableWidget : public UITouchDraggableWidget { public: virtual Uint32 getType() const; @@ -89,6 +89,16 @@ class EE_API UIScrollableWidget : public UIWidget { virtual void updateScroll(); virtual void onScrollChange(); + + virtual Uint32 onMouseWheel( const Vector2f& offset, bool flipped ); + + virtual bool supportsScrollController() const; + + virtual Vector2f getScrollControllerPosition() const; + + virtual Vector2f getScrollControllerMaxPosition() const; + + virtual void setScrollControllerPosition( const Vector2f& position ); }; }} // namespace EE::UI diff --git a/include/eepp/ui/uiscrollcontroller.hpp b/include/eepp/ui/uiscrollcontroller.hpp new file mode 100644 index 000000000..42bf33fde --- /dev/null +++ b/include/eepp/ui/uiscrollcontroller.hpp @@ -0,0 +1,85 @@ +#ifndef EE_UI_UISCROLLCONTROLLER_HPP +#define EE_UI_UISCROLLCONTROLLER_HPP + +#include +#include +#include + +using namespace EE::Math; +using namespace EE::System; + +namespace EE { namespace UI { + +/** + * @brief Allocation-free, frame-rate-independent scroll motion state. + * + * The controller operates in pixel coordinates but deliberately does not own a widget. This lets + * virtualized views, translated child views, and text editors share the same motion without + * forcing them into a common rendering hierarchy. + */ +class EE_API UIScrollController { + public: + UIScrollController(); + + /** Synchronizes the controller with an externally changed scroll position and stops motion. */ + void setPosition( const Vector2f& position, const Vector2f& maxPosition ); + + /** Adds a wheel displacement. Returns false when the target cannot move in either axis. */ + bool scrollBy( const Vector2f& delta, const Vector2f& currentPosition, + const Vector2f& maxPosition, bool smooth, Float durationScale = 1.f ); + + /** Starts direct manipulation at the current scroll position. */ + void beginDrag( const Vector2f& position, const Vector2f& maxPosition ); + + /** Applies a drag displacement and records a filtered release velocity. */ + bool dragBy( const Vector2f& delta, const Time& elapsed, const Vector2f& maxPosition ); + + /** Transitions a direct manipulation into inertial motion. */ + void endDrag(); + + /** Advances smooth or inertial motion. Returns true when the position changed. */ + bool update( const Time& elapsed, const Vector2f& maxPosition ); + + /** Stops all pending motion at the current controller position. */ + void stop(); + + bool isActive() const; + + bool isDragging() const; + + const Vector2f& getPosition() const; + + const Vector2f& getTarget() const; + + const Vector2f& getVelocity() const; + + Time getWheelScrollDuration() const; + + /** Sets the duration used to visually interpolate one wheel displacement. */ + void setWheelScrollDuration( const Time& duration ); + + Vector2f getDragDeceleration() const; + + void setDragDeceleration( const Vector2f& deceleration ); + + protected: + enum class Mode : Uint8 { Idle, Smooth, Dragging, Momentum }; + struct SmoothTween { + Vector2f delta; + Float elapsed{ 0.f }; + Float duration{ 0.f }; + }; + + Vector2f mPosition; + Vector2f mTarget; + Vector2f mVelocity; + Vector2f mDragDeceleration{ 5.f, 5.f }; + std::array mSmoothTweens; + Time mWheelScrollDuration{ Milliseconds( 100 ) }; + Uint32 mSmoothTweenCount{ 0 }; + Mode mMode{ Mode::Idle }; +}; + +}} // namespace EE::UI + +#endif // EE_UI_UISCROLLCONTROLLER_HPP diff --git a/include/eepp/ui/uiscrollview.hpp b/include/eepp/ui/uiscrollview.hpp index c8f731653..9e207cd28 100644 --- a/include/eepp/ui/uiscrollview.hpp +++ b/include/eepp/ui/uiscrollview.hpp @@ -106,6 +106,16 @@ class EE_API UIScrollView : public UITouchDraggableWidget { virtual bool isTouchOverAllowedChildren(); + virtual Uint32 onMouseWheel( const Vector2f& offset, bool flipped ); + + virtual bool supportsScrollController() const; + + virtual Vector2f getScrollControllerPosition() const; + + virtual Vector2f getScrollControllerMaxPosition() const; + + virtual void setScrollControllerPosition( const Vector2f& position ); + virtual void onParentChange(); void listenParent(); diff --git a/include/eepp/ui/uitouchdraggablewidget.hpp b/include/eepp/ui/uitouchdraggablewidget.hpp index b498d45d0..933a65f9f 100644 --- a/include/eepp/ui/uitouchdraggablewidget.hpp +++ b/include/eepp/ui/uitouchdraggablewidget.hpp @@ -1,6 +1,7 @@ #ifndef EE_UITOUCHDRAGGABLEWIDGET_HPP #define EE_UITOUCHDRAGGABLEWIDGET_HPP +#include #include namespace EE { namespace UI { @@ -29,6 +30,11 @@ class EE_API UITouchDraggableWidget : public UIWidget { UITouchDraggableWidget* setTouchDragDeceleration( const Vector2f& touchDragDeceleration ); + /** Enables interpolation of discrete wheel input. Disabled by default. */ + bool isSmoothScrollEnabled() const; + + UITouchDraggableWidget* setSmoothScrollEnabled( bool enabled ); + virtual bool applyProperty( const StyleSheetProperty& attribute ); virtual std::string getPropertyString( const PropertyDefinition* propertyDef, @@ -38,8 +44,11 @@ class EE_API UITouchDraggableWidget : public UIWidget { protected: Vector2f mTouchDragPoint; - Vector2f mTouchDragAcceleration; + Vector2f mTouchDragVelocity; Vector2f mTouchDragDeceleration; + UIScrollController mScrollController; + bool mSmoothScrollEnabled{ false }; + bool mApplyingScrollController{ false }; UITouchDraggableWidget( const std::string& tag ); @@ -47,6 +56,24 @@ class EE_API UITouchDraggableWidget : public UIWidget { virtual bool isTouchOverAllowedChildren(); + /** Returns true for widgets that expose an absolute pixel scroll position to the controller. */ + virtual bool supportsScrollController() const; + + virtual Vector2f getScrollControllerPosition() const; + + virtual Vector2f getScrollControllerMaxPosition() const; + + virtual void setScrollControllerPosition( const Vector2f& position ); + + /** Adds a pixel-space wheel displacement using the configured scroll behavior. */ + bool scrollBy( const Vector2f& delta, Float durationScale = 1.f ); + + /** Preserves sub-tick precision without allowing one native event to exceed a legacy step. */ + static Float getWheelScrollFactor( Float offset ); + + /** Synchronizes externally initiated scrolling and cancels pending motion. */ + void stopScrollController(); + virtual void scheduledUpdate( const Time& time ); virtual Uint32 onMessage( const NodeMessage* msg ); diff --git a/include/eepp/window/platformhelper.hpp b/include/eepp/window/platformhelper.hpp index 2fdcf0d92..f11b78224 100644 --- a/include/eepp/window/platformhelper.hpp +++ b/include/eepp/window/platformhelper.hpp @@ -20,6 +20,21 @@ class EE_API PlatformHelper { virtual void iconvFree( char* buf ) = 0; + /** Enables or disables momentum generated by the native window system for scroll devices. + * @return true when the platform supports changing this setting. + */ + virtual bool setNativeScrollMomentumEnabled( bool enabled ); + + /** Controls the native separator below a window title bar. + * @return true when the platform and window support changing this setting. + */ + virtual bool setWindowTitleBarSeparatorVisible( void* window, bool visible ); + + /** Sets a native window title bar color from 8-bit RGB components. + * @return true when the platform and window support changing this setting. + */ + virtual bool setWindowTitleBarColor( void* window, Uint8 red, Uint8 green, Uint8 blue ); + #if EE_PLATFORM == EE_PLATFORM_ANDROID /** @return The Activity object for the application */ virtual void* getActivity() = 0; diff --git a/premake4.lua b/premake4.lua index ad6ffd46d..6b77673e5 100644 --- a/premake4.lua +++ b/premake4.lua @@ -1643,7 +1643,8 @@ solution "eepp" language "C++" set_targetdir("libs/" .. os.get_real() .. "/") includedirs { "include", "src" } - files { "src/eepp/ui/platform/macos/macosmenubar.mm" } + files { "src/eepp/ui/platform/macos/macosmenubar.mm", + "src/eepp/window/platform/macos/platformhelper.mm" } buildoptions { "-x objective-c++" } if not is_vs() then buildoptions{ "-std=c++20" } @@ -1869,30 +1870,6 @@ solution "eepp" files { "src/tools/uieditor/*.cpp" } build_link_configuration( "eepp-UIEditor", true ) - if os.is("macosx") then - project "ecode-macos-helper-static" - kind "StaticLib" - language "C++" - files { "src/tools/ecode/macos/*.m" } - set_targetdir("libs/" .. os.get_real() .. "/thirdparty/") - - configuration "debug" - defines { "DEBUG", "EE_DEBUG", "EE_MEMORY_MANAGER" } - flags { "Symbols" } - buildoptions{ "-Wall" } - buildoptions{ "-g3" } - targetname ( "ecode-macos-helper-static-debug" ) - - configuration "release" - defines { "NDEBUG" } - flags { "OptimizeSpeed" } - if _OPTIONS["with-debug-symbols"] then - flags { "Symbols" } - end - buildoptions{ "-O3" } - targetname ( "ecode-macos-helper-static" ) - end - project "ecode" set_kind() language "C++" @@ -1908,7 +1885,6 @@ solution "eepp" end if os.is("macosx") then links { "CoreFoundation.framework", "CoreServices.framework", "Cocoa.framework" } - links { "ecode-macos-helper-static" } end if os.is_real("linux") then links { "util" } diff --git a/premake5.lua b/premake5.lua index ef3ef7f7e..2a65bffd7 100644 --- a/premake5.lua +++ b/premake5.lua @@ -1674,7 +1674,8 @@ workspace "eepp" language "C++" cppdialect "C++20" incdirs { "include", "src" } - files { "src/eepp/ui/platform/macos/macosmenubar.mm" } + files { "src/eepp/ui/platform/macos/macosmenubar.mm", + "src/eepp/window/platform/macos/platformhelper.mm" } buildoptions { "-x objective-c++" } build_base_cpp_configuration( "eepp-macos-helper" ) target_dir_lib( "" ) @@ -1901,26 +1902,6 @@ workspace "eepp" filter { "system:not windows", "system:not haiku" } links { "pthread" } - if os.istarget("macosx") then - project "ecode-macos-helper-static" - kind "StaticLib" - language "C++" - target_dir_thirdparty() - filter "system:macosx" - files { "src/tools/ecode/macos/*.m" } - filter { "configurations:debug*", "action:not vs*" } - defines { "DEBUG", "EE_DEBUG", "EE_MEMORY_MANAGER" } - symbols "On" - buildoptions{ "-Wall" } - targetname ( "ecode-macos-helper-static-debug" ) - filter { "configurations:release*", "action:not vs*" } - defines { "NDEBUG" } - optimize "Speed" - targetname ( "ecode-macos-helper-static" ) - filter { "configurations:release*", "action:not vs*", "options:with-debug-symbols" } - symbols "On" - end - project "ecode" set_kind() language "C++" @@ -1943,7 +1924,6 @@ workspace "eepp" linkoptions { _MAIN_SCRIPT_DIR .. "/bin/assets/icon/ecode.x64.res" } filter "system:macosx" links { "CoreFoundation.framework", "CoreServices.framework", "Cocoa.framework" } - links { "ecode-macos-helper-static" } filter { "system:not windows", "system:not haiku" } links { "pthread" } filter "system:linux" diff --git a/src/eepp/scene/eventdispatcher.cpp b/src/eepp/scene/eventdispatcher.cpp index 8d02cc523..1193148d4 100644 --- a/src/eepp/scene/eventdispatcher.cpp +++ b/src/eepp/scene/eventdispatcher.cpp @@ -80,8 +80,7 @@ void EventDispatcher::inputCallback( InputEvent* event ) { break; case InputEvent::MouseWheel: sendMouseWheel( { event->wheel.x, event->wheel.y }, - event->wheel.direction == InputEvent::WheelEvent::Normal ? true - : false ); + event->wheel.direction == InputEvent::WheelEvent::Flipped ); break; case InputEvent::SysWM: case InputEvent::VideoResize: @@ -288,7 +287,7 @@ void EventDispatcher::sendKeyDown( const Keycode& keyCode, const Scancode& scanc } void EventDispatcher::sendMouseWheel( const Vector2f& offset, bool flipped ) { - Node* node = mFocusNode; + Node* node = nullptr != mOverNode ? mOverNode : mFocusNode; while ( NULL != node ) { if ( node->isEnabled() && node->onMouseWheel( offset, flipped ) ) break; diff --git a/src/eepp/scene/node.cpp b/src/eepp/scene/node.cpp index b4d63ba14..683044eab 100644 --- a/src/eepp/scene/node.cpp +++ b/src/eepp/scene/node.cpp @@ -388,7 +388,7 @@ Uint32 Node::onMouseLeave( const Vector2i& Pos, const Uint32& Flags ) { } Uint32 Node::onMouseWheel( const Vector2f&, bool ) { - return 1; + return 0; } Uint32 Node::onCalculateDrag( const Vector2f&, const Uint32& ) { diff --git a/src/eepp/ui/css/stylesheetspecification.cpp b/src/eepp/ui/css/stylesheetspecification.cpp index 20930e08e..9b595a427 100644 --- a/src/eepp/ui/css/stylesheetspecification.cpp +++ b/src/eepp/ui/css/stylesheetspecification.cpp @@ -422,6 +422,8 @@ void StyleSheetSpecification::registerDefaultProperties() { registerProperty( PropertyId::TouchDrag, "touch-drag", "" ).setType( PropertyType::Bool ); registerProperty( PropertyId::TouchDragDeceleration, "touch-drag-deceleration", "" ) .setType( PropertyType::NumberFloat ); + registerProperty( PropertyId::ScrollBehavior, "scroll-behavior", "instant" ) + .setType( PropertyType::String ); registerProperty( PropertyId::WindowTitle, "window-title", "" ).setType( PropertyType::String ); registerProperty( PropertyId::WindowOpacity, "window-opacity", "" ) diff --git a/src/eepp/ui/uiapplication.cpp b/src/eepp/ui/uiapplication.cpp index d90ab8605..8d84a2e2f 100644 --- a/src/eepp/ui/uiapplication.cpp +++ b/src/eepp/ui/uiapplication.cpp @@ -14,6 +14,7 @@ #include #include #include +#include #include #include @@ -74,6 +75,7 @@ UIApplication::UIApplication( const WindowSettings& windowSettings, const Settin std::cerr << "Could not create window, exiting" << std::endl; return; } + Engine::instance()->getPlatformHelper()->setNativeScrollMomentumEnabled( true ); mWindow->setDeferNativeResourceDestructionOnClose( true ); mDidRun = true; diff --git a/src/eepp/ui/uicodeeditor.cpp b/src/eepp/ui/uicodeeditor.cpp index 310ca549a..076a4333d 100644 --- a/src/eepp/ui/uicodeeditor.cpp +++ b/src/eepp/ui/uicodeeditor.cpp @@ -1,5 +1,6 @@ #include "eepp/ui/uistyle.hpp" #include +#include #include #include #include @@ -163,7 +164,7 @@ const MouseBindings::ShortcutMap UICodeEditor::getDefaultMousebindings() { UICodeEditor::UICodeEditor( const std::string& elementTag, const bool& autoRegisterBaseCommands, const bool& autoRegisterBaseKeybindings ) : - UIWidget( elementTag ), + UITouchDraggableWidget( elementTag ), mFont( getUISceneNode()->getResourceScope()->findFont( "monospace" ).get() ), mDoc( std::make_shared() ), mAsyncLifetime( this, this ), @@ -272,7 +273,7 @@ Uint32 UICodeEditor::getType() const { } bool UICodeEditor::isType( const Uint32& type ) const { - return type == getType() || UIWidget::isType( type ); + return type == getType() || UITouchDraggableWidget::isType( type ); } void UICodeEditor::setTheme( UITheme* Theme ) { @@ -486,7 +487,9 @@ void UICodeEditor::draw() { clipSmartDisable(); } -void UICodeEditor::scheduledUpdate( const Time& ) { +void UICodeEditor::scheduledUpdate( const Time& time ) { + UITouchDraggableWidget::scheduledUpdate( time ); + if ( mDisableCursorBlinkingAfterAMinuteOfInactivity && mLastActivity.getElapsedTime() > Seconds( 60 ) ) { if ( !mCursorVisible ) { @@ -775,7 +778,23 @@ Uint32 UICodeEditor::onMessage( const NodeMessage* msg ) { setFocus(); } } - return UIWidget::onMessage( msg ); + return UITouchDraggableWidget::onMessage( msg ); +} + +bool UICodeEditor::supportsScrollController() const { + return true; +} + +Vector2f UICodeEditor::getScrollControllerPosition() const { + return mScroll; +} + +Vector2f UICodeEditor::getScrollControllerMaxPosition() const { + return getMaxScroll(); +} + +void UICodeEditor::setScrollControllerPosition( const Vector2f& position ) { + setScroll( position ); } void UICodeEditor::disableEditorFeatures( bool useDefaultStyle ) { @@ -1887,23 +1906,11 @@ Uint32 UICodeEditor::onMouseUp( const Vector2i& position, const Uint32& flags ) } else if ( flags & EE_BUTTON_WDMASK ) { if ( getInput()->isKeyModPressed() ) { mDoc->execute( "font-size-shrink" ); - } else if ( input->isModState( KEYMOD_SHIFT ) ) { - setScrollX( mScroll.x + mMouseWheelScroll ); - } else { - setScrollY( mScroll.y + mMouseWheelScroll ); } } else if ( flags & EE_BUTTON_WUMASK ) { if ( getInput()->isKeyModPressed() ) { mDoc->execute( "font-size-grow" ); - } else if ( input->isModState( KEYMOD_SHIFT ) ) { - setScrollX( mScroll.x - mMouseWheelScroll ); - } else { - setScrollY( mScroll.y - mMouseWheelScroll ); } - } else if ( flags & EE_BUTTON_WRMASK ) { - setScrollX( mScroll.x + mMouseWheelScroll ); - } else if ( flags & EE_BUTTON_WLMASK ) { - setScrollX( mScroll.x - mMouseWheelScroll ); } else if ( !minimapHover && ( flags & EE_BUTTON_RMASK ) ) { Vector2f localPos( convertToNodeSpace( position.asFloat() ) ); if ( localPos.x >= mPaddingPx.Left + getGutterWidth() && localPos.y >= mPluginsTopSpace ) @@ -1914,6 +1921,31 @@ Uint32 UICodeEditor::onMouseUp( const Vector2i& position, const Uint32& flags ) return UIWidget::onMouseUp( position, flags ); } +Uint32 UICodeEditor::onMouseWheel( const Vector2f& offset, bool ) { + Input* input = getInput(); + if ( input->isKeyModPressed() ) + return 1; + + Vector2f delta; + Float durationScale = 1.f; + if ( input->isModState( KEYMOD_SHIFT ) && offset.y != 0.f ) { + const Float factor = getWheelScrollFactor( offset.y ); + delta.x = -factor * mMouseWheelScroll; + durationScale = std::abs( factor ); + } else { + if ( offset.y != 0.f ) { + const Float factor = getWheelScrollFactor( offset.y ); + delta.y = -factor * mMouseWheelScroll; + durationScale = std::abs( factor ); + } else if ( offset.x != 0.f ) { + const Float factor = getWheelScrollFactor( offset.x ); + delta.x = factor * mMouseWheelScroll; + durationScale = std::abs( factor ); + } + } + return scrollBy( delta, durationScale ) ? 1 : 0; +} + Uint32 UICodeEditor::onMouseClick( const Vector2i& position, const Uint32& flags ) { mLastActivity.restart(); for ( auto& plugin : mPlugins ) @@ -2641,6 +2673,8 @@ void UICodeEditor::showMinimap( bool showMinimap ) { } bool UICodeEditor::setScrollX( const Float& val, bool emitEvent ) { + if ( !mApplyingScrollController ) + stopScrollController(); Float oldVal = mScroll.x; mScroll.x = eefloor( eeclamp( val, 0.f, getMaxScroll().x ) ); if ( oldVal != mScroll.x ) { @@ -2656,6 +2690,8 @@ bool UICodeEditor::setScrollX( const Float& val, bool emitEvent ) { } bool UICodeEditor::setScrollY( const Float& val, bool emitEvent ) { + if ( !mApplyingScrollController ) + stopScrollController(); Float oldVal = mScroll.y; mScroll.y = eefloor( eeclamp( val, 0, getMaxScroll().y ) ); if ( oldVal != mScroll.y ) { @@ -3196,7 +3232,7 @@ bool UICodeEditor::applyProperty( const StyleSheetProperty& attribute ) { break; } default: - return UIWidget::applyProperty( attribute ); + return UITouchDraggableWidget::applyProperty( attribute ); } return true; @@ -3250,12 +3286,12 @@ std::string UICodeEditor::getPropertyString( const PropertyDefinition* propertyD case PropertyId::Text: return mDoc->getLineTextUtf8( 0 ); default: - return UIWidget::getPropertyString( propertyDef, propertyIndex ); + return UITouchDraggableWidget::getPropertyString( propertyDef, propertyIndex ); } } std::vector UICodeEditor::getPropertiesImplemented() const { - auto props = UIWidget::getPropertiesImplemented(); + auto props = UITouchDraggableWidget::getPropertiesImplemented(); auto local = { PropertyId::Locked, PropertyId::Color, PropertyId::TextShadowColor, @@ -5910,7 +5946,7 @@ TextDirection UICodeEditor::getTextDirection() const { void UICodeEditor::loadFromXmlNode( const pugi::xml_node& node ) { beginAttributesTransaction(); - UIWidget::loadFromXmlNode( node ); + UITouchDraggableWidget::loadFromXmlNode( node ); std::string text; bool hasElementChildren = false; diff --git a/src/eepp/ui/uiscenenode.cpp b/src/eepp/ui/uiscenenode.cpp index 1a00927a9..17c0a5dd4 100644 --- a/src/eepp/ui/uiscenenode.cpp +++ b/src/eepp/ui/uiscenenode.cpp @@ -22,6 +22,7 @@ #include #include #include +#include #include #include #include @@ -391,6 +392,33 @@ Uint32 UISceneNode::getDefaultTextHints() const { return mDefaultTextHints; } +UISceneNode* UISceneNode::setSmoothScrollEnabled( bool enabled, bool applyNow ) { + UISceneNode* rootScene = this; + while ( rootScene->mHostUISceneNode ) + rootScene = rootScene->mHostUISceneNode; + + rootScene->mSmoothScrollEnabled = enabled; + if ( applyNow ) { + const auto applyToScene = [enabled]( auto&& self, UISceneNode* scene ) -> void { + for ( auto* widget : scene->findAllByType( + UI_TYPE_TOUCH_DRAGGABLE_WIDGET ) ) + widget->setSmoothScrollEnabled( enabled ); + for ( auto* childScene : scene->mChildUISceneNodes ) + self( self, childScene ); + }; + applyToScene( applyToScene, rootScene ); + } + + return this; +} + +bool UISceneNode::isSmoothScrollEnabled() const { + const UISceneNode* rootScene = this; + while ( rootScene->mHostUISceneNode ) + rootScene = rootScene->mHostUISceneNode; + return rootScene->mSmoothScrollEnabled; +} + Uint32 UISceneNode::resolveTextHints( Uint32 defaultHints, Uint32 overrideValue, Uint32 overrideMask ) { const Uint32 featureMask = TextHints::OpenTypeFeatures; diff --git a/src/eepp/ui/uiscrollablewidget.cpp b/src/eepp/ui/uiscrollablewidget.cpp index fc5c16cac..31d02c28c 100644 --- a/src/eepp/ui/uiscrollablewidget.cpp +++ b/src/eepp/ui/uiscrollablewidget.cpp @@ -1,11 +1,13 @@ +#include #include #include #include +#include namespace EE { namespace UI { UIScrollableWidget::UIScrollableWidget( const std::string& tag ) : - UIWidget( tag ), + UITouchDraggableWidget( tag ), mScrollViewType( ScrollViewType::Outside ), mVScrollMode( ScrollBarMode::Auto ), mHScrollMode( ScrollBarMode::Auto ), @@ -29,7 +31,7 @@ Uint32 UIScrollableWidget::getType() const { } bool UIScrollableWidget::isType( const Uint32& type ) const { - return UIWidget::getType() == type ? true : UIWidget::isType( type ); + return UIScrollableWidget::getType() == type ? true : UITouchDraggableWidget::isType( type ); } void UIScrollableWidget::onSizeChange() { @@ -239,6 +241,8 @@ void UIScrollableWidget::onScrollChange() {} void UIScrollableWidget::onValueChangeCb( const Event* ) { updateScroll(); + if ( !mApplyingScrollController ) + stopScrollController(); } std::string UIScrollableWidget::getPropertyString( const PropertyDefinition* propertyDef, @@ -261,12 +265,12 @@ std::string UIScrollableWidget::getPropertyString( const PropertyDefinition* pro case PropertyId::ScrollBarMode: return getViewType() == ScrollViewType::Overlay ? "overlay" : "outside"; default: - return UIWidget::getPropertyString( propertyDef, propertyIndex ); + return UITouchDraggableWidget::getPropertyString( propertyDef, propertyIndex ); } } std::vector UIScrollableWidget::getPropertiesImplemented() const { - auto props = UIWidget::getPropertiesImplemented(); + auto props = UITouchDraggableWidget::getPropertiesImplemented(); auto local = { PropertyId::VScrollMode, PropertyId::HScrollMode, PropertyId::ScrollBarStyle, PropertyId::ScrollBarMode }; props.insert( props.end(), local.begin(), local.end() ); @@ -358,7 +362,7 @@ bool UIScrollableWidget::applyProperty( const StyleSheetProperty& attribute ) { break; } default: - return UIWidget::applyProperty( attribute ); + return UITouchDraggableWidget::applyProperty( attribute ); } return true; @@ -366,34 +370,6 @@ bool UIScrollableWidget::applyProperty( const StyleSheetProperty& attribute ) { Uint32 UIScrollableWidget::onMessage( const NodeMessage* Msg ) { switch ( Msg->getMsg() ) { - case NodeMessage::MouseUp: { - bool moved = false; - if ( mVScroll->isEnabled() ) { - - if ( Msg->getFlags() & EE_BUTTON_WUMASK ) { - mVScroll->setValue( mVScroll->getValue() - mVScroll->getClickStep() ); - moved = true; - } else if ( Msg->getFlags() & EE_BUTTON_WDMASK ) { - mVScroll->setValue( mVScroll->getValue() + mVScroll->getClickStep() ); - moved = true; - } - } - - if ( mHScroll->isEnabled() ) { - if ( Msg->getFlags() & EE_BUTTON_WLMASK ) { - mHScroll->setValue( mHScroll->getValue() - mHScroll->getClickStep() ); - moved = true; - } else if ( Msg->getFlags() & EE_BUTTON_WRMASK ) { - mHScroll->setValue( mHScroll->getValue() + mHScroll->getClickStep() ); - moved = true; - } - } - - if ( moved ) - return 1; - - break; - } case NodeMessage::FocusLoss: { if ( NULL != getEventDispatcher() ) { Node* focusNode = getEventDispatcher()->getFocusNode(); @@ -408,7 +384,48 @@ Uint32 UIScrollableWidget::onMessage( const NodeMessage* Msg ) { break; } } - return UIWidget::onMessage( Msg ); + return UITouchDraggableWidget::onMessage( Msg ); +} + +Uint32 UIScrollableWidget::onMouseWheel( const Vector2f& offset, bool ) { + const Vector2f maxPosition( getScrollControllerMaxPosition() ); + Vector2f delta; + Float durationScale = 1.f; + if ( getInput()->isModState( KEYMOD_SHIFT ) && offset.y != 0.f && mHScroll->isEnabled() ) { + const Float factor = getWheelScrollFactor( offset.y ); + delta.x = -factor * mHScroll->getClickStep() * maxPosition.x; + durationScale = std::abs( factor ); + } else { + if ( offset.y != 0.f && mVScroll->isEnabled() ) { + const Float factor = getWheelScrollFactor( offset.y ); + delta.y = -factor * mVScroll->getClickStep() * maxPosition.y; + durationScale = std::abs( factor ); + } else if ( offset.x != 0.f && mHScroll->isEnabled() ) { + const Float factor = getWheelScrollFactor( offset.x ); + delta.x = factor * mHScroll->getClickStep() * maxPosition.x; + durationScale = std::abs( factor ); + } + } + return scrollBy( delta, durationScale ) ? 1 : 0; +} + +bool UIScrollableWidget::supportsScrollController() const { + return true; +} + +Vector2f UIScrollableWidget::getScrollControllerPosition() const { + return mScrollOffset; +} + +Vector2f UIScrollableWidget::getScrollControllerMaxPosition() const { + const Sizef area( getScrollableArea() ); + return { eemax( 0.f, area.x ), eemax( 0.f, area.y ) }; +} + +void UIScrollableWidget::setScrollControllerPosition( const Vector2f& position ) { + const Vector2f maxPosition( getScrollControllerMaxPosition() ); + mHScroll->setValue( maxPosition.x > 0.f ? position.x / maxPosition.x : 0.f ); + mVScroll->setValue( maxPosition.y > 0.f ? position.y / maxPosition.y : 0.f ); } }} // namespace EE::UI diff --git a/src/eepp/ui/uiscrollcontroller.cpp b/src/eepp/ui/uiscrollcontroller.cpp new file mode 100644 index 000000000..eaa016960 --- /dev/null +++ b/src/eepp/ui/uiscrollcontroller.cpp @@ -0,0 +1,210 @@ +#include +#include + +namespace EE { namespace UI { + +static Vector2f clampScrollPosition( const Vector2f& position, const Vector2f& maxPosition ) { + return { eeclamp( position.x, 0.f, eemax( 0.f, maxPosition.x ) ), + eeclamp( position.y, 0.f, eemax( 0.f, maxPosition.y ) ) }; +} + +static bool isNearlyZero( Float value, Float epsilon ) { + return std::abs( value ) <= epsilon; +} + +UIScrollController::UIScrollController() = default; + +void UIScrollController::setPosition( const Vector2f& position, const Vector2f& maxPosition ) { + mPosition = clampScrollPosition( position, maxPosition ); + mTarget = mPosition; + mVelocity = Vector2f::Zero; + mSmoothTweenCount = 0; + mMode = Mode::Idle; +} + +bool UIScrollController::scrollBy( const Vector2f& delta, const Vector2f& currentPosition, + const Vector2f& maxPosition, bool smooth, Float durationScale ) { + if ( mMode != Mode::Smooth ) { + mPosition = clampScrollPosition( currentPosition, maxPosition ); + mTarget = mPosition; + } + + const Vector2f oldTarget( mTarget ); + mTarget = clampScrollPosition( mTarget + delta, maxPosition ); + mVelocity = Vector2f::Zero; + + if ( oldTarget == mTarget ) + return false; + + if ( smooth ) { + const Vector2f effectiveDelta( mTarget - oldTarget ); + const Float requestedDistance = std::sqrt( delta.x * delta.x + delta.y * delta.y ); + const Float effectiveDistance = + std::sqrt( effectiveDelta.x * effectiveDelta.x + effectiveDelta.y * effectiveDelta.y ); + const Float duration = + mWheelScrollDuration.asSeconds() * eeclamp( durationScale, 0.f, 1.f ) * + ( requestedDistance > 0.f ? effectiveDistance / requestedDistance : 1.f ); + + if ( mSmoothTweenCount > 0 && mSmoothTweens[mSmoothTweenCount - 1].elapsed == 0.f && + mSmoothTweens[mSmoothTweenCount - 1].duration == duration ) { + mSmoothTweens[mSmoothTweenCount - 1].delta += effectiveDelta; + } else if ( mSmoothTweenCount < mSmoothTweens.size() ) { + mSmoothTweens[mSmoothTweenCount++] = { effectiveDelta, 0.f, duration }; + } else { + mSmoothTweens[mSmoothTweenCount - 1].delta += effectiveDelta; + } + mMode = Mode::Smooth; + } else { + mPosition = mTarget; + mSmoothTweenCount = 0; + mMode = Mode::Idle; + } + + return true; +} + +void UIScrollController::beginDrag( const Vector2f& position, const Vector2f& maxPosition ) { + mPosition = clampScrollPosition( position, maxPosition ); + mTarget = mPosition; + mVelocity = Vector2f::Zero; + mSmoothTweenCount = 0; + mMode = Mode::Dragging; +} + +bool UIScrollController::dragBy( const Vector2f& delta, const Time& elapsed, + const Vector2f& maxPosition ) { + if ( mMode != Mode::Dragging ) + beginDrag( mPosition, maxPosition ); + + const Vector2f oldPosition( mPosition ); + mPosition = clampScrollPosition( mPosition + delta, maxPosition ); + mTarget = mPosition; + + const Float seconds = eemax( 0.f, elapsed.asSeconds() ); + if ( seconds > 0.f ) { + const Vector2f instantaneousVelocity( ( mPosition - oldPosition ) / seconds ); + const Float blend = 1.f - std::exp( -20.f * seconds ); + mVelocity += ( instantaneousVelocity - mVelocity ) * blend; + } + + return oldPosition != mPosition; +} + +void UIScrollController::endDrag() { + if ( mMode != Mode::Dragging ) + return; + + if ( isNearlyZero( mVelocity.x, 1.f ) && isNearlyZero( mVelocity.y, 1.f ) ) { + stop(); + } else { + mMode = Mode::Momentum; + } +} + +bool UIScrollController::update( const Time& elapsed, const Vector2f& maxPosition ) { + if ( mMode == Mode::Idle || mMode == Mode::Dragging ) + return false; + + const Float seconds = eemax( 0.f, elapsed.asSeconds() ); + if ( seconds <= 0.f ) + return false; + + const Vector2f oldPosition( mPosition ); + if ( mMode == Mode::Smooth ) { + mTarget = clampScrollPosition( mTarget, maxPosition ); + Uint32 activeTweens = 0; + for ( Uint32 i = 0; i < mSmoothTweenCount; ++i ) { + auto& tween = mSmoothTweens[i]; + const Float oldProgress = + tween.duration > 0.f ? eeclamp( tween.elapsed / tween.duration, 0.f, 1.f ) : 1.f; + tween.elapsed += seconds; + const Float progress = + tween.duration > 0.f ? eeclamp( tween.elapsed / tween.duration, 0.f, 1.f ) : 1.f; + const Float oldEased = 1.f - ( 1.f - oldProgress ) * ( 1.f - oldProgress ); + const Float eased = 1.f - ( 1.f - progress ) * ( 1.f - progress ); + mPosition += tween.delta * ( eased - oldEased ); + + if ( progress < 1.f ) + mSmoothTweens[activeTweens++] = tween; + } + mSmoothTweenCount = activeTweens; + mPosition = clampScrollPosition( mPosition, maxPosition ); + + if ( mSmoothTweenCount == 0 ) { + mPosition = mTarget; + mMode = Mode::Idle; + } + } else { + const Vector2f decay( std::exp( -mDragDeceleration.x * seconds ), + std::exp( -mDragDeceleration.y * seconds ) ); + const Vector2f displacement( mVelocity.x * ( 1.f - decay.x ) / mDragDeceleration.x, + mVelocity.y * ( 1.f - decay.y ) / mDragDeceleration.y ); + const Vector2f unclampedPosition( mPosition + displacement ); + mPosition = clampScrollPosition( unclampedPosition, maxPosition ); + mTarget = mPosition; + + if ( mPosition.x != unclampedPosition.x ) + mVelocity.x = 0.f; + else + mVelocity.x *= decay.x; + + if ( mPosition.y != unclampedPosition.y ) + mVelocity.y = 0.f; + else + mVelocity.y *= decay.y; + + if ( isNearlyZero( mVelocity.x, 1.f ) ) + mVelocity.x = 0.f; + if ( isNearlyZero( mVelocity.y, 1.f ) ) + mVelocity.y = 0.f; + if ( mVelocity == Vector2f::Zero ) + mMode = Mode::Idle; + } + + return oldPosition != mPosition; +} + +void UIScrollController::stop() { + mTarget = mPosition; + mVelocity = Vector2f::Zero; + mSmoothTweenCount = 0; + mMode = Mode::Idle; +} + +bool UIScrollController::isActive() const { + return mMode != Mode::Idle; +} + +bool UIScrollController::isDragging() const { + return mMode == Mode::Dragging; +} + +const Vector2f& UIScrollController::getPosition() const { + return mPosition; +} + +const Vector2f& UIScrollController::getTarget() const { + return mTarget; +} + +const Vector2f& UIScrollController::getVelocity() const { + return mVelocity; +} + +Time UIScrollController::getWheelScrollDuration() const { + return mWheelScrollDuration; +} + +void UIScrollController::setWheelScrollDuration( const Time& duration ) { + mWheelScrollDuration = Seconds( eemax( 0.001f, static_cast( duration.asSeconds() ) ) ); +} + +Vector2f UIScrollController::getDragDeceleration() const { + return mDragDeceleration; +} + +void UIScrollController::setDragDeceleration( const Vector2f& deceleration ) { + mDragDeceleration = { eemax( 0.01f, deceleration.x ), eemax( 0.01f, deceleration.y ) }; +} + +}} // namespace EE::UI diff --git a/src/eepp/ui/uiscrollview.cpp b/src/eepp/ui/uiscrollview.cpp index 0498b848b..d15d34744 100644 --- a/src/eepp/ui/uiscrollview.cpp +++ b/src/eepp/ui/uiscrollview.cpp @@ -1,6 +1,8 @@ +#include #include #include #include +#include namespace EE { namespace UI { @@ -302,6 +304,8 @@ void UIScrollView::updateScroll() { void UIScrollView::onValueChangeCb( const Event* ) { updateScroll(); + if ( !mApplyingScrollController ) + stopScrollController(); } void UIScrollView::onScrollViewSizeChange( const Event* ) { @@ -445,24 +449,45 @@ bool UIScrollView::applyProperty( const StyleSheetProperty& attribute ) { } Uint32 UIScrollView::onMessage( const NodeMessage* Msg ) { - switch ( Msg->getMsg() ) { - case NodeMessage::MouseUp: { - if ( mScrollView && mVScroll->isEnabled() && 0 != mScrollView->getSize().getHeight() && - isTouchOverAllowedChildren() && Msg->getSender()->isUINode() && - !Msg->getSender()->asType()->isScrollable() ) { - if ( Msg->getFlags() & EE_BUTTON_WUMASK ) { - mVScroll->setValue( mVScroll->getValue() - mVScroll->getClickStep() ); - return 1; - } else if ( Msg->getFlags() & EE_BUTTON_WDMASK ) { - mVScroll->setValue( mVScroll->getValue() + mVScroll->getClickStep() ); - return 1; - } - } - } - } return UITouchDraggableWidget::onMessage( Msg ); } +Uint32 UIScrollView::onMouseWheel( const Vector2f& offset, bool ) { + if ( !mScrollView || !mVScroll->isEnabled() || !isTouchOverAllowedChildren() ) + return 0; + + if ( offset.y == 0.f ) + return 0; + + const Vector2f maxPosition( getScrollControllerMaxPosition() ); + const Float factor = getWheelScrollFactor( offset.y ); + const Float delta = -factor * mVScroll->getClickStep() * maxPosition.y; + return scrollBy( { 0.f, delta }, std::abs( factor ) ) ? 1 : 0; +} + +bool UIScrollView::supportsScrollController() const { + return true; +} + +Vector2f UIScrollView::getScrollControllerPosition() const { + const Vector2f maxPosition( getScrollControllerMaxPosition() ); + return { mHScroll->isEnabled() ? mHScroll->getValue() * maxPosition.x : 0.f, + mVScroll->isEnabled() ? mVScroll->getValue() * maxPosition.y : 0.f }; +} + +Vector2f UIScrollView::getScrollControllerMaxPosition() const { + if ( !mScrollView ) + return Vector2f::Zero; + return { eemax( 0.f, mScrollView->getPixelsSize().x - mContainer->getPixelsSize().x ), + eemax( 0.f, mScrollView->getPixelsSize().y - mContainer->getPixelsSize().y ) }; +} + +void UIScrollView::setScrollControllerPosition( const Vector2f& position ) { + const Vector2f maxPosition( getScrollControllerMaxPosition() ); + mHScroll->setValue( maxPosition.x > 0.f ? position.x / maxPosition.x : 0.f ); + mVScroll->setValue( maxPosition.y > 0.f ? position.y / maxPosition.y : 0.f ); +} + bool UIScrollView::isAutoSetClipStep() const { return mAutoSetClipStep; } diff --git a/src/eepp/ui/uitouchdraggablewidget.cpp b/src/eepp/ui/uitouchdraggablewidget.cpp index adf632ed2..79e7eff61 100644 --- a/src/eepp/ui/uitouchdraggablewidget.cpp +++ b/src/eepp/ui/uitouchdraggablewidget.cpp @@ -1,5 +1,7 @@ +#include #include #include +#include #include namespace EE { namespace UI { @@ -10,6 +12,9 @@ UITouchDraggableWidget* UITouchDraggableWidget::New() { UITouchDraggableWidget::UITouchDraggableWidget( const std::string& tag ) : UIWidget( tag ), mTouchDragDeceleration( 5.f, 5.f ) { + mScrollController.setDragDeceleration( mTouchDragDeceleration ); + if ( getUISceneNode() ) + mSmoothScrollEnabled = getUISceneNode()->isSmoothScrollEnabled(); subscribeScheduledUpdate(); } @@ -33,6 +38,12 @@ bool UITouchDraggableWidget::isTouchDragEnabled() const { } UITouchDraggableWidget* UITouchDraggableWidget::setTouchDragEnabled( const bool& enable ) { + if ( !enable && isTouchDragging() ) { + setTouchDragging( false ); + if ( nullptr != getEventDispatcher() && getEventDispatcher()->getNodeDragging() == this ) + getEventDispatcher()->setNodeDragging( nullptr ); + stopScrollController(); + } writeFlag( UI_TOUCH_DRAG_ENABLED, true == enable ); return this; } @@ -52,7 +63,21 @@ Vector2f UITouchDraggableWidget::getTouchDragDeceleration() const { UITouchDraggableWidget* UITouchDraggableWidget::setTouchDragDeceleration( const Vector2f& touchDragDeceleration ) { - mTouchDragDeceleration = touchDragDeceleration; + mTouchDragDeceleration = { eemax( 0.01f, touchDragDeceleration.x ), + eemax( 0.01f, touchDragDeceleration.y ) }; + mScrollController.setDragDeceleration( mTouchDragDeceleration ); + return this; +} + +bool UITouchDraggableWidget::isSmoothScrollEnabled() const { + return mSmoothScrollEnabled; +} + +UITouchDraggableWidget* UITouchDraggableWidget::setSmoothScrollEnabled( bool enabled ) { + if ( mSmoothScrollEnabled != enabled ) { + mSmoothScrollEnabled = enabled; + stopScrollController(); + } return this; } @@ -62,70 +87,118 @@ bool UITouchDraggableWidget::isTouchOverAllowedChildren() { return isMouseOverMeOrChildren(); } +bool UITouchDraggableWidget::supportsScrollController() const { + return false; +} + +Vector2f UITouchDraggableWidget::getScrollControllerPosition() const { + return Vector2f::Zero; +} + +Vector2f UITouchDraggableWidget::getScrollControllerMaxPosition() const { + return Vector2f::Zero; +} + +void UITouchDraggableWidget::setScrollControllerPosition( const Vector2f& ) {} + +bool UITouchDraggableWidget::scrollBy( const Vector2f& delta, Float durationScale ) { + if ( !supportsScrollController() ) + return false; + + const bool moved = mScrollController.scrollBy( delta, getScrollControllerPosition(), + getScrollControllerMaxPosition(), + mSmoothScrollEnabled, durationScale ); + if ( moved && !mSmoothScrollEnabled ) { + mApplyingScrollController = true; + setScrollControllerPosition( mScrollController.getPosition() ); + mApplyingScrollController = false; + } + return moved; +} + +Float UITouchDraggableWidget::getWheelScrollFactor( Float offset ) { + return eeclamp( offset, -1.f, 1.f ); +} + +void UITouchDraggableWidget::stopScrollController() { + if ( supportsScrollController() ) { + mScrollController.setPosition( getScrollControllerPosition(), + getScrollControllerMaxPosition() ); + } else { + mScrollController.stop(); + mTouchDragVelocity = Vector2f::Zero; + } +} + void UITouchDraggableWidget::scheduledUpdate( const Time& time ) { - if ( mEnabled && mVisible && isTouchDragEnabled() && NULL != getEventDispatcher() ) { + if ( !mEnabled || !mVisible || NULL == getEventDispatcher() ) + return; + + if ( isTouchDragEnabled() ) { if ( isTouchDragging() ) { // Mouse Not Down if ( !( getEventDispatcher()->getPressTrigger() & EE_BUTTON_LMASK ) ) { setTouchDragging( false ); getEventDispatcher()->setNodeDragging( NULL ); - return; + if ( supportsScrollController() ) + mScrollController.endDrag(); } - Float ms = time.asSeconds(); - Vector2f elapsed( ms, ms ); - Vector2f Pos( getEventDispatcher()->getMousePosf() ); + if ( isTouchDragging() ) { + const Float seconds = eemax( 0.f, time.asSeconds() ); + Vector2f pos( getEventDispatcher()->getMousePosf() ); - if ( mTouchDragPoint != Pos ) { - Vector2f diff = -( mTouchDragPoint - Pos ); + if ( mTouchDragPoint != pos ) { + Vector2f diff( pos - mTouchDragPoint ); - onTouchDragValueChange( diff ); + if ( supportsScrollController() ) { + mScrollController.dragBy( -diff, time, getScrollControllerMaxPosition() ); + mApplyingScrollController = true; + setScrollControllerPosition( mScrollController.getPosition() ); + mApplyingScrollController = false; + } else { + onTouchDragValueChange( diff ); + if ( seconds > 0.f ) { + const Vector2f velocity( diff / seconds ); + const Float blend = 1.f - std::exp( -20.f * seconds ); + mTouchDragVelocity += ( velocity - mTouchDragVelocity ) * blend; + } + } - mTouchDragAcceleration += elapsed * diff; + mTouchDragPoint = pos; - mTouchDragPoint = Pos; - - getEventDispatcher()->setNodeDragging( this ); - } else if ( mTouchDragAcceleration != Vector2f::Zero ) { - mTouchDragAcceleration -= elapsed * mTouchDragDeceleration; - } - } else { - // Deaccelerate - if ( mTouchDragAcceleration.x != 0 || mTouchDragAcceleration.y != 0 ) { - Float ms = getEventDispatcher()->getLastFrameTime().asSeconds(); - - if ( 0 != mTouchDragAcceleration.x ) { - bool wasPositiveX = mTouchDragAcceleration.x >= 0; - - if ( mTouchDragAcceleration.x > 0 ) - mTouchDragAcceleration.x -= mTouchDragDeceleration.x * ms; - else - mTouchDragAcceleration.x += mTouchDragDeceleration.x * ms; - - if ( wasPositiveX && mTouchDragAcceleration.x < 0 ) - mTouchDragAcceleration.x = 0; - else if ( !wasPositiveX && mTouchDragAcceleration.x > 0 ) - mTouchDragAcceleration.x = 0; + getEventDispatcher()->setNodeDragging( this ); + } else if ( supportsScrollController() ) { + mScrollController.dragBy( Vector2f::Zero, time, + getScrollControllerMaxPosition() ); + } else if ( mTouchDragVelocity != Vector2f::Zero ) { + mTouchDragVelocity *= std::exp( -20.f * seconds ); } - - if ( 0 != mTouchDragAcceleration.y ) { - bool wasPositiveY = mTouchDragAcceleration.y >= 0; - - if ( mTouchDragAcceleration.y > 0 ) - mTouchDragAcceleration.y -= mTouchDragDeceleration.y * ms; - else - mTouchDragAcceleration.y += mTouchDragDeceleration.y * ms; - - if ( wasPositiveY && mTouchDragAcceleration.y < 0 ) - mTouchDragAcceleration.y = 0; - else if ( !wasPositiveY && mTouchDragAcceleration.y > 0 ) - mTouchDragAcceleration.y = 0; - } - - onTouchDragValueChange( mTouchDragAcceleration ); } } } + + if ( !isTouchDragging() ) { + if ( supportsScrollController() && + mScrollController.update( time, getScrollControllerMaxPosition() ) ) { + mApplyingScrollController = true; + setScrollControllerPosition( mScrollController.getPosition() ); + mApplyingScrollController = false; + } else if ( !supportsScrollController() && mTouchDragVelocity != Vector2f::Zero ) { + const Float seconds = eemax( 0.f, time.asSeconds() ); + const Vector2f decay( std::exp( -mTouchDragDeceleration.x * seconds ), + std::exp( -mTouchDragDeceleration.y * seconds ) ); + onTouchDragValueChange( + { mTouchDragVelocity.x * ( 1.f - decay.x ) / mTouchDragDeceleration.x, + mTouchDragVelocity.y * ( 1.f - decay.y ) / mTouchDragDeceleration.y } ); + mTouchDragVelocity.x *= decay.x; + mTouchDragVelocity.y *= decay.y; + if ( std::abs( mTouchDragVelocity.x ) <= 1.f ) + mTouchDragVelocity.x = 0.f; + if ( std::abs( mTouchDragVelocity.y ) <= 1.f ) + mTouchDragVelocity.y = 0.f; + } + } } Uint32 UITouchDraggableWidget::onMessage( const NodeMessage* msg ) { @@ -135,12 +208,17 @@ Uint32 UITouchDraggableWidget::onMessage( const NodeMessage* msg ) { setTouchDragging( true ); getEventDispatcher()->setNodeDragging( this ); mTouchDragPoint = getEventDispatcher()->getMousePosf(); - mTouchDragAcceleration = Vector2f( 0, 0 ); + mTouchDragVelocity = Vector2f::Zero; + if ( supportsScrollController() ) + mScrollController.beginDrag( getScrollControllerPosition(), + getScrollControllerMaxPosition() ); return 1; } else if ( msg->getMsg() == NodeMessage::MouseUp && ( msg->getFlags() & EE_BUTTON_LMASK ) && isTouchDragging() && isTouchOverAllowedChildren() ) { setTouchDragging( false ); getEventDispatcher()->setNodeDragging( nullptr ); + if ( supportsScrollController() ) + mScrollController.endDrag(); return 1; } return 0; @@ -157,6 +235,8 @@ std::string UITouchDraggableWidget::getPropertyString( const PropertyDefinition* case PropertyId::TouchDragDeceleration: return String::fromFloat( getTouchDragDeceleration().x ) + " " + String::fromFloat( getTouchDragDeceleration().y ); + case PropertyId::ScrollBehavior: + return isSmoothScrollEnabled() ? "smooth" : "instant"; default: return UIWidget::getPropertyString( propertyDef, propertyIndex ); } @@ -164,7 +244,8 @@ std::string UITouchDraggableWidget::getPropertyString( const PropertyDefinition* std::vector UITouchDraggableWidget::getPropertiesImplemented() const { auto props = UIWidget::getPropertiesImplemented(); - auto local = { PropertyId::TouchDrag, PropertyId::TouchDragDeceleration }; + auto local = { PropertyId::TouchDrag, PropertyId::TouchDragDeceleration, + PropertyId::ScrollBehavior }; props.insert( props.end(), local.begin(), local.end() ); return props; } @@ -180,6 +261,9 @@ bool UITouchDraggableWidget::applyProperty( const StyleSheetProperty& attribute case PropertyId::TouchDragDeceleration: setTouchDragDeceleration( attribute.asVector2f() ); break; + case PropertyId::ScrollBehavior: + setSmoothScrollEnabled( String::iequals( attribute.getValue(), "smooth" ) ); + break; default: return UIWidget::applyProperty( attribute ); } diff --git a/src/eepp/window/backend/SDL3/inputsdl3.cpp b/src/eepp/window/backend/SDL3/inputsdl3.cpp index bfd2e0e80..1e3b35c18 100644 --- a/src/eepp/window/backend/SDL3/inputsdl3.cpp +++ b/src/eepp/window/backend/SDL3/inputsdl3.cpp @@ -301,17 +301,24 @@ void InputSDL::sendEvent( const SDL_Event& SDLEvent ) { Uint8 button; float x = SDLEvent.wheel.x; float y = SDLEvent.wheel.y; +#if SDL_VERSION_ATLEAST( 3, 2, 12 ) + const Sint32 integerX = SDLEvent.wheel.integer_x; + const Sint32 integerY = SDLEvent.wheel.integer_y; +#else + const Sint32 integerX = x > 0.f ? 1 : ( x < 0.f ? -1 : 0 ); + const Sint32 integerY = y > 0.f ? 1 : ( y < 0.f ? -1 : 0 ); +#endif - if ( y == 0 && x == 0 ) + if ( integerY == 0 && integerX == 0 ) break; - if ( y > 0 ) { + if ( integerY > 0 ) { button = EE_BUTTON_WHEELUP; - } else if ( y < 0 ) { + } else if ( integerY < 0 ) { button = EE_BUTTON_WHEELDOWN; - } else if ( x > 0 ) { + } else if ( integerX > 0 ) { button = EE_BUTTON_WHEELRIGHT; - } else if ( x < 0 ) { + } else if ( integerX < 0 ) { button = EE_BUTTON_WHEELLEFT; } else { return; diff --git a/src/eepp/window/platform/macos/platformhelper.hpp b/src/eepp/window/platform/macos/platformhelper.hpp new file mode 100644 index 000000000..4582323ba --- /dev/null +++ b/src/eepp/window/platform/macos/platformhelper.hpp @@ -0,0 +1,16 @@ +#ifndef EE_WINDOW_PLATFORM_MACOS_PLATFORMHELPER_HPP +#define EE_WINDOW_PLATFORM_MACOS_PLATFORMHELPER_HPP + +#include + +namespace EE { namespace Window { namespace Private { + +bool setNativeScrollMomentumEnabled( bool enabled ); + +bool setWindowTitleBarSeparatorVisible( void* window, bool visible ); + +bool setWindowTitleBarColor( void* window, Uint8 red, Uint8 green, Uint8 blue ); + +}}} // namespace EE::Window::Private + +#endif // EE_WINDOW_PLATFORM_MACOS_PLATFORMHELPER_HPP diff --git a/src/eepp/window/platform/macos/platformhelper.mm b/src/eepp/window/platform/macos/platformhelper.mm new file mode 100644 index 000000000..9f7930c60 --- /dev/null +++ b/src/eepp/window/platform/macos/platformhelper.mm @@ -0,0 +1,38 @@ +#import +#include + +namespace EE { namespace Window { namespace Private { + +bool setNativeScrollMomentumEnabled( bool enabled ) { + [[NSUserDefaults standardUserDefaults] setBool:enabled ? YES : NO + forKey:@"AppleMomentumScrollSupported"]; + return true; +} + +bool setWindowTitleBarSeparatorVisible( void* window, bool visible ) { + if ( nullptr == window ) + return false; + + if ( @available( macOS 11.0, * ) ) { + NSWindow* nsWindow = static_cast( window ); + [nsWindow setTitlebarSeparatorStyle:visible ? NSTitlebarSeparatorStyleAutomatic + : NSTitlebarSeparatorStyleNone]; + return true; + } + return false; +} + +bool setWindowTitleBarColor( void* window, Uint8 red, Uint8 green, Uint8 blue ) { + if ( nullptr == window ) + return false; + + NSWindow* nsWindow = static_cast( window ); + nsWindow.titlebarAppearsTransparent = YES; + nsWindow.backgroundColor = [NSColor colorWithRed:static_cast( red ) / 255.f + green:static_cast( green ) / 255.f + blue:static_cast( blue ) / 255.f + alpha:1.f]; + return true; +} + +}}} // namespace EE::Window::Private diff --git a/src/eepp/window/platformhelper.cpp b/src/eepp/window/platformhelper.cpp new file mode 100644 index 000000000..99dcbf433 --- /dev/null +++ b/src/eepp/window/platformhelper.cpp @@ -0,0 +1,40 @@ +#include + +#if EE_PLATFORM == EE_PLATFORM_MACOS +#include +#endif + +namespace EE { namespace Window { + +bool PlatformHelper::setNativeScrollMomentumEnabled( bool enabled ) { +#if EE_PLATFORM == EE_PLATFORM_MACOS + return Private::setNativeScrollMomentumEnabled( enabled ); +#else + (void)enabled; + return false; +#endif +} + +bool PlatformHelper::setWindowTitleBarSeparatorVisible( void* window, bool visible ) { +#if EE_PLATFORM == EE_PLATFORM_MACOS + return Private::setWindowTitleBarSeparatorVisible( window, visible ); +#else + (void)window; + (void)visible; + return false; +#endif +} + +bool PlatformHelper::setWindowTitleBarColor( void* window, Uint8 red, Uint8 green, Uint8 blue ) { +#if EE_PLATFORM == EE_PLATFORM_MACOS + return Private::setWindowTitleBarColor( window, red, green, blue ); +#else + (void)window; + (void)red; + (void)green; + (void)blue; + return false; +#endif +} + +}} // namespace EE::Window diff --git a/src/modules/eterm/include/eterm/ui/uiterminal.hpp b/src/modules/eterm/include/eterm/ui/uiterminal.hpp index ec30aeec1..ac2aede37 100644 --- a/src/modules/eterm/include/eterm/ui/uiterminal.hpp +++ b/src/modules/eterm/include/eterm/ui/uiterminal.hpp @@ -4,7 +4,7 @@ #include #include #include -#include +#include #include using namespace EE::UI; @@ -14,7 +14,7 @@ namespace eterm { namespace UI { class UITerminalFind; -class UITerminal : public UIWidget { +class UITerminal : public UITouchDraggableWidget { public: static UITerminal* New( Font* font, const Float& fontSize, const Sizef& pixelsSize, const std::string& program = "", @@ -157,6 +157,8 @@ class UITerminal : public UIWidget { virtual Uint32 onMouseUp( const Vector2i& position, const Uint32& flags ); + virtual Uint32 onMouseWheel( const Vector2f& offset, bool flipped ); + virtual void onPositionChange(); virtual void onSizeChange(); @@ -192,6 +194,14 @@ class UITerminal : public UIWidget { virtual void onScrollChange(); + virtual bool supportsScrollController() const; + + virtual Vector2f getScrollControllerPosition() const; + + virtual Vector2f getScrollControllerMaxPosition() const; + + virtual void setScrollControllerPosition( const Vector2f& position ); + void registerNewTerminal(); }; diff --git a/src/modules/eterm/src/eterm/ui/uiterminal.cpp b/src/modules/eterm/src/eterm/ui/uiterminal.cpp index 4ca7309f8..b6cf51cf4 100644 --- a/src/modules/eterm/src/eterm/ui/uiterminal.cpp +++ b/src/modules/eterm/src/eterm/ui/uiterminal.cpp @@ -1,3 +1,4 @@ +#include #include #include #include @@ -42,7 +43,7 @@ Uint32 UITerminal::getType() const { } bool UITerminal::isType( const Uint32& type ) const { - return getType() == type || UIWidget::isType( type ); + return getType() == type || UITouchDraggableWidget::isType( type ); } void UITerminal::draw() { @@ -81,7 +82,7 @@ void UITerminal::registerNewTerminal() { } UITerminal::UITerminal( const std::shared_ptr& terminalDisplay ) : - UIWidget( "terminal" ), + UITouchDraggableWidget( "terminal" ), mKeyBindings( getInput() ), mVScroll( UIScrollBar::NewVertical() ), mTerm( terminalDisplay ) { @@ -265,12 +266,12 @@ std::string UITerminal::getPropertyString( const PropertyDefinition* propertyDef case PropertyId::ScrollBarMode: return getScrollViewType() == ScrollViewType::Overlay ? "overlay" : "outside"; default: - return UIWidget::getPropertyString( propertyDef, propertyIndex ); + return UITouchDraggableWidget::getPropertyString( propertyDef, propertyIndex ); } } std::vector UITerminal::getPropertiesImplemented() const { - auto props = UIWidget::getPropertiesImplemented(); + auto props = UITouchDraggableWidget::getPropertiesImplemented(); auto local = { PropertyId::VScrollMode, PropertyId::ScrollBarStyle, PropertyId::ScrollBarMode }; props.insert( props.end(), local.begin(), local.end() ); return props; @@ -332,7 +333,7 @@ bool UITerminal::applyProperty( const StyleSheetProperty& attribute ) { break; } default: - return UIWidget::applyProperty( attribute ); + return UITouchDraggableWidget::applyProperty( attribute ); } return true; @@ -342,7 +343,8 @@ const std::shared_ptr& UITerminal::getTerm() const { return mTerm; } -void UITerminal::scheduledUpdate( const Time& ) { +void UITerminal::scheduledUpdate( const Time& time ) { + UITouchDraggableWidget::scheduledUpdate( time ); if ( !mTerm ) return; auto terminal = mTerm; @@ -558,10 +560,62 @@ Uint32 UITerminal::onMouseUp( const Vector2i& position, const Uint32& flags ) { onCreateContextMenu( position, flags ); return 1; } + const Uint32 modifiers = getInput()->getSanitizedModState(); + if ( ( modifiers == 0 || modifiers == KEYMOD_SHIFT ) && + ( flags & ( EE_BUTTON_WUMASK | EE_BUTTON_WDMASK | EE_BUTTON_WLMASK | EE_BUTTON_WRMASK ) ) ) + return 1; mTerm->onMouseUp( position, flags ); return 1; } +Uint32 UITerminal::onMouseWheel( const Vector2f& offset, bool ) { + const Uint32 modifiers = getInput()->getSanitizedModState(); + if ( modifiers != 0 && modifiers != KEYMOD_SHIFT ) + return 1; + if ( offset.y == 0.f ) + return 0; + + const Float rows = modifiers == KEYMOD_SHIFT ? getVisibleArea() : 1.f; + const Float multiplier = rows * mTerm->getLineHeight(); + const Float factor = getWheelScrollFactor( offset.y ); + return scrollBy( { 0.f, -factor * multiplier }, std::abs( factor ) ) ? 1 : 0; +} + +bool UITerminal::supportsScrollController() const { + return true; +} + +Vector2f UITerminal::getScrollControllerPosition() const { + const int scrollableArea = eemax( 0, getScrollableArea() ); + const Float lineHeight = mTerm ? mTerm->getLineHeight() : 0.f; + return { 0.f, mTerm ? eeclamp( static_cast( scrollableArea ) - + static_cast( mTerm->scrollPosition() ), + 0.f, static_cast( scrollableArea ) ) * + lineHeight + : 0.f }; +} + +Vector2f UITerminal::getScrollControllerMaxPosition() const { + return { 0.f, static_cast( eemax( 0, getScrollableArea() ) ) * + ( mTerm ? mTerm->getLineHeight() : 0.f ) }; +} + +void UITerminal::setScrollControllerPosition( const Vector2f& position ) { + const int scrollableArea = getScrollableArea(); + const Float lineHeight = mTerm ? mTerm->getLineHeight() : 0.f; + if ( scrollableArea <= 0 || lineHeight <= 0.f ) + return; + + const int scrollOffset = static_cast( + eefloor( eeclamp( position.y / lineHeight, 0.f, static_cast( scrollableArea ) ) ) ); + if ( scrollOffset == mScrollOffset ) + return; + + mScrollOffset = scrollOffset; + mVScroll->setValue( static_cast( mScrollOffset ) / scrollableArea, false ); + onScrollChange(); +} + void UITerminal::onPositionChange() { mTerm->setPosition( mScreenPos.trunc() ); UIWidget::onPositionChange(); diff --git a/src/tests/unit_tests/uiscrollcontroller_tests.cpp b/src/tests/unit_tests/uiscrollcontroller_tests.cpp new file mode 100644 index 000000000..ec9a8e29a --- /dev/null +++ b/src/tests/unit_tests/uiscrollcontroller_tests.cpp @@ -0,0 +1,131 @@ +#include "utest.h" +#include + +using namespace EE; +using namespace EE::System; +using namespace EE::UI; + +UTEST( UIScrollController, InstantScrollRetainsFractionalInputAndClamps ) { + UIScrollController controller; + const Vector2f maximum( 100.f, 200.f ); + + EXPECT_TRUE( controller.scrollBy( { 4.25f, -20.f }, { 10.5f, 5.5f }, maximum, false ) ); + EXPECT_NEAR( 14.75f, controller.getPosition().x, 0.0001f ); + EXPECT_NEAR( 0.f, controller.getPosition().y, 0.0001f ); + EXPECT_FALSE( controller.isActive() ); + EXPECT_FALSE( controller.scrollBy( { 200.f, -1.f }, { 100.f, 0.f }, maximum, false ) ); +} + +UTEST( UIScrollController, SmoothScrollAccumulatesTargetsWithoutJumping ) { + UIScrollController controller; + const Vector2f maximum( 500.f, 500.f ); + + EXPECT_TRUE( controller.scrollBy( { 0.f, 100.f }, Vector2f::Zero, maximum, true ) ); + EXPECT_TRUE( controller.scrollBy( { 0.f, 25.5f }, Vector2f::Zero, maximum, true ) ); + EXPECT_NEAR( 0.f, controller.getPosition().y, 0.0001f ); + EXPECT_NEAR( 125.5f, controller.getTarget().y, 0.0001f ); + EXPECT_TRUE( controller.isActive() ); + + controller.update( Milliseconds( 16 ), maximum ); + EXPECT_GT( controller.getPosition().y, 0.f ); + EXPECT_LT( controller.getPosition().y, controller.getTarget().y ); + for ( size_t i = 0; i < 100; ++i ) + controller.update( Milliseconds( 16 ), maximum ); + EXPECT_NEAR( 125.5f, controller.getPosition().y, 0.0001f ); + EXPECT_FALSE( controller.isActive() ); +} + +UTEST( UIScrollController, SmoothWheelStepsOverlapInsteadOfExtendingGestureDuration ) { + UIScrollController controller; + const Vector2f maximum( 500.f, 500.f ); + controller.setWheelScrollDuration( Milliseconds( 100 ) ); + + controller.scrollBy( { 0.f, 10.f }, Vector2f::Zero, maximum, true ); + controller.update( Milliseconds( 50 ), maximum ); + EXPECT_NEAR( 7.5f, controller.getPosition().y, 0.0001f ); + + controller.scrollBy( { 0.f, 10.f }, controller.getPosition(), maximum, true ); + controller.update( Milliseconds( 50 ), maximum ); + EXPECT_NEAR( 17.5f, controller.getPosition().y, 0.0001f ); + EXPECT_TRUE( controller.isActive() ); + + controller.update( Milliseconds( 50 ), maximum ); + EXPECT_NEAR( 20.f, controller.getPosition().y, 0.0001f ); + EXPECT_FALSE( controller.isActive() ); +} + +UTEST( UIScrollController, PartialWheelStepUsesProportionallyShorterTween ) { + UIScrollController controller; + const Vector2f maximum( 100.f, 100.f ); + controller.setWheelScrollDuration( Milliseconds( 100 ) ); + + controller.scrollBy( { 0.f, 10.f }, { 0.f, 95.f }, maximum, true ); + controller.update( Milliseconds( 25 ), maximum ); + EXPECT_NEAR( 98.75f, controller.getPosition().y, 0.0001f ); + EXPECT_TRUE( controller.isActive() ); + + controller.update( Milliseconds( 25 ), maximum ); + EXPECT_NEAR( 100.f, controller.getPosition().y, 0.0001f ); + EXPECT_FALSE( controller.isActive() ); +} + +UTEST( UIScrollController, SubTickInputScalesTweenDuration ) { + UIScrollController controller; + const Vector2f maximum( 100.f, 100.f ); + controller.setWheelScrollDuration( Milliseconds( 100 ) ); + + controller.scrollBy( { 0.f, 2.5f }, Vector2f::Zero, maximum, true, 0.25f ); + controller.update( Milliseconds( 12.5 ), maximum ); + EXPECT_NEAR( 1.875f, controller.getPosition().y, 0.0001f ); + EXPECT_TRUE( controller.isActive() ); + + controller.update( Milliseconds( 12.5 ), maximum ); + EXPECT_NEAR( 2.5f, controller.getPosition().y, 0.0001f ); + EXPECT_FALSE( controller.isActive() ); +} + +static UIScrollController smoothControllerAfter( size_t frames, const Time& elapsed ) { + UIScrollController controller; + controller.scrollBy( { 0.f, 300.f }, Vector2f::Zero, { 1000.f, 1000.f }, true ); + for ( size_t i = 0; i < frames; ++i ) + controller.update( elapsed, { 1000.f, 1000.f } ); + return controller; +} + +UTEST( UIScrollController, SmoothMotionIsFrameRateIndependent ) { + const auto atSixtyHz = smoothControllerAfter( 15, Seconds( 1. / 60. ) ); + const auto atOneTwentyHz = smoothControllerAfter( 30, Seconds( 1. / 120. ) ); + EXPECT_NEAR( atSixtyHz.getPosition().y, atOneTwentyHz.getPosition().y, 0.001f ); +} + +static UIScrollController momentumControllerAfter( size_t frames, const Time& elapsed ) { + UIScrollController controller; + const Vector2f maximum( 1000.f, 1000.f ); + controller.beginDrag( { 500.f, 500.f }, maximum ); + controller.dragBy( { 0.f, -20.f }, Milliseconds( 16 ), maximum ); + controller.endDrag(); + for ( size_t i = 0; i < frames; ++i ) + controller.update( elapsed, maximum ); + return controller; +} + +UTEST( UIScrollController, DragReleaseProducesFrameRateIndependentMomentum ) { + const auto atSixtyHz = momentumControllerAfter( 30, Seconds( 1. / 60. ) ); + const auto atOneTwentyHz = momentumControllerAfter( 60, Seconds( 1. / 120. ) ); + EXPECT_LT( atSixtyHz.getPosition().y, 480.f ); + EXPECT_NEAR( atSixtyHz.getPosition().y, atOneTwentyHz.getPosition().y, 0.001f ); + EXPECT_NEAR( atSixtyHz.getVelocity().y, atOneTwentyHz.getVelocity().y, 0.001f ); +} + +UTEST( UIScrollController, MomentumStopsAtScrollBounds ) { + UIScrollController controller; + const Vector2f maximum( 100.f, 100.f ); + controller.beginDrag( { 50.f, 5.f }, maximum ); + controller.dragBy( { 0.f, -20.f }, Milliseconds( 16 ), maximum ); + controller.endDrag(); + controller.update( Milliseconds( 16 ), maximum ); + + EXPECT_NEAR( 0.f, controller.getPosition().y, 0.0001f ); + EXPECT_NEAR( 0.f, controller.getVelocity().y, 0.0001f ); + EXPECT_FALSE( controller.isActive() ); +} diff --git a/src/tests/unit_tests/uiscrolling_tests.cpp b/src/tests/unit_tests/uiscrolling_tests.cpp new file mode 100644 index 000000000..4459b410f --- /dev/null +++ b/src/tests/unit_tests/uiscrolling_tests.cpp @@ -0,0 +1,88 @@ +#include "utest.h" +#include +#include +#include +#include + +using namespace EE; +using namespace EE::System; +using namespace EE::UI; + +class ScrollingTestEditor : public UICodeEditor { + public: + ScrollingTestEditor() : UICodeEditor() {} + + using UICodeEditor::onMouseWheel; +}; + +UTEST( UIScrolling, SceneSmoothScrollingDefaultsCanBeInheritedSnapshottedAndAppliedNow ) { + UIApplication app( + WindowSettings{ 320, 240, "eepp - scrolling test" }, + UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) ); + auto* scene = app.getUI(); + auto* editor = UICodeEditor::New(); + editor->setPixelsSize( 160, 80 ); + editor->setParent( scene->getRoot() ); + editor->getDocument().textInput( String( 100, '\n' ) ); + scene->flushDirtyStyleAndLayout(); + + EXPECT_FALSE( editor->isSmoothScrollEnabled() ); + scene->setSmoothScrollEnabled( true ); + EXPECT_TRUE( scene->isSmoothScrollEnabled() ); + EXPECT_FALSE( editor->isSmoothScrollEnabled() ); + + auto* newEditor = UICodeEditor::New(); + newEditor->setParent( scene->getRoot() ); + EXPECT_TRUE( newEditor->isSmoothScrollEnabled() ); + newEditor->setSmoothScrollEnabled( false ); + EXPECT_FALSE( newEditor->isSmoothScrollEnabled() ); + + auto* embeddedHost = UIWidget::New(); + embeddedHost->setParent( scene->getRoot() ); + auto* embeddedScene = UISceneNode::New(); + embeddedScene->setParent( embeddedHost ); + EXPECT_TRUE( embeddedScene->isSmoothScrollEnabled() ); + UICodeEditor* embeddedEditor; + { + auto context = embeddedScene->makeCurrent(); + embeddedEditor = UICodeEditor::New(); + embeddedEditor->setParent( embeddedScene->getRoot() ); + } + EXPECT_TRUE( embeddedEditor->isSmoothScrollEnabled() ); + + embeddedScene->setSmoothScrollEnabled( false, true ); + EXPECT_FALSE( scene->isSmoothScrollEnabled() ); + EXPECT_FALSE( embeddedScene->isSmoothScrollEnabled() ); + EXPECT_FALSE( editor->isSmoothScrollEnabled() ); + EXPECT_FALSE( newEditor->isSmoothScrollEnabled() ); + EXPECT_FALSE( embeddedEditor->isSmoothScrollEnabled() ); + + editor->setSmoothScrollEnabled( true ); + EXPECT_TRUE( editor->isSmoothScrollEnabled() ); + ASSERT_GT( editor->getMaxScroll().y, 10.75f ); + editor->setScroll( { 0.f, 10.75f } ); + EXPECT_NEAR( 10.f, editor->getScroll().y, 0.0001f ); + + eeDelete( editor ); +} + +UTEST( UIScrolling, CodeEditorKeepsLegacyWheelStepButSoftensFractionalMomentumTail ) { + UIApplication app( + WindowSettings{ 320, 240, "eepp - wheel precision test" }, + UIApplication::Settings( Sys::getProcessPath() + ".." + FileSystem::getOSSlash(), 1 ) ); + auto* editor = eeNew( ScrollingTestEditor, () ); + editor->setPixelsSize( 160, 80 ); + editor->setParent( app.getUI()->getRoot() ); + editor->getDocument().textInput( String( 100, '\n' ) ); + editor->setMouseWheelScroll( 50.f ); + app.getUI()->flushDirtyStyleAndLayout(); + + editor->setScrollY( 100.f ); + EXPECT_EQ( 1u, editor->onMouseWheel( { 0.f, -0.1f }, false ) ); + EXPECT_NEAR( 105.f, editor->getScroll().y, 0.0001f ); + + EXPECT_EQ( 1u, editor->onMouseWheel( { 0.f, -4.f }, false ) ); + EXPECT_NEAR( 155.f, editor->getScroll().y, 0.0001f ); + + eeDelete( editor ); +} diff --git a/src/tools/ecode/appconfig.cpp b/src/tools/ecode/appconfig.cpp index 22d0b0436..5f017f290 100644 --- a/src/tools/ecode/appconfig.cpp +++ b/src/tools/ecode/appconfig.cpp @@ -157,6 +157,10 @@ void AppConfig::load( const std::string& confPath, std::string& keybindingsPath, ui.openProjectInNewWindow = ini.getValueB( "ui", "open_project_in_new_window", false ); ui.nativeFileDialogs = ini.getValueB( "ui", "native_file_dialogs", false ); ui.imagesQuickPreview = ini.getValueB( "ui", "images_quick_preview", false ); + ui.smoothScroll = ini.getValueB( + "ui", "smooth_scroll", + ini.getValueB( "editor", "smooth_scroll", + ini.getValueB( "terminal", "smooth_scroll", false ) ) ); ui.panelPosition = panelPositionFromString( ini.getValue( "ui", "panel_position", "left" ) ); ui.sansSerifFont = ini.getValue( "ui", "serif_font", "fonts/NotoSans-Regular.ttf" ); ui.monospaceFont = ini.getValue( "ui", "monospace_font", "fonts/DejaVuSansMono.ttf" ); @@ -374,6 +378,9 @@ void AppConfig::save( const std::vector& recentFiles, ini.setValueB( "ui", "open_project_in_new_window", ui.openProjectInNewWindow ); ini.setValueB( "ui", "native_file_dialogs", ui.nativeFileDialogs ); ini.setValueB( "ui", "images_quick_preview", ui.imagesQuickPreview ); + ini.setValueB( "ui", "smooth_scroll", ui.smoothScroll ); + ini.deleteValue( "editor", "smooth_scroll" ); + ini.deleteValue( "terminal", "smooth_scroll" ); ini.setValue( "ui", "panel_position", panelPositionToString( ui.panelPosition ) ); ini.setValue( "ui", "serif_font", ui.sansSerifFont ); ini.setValue( "ui", "monospace_font", ui.monospaceFont ); @@ -479,7 +486,6 @@ void AppConfig::save( const std::vector& recentFiles, term.scrollBarMode == ScrollBarMode::Auto ? "auto"sv : ( term.scrollBarMode == ScrollBarMode::AlwaysOn ? "on"sv : "off" ) ); - ini.setValueB( "window", "vsync", context.VSync ); ini.setValue( "window", "glversion", Renderer::graphicsLibraryVersionToString( context.Version ) ); diff --git a/src/tools/ecode/appconfig.hpp b/src/tools/ecode/appconfig.hpp index df40d3d25..324ce1eef 100644 --- a/src/tools/ecode/appconfig.hpp +++ b/src/tools/ecode/appconfig.hpp @@ -72,6 +72,7 @@ struct UIConfig { bool nativeFileDialogs{ false }; bool imagesQuickPreview{ false }; bool editorFontInInputFields{ true }; + bool smoothScroll{ false }; PanelPosition panelPosition{ PanelPosition::Left }; std::string sansSerifFont; std::string monospaceFont; diff --git a/src/tools/ecode/ecode.cpp b/src/tools/ecode/ecode.cpp index 393851c28..8102de4b0 100644 --- a/src/tools/ecode/ecode.cpp +++ b/src/tools/ecode/ecode.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include @@ -55,10 +56,6 @@ using namespace std::literals; namespace fs = std::filesystem; using json = nlohmann::json; -#if EE_PLATFORM == EE_PLATFORM_MACOS -#include "macos/macos.hpp" -#endif - namespace ecode { Clock globalClock; @@ -2159,77 +2156,85 @@ static Uint32 DefaultSwitchToStatusPanelModifier = KeyMod::getDefaultSecondaryMo KeyBindings::ShortcutMap App::getLocalKeybindings() { return { { { KEY_PRINTSCREEN, KEYMOD_NONE }, "take-screenshot" }, - { { KEY_RETURN, KeyMod::getDefaultSecondaryModifier() | KeyMod::getDefaultModifier() }, - "fullscreen-toggle" }, - { { KEY_F3, KEYMOD_NONE }, "repeat-find" }, - { { KEY_F3, KEYMOD_SHIFT }, "find-prev" }, - { { KEY_F12, KEYMOD_NONE }, "console-toggle" }, - { { KEY_F, KeyMod::getDefaultModifier() }, "find-replace" }, - { { KEY_Q, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "close-app" }, - { { KEY_O, KeyMod::getDefaultModifier() }, "open-file" }, - { { KEY_W, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "download-file-web" }, - { { KEY_O, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "open-folder" }, - { { KEY_F11, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "debug-widget-tree-view" }, - { { KEY_K, KeyMod::getDefaultModifier() }, "open-locatebar" }, - { { KEY_P, KeyMod::getDefaultModifier() }, "open-command-palette" }, - { { KEY_COMMA, KeyMod::getDefaultModifier() }, "open-settings" }, - { { KEY_COMMA, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "open-project-settings" }, - { { KEY_F, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "open-global-search" }, - { { KEY_L, KeyMod::getDefaultModifier() }, "go-to-line" }, + { { KEY_RETURN, KeyMod::getDefaultSecondaryModifier() | KeyMod::getDefaultModifier() }, + "fullscreen-toggle" }, + { { KEY_F3, KEYMOD_NONE }, "repeat-find" }, { { KEY_F3, KEYMOD_SHIFT }, "find-prev" }, + { { KEY_F12, KEYMOD_NONE }, "console-toggle" }, + { { KEY_F, KeyMod::getDefaultModifier() }, "find-replace" }, + { { KEY_Q, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "close-app" }, + { { KEY_O, KeyMod::getDefaultModifier() }, "open-file" }, + { { KEY_W, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "download-file-web" }, + { { KEY_O, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "open-folder" }, + { { KEY_F11, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "debug-widget-tree-view" }, + { { KEY_K, KeyMod::getDefaultModifier() }, "open-locatebar" }, + { { KEY_P, KeyMod::getDefaultModifier() }, "open-command-palette" }, + { { KEY_COMMA, KeyMod::getDefaultModifier() }, "open-settings" }, + { { KEY_COMMA, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "open-project-settings" }, + { { KEY_F, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "open-global-search" }, + { { KEY_L, KeyMod::getDefaultModifier() }, "go-to-line" }, #if EE_PLATFORM == EE_PLATFORM_MACOS - { { KEY_M, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "menu-toggle" }, + { { KEY_M, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "menu-toggle" }, #else - { { KEY_M, KeyMod::getDefaultModifier() }, "menu-toggle" }, + { { KEY_M, KeyMod::getDefaultModifier() }, "menu-toggle" }, #endif - { { KEY_S, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "save-all" }, - { { KEY_F9, KeyMod::getDefaultSecondaryModifier() }, "switch-side-panel" }, - { { KEY_J, - KeyMod::getDefaultModifier() | KeyMod::getDefaultSecondaryModifier() | KEYMOD_SHIFT }, - "terminal-split-left" }, - { { KEY_L, - KeyMod::getDefaultModifier() | KeyMod::getDefaultSecondaryModifier() | KEYMOD_SHIFT }, - "terminal-split-right" }, - { { KEY_I, - KeyMod::getDefaultModifier() | KeyMod::getDefaultSecondaryModifier() | KEYMOD_SHIFT }, - "terminal-split-top" }, - { { KEY_K, - KeyMod::getDefaultModifier() | KeyMod::getDefaultSecondaryModifier() | KEYMOD_SHIFT }, - "terminal-split-bottom" }, - { { KEY_S, - KeyMod::getDefaultModifier() | KeyMod::getDefaultSecondaryModifier() | KEYMOD_SHIFT }, - "terminal-split-swap" }, - { { KEY_T, - KeyMod::getDefaultModifier() | KeyMod::getDefaultSecondaryModifier() | KEYMOD_SHIFT }, - "reopen-closed-tab" }, - { { KEY_1, DefaultSwitchToStatusPanelModifier }, "toggle-status-locate-bar" }, - { { KEY_2, DefaultSwitchToStatusPanelModifier }, "toggle-status-global-search-bar" }, - { { KEY_3, DefaultSwitchToStatusPanelModifier }, "toggle-status-terminal" }, - { { KEY_4, DefaultSwitchToStatusPanelModifier }, "toggle-status-build-output" }, - { { KEY_5, DefaultSwitchToStatusPanelModifier }, "toggle-status-app-output" }, - { { KEY_B, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "project-build-start-cancel" }, - { { KEY_C, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "project-build-cancel" }, - { { KEY_R, KeyMod::getDefaultModifier() }, "project-build-and-run" }, - { { KEY_O, KeyMod::getDefaultSecondaryModifier() | KEYMOD_SHIFT }, "show-open-documents" }, - { { KEY_K, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "open-workspace-symbol-search" }, - { { KEY_P, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "open-document-symbol-search" }, - { { KEY_N, KEYMOD_SHIFT | KeyMod::getDefaultSecondaryModifier() }, "create-new-window" }, + { { KEY_S, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "save-all" }, + { { KEY_F9, KeyMod::getDefaultSecondaryModifier() }, "switch-side-panel" }, + { { KEY_J, KeyMod::getDefaultModifier() | KeyMod::getDefaultSecondaryModifier() | + KEYMOD_SHIFT }, + "terminal-split-left" }, + { { KEY_L, KeyMod::getDefaultModifier() | KeyMod::getDefaultSecondaryModifier() | + KEYMOD_SHIFT }, + "terminal-split-right" }, + { { KEY_I, KeyMod::getDefaultModifier() | KeyMod::getDefaultSecondaryModifier() | + KEYMOD_SHIFT }, + "terminal-split-top" }, + { { KEY_K, KeyMod::getDefaultModifier() | KeyMod::getDefaultSecondaryModifier() | + KEYMOD_SHIFT }, + "terminal-split-bottom" }, + { { KEY_S, KeyMod::getDefaultModifier() | KeyMod::getDefaultSecondaryModifier() | + KEYMOD_SHIFT }, + "terminal-split-swap" }, + { { KEY_T, KeyMod::getDefaultModifier() | KeyMod::getDefaultSecondaryModifier() | + KEYMOD_SHIFT }, + "reopen-closed-tab" }, + { { KEY_1, DefaultSwitchToStatusPanelModifier }, "toggle-status-locate-bar" }, + { { KEY_2, DefaultSwitchToStatusPanelModifier }, "toggle-status-global-search-bar" }, + { { KEY_3, DefaultSwitchToStatusPanelModifier }, "toggle-status-terminal" }, + { { KEY_4, DefaultSwitchToStatusPanelModifier }, "toggle-status-build-output" }, + { { KEY_5, DefaultSwitchToStatusPanelModifier }, "toggle-status-app-output" }, + { { KEY_B, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, + "project-build-start-cancel" }, + { { KEY_C, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, "project-build-cancel" }, + { { KEY_R, KeyMod::getDefaultModifier() }, "project-build-and-run" }, + { { KEY_O, KeyMod::getDefaultSecondaryModifier() | KEYMOD_SHIFT }, + "show-open-documents" }, + { { KEY_K, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, + "open-workspace-symbol-search" }, + { { KEY_P, KeyMod::getDefaultModifier() | KEYMOD_SHIFT }, + "open-document-symbol-search" }, + { { KEY_N, KEYMOD_SHIFT | KeyMod::getDefaultSecondaryModifier() }, + "create-new-window" }, }; } // Old keybindings will be rebinded to the new keybindings when they are still set to the old // keybindind std::map App::getMigrateKeybindings() { - return { { "fullscreen-toggle", "alt+return" }, { "switch-to-tab-1", "alt+1" }, - { "switch-to-tab-2", "alt+2" }, { "switch-to-tab-3", "alt+3" }, - { "switch-to-tab-4", "alt+4" }, { "switch-to-tab-5", "alt+5" }, - { "switch-to-tab-6", "alt+6" }, { "switch-to-tab-7", "alt+7" }, - { "switch-to-tab-8", "alt+8" }, { "switch-to-tab-9", "alt+9" }, - { "switch-to-last-tab", "alt+0" }, + return { + { "fullscreen-toggle", "alt+return" }, { "switch-to-tab-1", "alt+1" }, + { "switch-to-tab-2", "alt+2" }, { "switch-to-tab-3", "alt+3" }, + { "switch-to-tab-4", "alt+4" }, { "switch-to-tab-5", "alt+5" }, + { "switch-to-tab-6", "alt+6" }, { "switch-to-tab-7", "alt+7" }, + { "switch-to-tab-8", "alt+8" }, { "switch-to-tab-9", "alt+9" }, + { "switch-to-last-tab", "alt+0" }, #if EE_PLATFORM == EE_PLATFORM_MACOS - { "menu-toggle", "mod+shift+m" }, + { "menu-toggle", "mod+shift+m" }, #endif - { "lock-toggle", "mod+shift+l" }, { "debug-widget-tree-view", "f11" }, - { "project-build-and-run", "f5" }, { "project-build-start", "mod+shift+b" } }; + { "lock-toggle", "mod+shift+l" }, { "debug-widget-tree-view", "f11" }, + { "project-build-and-run", "f5" }, { + "project-build-start", "mod+shift+b" + } + }; } std::vector App::getUnlockedCommands() { @@ -4518,8 +4523,8 @@ void App::tintTitleBar() { ->getVariableByName( "--back" ); if ( !backVar.isEmpty() ) { auto backColor( Color::fromString( backVar.getValue() ) ); - macOS_changeTitleBarColor( mWindow->getWindowHandler(), backColor.r / 255.f, - backColor.g / 255.f, backColor.b / 255.f ); + Engine::instance()->getPlatformHelper()->setWindowTitleBarColor( + mWindow->getWindowHandler(), backColor.r, backColor.g, backColor.b ); } } #endif @@ -4748,8 +4753,9 @@ void App::init( InitParameters& params ) { EE_PLATFORM == EE_PLATFORM_BSD #if EE_PLATFORM == EE_PLATFORM_MACOS - macOS_enableScrollMomentum(); - macOS_removeTitleBarSeparator( mWindow->getWindowHandler() ); + engine->getPlatformHelper()->setNativeScrollMomentumEnabled( true ); + engine->getPlatformHelper()->setWindowTitleBarSeparatorVisible( mWindow->getWindowHandler(), + false ); #endif mThreadPool->run( [this]() { @@ -4954,6 +4960,7 @@ void App::init( InitParameters& params ) { mUISceneNode = UISceneNode::New(); mLifetime.setDispatcher( mUISceneNode ); mUISceneNode->setThreadPool( mThreadPool ); + mUISceneNode->setSmoothScrollEnabled( mConfig.ui.smoothScroll ); mUIColorScheme = mConfig.ui.colorScheme; if ( params.language.empty() ) diff --git a/src/tools/ecode/macos/macos.hpp b/src/tools/ecode/macos/macos.hpp deleted file mode 100644 index a34362ee6..000000000 --- a/src/tools/ecode/macos/macos.hpp +++ /dev/null @@ -1,18 +0,0 @@ -#ifndef ECODE_MACOS_MACOS -#define ECODE_MACOS_MACOS - -#ifdef __cplusplus -extern "C" { -#endif - -void macOS_enableScrollMomentum(); - -void macOS_removeTitleBarSeparator( void* nsWindow ); - -void macOS_changeTitleBarColor( void* window, double red, double green, double blue ); - -#ifdef __cplusplus -} -#endif - -#endif diff --git a/src/tools/ecode/macos/macos.m b/src/tools/ecode/macos/macos.m deleted file mode 100644 index 02381be97..000000000 --- a/src/tools/ecode/macos/macos.m +++ /dev/null @@ -1,19 +0,0 @@ -#import -#include - -#include "macos.hpp" - -void macOS_enableScrollMomentum() { - [[NSUserDefaults standardUserDefaults] setBool:YES forKey:@"AppleMomentumScrollSupported"]; -} - -void macOS_removeTitleBarSeparator( void* window ) { - NSWindow* nsWindow = window; - [nsWindow setTitlebarSeparatorStyle:NSTitlebarSeparatorStyleNone]; -} - -void macOS_changeTitleBarColor( void* window, double red, double green, double blue ) { - NSWindow* nsWindow = window; - nsWindow.titlebarAppearsTransparent = YES; - nsWindow.backgroundColor = [NSColor colorWithRed:red green:green blue:blue alpha:1.]; -} diff --git a/src/tools/ecode/settingspanel.cpp b/src/tools/ecode/settingspanel.cpp index 412f68724..04cd59d5a 100644 --- a/src/tools/ecode/settingspanel.cpp +++ b/src/tools/ecode/settingspanel.cpp @@ -77,8 +77,9 @@ void SettingsPanel::create( Scope scope ) { addProjectSettings( state ); } state.panel->build(); - state.connections += state.window->connect( - Event::OnWindowReady, [&state]( const Event* ) { state.panel->focusSearch(); } ); + state.connections += state.window->connect( Event::OnWindowReady, [&state]( const Event* ) { + state.panel->runOnMainThread( [&state] { state.panel->focusSearch(); } ); + } ); state.connections += state.window->connect( Event::OnWindowClose, [this, &state, scope]( const Event* ) { for ( const auto& document : state.documents ) { @@ -203,6 +204,13 @@ void SettingsPanel::addUserSettings( PanelState& panel ) { mApp->i18n( "quick_preview_images_tooltip", "Preview images without opening a permanent editor tab." ) }, &mApp->getConfig().ui.imagesQuickPreview ); + addBool( + panel, + { "smoothScroll", "general.behavior", mApp->i18n( "smooth_scroll", "Smooth Scrolling" ), + mApp->i18n( "smooth_scroll_desc", + "Animate scrolling from mouse wheels and trackpads." ) }, + &mApp->getConfig().ui.smoothScroll, + [this]( bool value ) { mApp->getUISceneNode()->setSmoothScrollEnabled( value, true ); } ); addCategory( panel, "editor.appearance", mApp->i18n( "editor", "Editor" ), mApp->i18n( "appearance", "Appearance" ) ); @@ -1157,7 +1165,7 @@ void SettingsPanel::addUserSettings( PanelState& panel ) { [this, monitorRefreshRate, unlimitedFrameRate] { const auto value = mApp->getConfig().context.FrameRateLimit; return value == ContextSettings::FrameRateLimitScreenRefreshRate ? monitorRefreshRate - : value == 0 ? unlimitedFrameRate + : value == 0 ? unlimitedFrameRate : String( String::toString( value ) ); }, [this, monitorRefreshRate, unlimitedFrameRate]( const String& selection ) { diff --git a/src/tools/eterm/appconfig.cpp b/src/tools/eterm/appconfig.cpp index 35de007c7..b2186c2b9 100644 --- a/src/tools/eterm/appconfig.cpp +++ b/src/tools/eterm/appconfig.cpp @@ -33,6 +33,8 @@ void AppConfig::load() { terminal.closeOnExit = mIni.getValueB( "terminal", "close_on_exit", terminal.closeOnExit ); terminal.initialTabs = mIni.getValueU( "terminal", "initial_tabs", terminal.initialTabs ); terminal.exclusiveMode = mIni.getValueB( "terminal", "exclusive_mode", terminal.exclusiveMode ); + ui.smoothScroll = mIni.getValueB( + "ui", "smooth_scroll", mIni.getValueB( "terminal", "smooth_scroll", ui.smoothScroll ) ); const auto newTerminalBehavior = mIni.getValue( "terminal", "new_terminal_behavior", "current" ); terminal.newTerminalBehavior = @@ -104,6 +106,8 @@ bool AppConfig::savePreferences() { mIni.setValueB( "terminal", "close_on_exit", terminal.closeOnExit ); mIni.setValueU( "terminal", "initial_tabs", terminal.initialTabs ); mIni.setValueB( "terminal", "exclusive_mode", terminal.exclusiveMode ); + mIni.setValueB( "ui", "smooth_scroll", ui.smoothScroll ); + mIni.deleteValue( "terminal", "smooth_scroll" ); mIni.setValue( "terminal", "new_terminal_behavior", terminal.newTerminalBehavior == NewTerminalBehavior::VerticalSplit ? "vertical" : terminal.newTerminalBehavior == NewTerminalBehavior::HorizontalSplit diff --git a/src/tools/eterm/appconfig.hpp b/src/tools/eterm/appconfig.hpp index cc2df5461..8fdc5d6a6 100644 --- a/src/tools/eterm/appconfig.hpp +++ b/src/tools/eterm/appconfig.hpp @@ -38,6 +38,10 @@ struct TerminalConfig { ScrollBarMode scrollBarMode{ ScrollBarMode::Auto }; }; +struct UIConfig { + bool smoothScroll{ false }; +}; + struct FontConfig { std::string path; std::string fallbackPath; @@ -76,6 +80,7 @@ struct AppConfig { FontConfig font; WindowConfig window; ThemeConfig theme; + UIConfig ui; WindowStateConfig windowState; explicit AppConfig( std::string configPath ); diff --git a/src/tools/eterm/eterm.cpp b/src/tools/eterm/eterm.cpp index 7d5f342e3..49455f2fd 100644 --- a/src/tools/eterm/eterm.cpp +++ b/src/tools/eterm/eterm.cpp @@ -724,6 +724,7 @@ int App::run( int argc, char* argv[] ) { scene = app.getUI(); if ( !appWindow || !appWindow->isOpen() || !scene ) return EXIT_FAILURE; + scene->setSmoothScrollEnabled( config->ui.smoothScroll ); settingsActions = std::make_unique( this ); keybindingsPath = config->getConfigPath() + "keybindings.cfg"; loadKeybindings(); diff --git a/src/tools/eterm/settingspanel.cpp b/src/tools/eterm/settingspanel.cpp index 9075c211e..8e36149db 100644 --- a/src/tools/eterm/settingspanel.cpp +++ b/src/tools/eterm/settingspanel.cpp @@ -234,6 +234,14 @@ UIWindow* SettingsPanel::create( App& app ) { [value]( UITerminal* terminal ) { terminal->setExclusiveMode( value ); } ); app.savePreferences(); } ); + panel->addBool( { "smoothScroll", "terminal.behavior", + app.i18n( "smooth_scroll", "Smooth Scrolling" ), + app.i18n( "smooth_scroll_desc", + "Animate scrolling from mouse wheels and trackpads." ) }, + &app.config->ui.smoothScroll, [&app]( bool value ) { + app.scene->setSmoothScrollEnabled( value, true ); + app.savePreferences(); + } ); panel->addBool( { "closeTerminalTabOnExit", "terminal.behavior", app.i18n( "close_terminal_tab_on_exit", "Close Terminal Tab on Exit" ),