// zlib open source license // // Copyright (c) 2018 to 2023 David Forsgren Piuva // // This software is provided 'as-is', without any express or implied // warranty. In no event will the authors be held liable for any damages // arising from the use of this software. // // Permission is granted to anyone to use this software for any purpose, // including commercial applications, and to alter it and redistribute it // freely, subject to the following restrictions: // // 1. The origin of this software must not be misrepresented; you must not // claim that you wrote the original software. If you use this software // in a product, an acknowledgment in the product documentation would be // appreciated but is not required. // // 2. Altered source versions must be plainly marked as such, and must not be // misrepresented as being the original software. // // 3. This notice may not be removed or altered from any source // distribution. #ifndef DFPSR_GUI_VISUALCOMPONENT #define DFPSR_GUI_VISUALCOMPONENT #include "../persistent/includePersistent.h" #include "BackendWindow.h" #include "FlexRegion.h" #include "InputEvent.h" #include "componentStates.h" #include "VisualTheme.h" #include "../../api/imageAPI.h" #include "../../api/drawAPI.h" namespace dsr { // A reusable method for calling the media machine that allow providing additional variables as style flags. MediaResult component_generateImage(VisualTheme theme, MediaMethod &method, int width, int height, int red, int green, int blue, int pressed = 0, int focused = 0, int hovered = 0); class VisualComponent : public Persistent { PERSISTENT_DECLARATION(VisualComponent) public: // Relations // Handle to the backend window. Handle window; // TODO: Should a weak handle be implemented to safely avoid cycles, or is this safe enough? // Parent component VisualComponent *parent = nullptr; IRect givenSpace; // Remembering the local region that was reserved inside of the parent component. bool regionAccessed = false; // If someone requested access to the region, remember to update layout in case of new settings. // Child components List> children; // Remember the component used for a drag event. // Ensures that mouse down events are followed by mouse up events on the same component. int holdCount = 0; // Marked for removal from the parent when set to true. bool detach = false; // Remember the pressed component for sending mouse move events outside of its region. Handle dragComponent; private: // States // Use methods to set the current state, then have it copied to previousState after calling updateStateEvent in sendNotifications. ComponentState currentState = 0; ComponentState previousState = 0; private: // State updates // TODO: Can a faster update be made using a limited traversal from changed component to root, while reusing the same code for the update? // Called after changing direct states. void updateIndirectStates(); // Looking for recent state changes and sending notifications through updateStateEvent for each components that had a state change. // Deferring update notifications using this makes sure that events that trigger updates get to finish before the next one starts. // This reduces the risk of dead-locks, race-conditions, pointer errors... // Also checking which components are marked for removal and detaching them, so that the object is not deleted while a memeber method is being called. void sendNotifications(); // Remove the zeroes in addMask from ones in the component and all child components. void applyStateAndMask(ComponentState keepMask); // Clears directStates from all other components sharing the root, iff unique is true. // Adds directStates to the component. // Updates indirect states based on direct states. void addStateBits(ComponentState directStates, bool unique); // Removes directStates to the component. // Updates indirect states based on direct states. void removeStateBits(ComponentState directStates); public: // Focus is reset when a new component is focused. // It was clicked directly last time. inline bool isFocused() { return (this->currentState & componentState_focusDirect) != 0; } // One of its recursive children was clicked last time. inline bool ownsFocus() { return (this->currentState & (componentState_focusDirect | componentState_focusIndirect)) != 0; } // Remove focus from all of the component's children. void defocusChildren(); // Give focus to a trail from root to the component. void makeFocused(); public: // Hover is reset before assigned again using a bit mask. // The cursor hovered within this component without being occluded. bool isHovered() { return (this->currentState & componentState_hoverDirect) != 0; } // The cursor hovered within its region, but one of its recursive children got the direct hover state. bool ownsHover() { return (this->currentState & (componentState_hoverDirect | componentState_hoverIndirect)) != 0; } // Make the component directly hovered and its parents indirectly hovered. void hover(); // Remove the hover effect. void leave(); public: // Showing overlay inline bool showingOverlay() { return (this->currentState & componentState_showingOverlayDirect) != 0; } inline bool ownsOverlay() { return (this->currentState & componentState_showingOverlay) != 0; } void showOverlay(); void hideOverlay(); public: // Saved properties FlexRegion region; PersistentString name; PersistentInteger index; PersistentBoolean visible = PersistentBoolean(true); void declareAttributes(StructureDefinition &target) const override; public: Persistent* findAttribute(const ReadableString &name) override; public: // Generated automatically from region in applyLayout IRect location; void setLocation(const IRect &newLocation); // Applied reqursively while selecting the correct theme VisualTheme theme = theme_getDefault(); public: void applyTheme(VisualTheme theme); VisualTheme getTheme() const; public: // Constructor VisualComponent(); // Destructor virtual ~VisualComponent(); public: virtual bool isContainer() const; IRect getLocation(); void setRegion(const FlexRegion &newRegion); FlexRegion getRegion() const; void setVisible(bool visible); bool getVisible() const; void setName(const String& newName); String getName() const; void setIndex(int index); int getIndex() const; public: // Internal events that component classes override to know when something has changed. // Called after the component has been created, moved or resized. virtual void updateLocationEvent(const IRect& oldLocation, const IRect& newLocation); // Called after a component's state changed, when it is relatively safe to do so. // All state changes will be sent at the same time, because state changes are often used to trigger other changes. // Changes to the state made within the notification will not trigger new notifications, because the old state is saved after the call is finished. virtual void updateStateEvent(ComponentState oldState, ComponentState newState); public: // Callbacks that the application use by assigning lambdas to specific components in the interface. DECLARE_CALLBACK(pressedEvent, emptyCallback); DECLARE_CALLBACK(destroyEvent, emptyCallback); DECLARE_CALLBACK(mouseDownEvent, mouseCallback); DECLARE_CALLBACK(mouseUpEvent, mouseCallback); DECLARE_CALLBACK(mouseMoveEvent, mouseCallback); DECLARE_CALLBACK(mouseScrollEvent, mouseCallback); DECLARE_CALLBACK(keyDownEvent, keyboardCallback); DECLARE_CALLBACK(keyUpEvent, keyboardCallback); DECLARE_CALLBACK(keyTypeEvent, keyboardCallback); DECLARE_CALLBACK(selectEvent, indexCallback); public: // Returning a shader pointer to the topmost direct visible child that contains pixelPosition. // The pixelPosition is relative to the called component's upper left corner. Handle getDirectChild(const IVector2D& pixelPosition); // Returning a shared pointer to itself. // Currently not working for the root component because of limitations in C++. Handle getHandle(); public: // Draw the component // The component is responsible for drawing the component at this->location + offset. // The caller is responsible for drawing the background for any pixels in the component that might not be fully opaque. // If drawing out of bound, the pixels that are outside should be skipped without any warning nor crash. // To clip the drawing of a component when calling this, give a sub-image and adjust for the new coordinate system using offset. // If not implemented, a rectangle will mark the region where the component will be drawn as a reference. // targetImage is the image being drawn to. // offset is the upper left corner of the parent container relative to the image. // Clipping will affect the offset by being relative to the new sub-image. void draw(ImageRgbaU8& targetImage, const IVector2D& offset); // Draw the component itself to targetImage at relativeLocation. // The method is responsible for clipping without a warning when bound is outside of targetImage. virtual void drawSelf(ImageRgbaU8& targetImage, const IRect &relativeLocation); // Draw the component's overlays on top of other components in the window. // Overlays are drawn using absolute positions on the canvas. // The absoluteOffset is the location of the component's upper left corner relative to the whole window's canvas. // Use for anything that needs to be drawn on top of other components without being clipped by any parent components. virtual void drawOverlay(ImageRgbaU8& targetImage, const IVector2D &absoluteOffset); // Draw the component while skipping pixels outside of clipRegion // Multiple calls with non-overlapping clip regions should be equivalent to one call with the union of all clip regions. // This means that the draw methods should handle border clipping so that no extra borderlines or rounded edges appear from nowhere. // Example: // drawClipped(i, o, IRect(0, 0, 20, 20)) // Full region // <=> // drawClipped(i, o, IRect(0, 0, 10, 20)) // Left half // drawClipped(i, o, IRect(10, 0, 10, 20)) // Right half // Drawing with the whole target image as a clip region should be equivalent to a corresponding call to draw with the same targetImage and offset. // draw(i, o) <=> drawClipped(i, o, IRect(0, 0, i.width(), i.height())) void drawClipped(ImageRgbaU8 targetImage, const IVector2D& offset, const IRect& clipRegion); // Add a child component // Preconditions: // The parent's component type is a container. // The child does not already have a parent. void addChildComponent(Handle child); // Called with any persistent type when constructing child components from text bool addChild(Handle child) override; // Called when saving to text int getChildCount() const override; Handle getChild(int index) const override; // Returns true iff child is a member of the component // Searches recursively bool hasChild(VisualComponent *child) const; bool hasChild(Handle child) const; // Find the first child component with the requested name using a case sensitive match. // Returns: A shared pointer to the child or null if not found. Handle findChildByName(ReadableString name) const; Handle findChildByNameAndIndex(ReadableString name, int index) const; // Detach the component from any parent void detachFromParent(); // Adapt the location based on the space given by the parent. // The given space is usually a rectangle starting at the origin with the same dimensions as the parent component. // If the parent has decorations around the child components, the region may include some padding from which the flexible regions calculate the locations from in percents. // For example: A given space from 10 to 90 pixels will have 0% at 10 and 100% at 90. // A toolbar may give non-overlapping spaces that are assigned automatically to simplify the process of maintaining the layout while adding and removing child components. // TODO: How can internal changes to inner dimensions be detected by the parent to run this method again? virtual void applyLayout(const IRect& givenSpace); // Update layout when the component moved but the parent has the same dimensions void updateLayout(); // TODO: Remake desiredDimensions into a private variable, so that one can update it when attributes are changed and notify parents that they need to change size and redraw images. // Parent components that place child components automatically can ask them what their minimum useful dimensions are in pixels, so that their text will be visible. // The component can still be resized to less than these dimensions, because the outer components can't give more space than what is given by the window. virtual IVector2D getDesiredDimensions(); // Return true to turn off automatic drawing of and interaction with child components. virtual bool managesChildren(); // Get the component's absolute position relative to the window's client region. //IVector2D getAbsolutePosition(); // Calling updateLocationEvent without changing the location, to be used when a child component changed its desired dimensions from altering attributes. bool childChanged = false; // Called before rendering or getting mouse input in case that a child component changed desired dimensions. void updateChildLocations(); // Returns true iff the pixel relative to the parent container's upper left corner is inside of the component. // By default, it returns true when pixelPosition is within the component's location, because most component are solid. // The caller is responsible for checking if the component is visible (this->visible.value), so this method would return true if the pixelPosition is inside of an invisible component. virtual bool pointIsInside(const IVector2D& pixelPosition); // Returns true iff the pixelPosition relative to the parent container's upper left corner is inside of the component's overlay. // The caller is responsible for checking if the component is showing an overlay (this->showOverlay). virtual bool pointIsInsideOfOverlay(const IVector2D& pixelPosition); // Return true iff the pixelPosition relative to the parent container's upper left corner is inside of the region causing a hover effect. virtual bool pointIsInsideOfHover(const IVector2D& pixelPosition); // Send a mouse down event to the component // pixelPosition is relative to the component's own upper left corner. TODO: Does this make sense, or should it use parent coordinates? // The component is reponsible for bound checking, which can be used to either block the signal or pass to components below. // If recursive is true, notifications will be supressed to prevent duplicate events when called from within receiveMouseEvent. void sendMouseEvent(const MouseEvent& event, bool recursive = false); void sendKeyboardEvent(const KeyboardEvent& event); // Defines what the component does when it has received an event that didn't hit any sub components on the way. // pixelPosition is relative to the parent's (this->parent) upper left corner. // This is not a callback event, but a way for the component to handle events. virtual void receiveMouseEvent(const MouseEvent& event); virtual void receiveKeyboardEvent(const KeyboardEvent& event); // Notifies when the theme has been changed, so that temporary data depending on the theme can be replaced. virtual void changedTheme(VisualTheme newTheme); // Override to be notified about individual attribute changes virtual void changedAttribute(const ReadableString &name); // Override to be notified about location changes virtual void changedLocation(const IRect &oldLocation, const IRect &newLocation) {}; // Custom call handler to manipulate components across a generic API virtual String call(const ReadableString &methodName, const ReadableString &arguments); }; } #endif