diff --git a/demo/captioned_image.cpp b/demo/captioned_image.cpp index 31becfa6..af80a5a2 100644 --- a/demo/captioned_image.cpp +++ b/demo/captioned_image.cpp @@ -38,6 +38,8 @@ CaptionedImage::CaptionedImage() this->forwardXMLAttribute("imageHeight", this->image, "height"); this->forwardXMLAttribute("caption", this->label, "text"); + + this->addGestureRecognizer(new brls::TapGestureRecognizer(this, brls::TapGestureConfig(false, brls::SOUND_NONE, brls::SOUND_NONE, brls::SOUND_NONE))); } void CaptionedImage::onChildFocusGained(brls::View* directChild, brls::View* focusedView) diff --git a/library/borealis.mk b/library/borealis.mk index 7f2dc7d1..7e7ecbef 100644 --- a/library/borealis.mk +++ b/library/borealis.mk @@ -7,6 +7,7 @@ include $(TOPDIR)/$(current_dir)/lib/extern/switch-libpulsar/deps.mk SOURCES := $(SOURCES) \ $(current_dir)/lib/core \ + $(current_dir)/lib/core/touch \ $(current_dir)/lib/views \ $(current_dir)/lib/platforms/switch \ $(current_dir)/lib/extern/glad \ diff --git a/library/include/borealis.hpp b/library/include/borealis.hpp index 9495d9c3..7be47ca3 100644 --- a/library/include/borealis.hpp +++ b/library/include/borealis.hpp @@ -29,6 +29,8 @@ #include #include #include +#include +#include #include #include #include @@ -41,7 +43,7 @@ #include #include -//Views +// Views #include #include #include @@ -51,3 +53,8 @@ #include #include #include + +// Gestures +#include +#include +#include diff --git a/library/include/borealis/core/application.hpp b/library/include/borealis/core/application.hpp index 42449fa2..806b6fcf 100644 --- a/library/include/borealis/core/application.hpp +++ b/library/include/borealis/core/application.hpp @@ -1,6 +1,7 @@ /* Copyright 2019-2021 natinusala Copyright 2019 p-sam + Copyright 2021 XITRIX Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -36,6 +37,13 @@ namespace brls { +// Input types for entire app +enum class InputType +{ + GAMEPAD, // Gamepad or keyboard + TOUCH, // Touch screen +}; + typedef std::function XMLViewCreator; class Application @@ -190,6 +198,14 @@ class Application */ static std::string getLocale(); + /** + * Returns the current input type. + */ + inline static InputType getInputType() + { + return inputType; + } + private: inline static bool inited = false; inline static bool quitRequested = false; @@ -207,6 +223,11 @@ class Application inline static View* currentFocus; + // Return true if input type was changed + static bool setInputType(InputType type); + + inline static InputType inputType = InputType::GAMEPAD; + inline static unsigned blockInputsTokens = 0; // any value > 0 means inputs are blocked inline static std::string commonFooter = ""; diff --git a/library/include/borealis/core/audio.hpp b/library/include/borealis/core/audio.hpp index bd4d6617..6864755a 100644 --- a/library/include/borealis/core/audio.hpp +++ b/library/include/borealis/core/audio.hpp @@ -30,6 +30,8 @@ enum Sound SOUND_CLICK_ERROR, // played when the user clicks a disabled button / a view focused with no click action SOUND_HONK, // honk SOUND_CLICK_SIDEBAR, // played when a sidebar item is clicked + SOUND_TOUCH_UNFOCUS, // played when touch focus has been interrupted + SOUND_TOUCH, // played when touch doesn't require it's own click sound _SOUND_MAX, // not an actual sound, just used to count of many sounds there are }; @@ -57,7 +59,7 @@ class AudioPlayer * * Returns a boolean indicating if the sound has been played or not. */ - virtual bool play(enum Sound sound) = 0; + virtual bool play(enum Sound sound, float pitch = 1) = 0; }; // An AudioPlayer that does nothing @@ -69,7 +71,7 @@ class NullAudioPlayer : public AudioPlayer return false; } - bool play(enum Sound sound) override + bool play(enum Sound sound, float pitch) override { return false; } diff --git a/library/include/borealis/core/box.hpp b/library/include/borealis/core/box.hpp index cd39ae5e..a3edfcf3 100644 --- a/library/include/borealis/core/box.hpp +++ b/library/include/borealis/core/box.hpp @@ -65,6 +65,7 @@ class Box : public View void draw(NVGcontext* vg, float x, float y, float width, float height, Style style, FrameContext* ctx) override; View* getDefaultFocus() override; + View* hitTest(Point point) override; View* getNextFocus(FocusDirection direction, View* currentView) override; void willAppear(bool resetState) override; void willDisappear(bool resetState) override; diff --git a/library/include/borealis/core/geometry.hpp b/library/include/borealis/core/geometry.hpp new file mode 100644 index 00000000..82655b60 --- /dev/null +++ b/library/include/borealis/core/geometry.hpp @@ -0,0 +1,110 @@ +/* + Copyright 2021 XITRIX + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#pragma once + +#include + +namespace brls +{ + +// A structure that contains a point in a two-dimensional coordinate system. +struct Point +{ + float x; // The x-coordinate of the point. + float y; // The y-coordinate of the point. + + // Creates a point with location (0,0). + Point(); + + // Creates a point with coordinates specified as float values. + Point(float x, float y); + + Point operator+(const Point& a) const; + Point operator-(const Point& a) const; + Point operator/(const float& a) const; + Point operator*(const float& a) const; + bool operator==(const Point& other) const; + bool operator!=(const Point& other) const; + void operator+=(const Point& a); + void operator-=(const Point& a); +}; + +// A structure that contains width and height values. +struct Size +{ + float width; // A width value. + float height; // A height value. + + // Creates a size with zero width and height. + Size(); + + // Creates a size with dimensions specified as float values. + Size(float width, float height); + + Size operator+(const Size& a) const; + Size operator-(const Size& a) const; + Size operator/(const float& a) const; + Size operator*(const float& a) const; + bool operator==(const Size& other) const; +}; + +// Rect +// A structure that contains the location and dimensions of a rectangle. +struct Rect +{ + Point origin; // A point that specifies the coordinates of the rectangle’s origin. + Size size; // A size that specifies the height and width of the rectangle. + + // Creates a rectangle with origin (0,0) and size (0,0). + Rect(); + + // Creates a rectangle with the specified origin and size. + Rect(Point origin, Size size); + + // Creates a rectangle with coordinates and dimensions specified as float values. + Rect(float x, float y, float width, float height); + + // Returns the width of a rectangle. + float getWidth() const; + // Returns the height of a rectangle. + float getHeight() const; + + // Returns the smallest value for the x-coordinate of the rectangle. + float getMinX() const; + // Returns the smallest value for the y-coordinate of the rectangle. + float getMinY() const; + + // Returns the x-coordinate that establishes the center of a rectangle. + float getMidX() const; + // Returns the y-coordinate that establishes the center of the rectangle. + float getMidY() const; + + // Returns the largest value of the x-coordinate for the rectangle. + float getMaxX() const; + // Returns the largest value for the y-coordinate of the rectangle. + float getMaxY() const; + + bool operator==(const Rect& other) const; + + // Returns true if point is inside this Rect + bool pointInside(Point point); + + // Returns string with description of current Rect + std::string describe(); +}; + +} // namespace brls \ No newline at end of file diff --git a/library/include/borealis/core/gesture.hpp b/library/include/borealis/core/gesture.hpp new file mode 100644 index 00000000..fff96506 --- /dev/null +++ b/library/include/borealis/core/gesture.hpp @@ -0,0 +1,94 @@ +/* + Copyright 2021 XITRIX + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#pragma once + +#include +#include +#include + +namespace brls +{ + +class View; + +// Represents current gesture state +enum class GestureState +{ + INTERRUPTED, // Gesture has been interupted, no callbacks will come + UNSURE, // Gesture started recognition and not sure if it should interupt other recognizers + START, // Gesture sure that it match to conditions and will interupt other recognizers + STAY, // Gesture in process, user still hold finger on screen + END, // User released their finger from screen, final frame of gesture + FAILED, // Gesture failed conditions +}; + +/* +* Superclass for all recognizers +* +* To create a new type of gesture recognizer, you should implement +* recognitionLoop method. +* +* It should contain logic with changing gesture's state. +* Recognizers' first state is UNSURE, in that state calling stack cannot tell +* which gesture user tries to apply. I.E. user puts and holds finger on the screen, so it can't be +* told whether it's going to be a tap or a swipe +* +* If gesture has been recognized, change its state to START, but ONLY for the first frame, +* on the next frame it should be changed to STAY and remain the same until the end, then it +* should change state to END. +* +* When any recognizer changes its state to START, it sends an interrupt event to every recognizer in the stack, +* so don't forget to handle that case. +* +* If gesture does not apply to recognizer's pattern, change its state to FAILED. +* It could also be used as a placerholder when recognizer is not in use. +* +* Use touch argument to get current state of touch. +* +* View argument contains the view to which this recognizer is attached. +* +* Use soundToPlay pointer to set sound which will be played in current frame. +* Leave it empty or use SOUND_NONE to not play any sound. +*/ +class GestureRecognizer +{ + public: + virtual ~GestureRecognizer() { } + + // Main recognition loop, for internal usage only, should not be called anywhere, but Application + virtual GestureState recognitionLoop(TouchState touch, View* view, Sound* soundToPlay); + + // Interrupt this recognizer + // If onlyIfUnsureState == true recognizer will be interupted + // only if current state is UNSURE + void interrupt(bool onlyIfUnsureState); + + // If false, this recognizer will be skipped + bool isEnabled() const { return this->enabled; } + + // If false, this recognizer will be skipped + void setEnabled(bool enabled) { this->enabled = enabled; } + + // Get the current state of recognizer + GestureState getState() const { return state; } + + protected: + GestureState state = GestureState::FAILED; + bool enabled = true; +}; + +} // namespace brls diff --git a/library/include/borealis/core/input.hpp b/library/include/borealis/core/input.hpp index 05a966c3..bb895d61 100644 --- a/library/include/borealis/core/input.hpp +++ b/library/include/borealis/core/input.hpp @@ -1,5 +1,6 @@ /* Copyright 2021 natinusala + Copyright 2021 XITRIX Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -16,6 +17,8 @@ #pragma once +#include + namespace brls { @@ -66,11 +69,36 @@ enum ControllerAxis }; // Represents the state of the controller (a gamepad or a keyboard) in the current frame -typedef struct ControllerState +struct ControllerState { bool buttons[_BUTTON_MAX]; // true: pressed float axes[_AXES_MAX]; // from 0.0f to 1.0f -} ControllerState; +}; + +// Represents a touch phase in the current frame +enum class TouchPhase +{ + START, + STAY, + END, + NONE, +}; + +// Contains raw touch data, filled in by platform driver +struct RawTouchState +{ + bool pressed; + Point position; + Point scroll; +}; + +// Contains touch data automatically filled with current phase by the library +struct TouchState +{ + TouchPhase phase; + Point position; + Point scroll; +}; // Interface responsible for reporting input state to the application - button presses, // axis position and touch screen state @@ -83,6 +111,16 @@ class InputManager * Called once every frame to fill the given ControllerState struct with the controller state. */ virtual void updateControllerState(ControllerState* state) = 0; + + /** + * Called once every frame to fill the given RawTouchState struct with the raw touch data. + */ + virtual void updateTouchState(RawTouchState* state) = 0; + + /** + * Calculate current touch phase based on it's previous state + */ + static TouchState computeTouchState(RawTouchState currentTouch, TouchState lastFrameState); }; }; // namespace brls diff --git a/library/include/borealis/core/touch/pan_gesture.hpp b/library/include/borealis/core/touch/pan_gesture.hpp new file mode 100644 index 00000000..e34e51a1 --- /dev/null +++ b/library/include/borealis/core/touch/pan_gesture.hpp @@ -0,0 +1,90 @@ +/* + Copyright 2021 XITRIX + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#pragma once + +#include +#include + +namespace brls +{ + +// Contains info about acceleration on pan ends +struct PanAcceleration +{ + // distances in pixels + Point distance; + + // times to cover the distance + Point time; +}; + +// Current status of gesture +struct PanGestureStatus +{ + GestureState state; // Gesture state + Point position; // Current position + Point startPosition; // Start X position + Point delta; // Difference between current and previous positions by X + bool deltaOnly = false; // If true, current state will contain delta values ONLY + + // Acceleration info, NOT NULL ONLY from + // gesture callback and when current state is END + PanAcceleration acceleration; +}; + +typedef Event PanGestureEvent; + +// Axis of pan recognition start conditions +enum class PanAxis +{ + HORIZONTAL, // Triggers only on horizontal coordinate changes + VERTICAL, // Triggers only on vertical coordinate changes + ANY, // Any movement allowed +}; + +// Pan recognizer +// UNSURE: while touch not moved enough to recognize it as pan +// START: gesture has been recognized +// MOVE: gesture in process +// END: finger released, acceleration will be calculated +// FAILED: unsupported +class PanGestureRecognizer : public GestureRecognizer +{ + public: + PanGestureRecognizer(PanGestureEvent::Callback respond, PanAxis axis); + GestureState recognitionLoop(TouchState touch, View* view, Sound* soundToPlay) override; + + // Get pan gesture axis + PanAxis getAxis() const { return this->axis; } + + // Get current state of recognizer + PanGestureStatus getCurrentStatus(); + + // Get pan gesture event + PanGestureEvent getPanGestureEvent() const { return panEvent; } + + private: + PanGestureEvent panEvent; + Point position; + Point startPosition; + Point delta; + PanAxis axis; + std::vector posHistory; + GestureState lastState; +}; + +} // namespace brls diff --git a/library/include/borealis/core/touch/scroll_gesture.hpp b/library/include/borealis/core/touch/scroll_gesture.hpp new file mode 100644 index 00000000..4ad010d0 --- /dev/null +++ b/library/include/borealis/core/touch/scroll_gesture.hpp @@ -0,0 +1,43 @@ +/* + Copyright 2021 XITRIX + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#pragma once + +#include + +namespace brls +{ + +/* + * Scroll recognizer + * + * Child of Pan recognizer. + * The only difference is that Scroll recognizer will translate scrolling wheel to pan gestures. + * If NO_TOUCH_SCROLLING=true defined, will ignore scrolling by touch. + * + * If mouse translation used, the only available state is MOVE. + * Also PanGestureStatus will be returned with delta values ONLY. + * + * TODO: Reimplement scroll events when mouse input will be separated from touch + */ +class ScrollGestureRecognizer : public PanGestureRecognizer +{ + public: + ScrollGestureRecognizer(PanGestureEvent::Callback respond, PanAxis axis); + GestureState recognitionLoop(TouchState touch, View* view, Sound* soundToPlay) override; +}; + +} // namespace brls diff --git a/library/include/borealis/core/touch/tap_gesture.hpp b/library/include/borealis/core/touch/tap_gesture.hpp new file mode 100644 index 00000000..2269e1be --- /dev/null +++ b/library/include/borealis/core/touch/tap_gesture.hpp @@ -0,0 +1,80 @@ +/* + Copyright 2021 XITRIX + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#pragma once + +#include + +namespace brls +{ + +struct TapGestureConfig +{ + bool highlightOnSelect = true; + Sound unsureSound = SOUND_FOCUS_CHANGE; + Sound failedSound = SOUND_TOUCH_UNFOCUS; + Sound endSound = SOUND_CLICK; + + TapGestureConfig() { } + TapGestureConfig(bool highlightOnSelect, Sound unsureSound, Sound failedSound, Sound endSound) + { + this->highlightOnSelect = highlightOnSelect; + this->unsureSound = unsureSound; + this->failedSound = failedSound; + this->endSound = endSound; + } +}; + +struct TapGestureStatus +{ + GestureState state; // Gesture state + Point position; // Current position +}; +typedef Event TapGestureEvent; + +// Tap recognizer +// UNSURE: while touch moves inside of View bounds +// START: unsupported +// MOVE: unsupported +// END: touch released inside View's bounds +// FAILED: touch moved outside View's bounds +class TapGestureRecognizer : public GestureRecognizer +{ + public: + // Simple ctor which uses View's primary action as response which will be called only on recognizer state END. + TapGestureRecognizer(View* view, TapGestureConfig config = TapGestureConfig()); + + // Simple ctor with custom response which will be called only on recognizer state END. + TapGestureRecognizer(View* view, std::function respond, TapGestureConfig config = TapGestureConfig()); + + // Complex ctor with fully controllable response. + TapGestureRecognizer(TapGestureEvent::Callback respond); + + GestureState recognitionLoop(TouchState touch, View* view, Sound* soundToPlay) override; + + // Get current state of recognizer + TapGestureStatus getCurrentStatus(); + + // Get tap gesture event + TapGestureEvent getPanGestureEvent() const { return tapEvent; } + + private: + TapGestureEvent tapEvent; + Point position; + GestureState lastState; +}; + +} // namespace brls diff --git a/library/include/borealis/core/view.hpp b/library/include/borealis/core/view.hpp index 294d016e..7cfeb670 100644 --- a/library/include/borealis/core/view.hpp +++ b/library/include/borealis/core/view.hpp @@ -27,6 +27,8 @@ #include #include #include +#include +#include #include #include #include @@ -161,12 +163,12 @@ class View ViewBackground background = ViewBackground::NONE; void drawBackground(NVGcontext* vg, FrameContext* ctx, Style style); - void drawShadow(NVGcontext* vg, FrameContext* ctx, Style style, float x, float y, float width, float height); - void drawBorder(NVGcontext* vg, FrameContext* ctx, Style style, float x, float y, float width, float height); + void drawShadow(NVGcontext* vg, FrameContext* ctx, Style style, Rect frame); + void drawBorder(NVGcontext* vg, FrameContext* ctx, Style style, Rect frame); void drawHighlight(NVGcontext* vg, Theme theme, float alpha, Style style, bool background); - void drawClickAnimation(NVGcontext* vg, FrameContext* ctx, float x, float y, float width, float height); - void drawWireframe(FrameContext* ctx, float x, float y, float width, float height); - void drawLine(FrameContext* ctx, float x, float y, float width, float height); + void drawClickAnimation(NVGcontext* vg, FrameContext* ctx, Rect frame); + void drawWireframe(FrameContext* ctx, Rect frame); + void drawLine(FrameContext* ctx, Rect frame); Animatable highlightAlpha = 0.0f; float highlightPadding = 0.0f; @@ -191,16 +193,15 @@ class View bool hideHighlightBackground = false; - bool detached = false; - float detachedOriginX = 0.0f; - float detachedOriginY = 0.0f; + bool detached = false; + Point detachedOrigin; - float translationX = 0.0f; - float translationY = 0.0f; + Point translation; bool wireframeEnabled = false; std::vector actions; + std::vector gestureRecognizers; /** * Parent user data, typically the index of the view @@ -302,6 +303,7 @@ class View void shakeHighlight(FocusDirection direction); + Rect getFrame(); float getX(); float getY(); float getWidth(); @@ -1040,6 +1042,33 @@ class View return this->actions; } + /** + * Get the vector of all gesture recognizers attached to that view. + */ + const std::vector& getGestureRecognizers() + { + return this->gestureRecognizers; + } + + /** + * Interrupt every recognizer on this view. + * If onlyIfUnsureState == true, only recognizers with + * current state UNSURE will be interupted + */ + void interruptGestures(bool onlyIfUnsureState); + + /** + * Add new gesture recognizer on this view. + */ + void addGestureRecognizer(GestureRecognizer* recognizer); + + /** + * Called each frame when touch is registered. + * + * @returns sound to play invoked by touch recognizers. + */ + Sound gestureRecognizerRequest(TouchState touch, View* firstResponder); + /** * Called each frame * Do not override it to draw your view, @@ -1155,6 +1184,15 @@ class View */ virtual View* getDefaultFocus(); + /** + * Returns the view to focus with the corresponding screen coordinates in the view or its children, + * or nullptr if it hasn't been found. + * + * Research is done recursively by traversing the tree starting from this view. + * This view's parents are not traversed. + */ + virtual View* hitTest(Point point); + /** * Returns the next view to focus given the requested direction * and the currently focused view (as parent user data) diff --git a/library/include/borealis/platforms/glfw/glfw_input.hpp b/library/include/borealis/platforms/glfw/glfw_input.hpp index af2b1425..edd3ba74 100644 --- a/library/include/borealis/platforms/glfw/glfw_input.hpp +++ b/library/include/borealis/platforms/glfw/glfw_input.hpp @@ -32,7 +32,11 @@ class GLFWInputManager : public InputManager void updateControllerState(ControllerState* state) override; + void updateTouchState(RawTouchState* state) override; + private: + Point scrollOffset; + static void scrollCallback(GLFWwindow* window, double xoffset, double yoffset); GLFWwindow* window; }; diff --git a/library/include/borealis/platforms/switch/switch_audio.hpp b/library/include/borealis/platforms/switch/switch_audio.hpp index 80f9f2b8..8dfd1608 100644 --- a/library/include/borealis/platforms/switch/switch_audio.hpp +++ b/library/include/borealis/platforms/switch/switch_audio.hpp @@ -32,7 +32,7 @@ class SwitchAudioPlayer : public AudioPlayer ~SwitchAudioPlayer(); bool load(enum Sound sound) override; - bool play(enum Sound sound) override; + bool play(enum Sound sound, float pitch) override; private: bool init = false; diff --git a/library/include/borealis/platforms/switch/switch_input.hpp b/library/include/borealis/platforms/switch/switch_input.hpp index 054cb5ed..80b51f16 100644 --- a/library/include/borealis/platforms/switch/switch_input.hpp +++ b/library/include/borealis/platforms/switch/switch_input.hpp @@ -1,5 +1,6 @@ /* Copyright 2021 natinusala + Copyright (C) 2021 XITRIX Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -31,6 +32,8 @@ class SwitchInputManager : public InputManager void updateControllerState(ControllerState* state); + void updateTouchState(RawTouchState* state); + private: PadState padState; }; diff --git a/library/include/borealis/views/scrolling_frame.hpp b/library/include/borealis/views/scrolling_frame.hpp index 77cce2b5..7a7d3831 100644 --- a/library/include/borealis/views/scrolling_frame.hpp +++ b/library/include/borealis/views/scrolling_frame.hpp @@ -82,6 +82,7 @@ class ScrollingFrame : public Box void prebakeScrolling(); bool updateScrolling(bool animated); void startScrolling(bool animated, float newScroll); + void animateScrolling(float newScroll, float time); void scrollAnimationTick(); float getScrollingAreaTopBoundary(); diff --git a/library/lib/core/application.cpp b/library/lib/core/application.cpp index 16dd6bea..484936b5 100644 --- a/library/lib/core/application.cpp +++ b/library/lib/core/application.cpp @@ -2,6 +2,7 @@ Copyright 2019-2020 natinusala Copyright 2019 p-sam Copyright 2020 WerWolv + Copyright 2021 XITRIX Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -149,6 +150,7 @@ void Application::createWindow(std::string windowTitle) bool Application::mainLoop() { static ControllerState oldControllerState = {}; + static View* firstResponder; // Main loop callback if (!Application::platform->mainLoopIteration() || Application::quitRequested) @@ -159,10 +161,60 @@ bool Application::mainLoop() // Input ControllerState controllerState = {}; + RawTouchState rawTouch = {}; InputManager* inputManager = Application::platform->getInputManager(); + inputManager->updateTouchState(&rawTouch); inputManager->updateControllerState(&controllerState); + static TouchState oldTouch; + TouchState touchState = InputManager::computeTouchState(rawTouch, oldTouch); + oldTouch = touchState; + + // Touch controller events + switch (touchState.phase) + { + case TouchPhase::START: + Logger::debug("Touched at X: " + std::to_string(touchState.position.x) + ", Y: " + std::to_string(touchState.position.y)); + Application::setInputType(InputType::TOUCH); + + // Search for first responder, which will be the root of recognition tree + if (Application::activitiesStack.size() > 0) + firstResponder = Application::activitiesStack[Application::activitiesStack.size() - 1] + ->getContentView() + ->hitTest(touchState.position); + break; + case TouchPhase::NONE: + firstResponder = nullptr; + break; + default: + break; + } + + if (!firstResponder && (touchState.scroll.x != 0 || touchState.scroll.y != 0)) + { + Logger::debug("Touched at X: " + std::to_string(touchState.position.x) + ", Y: " + std::to_string(touchState.position.y)); + Application::setInputType(InputType::TOUCH); + + // Search for first responder, which will be the root of recognition tree + if (Application::activitiesStack.size() > 0) + firstResponder = Application::activitiesStack[Application::activitiesStack.size() - 1] + ->getContentView() + ->hitTest(touchState.position); + } + + if (firstResponder) + { + Sound sound = firstResponder->gestureRecognizerRequest(touchState, firstResponder); + float pitch = 1; + if (sound == SOUND_TOUCH) + { + // Play touch sound with random pitch + pitch = (rand() % 10) / 10.0f + 1.0f; + } + Application::getAudioPlayer()->play(sound, pitch); + } + // Trigger controller events bool anyButtonPressed = false; bool repeating = false; @@ -219,6 +271,10 @@ void Application::quit() void Application::navigate(FocusDirection direction) { + // Dismiss navigation if input type was changed + if (Application::setInputType(InputType::GAMEPAD)) + return; + View* currentFocus = Application::currentFocus; // Do nothing if there is no current focus @@ -315,6 +371,19 @@ void Application::onControllerButtonPressed(enum ControllerButton button, bool r } } +bool Application::setInputType(InputType type) +{ + if (type == Application::inputType) + return false; + + Application::inputType = type; + + if (type == InputType::GAMEPAD) + Application::currentFocus->onFocusGained(); + + return true; +} + View* Application::getCurrentFocus() { return Application::currentFocus; @@ -322,6 +391,10 @@ View* Application::getCurrentFocus() bool Application::handleAction(char button) { + // Dismiss if input type was changed + if (button == BUTTON_A && setInputType(InputType::GAMEPAD)) + return false; + if (Application::activitiesStack.empty()) return false; diff --git a/library/lib/core/box.cpp b/library/lib/core/box.cpp index c671deba..2cfa7937 100644 --- a/library/lib/core/box.cpp +++ b/library/lib/core/box.cpp @@ -316,6 +316,27 @@ View* Box::getDefaultFocus() return nullptr; } +View* Box::hitTest(Point point) +{ + // Check if touch fits in view frame + if (this->getFrame().pointInside(point)) + { + Logger::debug(describe() + ": --- X: " + std::to_string((int)getX()) + ", Y: " + std::to_string((int)getY()) + ", W: " + std::to_string((int)getWidth()) + ", H: " + std::to_string((int)getHeight())); + for (View* child : this->children) + { + View* result = child->hitTest(point); + + if (result) + return result; + } + + Logger::debug(describe() + ": OK"); + return this; + } + + return nullptr; +} + View* Box::getNextFocus(FocusDirection direction, View* currentView) { void* parentUserData = currentView->getParentUserData(); diff --git a/library/lib/core/geometry.cpp b/library/lib/core/geometry.cpp new file mode 100644 index 00000000..d6faceb3 --- /dev/null +++ b/library/lib/core/geometry.cpp @@ -0,0 +1,159 @@ +/* + Copyright 2021 XITRIX + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#include + +namespace brls +{ + +// Point +Point::Point() + : Point(0.0f, 0.0f) +{ +} + +Point::Point(float x, float y) +{ + this->x = x; + this->y = y; +} + +Point Point::operator+(const Point& a) const +{ + return Point(a.x + x, a.y + y); +} + +Point Point::operator-(const Point& a) const +{ + return Point(a.x - x, a.y - y); +} + +Point Point::operator/(const float& a) const +{ + return Point(x / a, y / a); +} + +Point Point::operator*(const float& a) const +{ + return Point(x * a, y * a); +} + +bool Point::operator==(const Point& other) const +{ + return x == other.x && y == other.y; +} + +bool Point::operator!=(const Point& other) const +{ + return x != other.x || y != other.y; +} + +void Point::operator+=(const Point& a) +{ + this->x += a.x; + this->y += a.y; +} + +void Point::operator-=(const Point& a) +{ + this->x -= a.x; + this->y -= a.y; +} + +// Size +Size::Size() + : Size(0.0f, 0.0f) +{ +} + +Size::Size(float width, float height) +{ + this->width = width; + this->height = height; +} + +Size Size::operator+(const Size& a) const +{ + return Size(a.width + width, a.height + height); +} + +Size Size::operator-(const Size& a) const +{ + return Size(a.width - width, a.height - height); +} + +Size Size::operator/(const float& a) const +{ + return Size(width / a, height / a); +} + +Size Size::operator*(const float& a) const +{ + return Size(width * a, height * a); +} + +bool Size::operator==(const Size& other) const +{ + return width == other.width && height == other.height; +} + +// Rect +Rect::Rect() +{ + this->origin = Point(); + this->size = Size(); +} + +Rect::Rect(Point origin, Size size) +{ + this->origin = origin; + this->size = size; +} + +Rect::Rect(float x, float y, float width, float height) +{ + this->origin = Point(x, y); + this->size = Size(width, height); +} + +float Rect::getWidth() const { return this->size.width; } +float Rect::getHeight() const { return this->size.height; } + +float Rect::getMinX() const { return this->origin.x; } +float Rect::getMinY() const { return this->origin.y; } + +float Rect::getMidX() const { return this->origin.x + getWidth() / 2; } +float Rect::getMidY() const { return this->origin.y + getHeight() / 2; } + +float Rect::getMaxX() const { return this->origin.x + getWidth(); } +float Rect::getMaxY() const { return this->origin.y + getHeight(); } + +bool Rect::operator==(const Rect& other) const +{ + return origin == other.origin && size == other.size; +} + +bool Rect::pointInside(Point point) +{ + return getMinX() <= point.x && getMaxX() >= point.x && getMinY() <= point.y && getMaxY() >= point.y; +} + +std::string Rect::describe() +{ + return "X: " + std::to_string((int)getMinX()) + ", Y: " + std::to_string((int)getMinY()) + ", W: " + std::to_string((int)getWidth()) + ", H: " + std::to_string((int)getHeight()); +} + +} // namespace brls diff --git a/library/lib/core/gesture.cpp b/library/lib/core/gesture.cpp new file mode 100644 index 00000000..ec09000a --- /dev/null +++ b/library/lib/core/gesture.cpp @@ -0,0 +1,35 @@ +/* + Copyright 2021 XITRIX + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#include + +namespace brls +{ + +GestureState GestureRecognizer::recognitionLoop(TouchState touch, View* view, Sound* soundToPlay) +{ + return GestureState::FAILED; +} + +void GestureRecognizer::interrupt(bool onlyIfUnsureState) +{ + if (onlyIfUnsureState && this->state != GestureState::UNSURE) + return; + + this->state = GestureState::INTERRUPTED; +} + +} diff --git a/library/lib/core/input.cpp b/library/lib/core/input.cpp new file mode 100644 index 00000000..8897244a --- /dev/null +++ b/library/lib/core/input.cpp @@ -0,0 +1,49 @@ +/* + Copyright 2021 XITRIX + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#include + +namespace brls +{ + +TouchState InputManager::computeTouchState(RawTouchState currentTouch, TouchState lastFrameState) +{ + if (currentTouch.pressed) + { + lastFrameState.position = currentTouch.position; + if (lastFrameState.phase == TouchPhase::START || lastFrameState.phase == TouchPhase::STAY) + lastFrameState.phase = TouchPhase::STAY; + else + lastFrameState.phase = TouchPhase::START; + } + else + { + if (lastFrameState.phase == TouchPhase::END || lastFrameState.phase == TouchPhase::NONE) + lastFrameState.phase = TouchPhase::NONE; + else + lastFrameState.phase = TouchPhase::END; + } + + if (currentTouch.scroll.x != 0 || currentTouch.scroll.y != 0) + { + lastFrameState.position = currentTouch.position; + } + lastFrameState.scroll = currentTouch.scroll; + + return lastFrameState; +} + +} // namespace brls diff --git a/library/lib/core/touch/pan_gesture.cpp b/library/lib/core/touch/pan_gesture.cpp new file mode 100644 index 00000000..81122c92 --- /dev/null +++ b/library/lib/core/touch/pan_gesture.cpp @@ -0,0 +1,159 @@ +/* + Copyright 2021 XITRIX + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#include + +// TODO: get real FPS +// Uses to calculate time to play acceleration animation +#define FPS 60.0f + +// Delta from touch starting point to current, when +// touch will be recognized as pan movement +#define MAX_DELTA_MOVEMENT 10 + +// Touch history limit which uses to calculate current pan speed +#define HISTORY_LIMIT 2 + +// Negative acceleration to calculate +// time to play acceleration animation +#define PAN_SCROLL_ACCELERATION -5000 + +namespace brls +{ + +PanGestureRecognizer::PanGestureRecognizer(PanGestureEvent::Callback respond, PanAxis axis) + : axis(axis) +{ + panEvent.subscribe(respond); +} + +GestureState PanGestureRecognizer::recognitionLoop(TouchState touch, View* view, Sound* soundToPlay) +{ + if (!enabled) + return GestureState::FAILED; + + // If not first touch frame and state is + // INTERRUPTED or FAILED, stop recognition + if (touch.phase != TouchPhase::START) + { + if (this->state == GestureState::INTERRUPTED || this->state == GestureState::FAILED) + { + if (this->state != lastState) + this->panEvent.fire(getCurrentStatus()); + + lastState = this->state; + return this->state; + } + } + + switch (touch.phase) + { + case TouchPhase::START: + this->posHistory.clear(); + this->state = GestureState::UNSURE; + this->startPosition = touch.position; + this->position = touch.position; + break; + case TouchPhase::STAY: + case TouchPhase::END: + + this->delta = touch.position - this->position; + + this->position = touch.position; + + // Check if pass any condition to set state START + if (this->state == GestureState::UNSURE) + { + if (fabs(this->startPosition.x - touch.position.x) > MAX_DELTA_MOVEMENT || fabs(this->startPosition.y - touch.position.y) > MAX_DELTA_MOVEMENT) + { + switch (axis) + { + case PanAxis::HORIZONTAL: + if (fabs(delta.x) > fabs(delta.y)) + this->state = GestureState::START; + break; + case PanAxis::VERTICAL: + if (fabs(delta.x) < fabs(delta.y)) + this->state = GestureState::START; + break; + case PanAxis::ANY: + this->state = GestureState::START; + break; + } + } + } + else + { + if (touch.phase == TouchPhase::STAY) + this->state = GestureState::STAY; + else + this->state = GestureState::END; + } + + // If last touch frame, calculate acceleration + + static PanAcceleration acceleration; + if (this->state == GestureState::END) + { + float time = posHistory.size() / FPS; + + float distanceX = posHistory[posHistory.size()].x - posHistory[0].x; + float distanceY = posHistory[posHistory.size()].y - posHistory[0].y; + + float velocityX = distanceX / time; + float velocityY = distanceY / time; + + acceleration.time.x = -fabs(velocityX) / PAN_SCROLL_ACCELERATION; + acceleration.time.y = -fabs(velocityY) / PAN_SCROLL_ACCELERATION; + + acceleration.distance.x = velocityX * acceleration.time.x / 2; + acceleration.distance.y = velocityY * acceleration.time.y / 2; + } + + if (this->state == GestureState::START || this->state == GestureState::STAY || this->state == GestureState::END) + { + PanGestureStatus state = getCurrentStatus(); + state.acceleration = acceleration; + this->panEvent.fire(state); + } + + break; + case TouchPhase::NONE: + this->state = GestureState::FAILED; + break; + } + + // Add current state to history + posHistory.insert(posHistory.begin(), this->position); + while (posHistory.size() > HISTORY_LIMIT) + { + posHistory.pop_back(); + } + + return this->state; +} + +PanGestureStatus PanGestureRecognizer::getCurrentStatus() +{ + return PanGestureStatus { + .state = this->state, + .position = this->position, + .startPosition = this->startPosition, + .delta = this->delta, + }; +} + +}; diff --git a/library/lib/core/touch/scroll_gesture.cpp b/library/lib/core/touch/scroll_gesture.cpp new file mode 100644 index 00000000..6501d439 --- /dev/null +++ b/library/lib/core/touch/scroll_gesture.cpp @@ -0,0 +1,57 @@ +/* + Copyright 2021 XITRIX + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#include + +#ifndef NO_TOUCH_SCROLLING +#define NO_TOUCH_SCROLLING false +#endif + +namespace brls +{ + +ScrollGestureRecognizer::ScrollGestureRecognizer(PanGestureEvent::Callback respond, PanAxis axis) + : PanGestureRecognizer(respond, axis) +{ +} + +GestureState ScrollGestureRecognizer::recognitionLoop(TouchState touch, View* view, Sound* soundToPlay) +{ + if (!enabled) + return GestureState::FAILED; + + GestureState result; + if (NO_TOUCH_SCROLLING) + result = GestureState::FAILED; + else + result = PanGestureRecognizer::recognitionLoop(touch, view, soundToPlay); + + if (result == GestureState::FAILED && (touch.scroll.x != 0 || touch.scroll.y != 0)) + { + PanGestureStatus status { + .state = GestureState::STAY, + .position = Point(), + .startPosition = Point(), + .delta = touch.scroll, + .deltaOnly = true, + }; + this->getPanGestureEvent().fire(status); + } + + return result; +} + +}; diff --git a/library/lib/core/touch/tap_gesture.cpp b/library/lib/core/touch/tap_gesture.cpp new file mode 100644 index 00000000..76a973f0 --- /dev/null +++ b/library/lib/core/touch/tap_gesture.cpp @@ -0,0 +1,140 @@ +/* + Copyright 2021 XITRIX + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +#include + +namespace brls +{ + +TapGestureRecognizer::TapGestureRecognizer(View* view, TapGestureConfig config) +{ + this->tapEvent.subscribe([view, config](TapGestureStatus status, Sound* soundToPlay) { + Application::giveFocus(view); + for (auto& action : view->getActions()) + { + if (action.button != static_cast(BUTTON_A)) + continue; + + if (action.available) + { + if (config.highlightOnSelect) + view->playClickAnimation(status.state != GestureState::UNSURE); + + switch (status.state) + { + case GestureState::UNSURE: + *soundToPlay = config.unsureSound; + break; + case GestureState::FAILED: + case GestureState::INTERRUPTED: + *soundToPlay = config.failedSound; + break; + case GestureState::END: + if (action.actionListener(view)) + *soundToPlay = action.sound; + break; + } + } + } + }); +} + +TapGestureRecognizer::TapGestureRecognizer(View* view, std::function respond, TapGestureConfig config) +{ + this->tapEvent.subscribe([view, respond, config](TapGestureStatus status, Sound* soundToPlay) { + Application::giveFocus(view); + if (config.highlightOnSelect) + view->playClickAnimation(status.state != GestureState::UNSURE); + + switch (status.state) + { + case GestureState::UNSURE: + *soundToPlay = config.unsureSound; + break; + case GestureState::FAILED: + case GestureState::INTERRUPTED: + *soundToPlay = config.failedSound; + break; + case GestureState::END: + *soundToPlay = config.endSound; + respond(); + break; + } + }); +} + +TapGestureRecognizer::TapGestureRecognizer(TapGestureEvent::Callback respond) +{ + tapEvent.subscribe(respond); +} + +GestureState TapGestureRecognizer::recognitionLoop(TouchState touch, View* view, Sound* soundToPlay) +{ + if (!enabled || touch.phase == TouchPhase::NONE) + return GestureState::FAILED; + + // If not first touch frame and state is + // INTERRUPTED or FAILED, stop recognition + if (touch.phase != TouchPhase::START) + { + if (this->state == GestureState::INTERRUPTED || this->state == GestureState::FAILED) + { + if (this->state != lastState) + this->tapEvent.fire(getCurrentStatus(), soundToPlay); + + lastState = this->state; + return this->state; + } + } + + switch (touch.phase) + { + case TouchPhase::START: + this->state = GestureState::UNSURE; + this->position = touch.position; + this->tapEvent.fire(getCurrentStatus(), soundToPlay); + break; + case TouchPhase::STAY: + // Check if touch is out view's bounds + // if true, FAIL recognition + if (touch.position.x < view->getX() || touch.position.x > view->getX() + view->getWidth() || touch.position.y < view->getY() || touch.position.y > view->getY() + view->getHeight()) + { + this->state = GestureState::FAILED; + this->tapEvent.fire(getCurrentStatus(), soundToPlay); + } + break; + case TouchPhase::END: + this->state = GestureState::END; + this->tapEvent.fire(getCurrentStatus(), soundToPlay); + break; + case TouchPhase::NONE: + this->state = GestureState::FAILED; + break; + } + + lastState = this->state; + return this->state; +} + +TapGestureStatus TapGestureRecognizer::getCurrentStatus() +{ + return TapGestureStatus { + .state = this->state, + .position = this->position, + }; +} + +}; diff --git a/library/lib/core/view.cpp b/library/lib/core/view.cpp index ee826ad3..52d9e898 100644 --- a/library/lib/core/view.cpp +++ b/library/lib/core/view.cpp @@ -1,6 +1,7 @@ /* Copyright 2019-2021 natinusala Copyright 2019 p-sam + Copyright 2021 XITRIX Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -95,6 +96,44 @@ NVGpaint View::a(NVGpaint paint) return newPaint; } +void View::interruptGestures(bool onlyIfUnsureState) +{ + for (GestureRecognizer* recognizer : getGestureRecognizers()) + recognizer->interrupt(onlyIfUnsureState); + + if (parent) + parent->interruptGestures(onlyIfUnsureState); +} + +void View::addGestureRecognizer(GestureRecognizer* recognizer) +{ + this->gestureRecognizers.push_back(recognizer); +} + +Sound View::gestureRecognizerRequest(TouchState touch, View* firstResponder) +{ + Sound soundToPlay = touch.phase == TouchPhase::START ? SOUND_TOUCH : SOUND_NONE; + + for (GestureRecognizer* recognizer : getGestureRecognizers()) + { + if (!recognizer->isEnabled()) + continue; + + GestureState state = recognizer->recognitionLoop(touch, this, &soundToPlay); + if (state == GestureState::START) + firstResponder->interruptGestures(true); + } + + Sound parentSound = SOUND_NONE; + if (parent) + parentSound = parent->gestureRecognizerRequest(touch, firstResponder); + + if (soundToPlay == SOUND_NONE) + soundToPlay = parentSound; + + return soundToPlay; +} + void View::frame(FrameContext* ctx) { if (this->visibility != Visibility::VISIBLE) @@ -109,10 +148,11 @@ void View::frame(FrameContext* ctx) if (this->themeOverride) ctx->theme = *themeOverride; - float x = this->getX(); - float y = this->getY(); - float width = this->getWidth(); - float height = this->getHeight(); + Rect frame = getFrame(); + float x = frame.getMinX(); + float y = frame.getMinY(); + float width = frame.getWidth(); + float height = frame.getHeight(); if (this->alpha > 0.0f && this->collapseState != 0.0f) { @@ -120,12 +160,12 @@ void View::frame(FrameContext* ctx) this->drawBackground(ctx->vg, ctx, style); // Draw shadow - if (this->shadowType != ShadowType::NONE && this->showShadow) - this->drawShadow(ctx->vg, ctx, style, x, y, width, height); + if (this->shadowType != ShadowType::NONE && (this->showShadow || Application::getInputType() == InputType::TOUCH)) + this->drawShadow(ctx->vg, ctx, style, frame); // Draw border if (this->borderThickness > 0.0f) - this->drawBorder(ctx->vg, ctx, style, x, y, width, height); + this->drawBorder(ctx->vg, ctx, style, frame); // Draw highlight background if (this->highlightAlpha > 0.0f && !this->hideHighlightBackground) @@ -133,7 +173,7 @@ void View::frame(FrameContext* ctx) // Draw click animation if (this->clickAlpha > 0.0f) - this->drawClickAnimation(ctx->vg, ctx, x, y, width, height); + this->drawClickAnimation(ctx->vg, ctx, frame); // Collapse clipping if (this->collapseState < 1.0f) @@ -150,9 +190,9 @@ void View::frame(FrameContext* ctx) this->drawHighlight(ctx->vg, ctx->theme, this->highlightAlpha, style, false); if (this->wireframeEnabled) - this->drawWireframe(ctx, x, y, width, height); + this->drawWireframe(ctx, frame); - this->drawLine(ctx, x, y, width, height); + this->drawLine(ctx, frame); //Reset clipping if (this->collapseState < 1.0f) @@ -185,7 +225,7 @@ void View::playClickAnimation(bool reverse) reverse ? EasingFunction::quadraticOut : EasingFunction::quadraticIn); this->clickAlpha.setEndCallback([this, reverse](bool finished) { - if (reverse) + if (reverse || Application::getInputType() == InputType::TOUCH) return; this->playClickAnimation(true); @@ -194,7 +234,7 @@ void View::playClickAnimation(bool reverse) this->clickAlpha.start(); } -void View::drawClickAnimation(NVGcontext* vg, FrameContext* ctx, float x, float y, float width, float height) +void View::drawClickAnimation(NVGcontext* vg, FrameContext* ctx, Rect frame) { Theme theme = ctx->theme; NVGcolor color = theme["brls/click_pulse"]; @@ -205,14 +245,14 @@ void View::drawClickAnimation(NVGcontext* vg, FrameContext* ctx, float x, float nvgBeginPath(vg); if (this->cornerRadius > 0.0f) - nvgRoundedRect(vg, x, y, width, height, this->cornerRadius); + nvgRoundedRect(vg, frame.getMinX(), frame.getMinY(), frame.getWidth(), frame.getHeight(), this->cornerRadius); else - nvgRect(vg, x, y, width, height); + nvgRect(ctx->vg, frame.getMinX(), frame.getMinY(), frame.getWidth(), frame.getHeight()); nvgFill(vg); } -void View::drawLine(FrameContext* ctx, float x, float y, float width, float height) +void View::drawLine(FrameContext* ctx, Rect frame) { // Don't setup and draw empty nvg path if there is no line to draw if (this->lineTop <= 0 && this->lineRight <= 0 && this->lineBottom <= 0 && this->lineLeft <= 0) @@ -222,28 +262,28 @@ void View::drawLine(FrameContext* ctx, float x, float y, float width, float heig nvgFillColor(ctx->vg, a(this->lineColor)); if (this->lineTop > 0) - nvgRect(ctx->vg, x, y, width, this->lineTop); + nvgRect(ctx->vg, frame.getMinX(), frame.getMinY(), frame.size.width, this->lineTop); if (this->lineRight > 0) - nvgRect(ctx->vg, x + width, y, this->lineRight, height); + nvgRect(ctx->vg, frame.getMaxX(), frame.getMinY(), this->lineRight, frame.size.height); if (this->lineBottom > 0) - nvgRect(ctx->vg, x, y + height - this->lineBottom, width, this->lineBottom); + nvgRect(ctx->vg, frame.getMinX(), frame.getMaxY() - this->lineBottom, frame.size.width, this->lineBottom); if (this->lineLeft > 0) - nvgRect(ctx->vg, x - this->lineLeft, y, this->lineLeft, height); + nvgRect(ctx->vg, frame.getMinX() - this->lineLeft, frame.getMinY(), this->lineLeft, frame.size.height); nvgFill(ctx->vg); } -void View::drawWireframe(FrameContext* ctx, float x, float y, float width, float height) +void View::drawWireframe(FrameContext* ctx, Rect frame) { nvgStrokeWidth(ctx->vg, 1); // Outline nvgBeginPath(ctx->vg); nvgStrokeColor(ctx->vg, nvgRGB(0, 0, 255)); - nvgRect(ctx->vg, x, y, width, height); + nvgRect(ctx->vg, frame.getMinX(), frame.getMinY(), frame.getWidth(), frame.getHeight()); nvgStroke(ctx->vg); if (this->hasParent()) @@ -252,13 +292,13 @@ void View::drawWireframe(FrameContext* ctx, float x, float y, float width, float nvgFillColor(ctx->vg, nvgRGB(0, 0, 255)); nvgBeginPath(ctx->vg); - nvgMoveTo(ctx->vg, x, y); - nvgLineTo(ctx->vg, x + width, y + height); + nvgMoveTo(ctx->vg, frame.getMinX(), frame.getMinY()); + nvgLineTo(ctx->vg, frame.getMaxX(), frame.getMaxY()); nvgFill(ctx->vg); nvgBeginPath(ctx->vg); - nvgMoveTo(ctx->vg, x + width, y); - nvgLineTo(ctx->vg, x, y + height); + nvgMoveTo(ctx->vg, frame.getMaxX(), frame.getMinY()); + nvgLineTo(ctx->vg, frame.getMinX(), frame.getMaxY()); nvgFill(ctx->vg); } @@ -273,19 +313,19 @@ void View::drawWireframe(FrameContext* ctx, float x, float y, float width, float // Top if (paddingTop > 0) - nvgRect(ctx->vg, x, y, width, paddingTop); + nvgRect(ctx->vg, frame.getMinX(), frame.getMinY(), frame.getWidth(), paddingTop); // Right if (paddingRight > 0) - nvgRect(ctx->vg, x + width - paddingRight, y, paddingRight, height); + nvgRect(ctx->vg, frame.getMaxX() - paddingRight, frame.getMinY(), paddingRight, frame.getHeight()); // Bottom if (paddingBottom > 0) - nvgRect(ctx->vg, x, y + height - paddingBottom, width, paddingBottom); + nvgRect(ctx->vg, frame.getMinX(), frame.getMaxY() - paddingBottom, frame.getWidth(), paddingBottom); // Left if (paddingLeft > 0) - nvgRect(ctx->vg, x, y, paddingLeft, height); + nvgRect(ctx->vg, frame.getMinX(), frame.getMinY(), paddingLeft, frame.getHeight()); nvgStroke(ctx->vg); @@ -300,33 +340,33 @@ void View::drawWireframe(FrameContext* ctx, float x, float y, float width, float // Top if (marginTop > 0) - nvgRect(ctx->vg, x - marginLeft, y - marginTop, width + marginLeft + marginRight, marginTop); + nvgRect(ctx->vg, frame.getMinX() - marginLeft, frame.getMinY() - marginTop, frame.getWidth() + marginLeft + marginRight, marginTop); // Right if (marginRight > 0) - nvgRect(ctx->vg, x + width, y - marginTop, marginRight, height + marginTop + marginBottom); + nvgRect(ctx->vg, frame.getMaxX(), frame.getMinY() - marginTop, marginRight, frame.getHeight() + marginTop + marginBottom); // Bottom if (marginBottom > 0) - nvgRect(ctx->vg, x - marginLeft, y + height, width + marginLeft + marginRight, marginBottom); + nvgRect(ctx->vg, frame.getMinX() - marginLeft, frame.getMaxY(), frame.getWidth() + marginLeft + marginRight, marginBottom); // Left if (marginLeft > 0) - nvgRect(ctx->vg, x - marginLeft, y - marginTop, marginLeft, height + marginTop + marginBottom); + nvgRect(ctx->vg, frame.getMinX() - marginLeft, frame.getMinY() - marginTop, marginLeft, frame.getHeight() + marginTop + marginBottom); nvgStroke(ctx->vg); } -void View::drawBorder(NVGcontext* vg, FrameContext* ctx, Style style, float x, float y, float width, float height) +void View::drawBorder(NVGcontext* vg, FrameContext* ctx, Style style, Rect frame) { nvgBeginPath(vg); nvgStrokeColor(vg, this->borderColor); nvgStrokeWidth(vg, this->borderThickness); - nvgRoundedRect(vg, x, y, width, height, this->cornerRadius); + nvgRoundedRect(vg, frame.getMinX(), frame.getMinY(), frame.getWidth(), frame.getHeight(), this->cornerRadius); nvgStroke(vg); } -void View::drawShadow(NVGcontext* vg, FrameContext* ctx, Style style, float x, float y, float width, float height) +void View::drawShadow(NVGcontext* vg, FrameContext* ctx, Style style, Rect frame) { float shadowWidth = 0.0f; float shadowFeather = 0.0f; @@ -349,19 +389,19 @@ void View::drawShadow(NVGcontext* vg, FrameContext* ctx, Style style, float x, f NVGpaint shadowPaint = nvgBoxGradient( vg, - x, y + shadowWidth, - width, height, + frame.getMinX(), frame.getMinY() + shadowWidth, + frame.getWidth(), frame.getHeight(), this->cornerRadius * 2, shadowFeather, RGBA(0, 0, 0, shadowOpacity * alpha), TRANSPARENT); nvgBeginPath(vg); nvgRect( vg, - x - shadowOffset, - y - shadowOffset, - width + shadowOffset * 2, - height + shadowOffset * 3); - nvgRoundedRect(vg, x, y, width, height, this->cornerRadius); + frame.getMinX() - shadowOffset, + frame.getMinY() - shadowOffset, + frame.getWidth() + shadowOffset * 2, + frame.getHeight() + shadowOffset * 3); + nvgRoundedRect(vg, frame.getMinX(), frame.getMinY(), frame.getWidth(), frame.getHeight(), this->cornerRadius); nvgPathWinding(vg, NVG_HOLE); nvgFillPaint(vg, shadowPaint); nvgFill(vg); @@ -425,6 +465,9 @@ void View::setAlpha(float alpha) void View::drawHighlight(NVGcontext* vg, Theme theme, float alpha, Style style, bool background) { + if (Application::getInputType() == InputType::TOUCH) + return; + nvgSave(vg); nvgResetScissor(vg); @@ -1000,24 +1043,23 @@ void View::invalidate() YGNodeCalculateLayout(this->ygNode, YGUndefined, YGUndefined, YGDirectionLTR); } +Rect View::getFrame() +{ + return Rect(getX(), getY(), getWidth(), getHeight()); +} + float View::getX() { - if (this->detached) - return this->detachedOriginX + this->translationX; - else if (this->hasParent()) - return this->getParent()->getX() + YGNodeLayoutGetLeft(this->ygNode) + this->translationX; - else - return YGNodeLayoutGetLeft(this->ygNode) + this->translationX; + if (this->hasParent()) + return this->getParent()->getX() + YGNodeLayoutGetLeft(this->ygNode) + this->translation.x; + return YGNodeLayoutGetLeft(this->ygNode) + this->translation.x; } float View::getY() { - if (this->detached) - return this->detachedOriginY + this->translationY; - else if (this->hasParent()) - return this->getParent()->getY() + YGNodeLayoutGetTop(this->ygNode) + this->translationY; - else - return YGNodeLayoutGetTop(this->ygNode) + this->translationY; + if (this->hasParent()) + return this->getParent()->getY() + YGNodeLayoutGetTop(this->ygNode) + this->translation.y; + return YGNodeLayoutGetTop(this->ygNode) + this->translation.y; } float View::getHeight(bool includeCollapse) @@ -1037,8 +1079,8 @@ void View::detach() void View::setDetachedPosition(float x, float y) { - this->detachedOriginX = x; - this->detachedOriginY = y; + this->detachedOrigin.x = x; + this->detachedOrigin.y = y; } bool View::isDetached() @@ -1250,6 +1292,9 @@ View::~View() for (tinyxml2::XMLDocument* document : this->boundDocuments) delete document; + + for (GestureRecognizer* recognizer : this->gestureRecognizers) + delete recognizer; } std::string View::getStringXMLAttributeValue(std::string value) @@ -1889,12 +1934,12 @@ void View::registerCommonAttributes() void View::setTranslationY(float translationY) { - this->translationY = translationY; + this->translation.y = translationY; } void View::setTranslationX(float translationX) { - this->translationX = translationX; + this->translation.x = translationX; } void View::setVisibility(Visibility visibility) @@ -2017,6 +2062,24 @@ View* View::getDefaultFocus() return nullptr; } +View* View::hitTest(Point point) +{ + // Check if can focus ourself first + if (!this->isFocusable()) + return nullptr; + + Rect frame = getFrame(); + Logger::debug(describe() + ": --- " + frame.describe()); + + if (frame.pointInside(point)) + { + Logger::debug(describe() + ": OK"); + return this; + } + + return nullptr; +} + void View::bindXMLDocument(tinyxml2::XMLDocument* document) { this->boundDocuments.push_back(document); diff --git a/library/lib/extern/switch-libpulsar/include/pulsar/player/player.h b/library/lib/extern/switch-libpulsar/include/pulsar/player/player.h index ff40ef14..2917c8ff 100644 --- a/library/lib/extern/switch-libpulsar/include/pulsar/player/player.h +++ b/library/lib/extern/switch-libpulsar/include/pulsar/player/player.h @@ -81,3 +81,9 @@ PLSR_RC plsrPlayerStop(PLSR_PlayerSoundId id); /// Free ressources used by a loaded sound void plsrPlayerFree(PLSR_PlayerSoundId id); + +/// Set sound pitch factor (effective next time it's played) +PLSR_RC plsrPlayerSetPitch(PLSR_PlayerSoundId id, float pitch); + +/// Set sound volume factor (effective next time it's played) +PLSR_RC plsrPlayerSetVolume(PLSR_PlayerSoundId id, float volume); diff --git a/library/lib/extern/switch-libpulsar/src/player/player.c b/library/lib/extern/switch-libpulsar/src/player/player.c index 90f50f14..10256bf5 100644 --- a/library/lib/extern/switch-libpulsar/src/player/player.c +++ b/library/lib/extern/switch-libpulsar/src/player/player.c @@ -156,4 +156,30 @@ void plsrPlayerFree(PLSR_PlayerSoundId id) { free(sound); } +PLSR_RC plsrPlayerSetPitch(PLSR_PlayerSoundId id, float pitch) { + if(id == PLSR_PLAYER_INVALID_SOUND) { + return _LOCAL_RC_MAKE(BadInput); + } + + PLSR_PlayerSound* sound = (PLSR_PlayerSound*)id; + for(unsigned int i = 0; i < sound->channelCount; i++) { + audrvVoiceSetPitch(&g_instance->driver, sound->channels[i].voiceId, pitch); + } + + return PLSR_RC_OK; +} + +PLSR_RC plsrPlayerSetVolume(PLSR_PlayerSoundId id, float volume) { + if(id == PLSR_PLAYER_INVALID_SOUND) { + return _LOCAL_RC_MAKE(BadInput); + } + + PLSR_PlayerSound* sound = (PLSR_PlayerSound*)id; + for(unsigned int i = 0; i < sound->channelCount; i++) { + audrvVoiceSetVolume(&g_instance->driver, sound->channels[i].voiceId, volume); + } + + return PLSR_RC_OK; +} + #endif diff --git a/library/lib/platforms/glfw/glfw_input.cpp b/library/lib/platforms/glfw/glfw_input.cpp index 7ca7c0dd..694f9f3a 100644 --- a/library/lib/platforms/glfw/glfw_input.cpp +++ b/library/lib/platforms/glfw/glfw_input.cpp @@ -1,5 +1,6 @@ /* Copyright 2021 natinusala + Copyright 2021 XITRIX Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -14,6 +15,7 @@ limitations under the License. */ +#include #include #include @@ -78,10 +80,18 @@ static void glfwJoystickCallback(int jid, int event) } } +void GLFWInputManager::scrollCallback(GLFWwindow* window, double xoffset, double yoffset) +{ + GLFWInputManager* self = (GLFWInputManager*)Application::getPlatform()->getInputManager(); + self->scrollOffset.x += xoffset * 10; + self->scrollOffset.y += yoffset * 10; +} + GLFWInputManager::GLFWInputManager(GLFWwindow* window) : window(window) { glfwSetJoystickCallback(glfwJoystickCallback); + glfwSetScrollCallback(window, scrollCallback); if (glfwJoystickIsGamepad(GLFW_JOYSTICK_1)) { @@ -110,4 +120,26 @@ void GLFWInputManager::updateControllerState(ControllerState* state) } } +bool sameSign(int a, int b) +{ + if (a == 0 || b == 0) + return true; + return (a >= 0) ^ (b < 0); +} + +void GLFWInputManager::updateTouchState(RawTouchState* state) +{ + // Get touchscreen state + double x, y; + glfwGetCursorPos(this->window, &x, &y); + + state->pressed = glfwGetMouseButton(window, GLFW_MOUSE_BUTTON_LEFT) == GLFW_PRESS; + state->position.x = x / Application::windowScale; + state->position.y = y / Application::windowScale; + state->scroll = scrollOffset; + + scrollOffset.x = 0; + scrollOffset.y = 0; +} + }; diff --git a/library/lib/platforms/switch/switch_audio.cpp b/library/lib/platforms/switch/switch_audio.cpp index a676c340..c4de86e3 100644 --- a/library/lib/platforms/switch/switch_audio.cpp +++ b/library/lib/platforms/switch/switch_audio.cpp @@ -35,6 +35,8 @@ const std::string SOUNDS_MAP[_SOUND_MAX] = { "SeKeyError", // SOUND_CLICK_ERROR "SeUnlockKeyZR", // SOUND_HONK "SeNaviDecide", // SOUND_CLICK_SIDEBAR + "SeTouchUnfocus", // SOUND_TOUCH_UNFOCUS + "SeTouch", // SOUND_TOUCH }; SwitchAudioPlayer::SwitchAudioPlayer() @@ -115,7 +117,7 @@ bool SwitchAudioPlayer::load(enum Sound sound) return true; } -bool SwitchAudioPlayer::play(enum Sound sound) +bool SwitchAudioPlayer::play(enum Sound sound, float pitch) { if (!this->init) return false; @@ -131,6 +133,7 @@ bool SwitchAudioPlayer::play(enum Sound sound) } // Play the sound + plsrPlayerSetPitch(this->sounds[sound], pitch); PLSR_RC rc = plsrPlayerPlay(this->sounds[sound]); if (PLSR_RC_FAILED(rc)) { diff --git a/library/lib/platforms/switch/switch_input.cpp b/library/lib/platforms/switch/switch_input.cpp index c9ecef87..9017f01c 100644 --- a/library/lib/platforms/switch/switch_input.cpp +++ b/library/lib/platforms/switch/switch_input.cpp @@ -14,6 +14,7 @@ limitations under the License. */ +#include #include namespace brls @@ -65,4 +66,21 @@ void SwitchInputManager::updateControllerState(ControllerState* state) } } +void SwitchInputManager::updateTouchState(RawTouchState* state) +{ + // Get touchscreen state + static HidTouchScreenState hidState = { 0 }; + + state->pressed = false; + if (hidGetTouchScreenStates(&hidState, 1)) + { + if (hidState.count > 0) + { + state->pressed = true; + state->position.x = hidState.touches[0].x / Application::windowScale; + state->position.y = hidState.touches[0].y / Application::windowScale; + } + } +} + } // namespace brls diff --git a/library/lib/views/button.cpp b/library/lib/views/button.cpp index e11cd3f9..3baba42e 100644 --- a/library/lib/views/button.cpp +++ b/library/lib/views/button.cpp @@ -18,6 +18,7 @@ #include #include +#include #include namespace brls @@ -79,6 +80,8 @@ Button::Button() }); this->applyStyle(); + + this->addGestureRecognizer(new TapGestureRecognizer(this)); } void Button::applyStyle() diff --git a/library/lib/views/scrolling_frame.cpp b/library/lib/views/scrolling_frame.cpp index efe172b5..64bc43c5 100644 --- a/library/lib/views/scrolling_frame.cpp +++ b/library/lib/views/scrolling_frame.cpp @@ -1,5 +1,6 @@ /* Copyright 2020-2021 natinusala + Copyright 2021 XITRIX Licensed under the Apache License, Version 2.0 (the "License"); you may not use this file except in compliance with the License. @@ -15,6 +16,8 @@ */ #include +#include +#include #include #include @@ -31,6 +34,49 @@ ScrollingFrame::ScrollingFrame() }); this->setMaximumAllowedXMLElements(1); + + addGestureRecognizer(new ScrollGestureRecognizer([this](PanGestureStatus state) { + if (state.state == GestureState::FAILED) + return; + + float contentHeight = this->getContentHeight(); + + if (state.deltaOnly) + { + float newScroll = (this->scrollY * contentHeight - (state.delta.y)) / contentHeight; + startScrolling(false, newScroll); + return; + } + + static float startY; + if (state.state == GestureState::START) + startY = this->scrollY * contentHeight; + + float newScroll = (startY - (state.position.y - state.startPosition.y)) / contentHeight; + + // Start animation + if (state.state != GestureState::END) + startScrolling(false, newScroll); + else + { + float time = state.acceleration.time.y * 1000.0f; + float newPos = this->scrollY * contentHeight + state.acceleration.distance.y; + + newScroll = newPos / contentHeight; + + if (newScroll == this->scrollY || time < 100) + return; + + animateScrolling(newScroll, time); + } + }, + PanAxis::VERTICAL)); + + // Stop scrolling on tap + addGestureRecognizer(new TapGestureRecognizer([this](brls::TapGestureStatus status, Sound* soundToPlay) { + if (status.state == GestureState::UNSURE) + this->scrollY.stop(); + })); } void ScrollingFrame::draw(NVGcontext* vg, float x, float y, float width, float height, Style style, FrameContext* ctx) @@ -135,26 +181,33 @@ void ScrollingFrame::startScrolling(bool animated, float newScroll) if (newScroll == this->scrollY) return; - this->scrollY.stop(); - if (animated) { Style style = Application::getStyle(); - - this->scrollY.reset(); - - this->scrollY.addStep(newScroll, style["brls/animations/highlight"], EasingFunction::quadraticOut); - - this->scrollY.setTickCallback([this] { - this->scrollAnimationTick(); - }); - - this->scrollY.start(); + animateScrolling(newScroll, style["brls/animations/highlight"]); } else { + this->scrollY.stop(); this->scrollY = newScroll; + this->scrollAnimationTick(); + this->invalidate(); } +} + +void ScrollingFrame::animateScrolling(float newScroll, float time) +{ + this->scrollY.stop(); + + this->scrollY.reset(); + + this->scrollY.addStep(newScroll, time, EasingFunction::quadraticOut); + + this->scrollY.setTickCallback([this] { + this->scrollAnimationTick(); + }); + + this->scrollY.start(); this->invalidate(); } @@ -175,7 +228,21 @@ float ScrollingFrame::getContentHeight() void ScrollingFrame::scrollAnimationTick() { if (this->contentView) - this->contentView->setTranslationY(-(this->scrollY * this->getContentHeight())); + { + float contentHeight = this->getContentHeight(); + float bottomLimit = (contentHeight - this->getScrollingAreaHeight()) / contentHeight; + + if (this->scrollY < 0) + this->scrollY = 0; + + if (this->scrollY > bottomLimit) + this->scrollY = bottomLimit; + + if (contentHeight <= getHeight()) + this->scrollY = 0; + + this->contentView->setTranslationY(-(this->scrollY * contentHeight)); + } } void ScrollingFrame::onChildFocusGained(View* directChild, View* focusedView) @@ -183,7 +250,8 @@ void ScrollingFrame::onChildFocusGained(View* directChild, View* focusedView) this->childFocused = true; // Start scrolling - this->updateScrolling(true); + if (Application::getInputType() != InputType::TOUCH) + this->updateScrolling(true); Box::onChildFocusGained(directChild, focusedView); } diff --git a/library/lib/views/sidebar.cpp b/library/lib/views/sidebar.cpp index d33e914f..68646d8e 100644 --- a/library/lib/views/sidebar.cpp +++ b/library/lib/views/sidebar.cpp @@ -18,6 +18,7 @@ #include #include +#include #include using namespace brls::literals; @@ -72,6 +73,28 @@ SidebarItem::SidebarItem() return true; }, false, SOUND_CLICK_SIDEBAR); + + this->addGestureRecognizer(new TapGestureRecognizer([this](TapGestureStatus status, Sound* soundToPlay) { + if (this->active) + return; + + this->playClickAnimation(status.state != GestureState::UNSURE); + + switch (status.state) + { + case GestureState::UNSURE: + *soundToPlay = SOUND_FOCUS_SIDEBAR; + break; + case GestureState::FAILED: + case GestureState::INTERRUPTED: + *soundToPlay = SOUND_TOUCH_UNFOCUS; + break; + case GestureState::END: + *soundToPlay = SOUND_CLICK_SIDEBAR; + Application::giveFocus(this); + break; + } + })); } void SidebarItem::setActive(bool active) diff --git a/library/meson.build b/library/meson.build index 87d923c9..4f5e8234 100644 --- a/library/meson.build +++ b/library/meson.build @@ -3,12 +3,14 @@ dep_glm = dependency('glm', version : '>=0.9.8') borealis_files = files( 'lib/core/logger.cpp', + 'lib/core/input.cpp', 'lib/core/application.cpp', 'lib/core/i18n.cpp', 'lib/core/theme.cpp', 'lib/core/style.cpp', 'lib/core/activity.cpp', 'lib/core/platform.cpp', + 'lib/core/geometry.cpp', 'lib/core/font.cpp', 'lib/core/util.cpp', 'lib/core/time.cpp', @@ -19,6 +21,11 @@ borealis_files = files( 'lib/core/box.cpp', 'lib/core/bind.cpp', + 'lib/core/gesture.cpp', + 'lib/core/touch/tap_gesture.cpp', + 'lib/core/touch/pan_gesture.cpp', + 'lib/core/touch/scroll_gesture.cpp', + 'lib/platforms/glfw/glfw_platform.cpp', 'lib/platforms/glfw/glfw_video.cpp', 'lib/platforms/glfw/glfw_input.cpp', @@ -70,4 +77,4 @@ borealis_include = include_directories( ) borealis_dependencies = [ dep_glfw3, dep_glm, ] -borealis_cpp_args = [ '-DYG_ENABLE_EVENTS', '-D__GLFW__', ] +borealis_cpp_args = [ '-DNO_TOUCH_SCROLLING=true', '-DYG_ENABLE_EVENTS', '-D__GLFW__', ] diff --git a/resources/i18n/en-US/demo.json b/resources/i18n/en-US/demo.json index c5a99f1d..0d7a9c6c 100644 --- a/resources/i18n/en-US/demo.json +++ b/resources/i18n/en-US/demo.json @@ -3,6 +3,7 @@ "tabs": { "components": "Basic components", + "scroll": "Scroll test", "layout": "Layout and alignment", "recycling": "Recycling lists", "popups": "Popups, notifications and dialogs", diff --git a/resources/i18n/en-US/scroll.json b/resources/i18n/en-US/scroll.json new file mode 100644 index 00000000..44f02621 --- /dev/null +++ b/resources/i18n/en-US/scroll.json @@ -0,0 +1,3 @@ +{ + "text": "Очень длинный-длинный текст :)\nПредисловие: Наконец появилась возможность добраться до интернета, сейчас мы находимся в Панамском канале и здесь есть wifi. Я на судне уже больше месяца и пока я здесь, я писал все интересное что здесь происходит и вот наконец есть возможность этим поделиться. Фотографий пока не будет, их я выложу или позже, или уже когда вернусь домой. Итак, понеслась:\n\n\nПервые впечатления\n\nПереночевав в гостинице в Гуаякиле, мы сели к агенту в машину и поехали на судно в Пуэрто Боливар. Доехали вопреки ожиданиям быстро, примерно за 3-4 часа. Погода была пасмурная и даже не смотря на то, что мы находимся недалеко от экватора, было прохладно. Почти все время, пока мы ехали, по обе стороны дороги были банановые плантации, но все равно в голове не укладывается: эти бананы грузят на суда в нескольких портах Эквадора десятками тысяч тонн каждый день, круглый год. Это ж несчастные бананы должны расти быстрее чем грибы.\n\nДороги.\nДороги в Эквадоре практически идеальные, хотя населенные пункты выглядят очень бедно. На дорогах много интересных машин, например очень много грузовиков - древних Фордов, которые я никогда раньше не видел. А еще несколько раз на глаза попадались старенькие Жигули :) А еще если кого-то обгоняешь и есть встречная машина, она обязательно включает фары. На больших машинах - грузовиках и автобусах, обязательно красуется местный тюнинг: машины разукрашенные, либо в наклейках, и обязательно везде огромное множество светодиодов, как будто новогодние елки едут и переливаются всеми цветами.\n\nСудно.\nНа первый взгляд судно неплохое, в относительно хорошем состоянии, хотя и 92 года постройки. Экипаж 19 человек - 11 русских и 8 филиппинцев, включая повара. Говорят, периодически становится тоскливо от егошних кулинарных изысков. Филиппинцы здесь рядовой состав, за ними постоянно нужно следить чтобы не натворили чего, среди них только один матрос по-настоящему ответственный и с руками из нужного места, все понимает с полуслова. Остальные - типичные Равшаны да Джамшуты. А еще один из них - гомосек О___о, в добавок к этому он опасный человек, в том плане, что легко впадает в состояние ступора и отключает мозг: был случай как он закрыл одного матроса в трюме, тот орал и тарабанил внутри, это заметил боцман, начал орать на этого персонажа, который, в свою очередь испуганно выпучив глаза, трясущимися руками продолжал закручивать барашки. В итоге боцман его отодвинул и выпустил матроса из трюма. Общение на английском языке, но из-за акцента не всегда с первого раз понятно что филиппинцы говорят, особенно по рации. Напимер, говорит он тебе: Бикарпуль! Бикарпуль! А потом, когда уже поздно, выясняется что это было \"Be careful!\"\n\nРабота.\nСразу, как только мы заселились, я не успел разложить вещи, как в мою голову ворвался такой поток информации, что ни в сказке сказать, ни топором не вырубить. Во-первых, на судне абсолютно все бумаги - мануалы, журналы, и так далее - все на английском языке. Даже блокнотик, в который записываются отчеты по грузовым операциям - и тот на английском. Бумаги... ооооо... Их тысячи, лежат в сотнях папок, плюс огромное количество документов на компьютерах. Это мне просто разорвало мозг в клочья, потому что с этим объемом информации надо ознакомиться и научиться работать в кротчайшие сроки. Постоянная беготня, постоянная суета, совсем не легко. А также надо как можно быстрее разобраться со всем оборудованием на мостике, а там его мама не горюй. В общем, пока что, свободного времени нет вообще. Абсолютно. Только ночью с 00:00 до 06:00 можно поспать. Но это продлится не долго, буквально 1-2 недели, потом океанский переход до Европы, можно будет уже спокойно стоять вахты, а в свободное время читать книги компании Seatrade, на случай если в Европе придет проверка и будет задавать вопросы.\n\nНу и немного о приятном.\nНеплохая одноместная каюта. Внутри несколько шкафов и полок, удобная кровать, койка напередохнуть, стол, стул, умывальник и внутрисудовой телефон. Также выдали 2 белых офицерских комбинезона с символикой компании, каску и персональную рацию. В моем распоряжении офицерский душ со стиральной машинкой и офицерская столовая. Во время швартовых операций мое место на мостике. Хотя и матросы на палубе не сильно напрягаются - там установлены гидравлические лебедки, не надо швартовные концы тягать вручную.\n\nНа этом пока все, фоток пока что нет, потому что просто некогда этим заниматься. Даже этот текст я пишу потому что просто появилось свободных 2 часа, отпустили отдохнуть, так как впереди 6-часовая вахта, а сразу после нее отшвартовка и идем в Гуаякиль, а во время отшвартовки и пока лоцман на судне - мое место на мостике. Так что, предстоит основательно заколебаться.\n\n8 августа 2013г. Пуэрто Боливар, Эквадор.\n\n\n\nРабота.\n\nЯ на судне уже почти 3 недели, все потихоньку начинает раскладываться по полочкам и становится понятным. Из Пуэрто Боливара мы пошли в Гуаякиль (Эквадор), а оттуда в Паиту (Перу). В этих портах мы загружались бананами и контейнерами. Потом мы взяли курс на Европу. Чтобы не делать огромный крюк вокруг Южной Америки, мы пошли в Панамский канал.\n\nПанамский канал.\nВозле входа в канал находится якорная стоянка, где куча судов стоит и ждет своей очереди на проход. Но, нам дали добро на проход практически без ожидания и постояв на якоре всего три часа, приехал лоцман, скомандовал \"dead slow ahead\" (самый малый вперед), я перевел ручку машинного телеграфа, похожую на автомобильный рычаг автоматической коробки передач в указанное положение, на что телеграф отозвался громким сигналом. Стрелка тахометра поползла вверх, судно слегка задрожало и мы медленно но верно начали набирать скорость и двигаться в сторону канала. Вход в канал со стороны океана выделен буями - десятки буев, выстроенные в две линии показывают единственный верный путь, выгладит грандиозно. А справа, за пальмами и дорогими виллами со стоящими рядом с ними роскошными яхтами, простирется огромный город небоскребов - Панама. А через десять минут открылось еще одно не менее грандиозное зрелище - строительство новых шлюзов, которые будут пропускать суда бОльших размеров, чем нынешние шлюзы. Но тут впереди из-за поворота показалась огромная стена, которая двигалась в нашу сторону. Навстречу нам шло судно-автомобилевоз Green Cove и так как канал - место совсем не широкое, автомобилевоз прошел от нас всего в 50 метрах, непривычно было стоя на мостике наблюдать столь близкое расхождение. А вот и буксиры. Это значит что мы подходим к шлюзам. Только мы подошли к шлюзу, как тут же нас привязали к резвым локомотивам - Мулам. Дальше все зависело только от них. 4 Мула держали нас с разных сторон и уже они руководили процессом, они нас резво разгоняли и останавливали в нужном месте и терпеливо ждали пока шлюз не наполнится водой. Так как наше судно не очень широкое, проблем с заходом в шлюз не было, а по длине получилось так, что за нами сзади поместилось еще одно небольшое судно. Правда мы оказались более быстроходными и пока мы шли от тихоокеанских до атлантических шлюзов по каналу и озеру Гатун, это судно безнадежно отстало, но нам пришлось его ждать :(\n\nАтлантический океан.\nПроходя мимо Бермудских островов, мы попали под влияние местной погоды - то светит солнце и стоит невыносимое пекло, то идет огромная туча и под ней стена ливня, сквозь которую ничего не видно, то все небо усыпано облаками разных форм, оттенков и размеров и сияет радуга. Интересно, в общем. Но потом все это прошло и была ясная жаркая погода почти без ветра. Вода гладкая и почти без волн, как будто в озере. Я себе Атлантический океан представлял иначе. Но через несколько дней атмосферное давление упало, поднялся ветер и нас начало валять с борта на борт. В общем весело, но не тогда, когда надо что-то делать. Например, надо точку на карту нанести, возьмешь линейку и тут эээх! Поехало все по столу и карандаш убежал. Или можно случайно ту же точку просто поставить совсем не туда, куда нужно. Но ничего, привыкли. Правда в душ было страшно ходить - там можно было легко убиться с такой качкой. И вот, мы пришли в Дувр (Великобритания). При заходе в порт открывается отличный вид: белые отвесные скалы огромной высоты, а наверху зеленые холмы. На одном из холмов располагается крепость , которая в данный момент является музеем. Эх, жаль что у нас была короткая стоянка, меньше суток. Так и не получилось сойти на берег. Но мы сейчас на линии, так что мы еще сюда вернемся, и не один раз. Паромы. Они большие и их много. Паромное сообщение с материком очень оживленное, примерно каждые 10-15 минут приходит или отходит очередной огромный паром, кое-как нашли интервал чтобы проскочить в порт и встать к причалу. Прошел день, грузовые операции по данному порту завершены и уже пора отчаливать и идти в следующий порт - Гамбург, Германия. Всего меньше суток переход и мы уже подходим к очередному порту. Гамбург находится на реке Эльба, в нескольких часах хода от моря. Пока шли до города, по обе стороны был приятный европейский пейзаж - аккуратные домики с мягким освещением, парки, беговые дорожки, все выглядит очень гармонично и уютно. На берегу пляжик с каяками, и даже прохладным вечером на пляже отдыхали люди в куртках, человек 30-50. Оставалось только смотреть и вздыхать. А чуть позже показался завод, с первого взгляда совсем неприметный, хоть и не маленький. А когда подошли чуть ближе, я увидел на стене логотип и надпись Airbus. Как сказал лоцман, это один из главных заводов Airbus в мире. И да, здание завода застеклено и со стороны реки было хорошо видно самолеты внутри. Точнее, самолеты в процессе сборки. У некоторых корпус еще желто-зеленого цвета, у других уже нанесена раскраска. В Гамбурге мы стояли так же как и в Дувре - всего день, тоже не удалось погулять, печально. И вот мы выгрузили часть бананов, разгрузили контейнера, погрузили новые и отправились в путь. Идем в Роттердам, Нидерланды (Голландия). Также переход меньше суток.\n\nРоттердам.\nВ этом городе случилось то, что я так долго ждал - наконец-то получилось сойти на берег. Город относительно чистый, хотя периодически мусор попадается, но совсем немного. Много парков, да и вообще много зелени. Много велосипедов, они везде, припаркованы почти у каждого дома, даже возде некоторых домов есть специальные маленькие гаражи для велосипедов, а вдоль всех дорог есть велосипедные дорожки с разделительной полосой и даже с собственными светофорами. Из водного транспорта есть водное такси и водный автобус. Живность. Я не видел ни одной кошки, зато по городским улицам бегают зайцы. :) На первых этажах домов в Роттердаме отсутствуют решетки, выглядит непривычно и опять же наталкивает на грустные мысли. Решили поменять немного денег и чтобы найти обменник обратились за помощью к местным жителям. Сидят два человека около 60 лет, спросили у них. Пока один объяснял дорогу, второй, со смугловатым лицом изъеденным морщинами, достал из кармана какие-то прибамбасы и начал их раскладывать: специальная коробочка была наполовину заполнена марихуаной, он насыпал ее на уже подготовленную бумажку и ловко завернул косячок размером чуть меньше обычной сигареты, улыбнулся ртом, в котором количество зубов можно было пересчитать по пальцам, и закурил. И да, мы там несколько раз видели магазины, которые нельзя встретить практически больше нигде в мире. Пора возвращаться на судно. Прыгнули в водный автобус и понеслись в сторону нашего причалм - Waalhaven. А на судне полным ходом идет погрузка. В одних трюмах уже стоят машины и экскаваторы, в другие грузят ящики с напитками и картошку. На крышки трюмов поставили еще пару экскаваторов и грузовиков, а также какие-то металлические конструкции. А на верхнюю палубу погрузили контейнеры, 60 штук. Почти все это добро мы повезем в Парамарибо, Суринам. В Роттердаме мы простояли почти 3 дня и вот, погрузка завершена и по причалу в нашу сторону идет человек в спасательном жилете и с чемоданом. Это лоцман. Я его проводил на мостик, включил ходовые огни, поставил на автоматической идентификационной системе (АИС) статус \"Under way using engine\" и вбил туда следующий порт назначения, поднял флаг \"Hotel\", обозначающий присутствие лоцмана на борту, согласовал АИС и радары с гирокомпасом и... мы поехали. Впереди нас ждет 10-дневный переход через Атлантический океан из Роттердама в Парамарибо.\n\n31 августа 2013г. Английский канал.\n\n\nОдин вечер.\nПодъезжая к морю на машине, либо подходя пешком, всегда замечаешь знакомый аромат и в голове появляется обычная мысль, которая иногда произносится вслух: Морем пахнет. А как пахнет море там, далеко-далеко, где не видно берегов, где под тобой глубина несколько километров? Или океан, где вокруг даже других судов не видно? Да никак оно не пахнет, там просто очень чистый воздух, очень легко дышится, но нет никаких запахов. Или запахи есть, но появились они на судне - то покрасят что-то и стоит резкий духан ацетоновой краски, то повар на камбузе готовит какую-нибудь вкусняшку и дразнящие ароматы ползут по коридорам. А морем не пахнет. Как так? Нет, я не принюхался и не привык к запаху моря, его на самом деле нет. И он появляется когда мы подходим к берегу. На самом деле пахнет не морем, это благоухают водоросли выброшенные на берег и еще что-нибудь вместе с ними. А звуки? в городе слышно суету, машины, людей и прочие шумы, в лесу - пенье птиц и шелест листьев, а в море? Особенно когда штиль и вода спокойная? А в море слышно работу главного двигателя. Не громко, находясь в каюте лишь слышно низкий гул, но его практически не замечаешь. Зато если спуститься в машинное отделение, вот там его прекрасно слышно - и гул, и свист турбины, и прочие шумы, которые в сумме своей громкостью напоминают запуск космической ракеты, к соплам которой тебя привязали ухом, чтоб лучше слышно было. Разговаривать в машинном отделении почти невозможно, с трудом удается разобрать слова когда тебе собеседник изо всех сил орет прямо в ухо. Поэтому там все носят шумозащитные наушники и не разговаривают :) Ну да ладно, вернемся на верхнюю палубу. А там стоят рефконтейнеры, в них находятся рефрежираторные установки для охлаждения содержимого и эти установки постоянно жужжат своими вентиляторами. Громко жужжат, разговаривать можно, а вот рацию можно и не услышать. Но можно пройти на бак (носовая часть судна) и там ничего этого практически не слышно - только шум ветра и шелест воды, которую рассекает нос судна. А еще говорят, мол, хочешь узнать где берег - ищи облака. Ерунда это. Атлантический океан, сотни, даже тысячи миль до берегов и... все небо в облаках, но не сплошная серая масса, а такое разнообразие, такое буйство красок, что диву даешься. Вечером пришел на вахту, время без пятнадцати восемь, а на небе такое... будто у Создателя разыгралась муза и он еле смог остановиться. Эту красоту сложно представить и невозможно передать словами. Горело все небо, всеми цветами, солнце медленно приближалось к горизонту, унося с собой очередной день и уступая место теплой ночи. (Да, здесь жнем жарко а ночью хорошо. Посередине атлантики вода ярко-синего цвета и ее температура +28 градусов, глубина около 3-5 километров). Слева недалеко от нас появилось какое-то движение - это стая дельфинов, весело выпрыгивая из воды спешит куда-то по своим важным дельфиньим делам. Смотрю, на радаре впереди появилась точка, милях в 5. Ну думаю, наверное помеха. Визуально ничего не видно. Смотрю на второй радар - там тоже есть засвет. Тааак, уже интересно, беру бинокль и... ничего не вижу. На обоих радарах точка приближается, причем сигнал уже идет четкий, явно не мусор засветился. Даже буй бы светился на радаре не так ярко. Проходит какое-то время, радары показывают объект в 3 милях от нас, двигается со скоростю 3 узла. Опять смотрю в бинокль - и ничего. Дельфины тоже были недалеко, но их радар не видел, так как опять же слишком мелкий объект. А тут вот оно, светится и никуда не девается. Кратчайшая дистанция была около двух миль, я так ничего и не увидел. Иногда радар бъет облака, но это выглядит абсолютно по-другому. Так что, что это было я не знаю. Вахта прошла спокойно, вокруг никого и ничего, глубина большая, бояться нечего. Вот уже без десяти двенадцать, на мостике скрипнула дверь и с хмурым лицом зашел принимать вахту третий помощник. Не выспался, видимо. А еще сегодня на его вахте переводятся часы на час назад, это значит что стоять ему на час больше. А мне на час больше спать :).\n-Все тихо-спокойно?\n-Да, как обычно.\n-Это радует (Егор улыбнулся, видимо мозг начал просыпаться)\nВремя без пяти, надо делать запись в судовой журнал, стою, пишу. И тут в ночной тиши из УКВ-радиостанции раздается смех. А мы уже несколько дней в эфире ничего не слышали - судов нету, а если и попадаются, то говорить с ними не о чем, всегда расходимя молча. Мы переглянулись, пожали плечами. Я продолжил писать в журнал, а Егор пошел смотреть радар, кто это там хулиганит. В этот момент из рации снова: хахааа-ха-ха-хаа... Мда, бывает же. Уже не обращаем внимания. И тут снова раздается смех и так совпало, что в этот момент с лампы подсведки стола, на котором я заполнял журнал, срывается висевшая на ней линейка и неожиданным шлепком падает прямо перед моим носом на журнал. \"Сука!!!\" - невольно вырвалось из меня. Как я себе в штаны не нагадил, я не знаю. Егор тоже вздрогнул от неожиданности, немая пауза. Молча вешаю линейку назад и через несколько секунд мы начинаем ржать. Ладно, задержусь немного, мы давно собирались настроить факсимильный приемник погоды, который тихонько стоит в радиорубке и не понятно, работает он вообще или нет. Включили. На приемнике загорелись лампочки. О, уже неплохо. Нашли частоты метеостанций, настроились на ближайшую и стали ждать. Потом решили оставить это дело на ночь, а утром проверить, авось чего примется. В итоге, через 12 часов мы вспомнили про приемник и пошли проверять. Всю ночь он принимал и распечатывал помехи и оттуда вылезли не погодные карты, а пять метров пестрых обоев. Решили пока эксперименты с погодным приемником приостановить и перенести на попозже, когда берега будут ближе. Все равно сейчас погода принимается специальной программой через спутниковый интернет.\n\n4 сентября 2013г. Атлантический океан." +} diff --git a/resources/xml/activity/main.xml b/resources/xml/activity/main.xml index d0e2dca5..9eb06aa4 100644 --- a/resources/xml/activity/main.xml +++ b/resources/xml/activity/main.xml @@ -7,6 +7,10 @@ + + + + diff --git a/resources/xml/tabs/scroll_test.xml b/resources/xml/tabs/scroll_test.xml new file mode 100644 index 00000000..5ce0627d --- /dev/null +++ b/resources/xml/tabs/scroll_test.xml @@ -0,0 +1,29 @@ + + + + + + + + + + + + +