mirror of https://gitlab.com/nakst/essence
8044 lines
283 KiB
C++
8044 lines
283 KiB
C++
// This file is part of the Essence operating system.
|
|
// It is released under the terms of the MIT license -- see LICENSE.md.
|
|
// Written by: nakst.
|
|
|
|
// TODO Styling features:
|
|
// - Specifying aspect ratio of element.
|
|
// - Animation parts (list of keyframes).
|
|
// - Ripple animations.
|
|
// - Exiting animations.
|
|
// - Morph from/to entrance/exit animations.
|
|
// TODO Close menus within menus (bug).
|
|
// TODO Keyboard navigation - menus; escape to restore default focus.
|
|
// TODO Middle click panning.
|
|
// TODO Scrollbar middle click and zooming.
|
|
// TODO Textboxes: date/time overlays, keyboard shortcut overlay, custom overlays.
|
|
// TODO Breadcrumb bar overflow menu; keep hover after recreating UI.
|
|
// TODO Textbox embedded objects.
|
|
// TODO Closing windows in menu/access key mode.
|
|
|
|
// Behaviour of activation clicks. --> Only ignore activation clicks from menus.
|
|
// Behaviour of the scroll wheel with regards to focused/hovered elements --> Scroll the hovered element only.
|
|
|
|
// TODO Get these from the theme file.
|
|
#define CONTAINER_SOLID_T ((int) (25 * theming.scale))
|
|
#define CONTAINER_SOLID_B ((int) (35 * theming.scale))
|
|
#define CONTAINER_SOLID_C ((int) (30 * theming.scale))
|
|
#define CONTAINER_EMBED_T ((int) (64 * theming.scale))
|
|
#define CONTAINER_EMBED_B ((int) (44 * theming.scale))
|
|
#define CONTAINER_EMBED_C ((int) (39 * theming.scale))
|
|
#define CONTAINER_OPAQUE_T ((int) (30 * theming.scale))
|
|
#define CONTAINER_OPAQUE_B ((int) (40 * theming.scale))
|
|
#define CONTAINER_OPAQUE_C ((int) (35 * theming.scale))
|
|
#define CONTAINER_MAXIMIZE_T ((int) (29 * theming.scale))
|
|
#define CONTAINER_MAXIMIZE_B ((int) (44 * theming.scale))
|
|
#define CONTAINER_MAXIMIZE_C ((int) (39 * theming.scale))
|
|
#define CONTAINER_TAB_BAND_HEIGHT ((int) (33 * theming.scale))
|
|
#define CONTAINER_RESIZE_BORDER ((int) (CONTAINER_EMBED_C - CONTAINER_SOLID_C))
|
|
#define CONTAINER_RESIZE_OFFSET ((int) (CONTAINER_RESIZE_BORDER / 2))
|
|
#define CONTAINER_SNAP_T ((int) (0 * theming.scale))
|
|
#define CONTAINER_SNAP_B ((int) (-4 * theming.scale))
|
|
#define CONTAINER_SNAP_OUTSIDE ((int) (-5 * theming.scale))
|
|
#define CONTAINER_SNAP_INSIDE ((int) (17 * theming.scale))
|
|
|
|
struct AccessKeyEntry {
|
|
char character;
|
|
int number;
|
|
EsElement *element;
|
|
EsRectangle bounds;
|
|
};
|
|
|
|
struct {
|
|
// Animations.
|
|
EsTimer animationSleepTimer;
|
|
bool animationSleep;
|
|
Array<EsElement *> animatingElements;
|
|
|
|
// Input.
|
|
bool draggingStarted, mouseButtonDown;
|
|
uint8_t leftModifiers, rightModifiers;
|
|
int lastClickX, lastClickY, lastClickButton;
|
|
|
|
// Menus.
|
|
bool menuMode;
|
|
|
|
// Access keys.
|
|
bool accessKeyMode, accessKeyModeJustExited;
|
|
|
|
struct {
|
|
Array<AccessKeyEntry> entries;
|
|
int numbers[26];
|
|
struct UIStyle *hintStyle;
|
|
EsWindow *window;
|
|
char typedCharacter;
|
|
} accessKeys;
|
|
|
|
// Misc.
|
|
Array<EsWindow *> allWindows;
|
|
HashTable keyboardShortcutNames;
|
|
EsElement *insertAfter;
|
|
|
|
// Resizing data.
|
|
bool resizing, resizingBothSides;
|
|
EsRectangle resizeStartBounds;
|
|
|
|
// Click chains.
|
|
double clickChainStartMs;
|
|
int clickChainCount;
|
|
EsElement *clickChainElement;
|
|
} gui;
|
|
|
|
struct TableCell {
|
|
uint16_t from[2], to[2];
|
|
};
|
|
|
|
// Miscellanous forward declarations.
|
|
void UIWindowPaintNow(EsWindow *window, ProcessMessageTiming *timing, bool afterResize);
|
|
void UIWindowLayoutNow(EsWindow *window, ProcessMessageTiming *timing);
|
|
EsElement *WindowGetMainPanel(EsWindow *window);
|
|
int AccessKeyLayerMessage(EsElement *element, EsMessage *message);
|
|
void AccessKeyModeExit();
|
|
int ProcessButtonMessage(EsElement *element, EsMessage *message);
|
|
void UIMouseUp(EsWindow *window, EsMessage *message, bool sendClick);
|
|
void UIMaybeRemoveFocusedElement(EsWindow *window);
|
|
EsTextStyle TextPlanGetPrimaryStyle(EsTextPlan *plan);
|
|
EsElement *UIFindHoverElementRecursively(EsElement *element, int offsetX, int offsetY, EsPoint position);
|
|
EsStyleID UIGetDefaultStyleVariant(EsStyleID style, EsElement *parent);
|
|
void AccessKeysCenterHint(EsElement *element, EsMessage *message);
|
|
void UIRemoveFocusFromElement(EsElement *oldFocus);
|
|
void UIQueueEnsureVisibleMessage(EsElement *element, bool center);
|
|
void ColorPickerCreate(EsElement *parent, struct ColorPickerHost host, uint32_t initialColor, bool showTextbox);
|
|
|
|
void InspectorSetup(EsWindow *window);
|
|
void InspectorNotifyElementEvent(EsElement *element, const char *cCategory, const char *cFormat, ...);
|
|
void InspectorNotifyElementCreated(EsElement *element);
|
|
void InspectorNotifyElementDestroyed(EsElement *element);
|
|
void InspectorNotifyElementMoved(EsElement *element, EsRectangle takenBounds);
|
|
void InspectorNotifyElementPainted(EsElement *element, EsPainter *painter);
|
|
void InspectorNotifyElementContentChanged(EsElement *element);
|
|
|
|
// Updating:
|
|
#define UI_STATE_RELAYOUT (1 << 0)
|
|
#define UI_STATE_RELAYOUT_CHILD (1 << 1)
|
|
#define UI_STATE_DESTROYING (1 << 2)
|
|
#define UI_STATE_DESTROYING_CHILD (1 << 3)
|
|
#define UI_STATE_IN_LAYOUT (1 << 4)
|
|
|
|
// Interaction state:
|
|
#define UI_STATE_FOCUS_WITHIN (1 << 5)
|
|
#define UI_STATE_FOCUSED (1 << 6)
|
|
#define UI_STATE_LOST_STRONG_FOCUS (1 << 7)
|
|
#define UI_STATE_ENTERED (1 << 8)
|
|
|
|
// Presence on arrays:
|
|
#define UI_STATE_ANIMATING (1 << 9)
|
|
#define UI_STATE_CHECK_VISIBLE (1 << 10)
|
|
#define UI_STATE_QUEUED_ENSURE_VISIBLE (1 << 11)
|
|
#define UI_STATE_ENSURE_VISIBLE_CENTER (1 << 12)
|
|
|
|
// Behaviour modifiers:
|
|
#define UI_STATE_STRONG_PRESSED (1 << 12)
|
|
#define UI_STATE_Z_STACK (1 << 13)
|
|
#define UI_STATE_COMMAND_BUTTON (1 << 14)
|
|
#define UI_STATE_BLOCK_INTERACTION (1 << 15)
|
|
#define UI_STATE_RADIO_GROUP (1 << 16)
|
|
|
|
// Miscellaneous state bits:
|
|
#define UI_STATE_TEMP (1 << 17)
|
|
#define UI_STATE_MENU_SOURCE (1 << 18)
|
|
#define UI_STATE_MENU_EXITING (1 << 19)
|
|
#define UI_STATE_INSPECTING (1 << 20)
|
|
#define UI_STATE_USE_MEASUREMENT_CACHE (1 << 21)
|
|
|
|
struct EsElement : EsElementPublic {
|
|
EsElementCallback messageClass;
|
|
EsElement *parent;
|
|
Array<EsElement *> children;
|
|
uint32_t state;
|
|
|
|
uint8_t transitionType, transitionFlags;
|
|
uint16_t customStyleState; // ORed to the style state in RefreshStyle.
|
|
uint16_t previousStyleState; // Set by RefreshStyleState.
|
|
uint16_t transitionDurationMs, transitionTimeMs;
|
|
uint64_t lastTimeStamp;
|
|
UIStyle *style;
|
|
UIStyleKey currentStyleKey;
|
|
ThemeAnimation animation;
|
|
EsPaintTarget *previousTransitionFrame;
|
|
|
|
int width, height, offsetX, offsetY;
|
|
uint8_t internalOffsetLeft, internalOffsetRight, internalOffsetTop, internalOffsetBottom;
|
|
TableCell tableCell;
|
|
|
|
void Destroy(bool manual = true);
|
|
void PrintTree(int depth = 0);
|
|
|
|
inline size_t GetChildCount() {
|
|
return children.Length();
|
|
}
|
|
|
|
inline EsElement *GetChild(uintptr_t index) {
|
|
EsAssert(index < children.Length()); // Invalid child index.
|
|
return children[index];
|
|
}
|
|
|
|
inline EsElement *GetChildByZ(uintptr_t index) {
|
|
EsMessage m = { ES_MSG_Z_ORDER };
|
|
m.zOrder.index = index, m.zOrder.child = GetChild(index);
|
|
if (m.zOrder.child->flags & ES_ELEMENT_NON_CLIENT) return m.zOrder.child;
|
|
if (ES_REJECTED == EsMessageSend(this, &m)) return nullptr;
|
|
EsAssert(!m.zOrder.child || m.zOrder.child->parent == this); // Child obtained from ES_MSG_Z_ORDER had different parent.
|
|
return m.zOrder.child;
|
|
}
|
|
|
|
inline EsRectangle GetInternalOffset() {
|
|
return ES_RECT_4(internalOffsetLeft, internalOffsetRight, internalOffsetTop, internalOffsetBottom);
|
|
}
|
|
|
|
void BringToFront() {
|
|
for (uintptr_t i = 0; i < parent->children.Length(); i++) {
|
|
if (parent->children[i] == this) {
|
|
InspectorNotifyElementDestroyed(this);
|
|
EsElement *swap = parent->children.Last();
|
|
parent->children.Last() = this;
|
|
parent->children[i] = swap;
|
|
InspectorNotifyElementCreated(this);
|
|
return;
|
|
}
|
|
}
|
|
|
|
EsAssert(false);
|
|
}
|
|
|
|
bool IsFocusable() {
|
|
if ((~flags & ES_ELEMENT_FOCUSABLE) || (flags & ES_ELEMENT_DISABLED) || (state & UI_STATE_DESTROYING)) {
|
|
return false;
|
|
}
|
|
|
|
EsElement *element = this;
|
|
|
|
while (element) {
|
|
if ((element->flags & ES_ELEMENT_BLOCK_FOCUS) || (element->state & UI_STATE_BLOCK_INTERACTION) || (element->flags & ES_ELEMENT_HIDDEN)) {
|
|
return false;
|
|
}
|
|
|
|
element = element->parent;
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
bool IsTabTraversable() {
|
|
return IsFocusable() && (~flags & ES_ELEMENT_NOT_TAB_TRAVERSABLE) && (!parent || ~parent->state & UI_STATE_RADIO_GROUP);
|
|
}
|
|
|
|
bool RefreshStyleState(); // Returns true if any observed bits have changed.
|
|
void RefreshStyle(UIStyleKey *oldStyleKey = nullptr, bool alreadyRefreshStyleState = false, bool force = false);
|
|
bool StartAnimating();
|
|
void SetStyle(EsStyleID stylePart, bool refreshIfChanged = true);
|
|
|
|
inline void MaybeRefreshStyle() {
|
|
if (RefreshStyleState()) {
|
|
RefreshStyle(nullptr, true);
|
|
}
|
|
}
|
|
|
|
inline EsRectangle GetWindowBounds(bool client = true) { return EsElementGetWindowBounds(this, client); }
|
|
inline EsRectangle GetScreenBounds(bool client = true) { return EsElementGetScreenBounds(this, client); }
|
|
inline EsRectangle GetBounds() { return ES_RECT_2S(width - internalOffsetLeft - internalOffsetRight, height - internalOffsetTop - internalOffsetBottom); }
|
|
|
|
#define PAINT_SHADOW (1 << 0) // Paint the shadow layers.
|
|
#define PAINT_NO_OFFSET (1 << 1) // Don't add the element's offset to the painter.
|
|
#define PAINT_NO_TRANSITION (1 << 2) // Ignore entrance/exit transitions.
|
|
#define PAINT_OVERLAY (1 << 3) // Paint the overlay layers.
|
|
#define PAINT_NO_BACKGROUND (1 << 4) // Don't paint the background.
|
|
void InternalPaint(EsPainter *painter, int flags);
|
|
|
|
void InternalMove(int _width, int _height, int _offsetX, int _offsetY); // Non-client offset.
|
|
void InternalCalculateRepaintRegion(int x, int y, bool forwards, bool overlappedBySibling = false);
|
|
bool InternalDestroy(); // Called after processing each message, to destroy any elements marked by ::Destroy.
|
|
|
|
int GetWidth(int height);
|
|
int GetHeight(int width);
|
|
|
|
void Repaint(bool all, EsRectangle region = ES_RECT_1(0) /* client coordinates */);
|
|
|
|
void Initialise(EsElement *_parent, uint64_t _flags, EsElementCallback _classCallback, EsStyleID style);
|
|
};
|
|
|
|
struct MeasurementCache {
|
|
int width0, width2, width2Height;
|
|
int height0, height2, height2Width;
|
|
|
|
bool Get(EsMessage *message, uint32_t *state) {
|
|
if (~(*state) & UI_STATE_USE_MEASUREMENT_CACHE) {
|
|
width0 = 0, width2 = 0, width2Height = 0;
|
|
height0 = 0, height2 = 0, height2Width = 0;
|
|
*state |= UI_STATE_USE_MEASUREMENT_CACHE;
|
|
}
|
|
|
|
if (message->type == ES_MSG_GET_WIDTH) {
|
|
if (message->measure.height && message->measure.height == width2Height) {
|
|
message->measure.width = width2;
|
|
} else if (!message->measure.height && width0) {
|
|
message->measure.width = width0;
|
|
} else {
|
|
return false;
|
|
}
|
|
} else {
|
|
if (message->measure.width && message->measure.width == height2Width) {
|
|
message->measure.height = height2;
|
|
} else if (!message->measure.width && height0) {
|
|
message->measure.height = height0;
|
|
} else {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
void Store(EsMessage *message) {
|
|
if (message->type == ES_MSG_GET_WIDTH) {
|
|
if (message->measure.height) {
|
|
width2 = message->measure.width;
|
|
width2Height = message->measure.height;
|
|
} else {
|
|
width0 = message->measure.width;
|
|
}
|
|
} else {
|
|
if (message->measure.width) {
|
|
height2Width = message->measure.width;
|
|
height2 = message->measure.height;
|
|
} else {
|
|
height0 = message->measure.height;
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
struct EsButton : EsElement {
|
|
char *label;
|
|
size_t labelBytes;
|
|
uint32_t iconID;
|
|
MeasurementCache measurementCache;
|
|
EsCommand *command;
|
|
EsCommandCallback onCommand;
|
|
EsElement *checkBuddy;
|
|
EsImageDisplay *imageDisplay;
|
|
};
|
|
|
|
struct MenuItem : EsButton {
|
|
// This shares the EsButton structure so that it can be used with EsButtonOnCommand.
|
|
EsGeneric menuItemContext;
|
|
};
|
|
|
|
struct EsImageDisplay : EsElement {
|
|
void *source;
|
|
size_t sourceBytes;
|
|
|
|
uint32_t *bits;
|
|
size_t width, height, stride;
|
|
};
|
|
|
|
struct ScrollPane {
|
|
EsElement *parent, *pad;
|
|
struct Scrollbar *bar[2];
|
|
double position[2];
|
|
int64_t limit[2];
|
|
int32_t fixedViewport[2];
|
|
bool enabled[2];
|
|
bool dragScrolling;
|
|
|
|
uint8_t mode[2];
|
|
uint16_t flags;
|
|
|
|
void Setup(EsElement *parent, uint8_t xMode, uint8_t yMode, uint16_t flags);
|
|
void SetPosition(int axis, double newPosition, bool sendMovedMessage = true);
|
|
void Refresh();
|
|
int ReceivedMessage(EsMessage *message);
|
|
|
|
inline void SetX(double scrollX, bool sendMovedMessage = true) { SetPosition(0, scrollX, sendMovedMessage); }
|
|
inline void SetY(double scrollY, bool sendMovedMessage = true) { SetPosition(1, scrollY, sendMovedMessage); }
|
|
|
|
// Internal.
|
|
bool RefreshLimit(int axis, int64_t *content);
|
|
};
|
|
|
|
struct PanelMovementItem {
|
|
EsElement *element;
|
|
EsRectangle oldBounds;
|
|
bool wasHidden;
|
|
};
|
|
|
|
struct EsPanel : EsElement {
|
|
// TODO Make this structure smaller?
|
|
|
|
ScrollPane scroll;
|
|
|
|
EsStyleID separatorStylePart;
|
|
bool addingSeparator;
|
|
uint64_t separatorFlags;
|
|
|
|
uint16_t bandCount[2];
|
|
EsPanelBand *bands[2];
|
|
uintptr_t tableIndex;
|
|
uint8_t *tableMemoryBase;
|
|
Array<EsPanelBandDecorator> bandDecorators;
|
|
|
|
// TODO This names overlap with fields in EsElement, they should probably be renamed.
|
|
uint16_t transitionType;
|
|
uint32_t transitionTimeMs,
|
|
transitionLengthMs;
|
|
|
|
bool destroyPreviousAfterTransitionCompletes;
|
|
EsElement *switchedTo,
|
|
*switchedFrom;
|
|
|
|
Array<PanelMovementItem> movementItems;
|
|
|
|
MeasurementCache measurementCache;
|
|
|
|
int GetGapMajor() {
|
|
return style->gapMajor;
|
|
}
|
|
|
|
int GetGapMinor() {
|
|
return style->gapMinor;
|
|
}
|
|
|
|
EsRectangle GetInsets() {
|
|
return style->insets;
|
|
}
|
|
|
|
int GetInsetWidth() {
|
|
EsRectangle insets = GetInsets();
|
|
return insets.l + insets.r;
|
|
}
|
|
|
|
int GetInsetHeight() {
|
|
EsRectangle insets = GetInsets();
|
|
return insets.t + insets.b;
|
|
}
|
|
};
|
|
|
|
struct EsTextDisplay : EsElement {
|
|
EsTextPlanProperties properties;
|
|
EsTextRun *textRuns;
|
|
size_t textRunCount;
|
|
char *contents;
|
|
|
|
bool usingSyntaxHighlighting;
|
|
|
|
MeasurementCache measurementCache;
|
|
EsTextPlan *plan;
|
|
int planWidth, planHeight;
|
|
};
|
|
|
|
struct ColorPickerHost {
|
|
struct EsElement *well;
|
|
bool *indeterminate;
|
|
bool hasOpacity;
|
|
};
|
|
|
|
void HeapDuplicate(void **pointer, size_t *outBytes, const void *data, size_t bytes) {
|
|
if (*pointer) {
|
|
EsHeapFree(*pointer);
|
|
}
|
|
|
|
if (!data && !bytes) {
|
|
*pointer = nullptr;
|
|
*outBytes = 0;
|
|
} else {
|
|
void *buffer = EsHeapAllocate(bytes, false);
|
|
|
|
if (buffer) {
|
|
EsMemoryCopy(buffer, data, bytes);
|
|
*pointer = buffer;
|
|
*outBytes = bytes;
|
|
} else {
|
|
*pointer = nullptr;
|
|
*outBytes = 0;
|
|
}
|
|
}
|
|
}
|
|
|
|
struct EsWindow : EsElement {
|
|
EsHandle handle;
|
|
EsObjectID id;
|
|
EsWindowStyle windowStyle;
|
|
uint32_t windowWidth, windowHeight;
|
|
|
|
// TODO Replace this with a bitset?
|
|
bool willUpdate, toolbarFillMode, doNotPaint;
|
|
bool restoreOnNextMove, resetPositionOnNextMove, receivedFirstResize, isMaximised;
|
|
bool hovering, activated, appearActivated;
|
|
bool visualizeRepaints, visualizeLayoutBounds, visualizePaintSteps; // Inspector properties.
|
|
|
|
uint8_t resizeType;
|
|
EsCursorStyle resizeCursor;
|
|
|
|
EsElement *mainPanel, *toolbar;
|
|
EsPanel *toolbarSwitcher;
|
|
Array<EsDialog *> dialogs;
|
|
|
|
EsPoint mousePosition;
|
|
|
|
EsElement *hovered,
|
|
*pressed,
|
|
*focused,
|
|
*inactiveFocus,
|
|
*dragged;
|
|
|
|
EsButton *enterButton,
|
|
*escapeButton,
|
|
*defaultEnterButton;
|
|
|
|
// An array of elements that we check are visible after every layout.
|
|
// e.g. image displays that want to unload the decoded bitmap when they are scrolled off-screen.
|
|
// TODO Support a more advanced queueing system for scroll panes asynchronous tasks.
|
|
Array<EsElement *> checkVisible;
|
|
bool processCheckVisible;
|
|
|
|
double animationTime;
|
|
|
|
EsRectangle beforeMaximiseBounds, targetBounds, animateFromBounds;
|
|
bool animateToTargetBoundsAfterResize;
|
|
|
|
EsPoint announcementBase;
|
|
|
|
EsRectangle updateRegion;
|
|
EsRectangle updateRegionInProgress; // For visualizePaintSteps.
|
|
|
|
Array<struct SizeAlternative> sizeAlternatives;
|
|
Array<struct UpdateAction> updateActions;
|
|
|
|
EsElement *source; // Menu source.
|
|
EsWindow *targetMenu; // The menu that keyboard events should be sent to.
|
|
};
|
|
|
|
struct UpdateAction {
|
|
EsElement *element;
|
|
EsGeneric context;
|
|
void (*callback)(EsElement *, EsGeneric);
|
|
};
|
|
|
|
struct SizeAlternative {
|
|
EsElement *small, *big;
|
|
int widthThreshold, heightThreshold;
|
|
};
|
|
|
|
// --------------------------------- Container windows.
|
|
|
|
#define RESIZE_LEFT (1)
|
|
#define RESIZE_RIGHT (2)
|
|
#define RESIZE_TOP (4)
|
|
#define RESIZE_BOTTOM (8)
|
|
#define RESIZE_TOP_LEFT (5)
|
|
#define RESIZE_TOP_RIGHT (6)
|
|
#define RESIZE_BOTTOM_LEFT (9)
|
|
#define RESIZE_BOTTOM_RIGHT (10)
|
|
#define RESIZE_MOVE (0)
|
|
|
|
#define SNAP_EDGE_MAXIMIZE (1)
|
|
#define SNAP_EDGE_LEFT (2)
|
|
#define SNAP_EDGE_RIGHT (3)
|
|
|
|
void WindowSnap(EsWindow *window, bool restored, bool dragging, uint8_t edge) {
|
|
if (window->isMaximised) {
|
|
return;
|
|
}
|
|
|
|
EsRectangle screen;
|
|
EsSyscall(ES_SYSCALL_SCREEN_WORK_AREA_GET, 0, (uintptr_t) &screen, 0, 0);
|
|
|
|
if (!window->restoreOnNextMove && !restored) {
|
|
window->beforeMaximiseBounds = EsWindowGetBounds(window);
|
|
}
|
|
|
|
window->restoreOnNextMove = true;
|
|
window->isMaximised = edge == SNAP_EDGE_MAXIMIZE;
|
|
|
|
EsRectangle bounds;
|
|
|
|
if (edge == SNAP_EDGE_MAXIMIZE) {
|
|
bounds.t = screen.t - CONTAINER_MAXIMIZE_T;
|
|
bounds.b = screen.b + CONTAINER_MAXIMIZE_B;
|
|
bounds.l = screen.l - CONTAINER_MAXIMIZE_C;
|
|
bounds.r = screen.r + CONTAINER_MAXIMIZE_C;
|
|
} else if (edge == SNAP_EDGE_LEFT) {
|
|
bounds.t = screen.t + CONTAINER_SNAP_T;
|
|
bounds.b = screen.b - CONTAINER_SNAP_B;
|
|
bounds.l = screen.l + CONTAINER_SNAP_OUTSIDE;
|
|
bounds.r = (screen.r + screen.l) / 2 + CONTAINER_SNAP_INSIDE;
|
|
} else if (edge == SNAP_EDGE_RIGHT) {
|
|
bounds.t = screen.t + CONTAINER_SNAP_T;
|
|
bounds.b = screen.b - CONTAINER_SNAP_B;
|
|
bounds.l = (screen.r + screen.l) / 2 - CONTAINER_SNAP_INSIDE;
|
|
bounds.r = screen.r - CONTAINER_SNAP_OUTSIDE;
|
|
}
|
|
|
|
EsSyscall(ES_SYSCALL_WINDOW_MOVE, window->handle, (uintptr_t) &bounds, 0,
|
|
ES_WINDOW_MOVE_DYNAMIC | (edge == SNAP_EDGE_MAXIMIZE ? ES_WINDOW_MOVE_MAXIMIZED : 0));
|
|
|
|
if (!dragging) {
|
|
window->resetPositionOnNextMove = true;
|
|
}
|
|
}
|
|
|
|
void WindowRestore(EsWindow *window, bool dynamic = true) {
|
|
if (!window->restoreOnNextMove) {
|
|
return;
|
|
}
|
|
|
|
window->isMaximised = false;
|
|
window->restoreOnNextMove = false;
|
|
EsSyscall(ES_SYSCALL_WINDOW_MOVE, window->handle, (uintptr_t) &window->beforeMaximiseBounds, 0, dynamic ? ES_WINDOW_MOVE_DYNAMIC : ES_FLAGS_DEFAULT);
|
|
}
|
|
|
|
void WindowChangeBounds(int direction, int newX, int newY, int *originalX, int *originalY, EsWindow *window,
|
|
bool bothSides = false, EsRectangle *startBounds = nullptr) {
|
|
EsRectangle previousBounds = EsWindowGetBounds(window);
|
|
int oldWidth = Width(previousBounds);
|
|
int oldHeight = Height(previousBounds);
|
|
|
|
bool restored = false, canSnap = true;
|
|
|
|
if (window->restoreOnNextMove) {
|
|
window->restoreOnNextMove = false;
|
|
oldWidth = window->beforeMaximiseBounds.r - window->beforeMaximiseBounds.l;
|
|
oldHeight = window->beforeMaximiseBounds.b - window->beforeMaximiseBounds.t;
|
|
restored = true;
|
|
}
|
|
|
|
EsRectangle bounds = previousBounds;
|
|
|
|
if (direction & RESIZE_LEFT) bounds.l = newX - CONTAINER_SOLID_C - CONTAINER_RESIZE_OFFSET;
|
|
if (direction & RESIZE_RIGHT) bounds.r = newX + CONTAINER_SOLID_C + CONTAINER_RESIZE_OFFSET;
|
|
if (direction & RESIZE_TOP) bounds.t = newY - CONTAINER_SOLID_T - CONTAINER_RESIZE_OFFSET;
|
|
if (direction & RESIZE_BOTTOM) bounds.b = newY + CONTAINER_SOLID_B + CONTAINER_RESIZE_OFFSET;
|
|
|
|
if (startBounds && direction != RESIZE_MOVE) {
|
|
if (direction & RESIZE_LEFT) bounds.r = gui.resizeStartBounds.r + (bothSides ? gui.resizeStartBounds.l - newX : 0);
|
|
if (direction & RESIZE_RIGHT) bounds.l = gui.resizeStartBounds.l + (bothSides ? gui.resizeStartBounds.r - newX : 0);
|
|
if (direction & RESIZE_TOP) bounds.b = gui.resizeStartBounds.b + (bothSides ? gui.resizeStartBounds.t - newY : 0);
|
|
if (direction & RESIZE_BOTTOM) bounds.t = gui.resizeStartBounds.t + (bothSides ? gui.resizeStartBounds.b - newY : 0);
|
|
}
|
|
|
|
EsRectangle screen;
|
|
EsSyscall(ES_SYSCALL_SCREEN_WORK_AREA_GET, 0, (uintptr_t) &screen, 0, 0);
|
|
|
|
int windowSnapRange = GetConstantNumber("windowSnapRange");
|
|
int windowMinimumWidth = GetConstantNumber("windowMinimumWidth");
|
|
int windowMinimumHeight = GetConstantNumber("windowMinimumHeight");
|
|
int windowRestoreDragYPosition = GetConstantNumber("windowRestoreDragYPosition");
|
|
|
|
window->isMaximised = false;
|
|
window->animateToTargetBoundsAfterResize = false;
|
|
window->animationTime = -1;
|
|
|
|
if (direction == RESIZE_MOVE) {
|
|
if (newY < screen.t + windowSnapRange && canSnap) {
|
|
WindowSnap(window, restored, true, SNAP_EDGE_MAXIMIZE);
|
|
return;
|
|
} else if (newX < screen.l + windowSnapRange && canSnap) {
|
|
WindowSnap(window, restored, true, SNAP_EDGE_LEFT);
|
|
return;
|
|
} else if (newX >= screen.r - windowSnapRange && canSnap) {
|
|
WindowSnap(window, restored, true, SNAP_EDGE_RIGHT);
|
|
return;
|
|
} else {
|
|
if (restored && window->resetPositionOnNextMove) {
|
|
// The user previously snapped/maximized the window in a previous operation.
|
|
// Therefore, the movement anchor won't be what the user expects.
|
|
// Try to put it in the center.
|
|
int positionAlongWindow = *originalX - previousBounds.l;
|
|
int maxPosition = previousBounds.r - previousBounds.l;
|
|
if (positionAlongWindow > maxPosition - oldWidth / 2) *originalX = gui.lastClickX = positionAlongWindow - maxPosition + oldWidth;
|
|
else if (positionAlongWindow > oldWidth / 2) *originalX = gui.lastClickX = oldWidth / 2;
|
|
*originalY = gui.lastClickY = windowRestoreDragYPosition;
|
|
window->resetPositionOnNextMove = false;
|
|
}
|
|
|
|
bounds.l = newX - *originalX;
|
|
bounds.t = newY - *originalY;
|
|
bounds.r = bounds.l + oldWidth;
|
|
bounds.b = bounds.t + oldHeight;
|
|
}
|
|
} else {
|
|
EsRectangle targetBounds = bounds;
|
|
|
|
#define WINDOW_CLAMP_SIZE(_size, _direction, _side, _otherSide, _minimum) \
|
|
if (_size(bounds) < windowMinimum ## _size && (direction & _direction)) { \
|
|
if (bothSides && startBounds) { \
|
|
int32_t center = (startBounds->_otherSide + startBounds->_side) / 2; \
|
|
targetBounds._side = center + (_minimum / 2); \
|
|
targetBounds._otherSide = center - (_minimum / 2); \
|
|
bounds._side = RubberBand(bounds._side, targetBounds._side); \
|
|
bounds._otherSide = RubberBand(bounds._otherSide, targetBounds._otherSide); \
|
|
} else { \
|
|
targetBounds._side = bounds._otherSide + _minimum; \
|
|
bounds._side = RubberBand(bounds._side, targetBounds._side); \
|
|
} \
|
|
}
|
|
|
|
WINDOW_CLAMP_SIZE(Width, RESIZE_LEFT, l, r, -windowMinimumWidth);
|
|
WINDOW_CLAMP_SIZE(Width, RESIZE_RIGHT, r, l, windowMinimumWidth);
|
|
WINDOW_CLAMP_SIZE(Height, RESIZE_TOP, t, b, -windowMinimumHeight);
|
|
WINDOW_CLAMP_SIZE(Height, RESIZE_BOTTOM, b, t, windowMinimumHeight);
|
|
|
|
window->animateToTargetBoundsAfterResize = !EsRectangleEquals(targetBounds, bounds);
|
|
window->animateFromBounds = bounds;
|
|
window->targetBounds = targetBounds;
|
|
window->resetPositionOnNextMove = window->restoreOnNextMove = false;
|
|
}
|
|
|
|
EsSyscall(ES_SYSCALL_WINDOW_MOVE, window->handle, (uintptr_t) &bounds, 0, ES_WINDOW_MOVE_DYNAMIC);
|
|
}
|
|
|
|
int ProcessWindowBorderMessage(EsWindow *window, EsMessage *message, EsRectangle bounds, bool addSolidInsets) {
|
|
if (message->type == ES_MSG_GET_CURSOR) {
|
|
EsPoint position = EsMouseGetPosition(window);
|
|
message->cursorStyle = ES_CURSOR_NORMAL;
|
|
|
|
if (window->isMaximised) {
|
|
window->resizeType = 0;
|
|
window->resizeCursor = message->cursorStyle;
|
|
} else {
|
|
int32_t solidC = addSolidInsets ? CONTAINER_SOLID_C : 0;
|
|
int32_t solidT = addSolidInsets ? CONTAINER_SOLID_T : 0;
|
|
int32_t solidB = addSolidInsets ? CONTAINER_SOLID_B : 0;
|
|
|
|
bool left = position.x < solidC + CONTAINER_RESIZE_BORDER;
|
|
bool right = position.x >= bounds.r - solidC - CONTAINER_RESIZE_BORDER;
|
|
bool top = position.y < solidT + CONTAINER_RESIZE_BORDER;
|
|
bool bottom = position.y >= bounds.b - solidB - CONTAINER_RESIZE_BORDER;
|
|
|
|
if (gui.resizing) {
|
|
message->cursorStyle = window->resizeCursor;
|
|
} else if (position.x < solidC || position.y < solidT || position.x >= bounds.r - solidC || position.y >= bounds.b - solidB) {
|
|
} else if ((right && top) || (bottom && left)) {
|
|
message->cursorStyle = ES_CURSOR_RESIZE_DIAGONAL_1;
|
|
} else if ((left && top) || (bottom && right)) {
|
|
message->cursorStyle = ES_CURSOR_RESIZE_DIAGONAL_2;
|
|
} else if (left || right) {
|
|
message->cursorStyle = ES_CURSOR_RESIZE_HORIZONTAL;
|
|
} else if (top || bottom) {
|
|
message->cursorStyle = ES_CURSOR_RESIZE_VERTICAL;
|
|
}
|
|
|
|
if (!window->pressed && !gui.mouseButtonDown) {
|
|
window->resizeType = (left ? RESIZE_LEFT : 0) | (right ? RESIZE_RIGHT : 0) | (top ? RESIZE_TOP : 0) | (bottom ? RESIZE_BOTTOM : 0);
|
|
window->resizeCursor = message->cursorStyle;
|
|
}
|
|
}
|
|
|
|
} else if (message->type == ES_MSG_MOUSE_LEFT_DOWN) {
|
|
gui.resizeStartBounds = EsWindowGetBounds(window);
|
|
} else if (message->type == ES_MSG_KEY_DOWN || message->type == ES_MSG_KEY_UP) {
|
|
gui.resizingBothSides = EsKeyboardIsCtrlHeld() && !window->isMaximised;
|
|
|
|
if (gui.resizing) {
|
|
EsPoint screenPosition = EsMouseGetPosition(nullptr);
|
|
WindowChangeBounds(window->resizeType, screenPosition.x, screenPosition.y,
|
|
&gui.lastClickX, &gui.lastClickY, window,
|
|
gui.resizingBothSides, &gui.resizeStartBounds);
|
|
}
|
|
} else if (message->type == ES_MSG_MOUSE_LEFT_DRAG) {
|
|
EsPoint screenPosition = EsMouseGetPosition(nullptr);
|
|
|
|
if (!window->isMaximised || window->resizeType == RESIZE_MOVE) {
|
|
WindowChangeBounds(window->resizeType, screenPosition.x, screenPosition.y,
|
|
&gui.lastClickX, &gui.lastClickY, window,
|
|
gui.resizingBothSides, &gui.resizeStartBounds);
|
|
gui.resizing = true;
|
|
}
|
|
} else if (message->type == ES_MSG_MOUSE_LEFT_UP) {
|
|
if (window->restoreOnNextMove) {
|
|
window->resetPositionOnNextMove = true;
|
|
}
|
|
|
|
if (window->animateToTargetBoundsAfterResize) {
|
|
window->animationTime = 0;
|
|
window->StartAnimating();
|
|
}
|
|
|
|
gui.resizing = false;
|
|
} else if (message->type == ES_MSG_ANIMATE && window->animateToTargetBoundsAfterResize) {
|
|
double progress = window->animationTime / 100.0;
|
|
window->animationTime += message->animate.deltaMs;
|
|
|
|
if (progress > 1 || progress < 0) {
|
|
message->animate.complete = true;
|
|
window->animateToTargetBoundsAfterResize = false;
|
|
} else {
|
|
progress = SmoothAnimationTimeSharp(progress);
|
|
EsRectangle bounds = EsRectangleLinearInterpolate(window->animateFromBounds, window->targetBounds, progress);
|
|
EsSyscall(ES_SYSCALL_WINDOW_MOVE, window->handle, (uintptr_t) &bounds, 0, ES_WINDOW_MOVE_DYNAMIC);
|
|
message->animate.complete = false;
|
|
}
|
|
} else {
|
|
return 0;
|
|
}
|
|
|
|
return ES_HANDLED;
|
|
}
|
|
|
|
// --------------------------------- Windows.
|
|
|
|
void UIWindowNeedsUpdate(EsWindow *window) {
|
|
if (!window->willUpdate && window->handle /* cleared in UIWindowDestroy, during InternalDestroy */) {
|
|
EsMessage m = { ES_MSG_UPDATE_WINDOW };
|
|
// Don't use the userland posted message queue, since we don't want this to block WM messages.
|
|
// This message will be received within the window's lifetime,
|
|
// because the window cannot be deallocated until ES_MSG_WINDOW_DESTROYED is received,
|
|
// and this message will always be received first.
|
|
EsSyscall(ES_SYSCALL_MESSAGE_POST, (uintptr_t) &m, (uintptr_t) window, ES_CURRENT_PROCESS, 0);
|
|
window->willUpdate = true;
|
|
}
|
|
}
|
|
|
|
void UIWindowDestroy(EsWindow *window) {
|
|
gui.allWindows.FindAndDeleteSwap(window, true);
|
|
AccessKeyModeExit();
|
|
EsSyscall(ES_SYSCALL_WINDOW_CLOSE, window->handle, 0, 0, 0);
|
|
EsHandleClose(window->handle);
|
|
window->checkVisible.Free();
|
|
window->sizeAlternatives.Free();
|
|
window->updateActions.Free();
|
|
window->dialogs.Free();
|
|
window->handle = ES_INVALID_HANDLE;
|
|
}
|
|
|
|
EsElement *WindowGetMainPanel(EsWindow *window) {
|
|
if (window->windowStyle == ES_WINDOW_MENU) {
|
|
if (!window->children[0]->GetChildCount()) {
|
|
EsMenuNextColumn((EsMenu *) window, ES_FLAGS_DEFAULT);
|
|
}
|
|
|
|
return window->children[0]->GetChild(window->children[0]->GetChildCount() - 1);
|
|
}
|
|
|
|
return window->mainPanel;
|
|
}
|
|
|
|
int ProcessRootMessage(EsElement *element, EsMessage *message) {
|
|
EsWindow *window = (EsWindow *) element;
|
|
EsRectangle bounds = window->GetBounds();
|
|
int response = 0;
|
|
|
|
if (window->windowStyle == ES_WINDOW_CONTAINER) {
|
|
if (message->type == ES_MSG_LAYOUT) {
|
|
EsElementMove(window->GetChild(0), CONTAINER_EMBED_C, CONTAINER_EMBED_T - CONTAINER_TAB_BAND_HEIGHT,
|
|
bounds.r - CONTAINER_EMBED_C * 2, CONTAINER_TAB_BAND_HEIGHT);
|
|
} else if (message->type == ES_MSG_UI_SCALE_CHANGED) {
|
|
// This message is also sent when the window is created.
|
|
EsRectangle solidInsets = ES_RECT_4(CONTAINER_SOLID_C, CONTAINER_SOLID_C, CONTAINER_SOLID_T, CONTAINER_SOLID_B);
|
|
EsSyscall(ES_SYSCALL_WINDOW_SET_PROPERTY, window->handle, ES_WINDOW_SOLID_TRUE, (uintptr_t) &solidInsets, ES_WINDOW_PROPERTY_SOLID);
|
|
EsRectangle embedInsets = ES_RECT_4(CONTAINER_EMBED_C, CONTAINER_EMBED_C, CONTAINER_EMBED_T, CONTAINER_EMBED_B);
|
|
EsSyscall(ES_SYSCALL_WINDOW_SET_PROPERTY, window->handle, (uintptr_t) &embedInsets, 0, ES_WINDOW_PROPERTY_EMBED_INSETS);
|
|
EsRectangle opaqueBounds = ES_RECT_4(CONTAINER_OPAQUE_C, window->windowWidth - CONTAINER_OPAQUE_C,
|
|
CONTAINER_OPAQUE_T, window->windowHeight - CONTAINER_OPAQUE_B);
|
|
EsSyscall(ES_SYSCALL_WINDOW_SET_PROPERTY, window->handle, (uintptr_t) &opaqueBounds, 0, ES_WINDOW_PROPERTY_OPAQUE_BOUNDS);
|
|
} else {
|
|
response = ProcessWindowBorderMessage(window, message, bounds, true);
|
|
}
|
|
} else if (window->windowStyle == ES_WINDOW_MENU) {
|
|
if (message->type == ES_MSG_PAINT_BACKGROUND) {
|
|
EsDrawClear(message->painter, message->painter->clip);
|
|
} else if (message->type == ES_MSG_LAYOUT) {
|
|
if (window->GetChildCount()) {
|
|
EsElementMove(window->GetChild(0), 0, 0, bounds.r, bounds.b);
|
|
}
|
|
} else if (message->type == ES_MSG_TRANSITION_COMPLETE) {
|
|
if (window->state & UI_STATE_MENU_EXITING) {
|
|
EsElementDestroy(window);
|
|
}
|
|
}
|
|
} else if (window->windowStyle == ES_WINDOW_INSPECTOR) {
|
|
if (message->type == ES_MSG_LAYOUT) {
|
|
if (window->GetChildCount()) {
|
|
EsElementMove(window->GetChild(0), 9, 9 + 30, bounds.r - 9 * 2, bounds.b - 9 * 2 - 30);
|
|
}
|
|
} else {
|
|
response = ProcessWindowBorderMessage(window, message, bounds, false);
|
|
}
|
|
} else if (window->windowStyle == ES_WINDOW_NORMAL) {
|
|
if (message->type == ES_MSG_LAYOUT) {
|
|
if (window->GetChildCount()) {
|
|
EsElement *toolbar = window->toolbarSwitcher;
|
|
|
|
if (window->toolbarFillMode) {
|
|
EsElementMove(window->GetChild(0), 0, 0, 0, 0);
|
|
EsElementMove(toolbar, 0, 0, bounds.r, bounds.b);
|
|
} else {
|
|
int toolbarHeight = toolbar->GetChildCount() ? toolbar->GetHeight(bounds.r)
|
|
: toolbar->style->metrics->minimumHeight;
|
|
EsElementMove(window->GetChild(0), 0, toolbarHeight, bounds.r, bounds.b - toolbarHeight);
|
|
EsElementMove(toolbar, 0, 0, bounds.r, toolbarHeight);
|
|
}
|
|
|
|
EsElementMove(window->GetChild(2), 0, 0, bounds.r, bounds.b);
|
|
}
|
|
}
|
|
} else if (window->windowStyle == ES_WINDOW_TIP || window->windowStyle == ES_WINDOW_PLAIN) {
|
|
if (message->type == ES_MSG_LAYOUT) {
|
|
if (window->GetChildCount()) {
|
|
EsElementMove(window->GetChild(0), 0, 0, bounds.r, bounds.b);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (message->type == ES_MSG_DESTROY) {
|
|
if (window->windowStyle != ES_WINDOW_NORMAL) {
|
|
EsSyscall(ES_SYSCALL_WINDOW_SET_PROPERTY, window->handle, ES_FLAGS_DEFAULT, 0, ES_WINDOW_PROPERTY_SOLID);
|
|
}
|
|
|
|
if (window->windowStyle == ES_WINDOW_MENU) {
|
|
EsAssert(window->state & UI_STATE_MENU_EXITING);
|
|
}
|
|
} else if (message->type == ES_MSG_MOUSE_LEFT_DOWN || message->type == ES_MSG_MOUSE_MIDDLE_DOWN || message->type == ES_MSG_MOUSE_RIGHT_DOWN) {
|
|
// Make sure that something can be dragged, otherwise elements will get mouse dragged messages when the mouse moves over them.
|
|
response = ES_HANDLED;
|
|
}
|
|
|
|
return response;
|
|
}
|
|
|
|
EsWindow *EsWindowCreate(EsInstance *instance, EsWindowStyle style) {
|
|
EsMessageMutexCheck();
|
|
|
|
for (uintptr_t i = 0; i < gui.allWindows.Length(); i++) {
|
|
UIMouseUp(gui.allWindows[i], nullptr, false);
|
|
}
|
|
|
|
EsWindow *window = (EsWindow *) EsHeapAllocate(sizeof(EsWindow), true);
|
|
if (!window) return nullptr;
|
|
|
|
if (!gui.allWindows.Add(window)) {
|
|
EsHeapFree(window);
|
|
return nullptr;
|
|
}
|
|
|
|
if (instance) {
|
|
window->instance = instance;
|
|
|
|
if (style == ES_WINDOW_NORMAL) {
|
|
// A handle to the instance is already open.
|
|
} else {
|
|
EsInstanceOpenReference(instance);
|
|
}
|
|
}
|
|
|
|
if (style == ES_WINDOW_NORMAL) {
|
|
window->handle = ((APIInstance *) instance->_private)->mainWindowHandle;
|
|
} else {
|
|
window->handle = EsSyscall(ES_SYSCALL_WINDOW_CREATE, style, 0, (uintptr_t) window, 0);
|
|
}
|
|
|
|
window->id = EsSyscall(ES_SYSCALL_WINDOW_GET_ID, window->handle, 0, 0, 0);
|
|
window->window = window;
|
|
window->Initialise(nullptr, ES_CELL_FILL, ProcessRootMessage, 0);
|
|
window->cName = "window";
|
|
window->width = window->windowWidth, window->height = window->windowHeight;
|
|
window->hovered = window;
|
|
window->hovering = true;
|
|
window->windowStyle = style;
|
|
window->RefreshStyle();
|
|
|
|
if (style == ES_WINDOW_NORMAL) {
|
|
EsSyscall(ES_SYSCALL_WINDOW_SET_PROPERTY, window->handle, 0, (uintptr_t) window, ES_WINDOW_PROPERTY_OBJECT);
|
|
window->activated = true;
|
|
EsPanel *panel = EsPanelCreate(window, ES_ELEMENT_NON_CLIENT | ES_CELL_FILL | ES_PANEL_Z_STACK);
|
|
panel->cName = "window stack";
|
|
window->mainPanel = EsPanelCreate(panel, ES_CELL_FILL);
|
|
window->mainPanel->cName = "window root";
|
|
window->toolbarSwitcher = EsPanelCreate(window, ES_ELEMENT_NON_CLIENT | ES_PANEL_SWITCHER | ES_CELL_FILL, ES_STYLE_PANEL_TOOLBAR_ROOT);
|
|
window->toolbarSwitcher->cName = "toolbar";
|
|
EsElement *accessKeyLayer = EsCustomElementCreate(window, ES_ELEMENT_NON_CLIENT | ES_CELL_FILL | ES_ELEMENT_NO_HOVER, 0);
|
|
accessKeyLayer->cName = "access key layer";
|
|
accessKeyLayer->messageUser = AccessKeyLayerMessage;
|
|
window->state |= UI_STATE_Z_STACK;
|
|
} else if (style == ES_WINDOW_INSPECTOR) {
|
|
EsSyscall(ES_SYSCALL_WINDOW_SET_PROPERTY, window->handle, ES_WINDOW_SOLID_TRUE, 0, ES_WINDOW_PROPERTY_SOLID);
|
|
window->SetStyle(ES_STYLE_PANEL_FILLED);
|
|
window->mainPanel = EsPanelCreate(window, ES_ELEMENT_NON_CLIENT | ES_CELL_FILL);
|
|
EsRectangle bounds = { 10, 600, 10, 500 };
|
|
EsSyscall(ES_SYSCALL_WINDOW_MOVE, window->handle, (uintptr_t) &bounds, 0, ES_WINDOW_MOVE_ADJUST_TO_FIT_SCREEN);
|
|
} else if (style == ES_WINDOW_TIP || style == ES_WINDOW_PLAIN) {
|
|
EsSyscall(ES_SYSCALL_WINDOW_SET_PROPERTY, window->handle, style == ES_WINDOW_PLAIN ? ES_WINDOW_SOLID_TRUE : ES_FLAGS_DEFAULT, 0, ES_WINDOW_PROPERTY_SOLID);
|
|
window->mainPanel = EsPanelCreate(window, ES_ELEMENT_NON_CLIENT | ES_CELL_FILL, 0);
|
|
} else if (style == ES_WINDOW_MENU) {
|
|
window->SetStyle(ES_STYLE_MENU_ROOT);
|
|
|
|
EsPanel *panel = EsPanelCreate(window, ES_ELEMENT_NON_CLIENT | ES_PANEL_HORIZONTAL | ES_CELL_FILL, ES_STYLE_MENU_CONTAINER);
|
|
panel->cName = "menu";
|
|
panel->separatorStylePart = ES_STYLE_MENU_SEPARATOR_VERTICAL;
|
|
panel->separatorFlags = ES_CELL_V_FILL;
|
|
|
|
panel->messageUser = [] (EsElement *element, EsMessage *message) {
|
|
if (message->type == ES_MSG_PAINT) {
|
|
EsPainter *painter = message->painter;
|
|
EsRectangle blurBounds = element->GetWindowBounds();
|
|
blurBounds = EsRectangleAddBorder(blurBounds, ((UIStyle *) painter->style)->insets);
|
|
EsSyscall(ES_SYSCALL_WINDOW_SET_PROPERTY, element->window->handle, (uintptr_t) &blurBounds, 0, ES_WINDOW_PROPERTY_BLUR_BOUNDS);
|
|
}
|
|
|
|
return 0;
|
|
};
|
|
|
|
window->mainPanel = panel;
|
|
|
|
EsElementStartTransition(window, ES_TRANSITION_FADE_IN);
|
|
|
|
EsSyscall(ES_SYSCALL_WINDOW_SET_PROPERTY, window->handle, ES_WINDOW_SOLID_TRUE, (uintptr_t) &panel->style->insets, ES_WINDOW_PROPERTY_SOLID);
|
|
EsSyscall(ES_SYSCALL_WINDOW_SET_PROPERTY, window->handle, BLEND_WINDOW_MATERIAL_GLASS, 0, ES_WINDOW_PROPERTY_MATERIAL);
|
|
}
|
|
|
|
if (style == ES_WINDOW_INSPECTOR) {
|
|
InspectorSetup(window);
|
|
}
|
|
|
|
return window;
|
|
}
|
|
|
|
void EsWindowAddSizeAlternative(EsWindow *window, EsElement *small, EsElement *big, int widthThreshold, int heightThreshold) {
|
|
EsMessageMutexCheck();
|
|
EsAssert(small->window == window && big->window == window);
|
|
|
|
SizeAlternative alternative = {};
|
|
alternative.small = small, alternative.big = big;
|
|
alternative.widthThreshold = widthThreshold, alternative.heightThreshold = heightThreshold;
|
|
window->sizeAlternatives.Add(alternative);
|
|
|
|
bool belowThreshold = window->width < widthThreshold || window->height < heightThreshold;
|
|
EsElementSetHidden(small, !belowThreshold);
|
|
EsElementSetHidden(big, belowThreshold);
|
|
}
|
|
|
|
// --------------------------------- Menus.
|
|
|
|
struct EsMenu : EsWindow {};
|
|
|
|
void EsMenuAddSeparator(EsMenu *menu) {
|
|
EsCustomElementCreate(menu, ES_CELL_H_FILL, ES_STYLE_MENU_SEPARATOR_HORIZONTAL)->cName = "menu separator";
|
|
}
|
|
|
|
void EsMenuNextColumn(EsMenu *menu, uint64_t flags) {
|
|
EsPanelCreate(menu->children[0], ES_PANEL_VERTICAL | ES_CELL_V_TOP | flags, ES_STYLE_MENU_COLUMN);
|
|
}
|
|
|
|
EsElement *EsMenuGetSource(EsMenu *menu) {
|
|
return ((EsWindow *) menu)->source;
|
|
}
|
|
|
|
EsMenu *EsMenuCreate(EsElement *source, uint32_t flags) {
|
|
EsWindow *menu = (EsWindow *) EsWindowCreate(source->instance, ES_WINDOW_MENU);
|
|
if (!menu) return nullptr;
|
|
menu->flags |= flags;
|
|
menu->activated = true;
|
|
menu->source = source;
|
|
return (EsMenu *) menu;
|
|
}
|
|
|
|
void EsMenuAddCommandsFromToolbar(EsMenu *menu, EsElement *element) {
|
|
for (uintptr_t i = 0; i < element->GetChildCount(); i++) {
|
|
EsElement *child = element->GetChild(i);
|
|
|
|
if (child->flags & ES_ELEMENT_NON_CLIENT) {
|
|
continue;
|
|
}
|
|
|
|
if (child->messageClass == ProcessButtonMessage) {
|
|
EsButton *button = (EsButton *) child;
|
|
|
|
if (button->command) {
|
|
EsMenuAddCommand(menu, button->command->check, button->label, button->labelBytes, button->command);
|
|
}
|
|
} else {
|
|
EsMenuAddCommandsFromToolbar(menu, child);
|
|
}
|
|
}
|
|
}
|
|
|
|
void EsMenuClose(EsMenu *menu) {
|
|
EsAssert(menu->windowStyle == ES_WINDOW_MENU);
|
|
if (menu->state & UI_STATE_MENU_EXITING) return;
|
|
EsSyscall(ES_SYSCALL_WINDOW_SET_PROPERTY, menu->handle, BLEND_WINDOW_MATERIAL_NONE, 0, ES_WINDOW_PROPERTY_MATERIAL);
|
|
EsAssert(menu->source->state & UI_STATE_MENU_SOURCE);
|
|
menu->source->state &= ~UI_STATE_MENU_SOURCE;
|
|
menu->source->MaybeRefreshStyle();
|
|
EsAssert(menu->source->window->targetMenu == menu);
|
|
menu->source->window->targetMenu = nullptr;
|
|
menu->mainPanel->state |= UI_STATE_BLOCK_INTERACTION;
|
|
menu->state |= UI_STATE_MENU_EXITING; // Set flag before EsElementStartTransition to receive ES_MSG_TRANSITION_COMPLETE when animations disabled.
|
|
EsElementStartTransition(menu, ES_TRANSITION_ZOOM_OUT_LIGHT, ES_ELEMENT_TRANSITION_EXIT);
|
|
}
|
|
|
|
void EsMenuShow(EsMenu *menu, int fixedWidth, int fixedHeight) {
|
|
EsAssert(!menu->source->window->targetMenu);
|
|
EsAssert(~menu->source->state & UI_STATE_MENU_SOURCE);
|
|
menu->source->state |= UI_STATE_MENU_SOURCE;
|
|
menu->source->MaybeRefreshStyle();
|
|
menu->source->window->targetMenu = menu;
|
|
|
|
EsRectangle menuInsets = menu->GetChild(0)->style->insets;
|
|
if (fixedWidth) fixedWidth += menuInsets.l + menuInsets.r;
|
|
if (fixedHeight) fixedHeight += menuInsets.t + menuInsets.b;
|
|
int width = fixedWidth ?: menu->GetChild(0)->GetWidth(fixedHeight);
|
|
int height = menu->GetChild(0)->GetHeight(width);
|
|
|
|
if (fixedHeight) {
|
|
if (menu->flags & ES_MENU_MAXIMUM_HEIGHT) {
|
|
if (height >= fixedHeight) {
|
|
height = fixedHeight;
|
|
}
|
|
} else {
|
|
height = fixedHeight;
|
|
}
|
|
}
|
|
|
|
EsPoint position;
|
|
|
|
if (~menu->flags & ES_MENU_AT_CURSOR) {
|
|
EsRectangle bounds = menu->source->GetScreenBounds(false);
|
|
|
|
position = ES_POINT(bounds.l - (width + menuInsets.l - menuInsets.r) / 2 + menu->source->width / 2,
|
|
bounds.b - menuInsets.t);
|
|
|
|
// Try to the keep the menu within the bounds of the source window.
|
|
|
|
EsRectangle windowBounds = menu->source->window->GetScreenBounds();
|
|
|
|
if (position.x + width - menuInsets.r >= windowBounds.r) {
|
|
position.x = windowBounds.r - width - 1 + menuInsets.r;
|
|
}
|
|
|
|
if (position.x + menuInsets.l < windowBounds.l) {
|
|
position.x = windowBounds.l - menuInsets.l;
|
|
}
|
|
|
|
if (position.y + height - menuInsets.b >= windowBounds.b) {
|
|
position.y = windowBounds.b - height - 1 + menuInsets.b;
|
|
}
|
|
|
|
if (position.y + menuInsets.t < windowBounds.t) {
|
|
position.y = windowBounds.t - menuInsets.t;
|
|
}
|
|
} else {
|
|
position = EsMouseGetPosition();
|
|
position.x -= menuInsets.l, position.y -= menuInsets.t;
|
|
}
|
|
|
|
menu->width = width, menu->height = height;
|
|
EsRectangle bounds = ES_RECT_4(position.x, position.x + width, position.y, position.y + height);
|
|
EsSyscall(ES_SYSCALL_WINDOW_MOVE, menu->handle, (uintptr_t) &bounds, 0, ES_WINDOW_MOVE_ADJUST_TO_FIT_SCREEN | ES_WINDOW_MOVE_ALWAYS_ON_TOP);
|
|
}
|
|
|
|
void EsMenuCloseAll() {
|
|
for (uintptr_t i = 0; i < gui.allWindows.Length(); i++) {
|
|
if (gui.allWindows[i]->windowStyle == ES_WINDOW_MENU) {
|
|
EsMenuClose((EsMenu *) gui.allWindows[i]);
|
|
}
|
|
}
|
|
}
|
|
|
|
// --------------------------------- Paint targets.
|
|
|
|
bool EsPaintTargetTake(EsPaintTarget *target, size_t width, size_t height, bool hasAlphaChannel = true) {
|
|
EsMemoryZero(target, sizeof(EsPaintTarget));
|
|
target->fullAlpha = hasAlphaChannel;
|
|
target->width = width;
|
|
target->height = height;
|
|
target->stride = width * 4;
|
|
target->bits = EsHeapAllocate(target->stride * target->height, true);
|
|
return target->bits != nullptr;
|
|
}
|
|
|
|
EsPaintTarget *EsPaintTargetCreate(size_t width, size_t height, bool hasAlphaChannel) {
|
|
EsPaintTarget *target = (EsPaintTarget *) EsHeapAllocate(sizeof(EsPaintTarget), true);
|
|
|
|
if (!target) {
|
|
return nullptr;
|
|
} else if (EsPaintTargetTake(target, width, height, hasAlphaChannel)) {
|
|
return target;
|
|
} else {
|
|
EsHeapFree(target);
|
|
return nullptr;
|
|
}
|
|
}
|
|
|
|
EsPaintTarget *EsPaintTargetCreateFromBitmap(uint32_t *bits, size_t width, size_t height, bool hasAlphaChannel) {
|
|
EsPaintTarget *target = (EsPaintTarget *) EsHeapAllocate(sizeof(EsPaintTarget), true);
|
|
|
|
if (!target) {
|
|
return nullptr;
|
|
}
|
|
|
|
target->bits = bits;
|
|
target->width = width;
|
|
target->height = height;
|
|
target->stride = width * 4;
|
|
target->fullAlpha = hasAlphaChannel;
|
|
target->fromBitmap = true;
|
|
return target;
|
|
}
|
|
|
|
void EsPaintTargetClear(EsPaintTarget *t) {
|
|
EsPainter painter = {};
|
|
painter.clip.r = t->width;
|
|
painter.clip.b = t->height;
|
|
painter.target = t;
|
|
EsDrawClear(&painter, painter.clip);
|
|
}
|
|
|
|
void EsPaintTargetReturn(EsPaintTarget *target) {
|
|
EsHeapFree(target->bits);
|
|
}
|
|
|
|
void EsPaintTargetDestroy(EsPaintTarget *target) {
|
|
if (!target->fromBitmap) EsHeapFree(target->bits);
|
|
EsHeapFree(target);
|
|
}
|
|
|
|
void EsPaintTargetEndDirectAccess(EsPaintTarget *target) {
|
|
// Don't need to do anything, currently.
|
|
(void) target;
|
|
}
|
|
|
|
void EsPaintTargetGetSize(EsPaintTarget *target, size_t *width, size_t *height) {
|
|
if (width) *width = target->width;
|
|
if (height) *height = target->height;
|
|
}
|
|
|
|
void EsPaintTargetStartDirectAccess(EsPaintTarget *target, uint32_t **bits, size_t *width, size_t *height, size_t *stride) {
|
|
if (bits) *bits = (uint32_t *) target->bits;
|
|
if (width) *width = target->width;
|
|
if (height) *height = target->height;
|
|
if (stride) *stride = target->stride;
|
|
}
|
|
|
|
// --------------------------------- Transitions.
|
|
|
|
EsRectangle UIGetTransitionEffectRectangle(EsRectangle bounds, EsTransitionType type, double progress, bool to) {
|
|
int width = Width(bounds), height = Height(bounds);
|
|
double ratio = (double) height / (double) width;
|
|
|
|
if (type == ES_TRANSITION_FADE_IN || type == ES_TRANSITION_FADE_OUT || type == ES_TRANSITION_FADE_VIA_TRANSPARENT || type == ES_TRANSITION_FADE) {
|
|
return bounds;
|
|
} else if (!to) {
|
|
if (type == ES_TRANSITION_SLIDE_UP) {
|
|
return ES_RECT_4(bounds.l, bounds.r,
|
|
bounds.t - progress * height / 2, bounds.b - progress * height / 2);
|
|
} else if (type == ES_TRANSITION_SLIDE_DOWN) {
|
|
return ES_RECT_4(bounds.l, bounds.r,
|
|
bounds.t + progress * height / 2, bounds.b + progress * height / 2);
|
|
} else if (type == ES_TRANSITION_COVER_UP) {
|
|
return ES_RECT_4(bounds.l, bounds.r,
|
|
bounds.t, bounds.b);
|
|
} else if (type == ES_TRANSITION_COVER_DOWN) {
|
|
return ES_RECT_4(bounds.l, bounds.r,
|
|
bounds.t, bounds.b);
|
|
} else if (type == ES_TRANSITION_SQUISH_UP || type == ES_TRANSITION_REVEAL_UP) {
|
|
return ES_RECT_4(bounds.l, bounds.r,
|
|
bounds.t, bounds.t + height * (1 - progress));
|
|
} else if (type == ES_TRANSITION_SQUISH_DOWN || type == ES_TRANSITION_REVEAL_DOWN) {
|
|
return ES_RECT_4(bounds.l, bounds.r,
|
|
bounds.t + height * progress, bounds.b);
|
|
} else if (type == ES_TRANSITION_ZOOM_OUT) {
|
|
return ES_RECT_4(bounds.l + 20 * progress, bounds.r - 20 * progress,
|
|
bounds.t + 20 * progress * ratio, bounds.b - 20 * progress * ratio);
|
|
} else if (type == ES_TRANSITION_ZOOM_IN) {
|
|
return ES_RECT_4(bounds.l - 20 * progress, bounds.r + 20 * progress,
|
|
bounds.t - 20 * progress * ratio, bounds.b + 20 * progress * ratio);
|
|
} else if (type == ES_TRANSITION_ZOOM_OUT_LIGHT) {
|
|
return ES_RECT_4(bounds.l + 5 * progress, bounds.r - 5 * progress,
|
|
bounds.t + 5 * progress * ratio, bounds.b - 5 * progress * ratio);
|
|
} else if (type == ES_TRANSITION_ZOOM_IN_LIGHT) {
|
|
return ES_RECT_4(bounds.l - 5 * progress, bounds.r + 5 * progress,
|
|
bounds.t - 5 * progress * ratio, bounds.b + 5 * progress * ratio);
|
|
} else if (type == ES_TRANSITION_SLIDE_UP_OVER) {
|
|
return ES_RECT_4(bounds.l, bounds.r,
|
|
bounds.t - progress * height / 4, bounds.b - progress * height / 4);
|
|
} else if (type == ES_TRANSITION_SLIDE_DOWN_OVER) {
|
|
return ES_RECT_4(bounds.l, bounds.r,
|
|
bounds.t + progress * height / 4, bounds.b + progress * height / 4);
|
|
} else if (type == ES_TRANSITION_SLIDE_UP_UNDER) {
|
|
return ES_RECT_4(bounds.l, bounds.r,
|
|
bounds.t - progress * height / 2, bounds.b - progress * height / 2);
|
|
} else if (type == ES_TRANSITION_SLIDE_DOWN_UNDER) {
|
|
return ES_RECT_4(bounds.l, bounds.r,
|
|
bounds.t + progress * height / 2, bounds.b + progress * height / 2);
|
|
}
|
|
} else {
|
|
if (type == ES_TRANSITION_SLIDE_UP) {
|
|
return ES_RECT_4(bounds.l, bounds.r,
|
|
bounds.t + (1 - progress) * height / 2, bounds.b + (1 - progress) * height / 2);
|
|
} else if (type == ES_TRANSITION_SLIDE_DOWN) {
|
|
return ES_RECT_4(bounds.l, bounds.r,
|
|
bounds.t - (1 - progress) * height / 2, bounds.b - (1 - progress) * height / 2);
|
|
} else if (type == ES_TRANSITION_COVER_UP) {
|
|
return ES_RECT_4(bounds.l, bounds.r,
|
|
bounds.t + (1 - progress) * height, bounds.b + (1 - progress) * height);
|
|
} else if (type == ES_TRANSITION_COVER_DOWN) {
|
|
return ES_RECT_4(bounds.l, bounds.r,
|
|
bounds.t - (1 - progress) * height, bounds.b - (1 - progress) * height);
|
|
} else if (type == ES_TRANSITION_SQUISH_UP || type == ES_TRANSITION_REVEAL_UP) {
|
|
return ES_RECT_4(bounds.l, bounds.r,
|
|
bounds.t + (1 - progress) * height, bounds.b);
|
|
} else if (type == ES_TRANSITION_SQUISH_DOWN || type == ES_TRANSITION_REVEAL_DOWN) {
|
|
return ES_RECT_4(bounds.l, bounds.r,
|
|
bounds.t, bounds.t + progress * height);
|
|
} else if (type == ES_TRANSITION_ZOOM_OUT) {
|
|
return ES_RECT_4(bounds.l - 20 * (1 - progress), bounds.r + 20 * (1 - progress),
|
|
bounds.t - 20 * (1 - progress) * ratio, bounds.b + 20 * (1 - progress) * ratio);
|
|
} else if (type == ES_TRANSITION_ZOOM_IN) {
|
|
return ES_RECT_4(bounds.l + 20 * (1 - progress), bounds.r - 20 * (1 - progress) + 0.5,
|
|
bounds.t + 20 * (1 - progress) * ratio, bounds.b - 20 * (1 - progress) * ratio + 0.5);
|
|
} else if (type == ES_TRANSITION_ZOOM_OUT_LIGHT) {
|
|
return ES_RECT_4(bounds.l - 5 * (1 - progress), bounds.r + 5 * (1 - progress),
|
|
bounds.t - 5 * (1 - progress) * ratio, bounds.b + 5 * (1 - progress) * ratio);
|
|
} else if (type == ES_TRANSITION_ZOOM_IN_LIGHT) {
|
|
return ES_RECT_4(bounds.l + 5 * (1 - progress), bounds.r - 5 * (1 - progress) + 0.5,
|
|
bounds.t + 5 * (1 - progress) * ratio, bounds.b - 5 * (1 - progress) * ratio + 0.5);
|
|
} else if (type == ES_TRANSITION_SLIDE_UP_OVER) {
|
|
return ES_RECT_4(bounds.l, bounds.r,
|
|
bounds.t + (1 - progress) * height / 2, bounds.b + (1 - progress) * height / 2);
|
|
} else if (type == ES_TRANSITION_SLIDE_DOWN_OVER) {
|
|
return ES_RECT_4(bounds.l, bounds.r,
|
|
bounds.t - (1 - progress) * height / 2, bounds.b - (1 - progress) * height / 2);
|
|
} else if (type == ES_TRANSITION_SLIDE_UP_UNDER) {
|
|
return ES_RECT_4(bounds.l, bounds.r,
|
|
bounds.t + (1 - progress) * height / 4, bounds.b + (1 - progress) * height / 4);
|
|
} else if (type == ES_TRANSITION_SLIDE_DOWN_UNDER) {
|
|
return ES_RECT_4(bounds.l, bounds.r,
|
|
bounds.t - (1 - progress) * height / 4, bounds.b - (1 - progress) * height / 4);
|
|
}
|
|
}
|
|
|
|
EsAssert(false); // Unknown transition type.
|
|
return {};
|
|
}
|
|
|
|
void UIDrawTransitionEffect(EsPainter *painter, EsPaintTarget *sourceSurface, EsRectangle bounds, EsTransitionType type, double progress, bool to) {
|
|
// TODO Proper blending in the FADE transition.
|
|
|
|
if ((type == ES_TRANSITION_FADE_OUT && to) || (type == ES_TRANSITION_FADE_IN && !to)) {
|
|
return;
|
|
}
|
|
|
|
if (type == ES_TRANSITION_FADE_VIA_TRANSPARENT) {
|
|
if (to) {
|
|
progress = ClampDouble(0.0, 1.0, progress * 2.0 - 1.0);
|
|
} else {
|
|
progress = ClampDouble(0.0, 1.0, progress * 2.0);
|
|
}
|
|
}
|
|
|
|
EsRectangle destinationRegion = UIGetTransitionEffectRectangle(bounds, type, progress, to);
|
|
EsRectangle sourceRegion = ES_RECT_4(0, bounds.r - bounds.l, 0, bounds.b - bounds.t);
|
|
uint16_t alpha = (to ? progress : (1 - progress)) * 255;
|
|
EsDrawPaintTarget(painter, sourceSurface, destinationRegion, sourceRegion, alpha);
|
|
}
|
|
|
|
void EsElementStartTransition(EsElement *element, EsTransitionType transitionType, uint32_t flags, float timeMultiplier) {
|
|
uint32_t durationMs = timeMultiplier * GetConstantNumber("transitionTime") * api.global->animationTimeMultiplier;
|
|
|
|
if (!durationMs) {
|
|
EsMessage m = { .type = ES_MSG_TRANSITION_COMPLETE };
|
|
EsMessageSend(element, &m);
|
|
return;
|
|
}
|
|
|
|
if (~element->state & UI_STATE_ENTERED) {
|
|
flags |= ES_ELEMENT_TRANSITION_ENTRANCE;
|
|
}
|
|
|
|
if (transitionType == ES_TRANSITION_FADE_IN) {
|
|
flags |= ES_ELEMENT_TRANSITION_ENTRANCE;
|
|
} else if (transitionType == ES_TRANSITION_FADE_OUT) {
|
|
flags |= ES_ELEMENT_TRANSITION_EXIT;
|
|
}
|
|
|
|
if (element->previousTransitionFrame) {
|
|
EsPaintTargetDestroy(element->previousTransitionFrame);
|
|
element->previousTransitionFrame = nullptr;
|
|
}
|
|
|
|
if (~flags & ES_ELEMENT_TRANSITION_ENTRANCE) {
|
|
EsRectangle paintOutsets = element->style->paintOutsets;
|
|
int width = element->width + paintOutsets.l + paintOutsets.r;
|
|
int height = element->height + paintOutsets.t + paintOutsets.b;
|
|
|
|
element->previousTransitionFrame = EsPaintTargetCreate(width, height, true);
|
|
|
|
if (element->previousTransitionFrame) {
|
|
EsPainter painter = {};
|
|
painter.clip = ES_RECT_4(0, width, 0, height);
|
|
painter.offsetX = paintOutsets.l;
|
|
painter.offsetY = paintOutsets.t;
|
|
painter.target = element->previousTransitionFrame;
|
|
element->InternalPaint(&painter, PAINT_NO_TRANSITION | PAINT_NO_OFFSET
|
|
| ((flags & ES_ELEMENT_TRANSITION_CONTENT_ONLY) ? PAINT_NO_BACKGROUND : 0));
|
|
}
|
|
}
|
|
|
|
element->transitionTimeMs = 0;
|
|
element->transitionFlags = flags;
|
|
element->transitionDurationMs = durationMs;
|
|
element->transitionType = transitionType;
|
|
element->StartAnimating();
|
|
}
|
|
|
|
// --------------------------------- Painting.
|
|
|
|
EsRectangle EsPainterBoundsClient(EsPainter *painter) {
|
|
return ES_RECT_4(painter->offsetX, painter->offsetX + painter->width, painter->offsetY, painter->offsetY + painter->height);
|
|
}
|
|
|
|
EsRectangle EsPainterBoundsInset(EsPainter *painter) {
|
|
UIStyle *style = (UIStyle *) painter->style;
|
|
return ES_RECT_4(painter->offsetX + style->insets.l, painter->offsetX + painter->width - style->insets.r,
|
|
painter->offsetY + style->insets.t, painter->offsetY + painter->height - style->insets.b);
|
|
}
|
|
|
|
#if 0
|
|
EsDeviceColor EsPainterRealizeColorRGB(EsPainter *painter, float alpha, float red, float green, float blue) {
|
|
// TODO Convert the color to the device's color space.
|
|
EsAssert(painter);
|
|
return ((uint32_t) (alpha * 255.0f) << 24)
|
|
+ ((uint32_t) (red * 255.0f) << 16)
|
|
+ ((uint32_t) (green * 255.0f) << 8)
|
|
+ ((uint32_t) (blue * 255.0f) << 0);
|
|
}
|
|
#endif
|
|
|
|
void EsElement::Repaint(bool all, EsRectangle region) {
|
|
// TODO Optimisation: don't paint if overlapped by an opaque child or sibling.
|
|
|
|
if (all) {
|
|
region.l = -style->paintOutsets.l, region.r = width + style->paintOutsets.r;
|
|
region.t = -style->paintOutsets.t, region.b = height + style->paintOutsets.b;
|
|
} else {
|
|
region = Translate(region, -internalOffsetLeft, -internalOffsetTop);
|
|
}
|
|
|
|
if (parent) {
|
|
EsRectangle parentBounds = parent->GetWindowBounds(false);
|
|
|
|
region = Translate(region, offsetX + parentBounds.l, offsetY + parentBounds.t);
|
|
|
|
if (parent->style->metrics->clipEnabled) {
|
|
Rectangle16 clipInsets = parent->style->metrics->clipInsets;
|
|
region = EsRectangleIntersection(region, EsRectangleAddBorder(parentBounds, RECT16_TO_RECT(clipInsets)));
|
|
}
|
|
}
|
|
|
|
if (window) {
|
|
if (THEME_RECT_VALID(region)) {
|
|
window->updateRegion = EsRectangleBounding(window->updateRegion, region);
|
|
}
|
|
|
|
UIWindowNeedsUpdate(window);
|
|
}
|
|
}
|
|
|
|
void EsElement::InternalPaint(EsPainter *painter, int paintFlags) {
|
|
if (width <= 0 || height <= 0 || (flags & ES_ELEMENT_HIDDEN)) {
|
|
return;
|
|
}
|
|
|
|
state |= UI_STATE_ENTERED;
|
|
|
|
int pOffsetX = painter->offsetX;
|
|
int pOffsetY = painter->offsetY;
|
|
|
|
if (~paintFlags & PAINT_NO_OFFSET) {
|
|
pOffsetX += offsetX;
|
|
pOffsetY += offsetY;
|
|
}
|
|
|
|
// Is it possible for this element to paint within the clip?
|
|
|
|
{
|
|
EsRectangle area;
|
|
area.l = pOffsetX - style->paintOutsets.l;
|
|
area.r = pOffsetX + width + style->paintOutsets.r;
|
|
area.t = pOffsetY - style->paintOutsets.t;
|
|
area.b = pOffsetY + height + style->paintOutsets.b;
|
|
|
|
if (!THEME_RECT_VALID(EsRectangleIntersection(area, painter->clip))) {
|
|
return;
|
|
}
|
|
}
|
|
|
|
EsPerformanceTimerPush();
|
|
|
|
// Get the interpolated style.
|
|
|
|
EsPainter oldPainter = *painter;
|
|
|
|
UIStyle *interpolatedStyle = ThemeAnimationComplete(&animation) ? style : ThemeStyleInterpolate(style, &animation);
|
|
EsDefer(if (interpolatedStyle != style) EsHeapFree(interpolatedStyle));
|
|
painter->style = interpolatedStyle;
|
|
|
|
painter->offsetX = pOffsetX, painter->offsetY = pOffsetY;
|
|
painter->width = width, painter->height = height;
|
|
|
|
// Get the child type of the element.
|
|
|
|
int childType = 0;
|
|
|
|
if (parent && parent->GetChildCount() && (parent->flags & ES_ELEMENT_AUTO_GROUP)) {
|
|
if (parent->GetChildCount() == 1 && parent->GetChild(0) == this) {
|
|
childType = THEME_CHILD_TYPE_ONLY;
|
|
} else if (parent->GetChild(0) == this) {
|
|
childType = (parent->flags & ES_ELEMENT_LAYOUT_HINT_REVERSE) ? THEME_CHILD_TYPE_LAST : THEME_CHILD_TYPE_FIRST;
|
|
} else if (parent->GetChild(parent->GetChildCount() - 1) == this) {
|
|
childType = (parent->flags & ES_ELEMENT_LAYOUT_HINT_REVERSE) ? THEME_CHILD_TYPE_FIRST : THEME_CHILD_TYPE_LAST;
|
|
} else {
|
|
childType = THEME_CHILD_TYPE_NONE;
|
|
}
|
|
|
|
if (parent->flags & ES_ELEMENT_LAYOUT_HINT_HORIZONTAL) {
|
|
childType |= THEME_CHILD_TYPE_HORIZONTAL;
|
|
}
|
|
}
|
|
|
|
if (paintFlags & PAINT_SHADOW) {
|
|
interpolatedStyle->PaintLayers(painter, ES_RECT_2S(painter->width, painter->height), childType, THEME_LAYER_MODE_SHADOW);
|
|
} else if (paintFlags & PAINT_OVERLAY) {
|
|
interpolatedStyle->PaintLayers(painter, ES_RECT_2S(painter->width, painter->height), childType, THEME_LAYER_MODE_OVERLAY);
|
|
|
|
// Paint layout bounds, if active.
|
|
|
|
if (window->visualizeLayoutBounds) {
|
|
EsDrawRectangle(painter, EsPainterBoundsClient(painter), 0, 0x7FFF0000, ES_RECT_1(1));
|
|
EsDrawRectangle(painter, EsPainterBoundsInset(painter), 0, 0x7F0000FF, ES_RECT_1(1));
|
|
}
|
|
} else if (transitionTimeMs < transitionDurationMs && (~paintFlags & PAINT_NO_TRANSITION)) {
|
|
double progress = SmoothAnimationTime((double) transitionTimeMs / (double) transitionDurationMs);
|
|
EsRectangle bounds = EsPainterBoundsClient(painter);
|
|
EsPaintTarget target;
|
|
|
|
EsRectangle paintOutsets = style->paintOutsets;
|
|
int targetWidth = width + paintOutsets.l + paintOutsets.r;
|
|
int targetHeight = height + paintOutsets.t + paintOutsets.b;
|
|
bounds.l -= paintOutsets.l, bounds.r += paintOutsets.r;
|
|
bounds.t -= paintOutsets.t, bounds.b += paintOutsets.b;
|
|
|
|
if (transitionFlags & ES_ELEMENT_TRANSITION_CONTENT_ONLY) {
|
|
EsMessage m;
|
|
m.type = ES_MSG_PAINT_BACKGROUND;
|
|
m.painter = painter;
|
|
|
|
if (!EsMessageSend(this, &m)) {
|
|
interpolatedStyle->PaintLayers(painter, ES_RECT_2S(painter->width, painter->height), childType, THEME_LAYER_MODE_BACKGROUND);
|
|
}
|
|
}
|
|
|
|
if (previousTransitionFrame) {
|
|
UIDrawTransitionEffect(painter, previousTransitionFrame, bounds, (EsTransitionType) transitionType, progress, false);
|
|
}
|
|
|
|
if (~transitionFlags & ES_ELEMENT_TRANSITION_EXIT) {
|
|
if (EsPaintTargetTake(&target, targetWidth, targetHeight)) {
|
|
EsPainter p = {};
|
|
p.clip = ES_RECT_4(0, targetWidth, 0, targetHeight);
|
|
p.offsetX = paintOutsets.l;
|
|
p.offsetY = paintOutsets.t;
|
|
p.target = ⌖
|
|
InternalPaint(&p, PAINT_NO_TRANSITION | PAINT_NO_OFFSET
|
|
| ((transitionFlags & ES_ELEMENT_TRANSITION_CONTENT_ONLY) ? PAINT_NO_BACKGROUND : 0));
|
|
UIDrawTransitionEffect(painter, &target, bounds, (EsTransitionType) transitionType, progress, true);
|
|
EsPaintTargetReturn(&target);
|
|
} else {
|
|
goto paintBackground;
|
|
}
|
|
}
|
|
} else {
|
|
paintBackground:;
|
|
EsMessage m;
|
|
|
|
if (~paintFlags & PAINT_NO_BACKGROUND) {
|
|
m.type = ES_MSG_PAINT_BACKGROUND;
|
|
m.painter = painter;
|
|
|
|
if (!EsMessageSend(this, &m)) {
|
|
// TODO Optimisation: don't paint if overlapped by an opaque child.
|
|
interpolatedStyle->PaintLayers(painter, ES_RECT_2S(painter->width, painter->height), childType, THEME_LAYER_MODE_BACKGROUND);
|
|
}
|
|
}
|
|
|
|
// Apply the clipping insets.
|
|
|
|
EsRectangle oldClip = painter->clip;
|
|
|
|
if (style->metrics->clipEnabled && (~flags & ES_ELEMENT_NO_CLIP)) {
|
|
Rectangle16 insets = style->metrics->clipInsets;
|
|
EsRectangle content = ES_RECT_4(painter->offsetX + insets.l, painter->offsetX + width - insets.r,
|
|
painter->offsetY + insets.t, painter->offsetY + height - insets.b);
|
|
painter->clip = EsRectangleIntersection(content, painter->clip);
|
|
}
|
|
|
|
if (THEME_RECT_VALID(painter->clip)) {
|
|
// Paint the content.
|
|
|
|
painter->width -= internalOffsetLeft + internalOffsetRight;
|
|
painter->height -= internalOffsetTop + internalOffsetBottom;
|
|
painter->offsetX += internalOffsetLeft, painter->offsetY += internalOffsetTop;
|
|
|
|
m.type = ES_MSG_PAINT;
|
|
m.painter = painter;
|
|
EsMessageSend(this, &m);
|
|
|
|
painter->width += internalOffsetLeft + internalOffsetRight;
|
|
painter->height += internalOffsetTop + internalOffsetBottom;
|
|
painter->offsetX -= internalOffsetLeft, painter->offsetY -= internalOffsetTop;
|
|
|
|
// Paint the children.
|
|
// TODO Optimisation: don't paint children overlapped by an opaque sibling.
|
|
|
|
m.type = ES_MSG_PAINT_CHILDREN;
|
|
m.painter = painter;
|
|
|
|
if (!EsMessageSend(this, &m)) {
|
|
bool isZStack = state & UI_STATE_Z_STACK;
|
|
|
|
EsMessage zOrder;
|
|
zOrder.type = ES_MSG_BEFORE_Z_ORDER;
|
|
zOrder.beforeZOrder.start = 0;
|
|
zOrder.beforeZOrder.nonClient = zOrder.beforeZOrder.end = children.Length();
|
|
zOrder.beforeZOrder.clip = Translate(painter->clip, -painter->offsetX, -painter->offsetY);
|
|
EsMessageSend(this, &zOrder);
|
|
|
|
if (isZStack) {
|
|
// Elements cast shadows on each other.
|
|
|
|
for (uintptr_t i = zOrder.beforeZOrder.start; i < zOrder.beforeZOrder.end; i++) {
|
|
EsElement *child = GetChildByZ(i);
|
|
if (!child) continue;
|
|
child->InternalPaint(painter, PAINT_SHADOW);
|
|
child->InternalPaint(painter, ES_FLAGS_DEFAULT);
|
|
child->InternalPaint(painter, PAINT_OVERLAY);
|
|
}
|
|
|
|
for (uintptr_t i = zOrder.beforeZOrder.nonClient; i < children.Length(); i++) {
|
|
EsElement *child = GetChildByZ(i);
|
|
if (!child) continue;
|
|
child->InternalPaint(painter, PAINT_SHADOW);
|
|
child->InternalPaint(painter, ES_FLAGS_DEFAULT);
|
|
child->InternalPaint(painter, PAINT_OVERLAY);
|
|
}
|
|
} else {
|
|
// Elements cast shadows on the container.
|
|
|
|
for (uintptr_t i = zOrder.beforeZOrder.start; i < zOrder.beforeZOrder.end; i++) {
|
|
EsElement *child = GetChildByZ(i);
|
|
if (child) child->InternalPaint(painter, PAINT_SHADOW);
|
|
}
|
|
|
|
for (uintptr_t i = zOrder.beforeZOrder.nonClient; i < children.Length(); i++) {
|
|
EsElement *child = GetChildByZ(i);
|
|
if (child) child->InternalPaint(painter, PAINT_SHADOW);
|
|
}
|
|
|
|
for (uintptr_t i = zOrder.beforeZOrder.start; i < zOrder.beforeZOrder.end; i++) {
|
|
EsElement *child = GetChildByZ(i);
|
|
if (child) child->InternalPaint(painter, ES_FLAGS_DEFAULT);
|
|
}
|
|
|
|
for (uintptr_t i = zOrder.beforeZOrder.nonClient; i < children.Length(); i++) {
|
|
EsElement *child = GetChildByZ(i);
|
|
if (child) child->InternalPaint(painter, ES_FLAGS_DEFAULT);
|
|
}
|
|
|
|
for (uintptr_t i = zOrder.beforeZOrder.start; i < zOrder.beforeZOrder.end; i++) {
|
|
EsElement *child = GetChildByZ(i);
|
|
if (child) child->InternalPaint(painter, PAINT_OVERLAY);
|
|
}
|
|
|
|
for (uintptr_t i = zOrder.beforeZOrder.nonClient; i < children.Length(); i++) {
|
|
EsElement *child = GetChildByZ(i);
|
|
if (child) child->InternalPaint(painter, PAINT_OVERLAY);
|
|
}
|
|
}
|
|
|
|
zOrder.type = ES_MSG_AFTER_Z_ORDER;
|
|
EsMessageSend(this, &zOrder);
|
|
}
|
|
}
|
|
|
|
m.type = ES_MSG_PAINT_FOREGROUND;
|
|
m.painter = painter;
|
|
EsMessageSend(this, &m);
|
|
|
|
// Let the inspector draw some decorations over the element.
|
|
|
|
painter->clip = oldClip;
|
|
InspectorNotifyElementPainted(this, painter);
|
|
}
|
|
|
|
*painter = oldPainter;
|
|
|
|
if (window->visualizePaintSteps && ES_RECT_VALID(window->updateRegionInProgress) && painter->target->forWindowManager) {
|
|
EsSyscall(ES_SYSCALL_WINDOW_SET_BITS, window->handle, (uintptr_t) &window->updateRegionInProgress,
|
|
(uintptr_t) painter->target->bits, WINDOW_SET_BITS_NORMAL);
|
|
}
|
|
}
|
|
|
|
// --------------------------------- Animations.
|
|
|
|
bool EsElement::StartAnimating() {
|
|
if ((state & UI_STATE_ANIMATING) || (state & UI_STATE_DESTROYING)) return false;
|
|
if (!gui.animatingElements.Add(this)) return false;
|
|
gui.animationSleep = false;
|
|
state |= UI_STATE_ANIMATING;
|
|
lastTimeStamp = 0;
|
|
return true;
|
|
}
|
|
|
|
void ProcessAnimations() {
|
|
uint64_t timeStamp = ProcessorReadTimeStamp();
|
|
int64_t waitMs = -1;
|
|
|
|
for (uintptr_t i = 0; i < gui.animatingElements.Length(); i++) {
|
|
EsElement *element = gui.animatingElements[i];
|
|
// EsPrint("Animating %x...\n", element);
|
|
EsAssert(element->state & UI_STATE_ANIMATING); // Element was not animating but was in the animating elements list.
|
|
|
|
if (element->lastTimeStamp == 0) {
|
|
element->lastTimeStamp = timeStamp;
|
|
}
|
|
|
|
EsMessage m = {};
|
|
m.type = ES_MSG_ANIMATE;
|
|
int64_t deltaMs = (timeStamp - element->lastTimeStamp) / api.startupInformation->timeStampTicksPerMs;
|
|
m.animate.deltaMs = deltaMs;
|
|
m.animate.complete = true;
|
|
|
|
if (!m.animate.deltaMs) {
|
|
waitMs = 0;
|
|
continue;
|
|
}
|
|
|
|
if (ThemeAnimationStep(&element->animation, m.animate.deltaMs)) {
|
|
element->Repaint(true, ES_RECT_1(0));
|
|
}
|
|
|
|
element->transitionTimeMs += m.animate.deltaMs;
|
|
bool transitionComplete = element->transitionTimeMs >= element->transitionDurationMs;
|
|
|
|
if (element->transitionDurationMs) {
|
|
element->Repaint(true, ES_RECT_1(0));
|
|
|
|
if (transitionComplete) {
|
|
element->transitionDurationMs = 0;
|
|
|
|
if (element->previousTransitionFrame) {
|
|
EsPaintTargetDestroy(element->previousTransitionFrame);
|
|
element->previousTransitionFrame = nullptr;
|
|
}
|
|
|
|
if (element->transitionFlags & ES_ELEMENT_TRANSITION_HIDE_AFTER_COMPLETE) {
|
|
EsElementSetHidden(element, true);
|
|
}
|
|
|
|
EsMessage m = { .type = ES_MSG_TRANSITION_COMPLETE };
|
|
EsMessageSend(element, &m);
|
|
}
|
|
}
|
|
|
|
bool backgroundAnimationComplete = ThemeAnimationComplete(&element->animation);
|
|
|
|
EsMessageSend(element, &m);
|
|
|
|
if (m.animate.complete && backgroundAnimationComplete && transitionComplete) {
|
|
gui.animatingElements.DeleteSwap(i);
|
|
element->state &= ~UI_STATE_ANIMATING;
|
|
i--;
|
|
} else if (m.animate.waitMs < waitMs || waitMs == -1) {
|
|
waitMs = m.animate.waitMs;
|
|
}
|
|
|
|
element->lastTimeStamp += m.animate.deltaMs * api.startupInformation->timeStampTicksPerMs;
|
|
}
|
|
|
|
if (waitMs > 0) {
|
|
gui.animationSleep = true;
|
|
|
|
EsTimerCancel(gui.animationSleepTimer);
|
|
gui.animationSleepTimer = EsTimerSet(waitMs, [] (EsGeneric) { gui.animationSleep = false; }, nullptr);
|
|
}
|
|
}
|
|
|
|
// --------------------------------- Style state management.
|
|
// TODO Move into theme.cpp?
|
|
|
|
bool EsElement::RefreshStyleState() {
|
|
uint16_t styleStateFlags = customStyleState;
|
|
|
|
if (flags & ES_ELEMENT_DISABLED) {
|
|
styleStateFlags |= THEME_PRIMARY_STATE_DISABLED;
|
|
} else if (!window->activated && !window->appearActivated) {
|
|
styleStateFlags |= THEME_PRIMARY_STATE_INACTIVE;
|
|
} else {
|
|
if (((window->pressed == this && gui.lastClickButton == ES_MSG_MOUSE_LEFT_DOWN)
|
|
&& (window->hovered == this || gui.draggingStarted || (state & UI_STATE_STRONG_PRESSED)))
|
|
|| (state & UI_STATE_MENU_SOURCE)) {
|
|
styleStateFlags |= THEME_PRIMARY_STATE_PRESSED;
|
|
} else if ((window->hovered == this && !window->pressed && api.global->enableHoverState) || window->pressed == this) {
|
|
styleStateFlags |= THEME_PRIMARY_STATE_HOVERED;
|
|
} else {
|
|
styleStateFlags |= THEME_PRIMARY_STATE_IDLE;
|
|
}
|
|
|
|
if (state & UI_STATE_FOCUSED) {
|
|
styleStateFlags |= THEME_STATE_FOCUSED;
|
|
}
|
|
}
|
|
|
|
bool observedBitsChanged = false;
|
|
|
|
if (!style || style->IsStateChangeObserved(styleStateFlags, previousStyleState)) {
|
|
observedBitsChanged = true;
|
|
}
|
|
|
|
previousStyleState = styleStateFlags;
|
|
|
|
return observedBitsChanged;
|
|
}
|
|
|
|
void EsElement::RefreshStyle(UIStyleKey *_oldStyleKey, bool alreadyRefreshStyleState, bool force) {
|
|
// Compute state flags.
|
|
|
|
if (!alreadyRefreshStyleState) {
|
|
RefreshStyleState();
|
|
}
|
|
|
|
uint16_t styleStateFlags = previousStyleState;
|
|
|
|
// Initialise the style.
|
|
|
|
UIStyleKey oldStyleKey = _oldStyleKey ? *_oldStyleKey : currentStyleKey;
|
|
currentStyleKey.stateFlags = styleStateFlags;
|
|
currentStyleKey.scale = theming.scale;
|
|
|
|
if (!force && 0 == EsMemoryCompare(¤tStyleKey, &oldStyleKey, sizeof(UIStyleKey)) && style) {
|
|
return;
|
|
}
|
|
|
|
if (~state & UI_STATE_ENTERED) {
|
|
if (style) style->CloseReference();
|
|
oldStyleKey = currentStyleKey;
|
|
oldStyleKey.stateFlags |= THEME_STATE_BEFORE_ENTER;
|
|
style = GetStyle(oldStyleKey, false);
|
|
}
|
|
|
|
UIStyle *oldStyle = style;
|
|
style = GetStyle(currentStyleKey, false); // TODO Forcing new styles if force flag set.
|
|
|
|
state &= ~UI_STATE_USE_MEASUREMENT_CACHE;
|
|
|
|
// Respond to modifications.
|
|
|
|
bool repaint = false, animate = false;
|
|
|
|
if (force) {
|
|
repaint = true;
|
|
}
|
|
|
|
if (oldStyle) {
|
|
if (oldStyle->style == style->style && api.global->animationTimeMultiplier > 0.01f) {
|
|
ThemeAnimationBuild(&animation, oldStyle, oldStyleKey.stateFlags, currentStyleKey.stateFlags);
|
|
animate = !ThemeAnimationComplete(&animation);
|
|
} else {
|
|
ThemeAnimationDestroy(&animation);
|
|
}
|
|
|
|
repaint = true;
|
|
} else {
|
|
repaint = animate = true;
|
|
}
|
|
|
|
if (repaint) {
|
|
if (animate) StartAnimating();
|
|
Repaint(true, ES_RECT_1(0));
|
|
}
|
|
|
|
// Delete the old style if necessary.
|
|
|
|
if (oldStyle) {
|
|
oldStyle->CloseReference();
|
|
}
|
|
}
|
|
|
|
void EsElement::SetStyle(EsStyleID part, bool refreshIfChanged) {
|
|
UIStyleKey oldStyleKey = currentStyleKey;
|
|
currentStyleKey.part = (uintptr_t) part;
|
|
|
|
if (currentStyleKey.part != oldStyleKey.part && refreshIfChanged) {
|
|
RefreshStyle(&oldStyleKey);
|
|
}
|
|
}
|
|
|
|
// --------------------------------- Layouting.
|
|
|
|
EsRectangle LayoutCell(EsElement *element, int width, int height) {
|
|
uint64_t layout = element->flags;
|
|
|
|
int maximumWidth = element->style->metrics->maximumWidth ?: ES_PANEL_BAND_SIZE_DEFAULT;
|
|
int minimumWidth = element->style->metrics->minimumWidth ?: ES_PANEL_BAND_SIZE_DEFAULT;
|
|
int maximumHeight = element->style->metrics->maximumHeight ?: ES_PANEL_BAND_SIZE_DEFAULT;
|
|
int minimumHeight = element->style->metrics->minimumHeight ?: ES_PANEL_BAND_SIZE_DEFAULT;
|
|
|
|
if (layout & ES_CELL_H_EXPAND) maximumWidth = INT_MAX;
|
|
if (layout & ES_CELL_H_SHRINK) minimumWidth = 0;
|
|
if (layout & ES_CELL_V_EXPAND) maximumHeight = INT_MAX;
|
|
if (layout & ES_CELL_V_SHRINK) minimumHeight = 0;
|
|
|
|
if (maximumWidth == ES_PANEL_BAND_SIZE_DEFAULT || minimumWidth == ES_PANEL_BAND_SIZE_DEFAULT) {
|
|
int width = element->GetWidth(height);
|
|
if (maximumWidth == ES_PANEL_BAND_SIZE_DEFAULT) maximumWidth = width;
|
|
if (minimumWidth == ES_PANEL_BAND_SIZE_DEFAULT) minimumWidth = width;
|
|
}
|
|
|
|
int preferredWidth = ClampInteger(minimumWidth, maximumWidth, width);
|
|
|
|
if (maximumHeight == ES_PANEL_BAND_SIZE_DEFAULT || minimumHeight == ES_PANEL_BAND_SIZE_DEFAULT) {
|
|
int height = element->GetHeight(preferredWidth);
|
|
if (maximumHeight == ES_PANEL_BAND_SIZE_DEFAULT) maximumHeight = height;
|
|
if (minimumHeight == ES_PANEL_BAND_SIZE_DEFAULT) minimumHeight = height;
|
|
}
|
|
|
|
int preferredHeight = ClampInteger(minimumHeight, maximumHeight, height);
|
|
|
|
EsRectangle bounds = ES_RECT_4(0, width, 0, height);
|
|
|
|
if ((layout & (ES_CELL_H_LEFT | ES_CELL_H_RIGHT)) == ES_CELL_H_LEFT) {
|
|
bounds.r = bounds.l + preferredWidth;
|
|
} else if ((layout & (ES_CELL_H_LEFT | ES_CELL_H_RIGHT)) == ES_CELL_H_RIGHT) {
|
|
bounds.l = bounds.r - preferredWidth;
|
|
} else {
|
|
bounds.l = bounds.l + width / 2 - preferredWidth / 2;
|
|
bounds.r = bounds.l + preferredWidth;
|
|
}
|
|
|
|
if ((layout & (ES_CELL_V_TOP | ES_CELL_V_BOTTOM)) == ES_CELL_V_TOP) {
|
|
bounds.b = bounds.t + preferredHeight;
|
|
} else if ((layout & (ES_CELL_V_TOP | ES_CELL_V_BOTTOM)) == ES_CELL_V_BOTTOM) {
|
|
bounds.t = bounds.b - preferredHeight;
|
|
} else {
|
|
bounds.t = bounds.t + height / 2 - preferredHeight / 2;
|
|
bounds.b = bounds.t + preferredHeight;
|
|
}
|
|
|
|
return bounds;
|
|
}
|
|
|
|
void EsElementMove(EsElement *element, int x, int y, int width, int height, bool applyCellLayout) {
|
|
EsMessageMutexCheck();
|
|
|
|
if (applyCellLayout) {
|
|
EsRectangle bounds = LayoutCell(element, width, height);
|
|
width = Width(bounds), height = Height(bounds);
|
|
x += bounds.l, y += bounds.t;
|
|
}
|
|
|
|
element->InternalMove(width, height, x, y);
|
|
}
|
|
|
|
void PanelMoveChild(EsElement *element, int width, int height, int offsetX, int offsetY) {
|
|
EsPanel *panel = (EsPanel *) element->parent;
|
|
|
|
{
|
|
EsRectangle bounds = LayoutCell(element, width, height);
|
|
width = Width(bounds), height = Height(bounds);
|
|
offsetX += bounds.l, offsetY += bounds.t;
|
|
}
|
|
|
|
float progress = panel->transitionLengthMs ? SmoothAnimationTime((float) panel->transitionTimeMs / panel->transitionLengthMs) : 1;
|
|
|
|
// TODO Make this faster than O(n^2).
|
|
|
|
for (uintptr_t i = 0; i < panel->movementItems.Length(); i++) {
|
|
PanelMovementItem *item = &panel->movementItems[i];
|
|
|
|
if (item->element != element) {
|
|
continue;
|
|
}
|
|
|
|
if (item->wasHidden) {
|
|
break;
|
|
} else {
|
|
int oldWidth = Width(item->oldBounds);
|
|
int oldHeight = Height(item->oldBounds);
|
|
int oldOffsetX = item->oldBounds.l;
|
|
int oldOffsetY = item->oldBounds.t;
|
|
|
|
element->InternalMove(LinearInterpolate(oldWidth, width, progress),
|
|
LinearInterpolate(oldHeight, height, progress),
|
|
LinearInterpolate(oldOffsetX, offsetX, progress),
|
|
LinearInterpolate(oldOffsetY, offsetY, progress));
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
element->InternalMove(width, height, offsetX, offsetY);
|
|
}
|
|
|
|
void LayoutTable(EsPanel *panel, EsMessage *message) {
|
|
bool debug = false;
|
|
|
|
bool has[2] = {};
|
|
int in[2] = {};
|
|
int out[2] = {};
|
|
|
|
if (message->type == ES_MSG_GET_WIDTH) {
|
|
in[1] = message->measure.height;
|
|
has[1] = has[1];
|
|
} else if (message->type == ES_MSG_GET_HEIGHT) {
|
|
in[0] = message->measure.width;
|
|
has[0] = in[0];
|
|
} else {
|
|
EsRectangle bounds = panel->GetBounds();
|
|
in[0] = bounds.r, in[1] = bounds.b;
|
|
has[0] = has[1] = true;
|
|
}
|
|
|
|
// if (debug) EsPrint("LayoutTable, %z, %d/%d\n", isMeasure ? "measure" : "layout", in[0], in[1]);
|
|
|
|
size_t childCount = panel->GetChildCount();
|
|
|
|
EsHeapFree(panel->tableMemoryBase);
|
|
panel->tableMemoryBase = nullptr;
|
|
|
|
uint8_t *memoryBase = (uint8_t *) EsHeapAllocate(sizeof(int) * childCount * 2 + sizeof(EsPanelBand) * (panel->bandCount[0] + panel->bandCount[1]), true), *memory = memoryBase;
|
|
|
|
if (!memoryBase) {
|
|
return;
|
|
}
|
|
|
|
// NOTE These must be contiguous at come at the start of memoryBase, so that the band decorators can look up band positions.
|
|
EsPanelBand *calculatedProperties[2];
|
|
calculatedProperties[0] = (EsPanelBand *) memory; memory += sizeof(EsPanelBand) * panel->bandCount[0];
|
|
calculatedProperties[1] = (EsPanelBand *) memory; memory += sizeof(EsPanelBand) * panel->bandCount[1];
|
|
|
|
int *calculatedSize[2];
|
|
calculatedSize[0] = (int *) memory; memory += sizeof(int) * childCount;
|
|
calculatedSize[1] = (int *) memory; memory += sizeof(int) * childCount;
|
|
|
|
for (int axis = 0; axis < 2; axis++) {
|
|
if (panel->bands[axis]) {
|
|
EsMemoryCopy(calculatedProperties[axis], panel->bands[axis], sizeof(EsPanelBand) * panel->bandCount[axis]);
|
|
} else {
|
|
for (uintptr_t i = 0; i < panel->bandCount[axis]; i++) {
|
|
calculatedProperties[axis][i].preferredSize
|
|
= calculatedProperties[axis][i].maximumSize
|
|
= calculatedProperties[axis][i].minimumSize
|
|
= ES_PANEL_BAND_SIZE_DEFAULT;
|
|
}
|
|
}
|
|
}
|
|
|
|
EsRectangle insets = panel->GetInsets();
|
|
|
|
for (int _axis = 0; _axis < 2; _axis++) {
|
|
int axis = (~panel->flags & ES_PANEL_HORIZONTAL) ? (1 - _axis) : _axis;
|
|
int gapSize = _axis ? panel->GetGapMinor() : panel->GetGapMajor();
|
|
int insetStart = axis ? insets.t : insets.l;
|
|
int insetEnd = axis ? insets.b : insets.r;
|
|
|
|
if (debug) EsPrint("\tAxis %d\n", axis);
|
|
|
|
for (uintptr_t i = 0; i < childCount; i++) {
|
|
EsElement *child = panel->GetChild(i);
|
|
if (child->flags & (ES_ELEMENT_HIDDEN | ES_ELEMENT_NON_CLIENT)) continue;
|
|
|
|
// Step 1: Find the preferred size of the children for this axis.
|
|
|
|
int size;
|
|
|
|
if ((child->flags & (axis ? ES_CELL_V_PUSH : ES_CELL_H_PUSH)) && has[axis]) {
|
|
size = 0;
|
|
} else {
|
|
int alternate = _axis ? calculatedSize[1 - axis][i] : 0;
|
|
size = axis ? child->GetHeight(alternate) : child->GetWidth(alternate);
|
|
}
|
|
|
|
if (debug) EsPrint("\tChild %d (%z) in cells %d->%d has size %d\n", i, child->cName, child->tableCell.from[axis], child->tableCell.to[axis], size);
|
|
|
|
// Step 2: Find the preferred size of each band on this axis.
|
|
|
|
int bandSpan = child->tableCell.to[axis] - child->tableCell.from[axis] + 1;
|
|
int totalGapSize = (bandSpan - 1) * gapSize;
|
|
|
|
int preferredSizePerBand = (size - totalGapSize) / bandSpan;
|
|
int maximumSizeValue = axis ? child->style->metrics->maximumHeight : child->style->metrics->maximumWidth;
|
|
int minimumSizeValue = axis ? child->style->metrics->minimumHeight : child->style->metrics->minimumWidth;
|
|
int maximumSizePerBand = maximumSizeValue ? (((int) maximumSizeValue - totalGapSize) / bandSpan) : ES_PANEL_BAND_SIZE_DEFAULT;
|
|
int minimumSizePerBand = maximumSizeValue ? (((int) minimumSizeValue - totalGapSize) / bandSpan) : ES_PANEL_BAND_SIZE_DEFAULT;
|
|
|
|
for (int j = child->tableCell.from[axis]; j <= child->tableCell.to[axis]; j++) {
|
|
EsAssert(j >= 0 && j < panel->bandCount[axis]); // Invalid element cell.
|
|
|
|
EsPanelBand *band = calculatedProperties[axis] + j;
|
|
|
|
if (child->flags & (axis ? ES_CELL_V_PUSH : ES_CELL_H_PUSH)) {
|
|
if (!band->push) {
|
|
band->push = 1;
|
|
}
|
|
}
|
|
|
|
if (!panel->bands[axis] || panel->bands[axis][j].preferredSize == ES_PANEL_BAND_SIZE_DEFAULT) {
|
|
if (band->preferredSize != ES_PANEL_BAND_SIZE_DEFAULT) {
|
|
if (band->preferredSize < preferredSizePerBand) {
|
|
band->preferredSize = preferredSizePerBand;
|
|
}
|
|
} else {
|
|
band->preferredSize = preferredSizePerBand;
|
|
}
|
|
}
|
|
|
|
if (!panel->bands[axis] || panel->bands[axis][j].maximumSize == ES_PANEL_BAND_SIZE_DEFAULT) {
|
|
if (maximumSizePerBand != ES_PANEL_BAND_SIZE_DEFAULT) {
|
|
if (band->maximumSize != ES_PANEL_BAND_SIZE_DEFAULT) {
|
|
if (band->maximumSize > maximumSizePerBand) {
|
|
band->maximumSize = maximumSizePerBand;
|
|
}
|
|
} else {
|
|
band->maximumSize = maximumSizePerBand;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!panel->bands[axis] || panel->bands[axis][j].minimumSize == ES_PANEL_BAND_SIZE_DEFAULT) {
|
|
if (minimumSizePerBand != ES_PANEL_BAND_SIZE_DEFAULT) {
|
|
if (band->minimumSize != ES_PANEL_BAND_SIZE_DEFAULT) {
|
|
if (band->minimumSize < minimumSizePerBand) {
|
|
band->minimumSize = minimumSizePerBand;
|
|
}
|
|
} else {
|
|
band->minimumSize = minimumSizePerBand;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
// Step 3: Work out the size of each band.
|
|
|
|
if (has[axis]) {
|
|
int contentSpace = in[axis] - insetStart - insetEnd - (panel->bandCount[axis] - 1) * gapSize;
|
|
|
|
for (int i = 0; i < panel->bandCount[axis]; i++) {
|
|
EsPanelBand *band = calculatedProperties[axis] + i;
|
|
if (band->minimumSize == ES_PANEL_BAND_SIZE_DEFAULT) band->minimumSize = 0;
|
|
if (band->preferredSize == ES_PANEL_BAND_SIZE_DEFAULT) band->preferredSize = 0;
|
|
if (band->maximumSize == ES_PANEL_BAND_SIZE_DEFAULT) band->maximumSize = INT_MAX;
|
|
}
|
|
|
|
int usedSpace = 0;
|
|
|
|
for (int i = 0; i < panel->bandCount[axis]; i++) {
|
|
usedSpace += calculatedProperties[axis][i].preferredSize;
|
|
}
|
|
|
|
bool shrink = usedSpace > contentSpace;
|
|
int remainingDifference = AbsoluteInteger(usedSpace - contentSpace);
|
|
|
|
while (remainingDifference > 0) {
|
|
int availableWeight = 0;
|
|
|
|
for (int i = 0; i < panel->bandCount[axis]; i++) {
|
|
EsPanelBand *band = calculatedProperties[axis] + i;
|
|
availableWeight += shrink ? band->pull : band->push;
|
|
}
|
|
|
|
if (!availableWeight) {
|
|
break; // There are no more flexible bands.
|
|
}
|
|
|
|
int perWeight = remainingDifference / availableWeight, perWeightExtra = remainingDifference % availableWeight;
|
|
bool stable = true;
|
|
|
|
for (int i = 0; i < panel->bandCount[axis]; i++) {
|
|
EsPanelBand *band = calculatedProperties[axis] + i;
|
|
int available = shrink ? (band->preferredSize - band->minimumSize) : (band->maximumSize - band->preferredSize);
|
|
int weight = shrink ? band->pull : band->push;
|
|
int change = weight * perWeight;
|
|
int extra = MinimumInteger(perWeightExtra, weight);
|
|
change += extra, perWeightExtra -= extra;
|
|
|
|
if (change > available) {
|
|
band->preferredSize = shrink ? band->minimumSize : band->maximumSize;
|
|
band->pull = band->push = 0;
|
|
remainingDifference -= available;
|
|
stable = false;
|
|
}
|
|
}
|
|
|
|
if (stable) {
|
|
perWeightExtra = remainingDifference % availableWeight;
|
|
|
|
for (int i = 0; i < panel->bandCount[axis]; i++) {
|
|
EsPanelBand *band = calculatedProperties[axis] + i;
|
|
int weight = shrink ? band->pull : band->push;
|
|
int change = weight * perWeight;
|
|
int extra = MinimumInteger(perWeightExtra, weight);
|
|
change += extra, perWeightExtra -= extra;
|
|
band->preferredSize += (shrink ? -1 : 1) * change;
|
|
if (band->preferredSize < band->minimumSize) band->preferredSize = band->minimumSize;
|
|
if (band->preferredSize > band->maximumSize) band->preferredSize = band->maximumSize;
|
|
}
|
|
|
|
break; // We've found a working configuration.
|
|
}
|
|
}
|
|
}
|
|
|
|
// Step 4: Work out the final size of each child.
|
|
|
|
for (uintptr_t i = 0; i < childCount; i++) {
|
|
EsElement *child = panel->GetChild(i);
|
|
if (child->flags & (ES_ELEMENT_HIDDEN | ES_ELEMENT_NON_CLIENT)) continue;
|
|
|
|
int size = (child->tableCell.to[axis] - child->tableCell.from[axis]) * gapSize;
|
|
|
|
for (int j = child->tableCell.from[axis]; j <= child->tableCell.to[axis]; j++) {
|
|
EsPanelBand *band = calculatedProperties[axis] + j;
|
|
size += band->preferredSize;
|
|
}
|
|
|
|
calculatedSize[axis][i] = size;
|
|
}
|
|
|
|
// Step 5: Calculate justification gap.
|
|
|
|
if ((axis ? (panel->flags & ES_PANEL_TABLE_V_JUSTIFY) : (panel->flags & ES_PANEL_TABLE_H_JUSTIFY))
|
|
&& panel->bandCount[axis] > 1 && message->type == ES_MSG_LAYOUT) {
|
|
int32_t usedSize = 0;
|
|
|
|
for (int i = 0; i < panel->bandCount[axis]; i++) {
|
|
usedSize += calculatedProperties[axis][i].preferredSize;
|
|
}
|
|
|
|
gapSize = (in[axis] - usedSize) / (panel->bandCount[axis] - 1);
|
|
}
|
|
|
|
// Step 6: Calculate the position of the bands.
|
|
|
|
int position = insetStart;
|
|
|
|
for (int i = 0; i < panel->bandCount[axis]; i++) {
|
|
if (i) position += gapSize;
|
|
EsPanelBand *band = calculatedProperties[axis] + i;
|
|
int size = band->preferredSize;
|
|
band->maximumSize = position; // HACK Aliasing maximumSize with position.
|
|
position += size;
|
|
}
|
|
|
|
out[axis] = position + insetEnd;
|
|
}
|
|
|
|
// Step 7: Move the children to their new location.
|
|
|
|
if (message->type == ES_MSG_GET_WIDTH) {
|
|
message->measure.width = out[0];
|
|
} else if (message->type == ES_MSG_GET_HEIGHT) {
|
|
message->measure.height = out[1];
|
|
} else {
|
|
for (uintptr_t i = 0; i < childCount; i++) {
|
|
EsElement *element = panel->GetChild(i);
|
|
|
|
if (element->flags & ES_ELEMENT_NON_CLIENT) {
|
|
continue;
|
|
} else if (element->flags & ES_ELEMENT_HIDDEN) {
|
|
element->InternalMove(0, 0, -1, -1);
|
|
continue;
|
|
}
|
|
|
|
int position[2], size[2];
|
|
|
|
for (int axis = 0; axis < 2; axis++) {
|
|
position[axis] = calculatedProperties[axis][element->tableCell.from[axis]].maximumSize;
|
|
size[axis] = calculatedSize[axis][i];
|
|
}
|
|
|
|
PanelMoveChild(element, size[0], size[1], position[0] - panel->scroll.position[0], position[1] - panel->scroll.position[1]);
|
|
}
|
|
}
|
|
|
|
if (debug) {
|
|
EsPrint("\t%d/%d\n", out[0], out[1]);
|
|
}
|
|
|
|
panel->tableMemoryBase = memoryBase;
|
|
}
|
|
|
|
int LayoutStackDeterminePerPush(EsPanel *panel, int available, int secondary) {
|
|
size_t childCount = panel->GetChildCount();
|
|
int fill = 0, count = 0, perPush = 0;
|
|
|
|
for (uintptr_t i = 0; i < childCount; i++) {
|
|
EsElement *child = panel->GetChild(i);
|
|
if (child->flags & (ES_ELEMENT_HIDDEN | ES_ELEMENT_NON_CLIENT)) continue;
|
|
|
|
count++;
|
|
|
|
if (panel->flags & ES_PANEL_HORIZONTAL) {
|
|
if (child->flags & ES_CELL_H_PUSH) {
|
|
fill++;
|
|
} else if (available > 0) {
|
|
available -= child->GetWidth(secondary);
|
|
}
|
|
} else {
|
|
if (child->flags & ES_CELL_V_PUSH) {
|
|
fill++;
|
|
} else if (available > 0) {
|
|
available -= child->GetHeight(secondary);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (count) {
|
|
available -= (count - 1) * panel->GetGapMajor();
|
|
}
|
|
|
|
if (available > 0 && fill) {
|
|
perPush = available / fill;
|
|
}
|
|
|
|
return perPush;
|
|
}
|
|
|
|
void LayoutStackSecondary(EsPanel *panel, EsMessage *message) {
|
|
bool horizontal = panel->flags & ES_PANEL_HORIZONTAL;
|
|
size_t childCount = panel->GetChildCount();
|
|
EsRectangle insets = panel->GetInsets();
|
|
int size = 0;
|
|
|
|
int primary = horizontal ? message->measure.width : message->measure.height;
|
|
int perPush = 0;
|
|
|
|
if (panel->state & UI_STATE_INSPECTING) {
|
|
InspectorNotifyElementEvent(panel, "layout", "Measuring stack on secondary axis with %d children, insets %R; provided primary size is %d.\n",
|
|
childCount, insets, primary);
|
|
}
|
|
|
|
if (primary) {
|
|
if (horizontal) primary -= insets.l + insets.r;
|
|
else primary -= insets.t + insets.b;
|
|
perPush = LayoutStackDeterminePerPush(panel, primary, 0);
|
|
}
|
|
|
|
for (uintptr_t i = 0; i < childCount; i++) {
|
|
EsElement *child = panel->GetChild(i);
|
|
if (child->flags & (ES_ELEMENT_HIDDEN | ES_ELEMENT_NON_CLIENT)) continue;
|
|
|
|
if (horizontal) {
|
|
int height = child->GetHeight((child->flags & ES_CELL_H_PUSH) ? perPush : 0);
|
|
if (height > size) size = height;
|
|
} else {
|
|
int width = child->GetWidth((child->flags & ES_CELL_V_PUSH) ? perPush : 0);
|
|
if (width > size) size = width;
|
|
}
|
|
}
|
|
|
|
if (horizontal) message->measure.height = size + insets.t + insets.b;
|
|
else message->measure.width = size + insets.l + insets.r;
|
|
}
|
|
|
|
void LayoutStackPrimary(EsPanel *panel, EsMessage *message) {
|
|
bool horizontal = panel->flags & ES_PANEL_HORIZONTAL;
|
|
bool reverse = panel->flags & ES_PANEL_REVERSE;
|
|
EsRectangle bounds = panel->GetBounds();
|
|
size_t childCount = panel->GetChildCount();
|
|
|
|
EsRectangle insets = panel->GetInsets();
|
|
int gap = panel->GetGapMajor();
|
|
|
|
if (message->type != ES_MSG_LAYOUT && (panel->state & UI_STATE_INSPECTING)) {
|
|
InspectorNotifyElementEvent(panel, "layout", "Measuring stack on primary axis with %d children, gap %d, insets %R.\n", childCount, gap, insets);
|
|
}
|
|
|
|
if (message->type == ES_MSG_LAYOUT && (panel->state & UI_STATE_INSPECTING)) {
|
|
InspectorNotifyElementEvent(panel, "layout", "LayoutStack into %R with %d children, gap %d, insets %R.\n", bounds, childCount, gap, insets);
|
|
}
|
|
|
|
int hBase = message->type == ES_MSG_GET_HEIGHT ? message->measure.width : Width(bounds);
|
|
int vBase = message->type == ES_MSG_GET_WIDTH ? message->measure.height : Height(bounds);
|
|
|
|
if (message->type == ES_MSG_LAYOUT) {
|
|
// If we scroll on a given axis, assume it extends to infinity during layout.
|
|
// During measurement, ScrollPane::ReceivedMessage does this for us.
|
|
if (panel->scroll.mode[0]) hBase = 0;
|
|
if (panel->scroll.mode[1]) vBase = 0;
|
|
}
|
|
|
|
int hSpace = hBase ? (hBase - insets.l - insets.r) : 0;
|
|
int vSpace = vBase ? (vBase - insets.t - insets.b) : 0;
|
|
int available = horizontal ? hSpace : vSpace;
|
|
int perPush = LayoutStackDeterminePerPush(panel, available, horizontal ? vSpace : hSpace);
|
|
|
|
int secondary1 = horizontal ? insets.t : insets.l;
|
|
int secondary2 = horizontal ? bounds.b - insets.b : bounds.r - insets.r;
|
|
|
|
int position = horizontal ? (reverse ? insets.r : insets.l) : (reverse ? insets.b : insets.t);
|
|
bool anyNonHiddenChildren = false;
|
|
|
|
if (message->type == ES_MSG_LAYOUT) {
|
|
if (!horizontal && panel->scroll.enabled[0]) {
|
|
secondary2 += panel->scroll.limit[0];
|
|
} else if (horizontal && panel->scroll.enabled[1]) {
|
|
secondary2 += panel->scroll.limit[1];
|
|
}
|
|
}
|
|
|
|
for (uintptr_t i = 0; i < childCount; i++) {
|
|
EsElement *child = panel->GetChild(i);
|
|
if (child->flags & (ES_ELEMENT_HIDDEN | ES_ELEMENT_NON_CLIENT)) continue;
|
|
EsRectangle relative;
|
|
anyNonHiddenChildren = true;
|
|
|
|
if (horizontal) {
|
|
int width = (child->flags & ES_CELL_H_PUSH) ? perPush : child->GetWidth(vSpace);
|
|
|
|
if (reverse) {
|
|
relative = ES_RECT_4(bounds.r - position - width, bounds.r - position, secondary1, secondary2);
|
|
} else {
|
|
relative = ES_RECT_4(position, position + width, insets.t, bounds.b - insets.b);
|
|
}
|
|
|
|
position += width + gap;
|
|
} else {
|
|
int height = (child->flags & ES_CELL_V_PUSH) ? perPush : child->GetHeight(hSpace);
|
|
|
|
if (reverse) {
|
|
relative = ES_RECT_4(secondary1, secondary2, bounds.b - position - height, bounds.b - position);
|
|
} else {
|
|
relative = ES_RECT_4(secondary1, secondary2, position, position + height);
|
|
}
|
|
|
|
position += height + gap;
|
|
}
|
|
|
|
if (message->type == ES_MSG_LAYOUT) {
|
|
if (panel->state & UI_STATE_INSPECTING) {
|
|
InspectorNotifyElementEvent(panel, "layout", "\tMove child %d into %R.\n", i, relative);
|
|
}
|
|
|
|
EsRectangle childBounds = Translate(relative, -panel->scroll.position[0], -panel->scroll.position[1]);
|
|
PanelMoveChild(child, Width(childBounds), Height(childBounds), childBounds.l, childBounds.t);
|
|
}
|
|
}
|
|
|
|
if (anyNonHiddenChildren) position -= gap;
|
|
|
|
if (message->type == ES_MSG_GET_WIDTH) {
|
|
message->measure.width = position + (reverse ? insets.l : insets.r);
|
|
} else if (message->type == ES_MSG_GET_HEIGHT) {
|
|
message->measure.height = position + (reverse ? insets.t : insets.b);
|
|
}
|
|
}
|
|
|
|
void LayoutStack(EsPanel *panel, EsMessage *message) {
|
|
bool horizontal = panel->flags & ES_PANEL_HORIZONTAL;
|
|
|
|
if (message->type == ES_MSG_LAYOUT
|
|
|| (message->type == ES_MSG_GET_WIDTH && horizontal)
|
|
|| (message->type == ES_MSG_GET_HEIGHT && !horizontal)) {
|
|
LayoutStackPrimary(panel, message);
|
|
} else {
|
|
LayoutStackSecondary(panel, message);
|
|
}
|
|
}
|
|
|
|
int EsElement::GetWidth(int height) {
|
|
if (style->preferredWidth) return style->preferredWidth;
|
|
if (!height) height = style->preferredHeight;
|
|
else if (style->preferredHeight && style->preferredHeight > height && (~flags & (ES_CELL_V_SHRINK))) height = style->preferredHeight;
|
|
else if (style->preferredHeight && style->preferredHeight < height && (~flags & (ES_CELL_V_EXPAND))) height = style->preferredHeight;
|
|
else if (style->metrics->minimumHeight && style->metrics->minimumHeight > height) height = style->metrics->minimumHeight;
|
|
else if (style->metrics->maximumHeight && style->metrics->maximumHeight < height) height = style->metrics->maximumHeight;
|
|
EsMessage m = { ES_MSG_GET_WIDTH };
|
|
m.measure.height = height;
|
|
EsMessageSend(this, &m);
|
|
int width = m.measure.width;
|
|
if (style->metrics->minimumWidth && style->metrics->minimumWidth > width) width = style->metrics->minimumWidth;
|
|
if (style->metrics->maximumWidth && style->metrics->maximumWidth < width) width = style->metrics->maximumWidth;
|
|
return width;
|
|
}
|
|
|
|
int EsElement::GetHeight(int width) {
|
|
if (style->preferredHeight) return style->preferredHeight;
|
|
if (!width) width = style->preferredWidth;
|
|
else if (style->preferredWidth && style->preferredWidth > width && (~flags & (ES_CELL_H_SHRINK))) width = style->preferredWidth;
|
|
else if (style->preferredWidth && style->preferredWidth < width && (~flags & (ES_CELL_H_EXPAND))) width = style->preferredWidth;
|
|
else if (style->metrics->minimumWidth && style->metrics->minimumWidth > width) width = style->metrics->minimumWidth;
|
|
else if (style->metrics->maximumWidth && style->metrics->maximumWidth < width) width = style->metrics->maximumWidth;
|
|
EsMessage m = { ES_MSG_GET_HEIGHT };
|
|
m.measure.width = width;
|
|
EsMessageSend(this, &m);
|
|
int height = m.measure.height;
|
|
if (style->metrics->minimumHeight && style->metrics->minimumHeight > height) height = style->metrics->minimumHeight;
|
|
if (style->metrics->maximumHeight && style->metrics->maximumHeight < height) height = style->metrics->maximumHeight;
|
|
return height;
|
|
}
|
|
|
|
void EsElement::InternalMove(int _width, int _height, int _offsetX, int _offsetY) {
|
|
EsAssert(~state & UI_STATE_IN_LAYOUT);
|
|
|
|
// Add the internal offset.
|
|
|
|
if (parent) {
|
|
_offsetX += parent->internalOffsetLeft;
|
|
_offsetY += parent->internalOffsetTop;
|
|
}
|
|
|
|
// What has changed?
|
|
|
|
bool hasPositionChanged = _offsetX != offsetX || _offsetY != offsetY;
|
|
bool hasSizeChanged = _width != width || _height != height;
|
|
bool relayoutRequested = state & UI_STATE_RELAYOUT;
|
|
bool relayoutChild = state & UI_STATE_RELAYOUT_CHILD;
|
|
int oldOffsetX = offsetX, oldOffsetY = offsetY;
|
|
|
|
// Update the variables.
|
|
|
|
offsetX = _offsetX;
|
|
offsetY = _offsetY;
|
|
width = _width;
|
|
height = _height;
|
|
state &= ~(UI_STATE_RELAYOUT | UI_STATE_RELAYOUT_CHILD);
|
|
|
|
if (!relayoutRequested && !hasSizeChanged) {
|
|
// If our size hasn't changed and a relayout wasn't requested, then we don't need to do any layouting.
|
|
|
|
if (hasPositionChanged) {
|
|
// Clear the old position.
|
|
|
|
if (parent) {
|
|
EsRectangle paintOutsets = style->paintOutsets;
|
|
EsRectangle rectangle = ES_RECT_4(oldOffsetX - paintOutsets.l, oldOffsetX + width + paintOutsets.r,
|
|
oldOffsetY - paintOutsets.t, oldOffsetY + height + paintOutsets.b);
|
|
parent->Repaint(false, rectangle);
|
|
}
|
|
|
|
// Repaint if we've moved.
|
|
|
|
Repaint(true);
|
|
}
|
|
|
|
if (relayoutChild) {
|
|
for (uintptr_t i = 0; i < children.Length(); i++) {
|
|
if (children[i]->state & (UI_STATE_RELAYOUT | UI_STATE_RELAYOUT_CHILD)) {
|
|
children[i]->InternalMove(children[i]->width, children[i]->height, children[i]->offsetX, children[i]->offsetY);
|
|
}
|
|
}
|
|
}
|
|
} else {
|
|
// Tell the element to layout its contents.
|
|
|
|
EsPerformanceTimerPush();
|
|
EsMessage m = { ES_MSG_LAYOUT };
|
|
m.layout.sizeChanged = hasSizeChanged;
|
|
state |= UI_STATE_IN_LAYOUT;
|
|
EsMessageSend(this, &m);
|
|
state &= ~UI_STATE_IN_LAYOUT;
|
|
|
|
if (state & UI_STATE_INSPECTING) {
|
|
InspectorNotifyElementEvent(this, "layout", "Layout in %Fms.\n", EsPerformanceTimerPop() * 1000);
|
|
}
|
|
|
|
// Ensure that any visible children that requested relayout were indeed updated.
|
|
|
|
for (uintptr_t i = 0; i < children.Length(); i++) {
|
|
EsAssert((children[i]->flags & ES_ELEMENT_HIDDEN)
|
|
|| !(children[i]->state & (UI_STATE_RELAYOUT | UI_STATE_RELAYOUT_CHILD)));
|
|
}
|
|
|
|
// Repaint.
|
|
|
|
Repaint(true);
|
|
}
|
|
|
|
if (window != this) {
|
|
window->processCheckVisible = true;
|
|
}
|
|
|
|
if (hasPositionChanged || hasSizeChanged) {
|
|
InspectorNotifyElementMoved(this, ES_RECT_4(offsetX, offsetX + width, offsetY, offsetY + height));
|
|
}
|
|
}
|
|
|
|
EsRectangle EsElementGetPreferredSize(EsElement *element) {
|
|
EsMessageMutexCheck();
|
|
|
|
return ES_RECT_4(0, element->style->preferredWidth, 0, element->style->preferredHeight);
|
|
}
|
|
|
|
void EsElementRelayout(EsElement *element) {
|
|
if (element->state & (UI_STATE_DESTROYING | UI_STATE_IN_LAYOUT)) return;
|
|
element->state |= UI_STATE_RELAYOUT;
|
|
UIWindowNeedsUpdate(element->window);
|
|
|
|
while (element && (~element->state & UI_STATE_IN_LAYOUT)) {
|
|
element->state |= UI_STATE_RELAYOUT_CHILD;
|
|
element = element->parent;
|
|
}
|
|
}
|
|
|
|
void EsElementUpdateContentSize(EsElement *element, uint32_t flags) {
|
|
if (element->state & UI_STATE_DESTROYING) return;
|
|
if (!flags) flags = ES_ELEMENT_UPDATE_CONTENT_WIDTH | ES_ELEMENT_UPDATE_CONTENT_HEIGHT;
|
|
|
|
while (element && (~element->state & UI_STATE_IN_LAYOUT) && flags) {
|
|
element->state &= ~UI_STATE_USE_MEASUREMENT_CACHE;
|
|
EsElementRelayout(element);
|
|
|
|
if (element->style->preferredWidth || ((element->flags & ES_CELL_H_FILL) == ES_CELL_H_FILL)) {
|
|
flags &= ~ES_ELEMENT_UPDATE_CONTENT_WIDTH;
|
|
}
|
|
|
|
if (element->style->preferredHeight || ((element->flags & ES_CELL_V_FILL) == ES_CELL_V_FILL)) {
|
|
flags &= ~ES_ELEMENT_UPDATE_CONTENT_HEIGHT;
|
|
}
|
|
|
|
element = element->parent;
|
|
}
|
|
}
|
|
|
|
// --------------------------------- Scrollbars.
|
|
|
|
// #define ENABLE_SMOOTH_SCROLLING
|
|
|
|
struct Scrollbar : EsElement {
|
|
EsButton *up, *down;
|
|
EsElement *thumb;
|
|
double position, autoScrollSpeed, smoothScrollTarget;
|
|
int viewportSize, contentSize, thumbSize, oldThumbPosition, thumbPosition, originalThumbPosition, oldPosition;
|
|
bool horizontal;
|
|
};
|
|
|
|
void ScrollbarLayout(Scrollbar *scrollbar) {
|
|
// TODO Do this as an UpdateAction?
|
|
|
|
if (scrollbar->viewportSize >= scrollbar->contentSize || scrollbar->viewportSize <= 0 || scrollbar->contentSize <= 0) {
|
|
EsElementSetDisabled(scrollbar, true);
|
|
} else {
|
|
EsElementSetDisabled(scrollbar, false);
|
|
EsRectangle bounds = scrollbar->GetBounds();
|
|
|
|
if (scrollbar->horizontal) {
|
|
scrollbar->thumbSize = scrollbar->viewportSize * (bounds.r - scrollbar->height * 2) / scrollbar->contentSize;
|
|
|
|
if (scrollbar->thumbSize < scrollbar->thumb->style->preferredWidth) {
|
|
scrollbar->thumbSize = scrollbar->thumb->style->preferredWidth;
|
|
}
|
|
|
|
if (scrollbar->thumbSize > Width(bounds) - scrollbar->height * 2) {
|
|
scrollbar->thumbSize = Width(bounds) - scrollbar->height * 2;
|
|
}
|
|
|
|
scrollbar->thumbPosition = LinearMap(0, scrollbar->contentSize - scrollbar->viewportSize,
|
|
scrollbar->height, bounds.r - scrollbar->thumbSize - scrollbar->height, scrollbar->smoothScrollTarget);
|
|
|
|
EsElementMove(scrollbar->up, 0, 0, (int) scrollbar->thumbPosition + scrollbar->thumbSize / 2, scrollbar->thumb->style->preferredHeight);
|
|
EsElementMove(scrollbar->thumb, (int) scrollbar->thumbPosition, 0, scrollbar->thumbSize, scrollbar->thumb->style->preferredHeight);
|
|
EsElementMove(scrollbar->down, (int) scrollbar->thumbPosition + scrollbar->thumbSize / 2, 0,
|
|
bounds.r - scrollbar->thumbSize / 2 - (int) scrollbar->thumbPosition,
|
|
scrollbar->thumb->style->preferredHeight);
|
|
} else {
|
|
scrollbar->thumbSize = scrollbar->viewportSize * (bounds.b - scrollbar->width * 2) / scrollbar->contentSize;
|
|
|
|
if (scrollbar->thumbSize < scrollbar->thumb->style->preferredHeight) {
|
|
scrollbar->thumbSize = scrollbar->thumb->style->preferredHeight;
|
|
}
|
|
|
|
if (scrollbar->thumbSize > Height(bounds) - scrollbar->width * 2) {
|
|
scrollbar->thumbSize = Height(bounds) - scrollbar->width * 2;
|
|
}
|
|
|
|
scrollbar->thumbPosition = LinearMap(0, scrollbar->contentSize - scrollbar->viewportSize,
|
|
scrollbar->width, bounds.b - scrollbar->thumbSize - scrollbar->width, scrollbar->smoothScrollTarget);
|
|
|
|
EsElementMove(scrollbar->up, 0, 0, scrollbar->thumb->style->preferredWidth, (int) scrollbar->thumbPosition + scrollbar->thumbSize / 2);
|
|
EsElementMove(scrollbar->thumb, 0, (int) scrollbar->thumbPosition, scrollbar->thumb->style->preferredWidth, scrollbar->thumbSize);
|
|
EsElementMove(scrollbar->down, 0, (int) scrollbar->thumbPosition + scrollbar->thumbSize / 2,
|
|
scrollbar->thumb->style->preferredWidth,
|
|
bounds.b - scrollbar->thumbSize / 2 - (int) scrollbar->thumbPosition);
|
|
}
|
|
}
|
|
}
|
|
|
|
void ScrollbarSetMeasurements(Scrollbar *scrollbar, int viewportSize, int contentSize) {
|
|
EsMessageMutexCheck();
|
|
|
|
if (scrollbar->viewportSize == viewportSize && scrollbar->contentSize == contentSize) {
|
|
return;
|
|
}
|
|
|
|
scrollbar->viewportSize = viewportSize;
|
|
scrollbar->contentSize = contentSize;
|
|
|
|
ScrollbarLayout(scrollbar);
|
|
}
|
|
|
|
void ScrollbarSetPosition(Scrollbar *scrollbar, double position, bool sendMovedMessage, bool smoothScroll) {
|
|
EsMessageMutexCheck();
|
|
|
|
if (position > scrollbar->contentSize - scrollbar->viewportSize) position = scrollbar->contentSize - scrollbar->viewportSize;
|
|
if (position < 0) position = 0;
|
|
|
|
scrollbar->smoothScrollTarget = position;
|
|
|
|
int previous = scrollbar->position;
|
|
|
|
#ifdef ENABLE_SMOOTH_SCROLLING
|
|
if (smoothScroll && OSCRTabsf(position - scrollbar->position) > 10) {
|
|
scrollbar->StartAnimating();
|
|
} else {
|
|
scrollbar->position = position;
|
|
}
|
|
#else
|
|
(void) smoothScroll;
|
|
scrollbar->position = position;
|
|
#endif
|
|
|
|
EsRectangle bounds = scrollbar->GetBounds();
|
|
|
|
scrollbar->thumbPosition = LinearMap(0, scrollbar->contentSize - scrollbar->viewportSize,
|
|
0, (scrollbar->horizontal ? bounds.r : bounds.b) - scrollbar->thumbSize, position);
|
|
|
|
if (sendMovedMessage && scrollbar->oldPosition != (int) scrollbar->position) {
|
|
EsMessage m = { ES_MSG_SCROLLBAR_MOVED };
|
|
m.scroll.scroll = (int) position;
|
|
m.scroll.previous = previous;
|
|
EsMessageSend(scrollbar, &m);
|
|
}
|
|
|
|
if (scrollbar->thumbPosition != scrollbar->oldThumbPosition) {
|
|
ScrollbarLayout(scrollbar);
|
|
}
|
|
|
|
scrollbar->oldThumbPosition = scrollbar->thumbPosition;
|
|
scrollbar->oldPosition = scrollbar->position;
|
|
}
|
|
|
|
int ProcessScrollbarButtonMessage(EsElement *element, EsMessage *message) {
|
|
Scrollbar *scrollbar = (Scrollbar *) element->parent;
|
|
|
|
if (message->type == ES_MSG_MOUSE_LEFT_DOWN) {
|
|
element->state |= UI_STATE_STRONG_PRESSED;
|
|
|
|
#define UI_SCROLLBAR_AUTO_SPEED (10.0)
|
|
scrollbar->autoScrollSpeed = scrollbar->viewportSize / UI_SCROLLBAR_AUTO_SPEED / (100);
|
|
|
|
if (scrollbar->up == element) {
|
|
scrollbar->autoScrollSpeed *= -1;
|
|
}
|
|
|
|
element->StartAnimating();
|
|
} else if (message->type == ES_MSG_MOUSE_LEFT_UP) {
|
|
element->state &= ~UI_STATE_STRONG_PRESSED;
|
|
scrollbar->autoScrollSpeed = 0;
|
|
} else if (message->type == ES_MSG_ANIMATE) {
|
|
if (scrollbar->autoScrollSpeed) {
|
|
ScrollbarSetPosition(scrollbar, scrollbar->autoScrollSpeed * message->animate.deltaMs + scrollbar->position, true, false);
|
|
message->animate.waitMs = 0;
|
|
message->animate.complete = false;
|
|
} else {
|
|
message->animate.complete = true;
|
|
}
|
|
} else {
|
|
return 0;
|
|
}
|
|
|
|
return ES_HANDLED;
|
|
}
|
|
|
|
Scrollbar *ScrollbarCreate(EsElement *parent, uint64_t flags) {
|
|
Scrollbar *scrollbar = (Scrollbar *) EsHeapAllocate(sizeof(Scrollbar), true);
|
|
if (!scrollbar) return nullptr;
|
|
scrollbar->thumb = (EsElement *) EsHeapAllocate(sizeof(EsElement), true);
|
|
|
|
if (flags & ES_SCROLLBAR_HORIZONTAL) {
|
|
scrollbar->horizontal = true;
|
|
}
|
|
|
|
scrollbar->Initialise(parent, flags, [] (EsElement *element, EsMessage *message) {
|
|
Scrollbar *scrollbar = (Scrollbar *) element;
|
|
|
|
if (message->type == ES_MSG_LAYOUT) {
|
|
ScrollbarLayout(scrollbar);
|
|
} else if (message->type == ES_MSG_ANIMATE) {
|
|
if (scrollbar->position != scrollbar->smoothScrollTarget) {
|
|
double factor = EsCRTexp2f(-5.0f / message->animate.deltaMs);
|
|
scrollbar->position += (scrollbar->smoothScrollTarget - scrollbar->position) * factor;
|
|
ScrollbarSetPosition(scrollbar, scrollbar->smoothScrollTarget, true, true);
|
|
bool done = scrollbar->position == scrollbar->smoothScrollTarget;
|
|
message->animate.waitMs = 0, message->animate.complete = done;
|
|
} else message->animate.complete = true;
|
|
} else {
|
|
return 0;
|
|
}
|
|
|
|
return ES_HANDLED;
|
|
}, 0);
|
|
|
|
scrollbar->cName = "scrollbar";
|
|
|
|
scrollbar->up = EsButtonCreate(scrollbar, ES_CELL_FILL);
|
|
scrollbar->up->messageUser = ProcessScrollbarButtonMessage;
|
|
scrollbar->down = EsButtonCreate(scrollbar, ES_CELL_FILL);
|
|
scrollbar->down->messageUser = ProcessScrollbarButtonMessage;
|
|
|
|
scrollbar->thumb->Initialise(scrollbar, ES_CELL_FILL, [] (EsElement *element, EsMessage *message) {
|
|
Scrollbar *scrollbar = (Scrollbar *) element->parent;
|
|
EsRectangle bounds = scrollbar->GetBounds();
|
|
|
|
if (message->type == ES_MSG_MOUSE_LEFT_DRAG) {
|
|
if (scrollbar->horizontal) {
|
|
float p = LinearMap(scrollbar->height, bounds.r - scrollbar->thumbSize - scrollbar->height, 0, scrollbar->contentSize - scrollbar->viewportSize,
|
|
message->mouseDragged.newPositionX - message->mouseDragged.originalPositionX + scrollbar->originalThumbPosition);
|
|
ScrollbarSetPosition(scrollbar, p, true, true);
|
|
} else {
|
|
float p = LinearMap(scrollbar->width, bounds.b - scrollbar->thumbSize - scrollbar->width, 0, scrollbar->contentSize - scrollbar->viewportSize,
|
|
message->mouseDragged.newPositionY - message->mouseDragged.originalPositionY + scrollbar->originalThumbPosition);
|
|
ScrollbarSetPosition(scrollbar, p, true, true);
|
|
}
|
|
} else if (message->type == ES_MSG_MOUSE_LEFT_DOWN) {
|
|
scrollbar->originalThumbPosition = scrollbar->thumbPosition;
|
|
} else if (message->type == ES_MSG_MOUSE_RIGHT_DOWN || message->type == ES_MSG_MOUSE_MIDDLE_DOWN) {
|
|
// TODO.
|
|
} else {
|
|
return 0;
|
|
}
|
|
|
|
return ES_HANDLED;
|
|
}, 0);
|
|
|
|
scrollbar->thumb->cName = "scrollbar thumb";
|
|
|
|
if (scrollbar->horizontal) {
|
|
scrollbar->up->SetStyle(ES_STYLE_PUSH_BUTTON_SCROLLBAR_LEFT);
|
|
scrollbar->down->SetStyle(ES_STYLE_PUSH_BUTTON_SCROLLBAR_RIGHT);
|
|
scrollbar->thumb->SetStyle(ES_STYLE_SCROLLBAR_THUMB_HORIZONTAL);
|
|
scrollbar->SetStyle(ES_STYLE_SCROLLBAR_BAR_HORIZONTAL);
|
|
} else {
|
|
scrollbar->up->SetStyle(ES_STYLE_PUSH_BUTTON_SCROLLBAR_UP);
|
|
scrollbar->down->SetStyle(ES_STYLE_PUSH_BUTTON_SCROLLBAR_DOWN);
|
|
scrollbar->thumb->SetStyle(ES_STYLE_SCROLLBAR_THUMB_VERTICAL);
|
|
scrollbar->SetStyle(ES_STYLE_SCROLLBAR_BAR_VERTICAL);
|
|
}
|
|
|
|
scrollbar->up->flags &= ~ES_ELEMENT_FOCUSABLE;
|
|
scrollbar->down->flags &= ~ES_ELEMENT_FOCUSABLE;
|
|
|
|
return scrollbar;
|
|
}
|
|
|
|
void ScrollPane::Setup(EsElement *_parent, uint8_t _xMode, uint8_t _yMode, uint16_t _flags) {
|
|
parent = _parent;
|
|
mode[0] = _xMode;
|
|
mode[1] = _yMode;
|
|
flags = _flags;
|
|
|
|
if (mode[0] == ES_SCROLL_MODE_NONE) flags &= ~ES_SCROLL_X_DRAG;
|
|
if (mode[1] == ES_SCROLL_MODE_NONE) flags &= ~ES_SCROLL_Y_DRAG;
|
|
|
|
for (int axis = 0; axis < 2; axis++) {
|
|
if (mode[axis] == ES_SCROLL_MODE_FIXED || mode[axis] == ES_SCROLL_MODE_AUTO) {
|
|
uint64_t flags = ES_CELL_FILL | ES_ELEMENT_NON_CLIENT | (axis ? ES_SCROLLBAR_VERTICAL : ES_SCROLLBAR_HORIZONTAL);
|
|
|
|
if (!bar[axis]) {
|
|
bar[axis] = ScrollbarCreate(parent, flags);
|
|
|
|
if (!bar[axis]) {
|
|
continue;
|
|
}
|
|
}
|
|
|
|
bar[axis]->userData = this;
|
|
|
|
bar[axis]->messageUser = [] (EsElement *element, EsMessage *message) {
|
|
ScrollPane *pane = (ScrollPane *) element->userData.p;
|
|
|
|
if (message->type == ES_MSG_SCROLLBAR_MOVED) {
|
|
int axis = (element->flags & ES_SCROLLBAR_HORIZONTAL) ? 0 : 1;
|
|
EsMessage m = *message;
|
|
m.type = axis ? ES_MSG_SCROLL_Y : ES_MSG_SCROLL_X;
|
|
pane->position[axis] = m.scroll.scroll;
|
|
EsMessageSend(pane->parent, &m);
|
|
}
|
|
|
|
return 0;
|
|
};
|
|
} else if (bar[axis]) {
|
|
EsElementDestroy(bar[axis]);
|
|
bar[axis] = nullptr;
|
|
}
|
|
}
|
|
|
|
if (bar[0] && bar[1]) {
|
|
if (!pad) {
|
|
pad = EsCustomElementCreate(parent, ES_CELL_FILL | ES_ELEMENT_NON_CLIENT, ES_STYLE_SCROLLBAR_PAD);
|
|
|
|
if (pad) {
|
|
pad->cName = "scrollbar pad";
|
|
|
|
pad->messageUser = [] (EsElement *, EsMessage *message) {
|
|
// Don't let clicks through.
|
|
return message->type == ES_MSG_MOUSE_LEFT_DOWN
|
|
|| message->type == ES_MSG_MOUSE_MIDDLE_DOWN
|
|
|| message->type == ES_MSG_MOUSE_RIGHT_DOWN ? ES_HANDLED : 0;
|
|
};
|
|
}
|
|
}
|
|
} else if (pad) {
|
|
EsElementDestroy(pad);
|
|
pad = nullptr;
|
|
}
|
|
}
|
|
|
|
int ScrollPane::ReceivedMessage(EsMessage *message) {
|
|
if (message->type == ES_MSG_LAYOUT) {
|
|
Refresh();
|
|
} else if (message->type == ES_MSG_MOUSE_LEFT_DRAG || message->type == ES_MSG_MOUSE_RIGHT_DRAG || message->type == ES_MSG_MOUSE_MIDDLE_DRAG) {
|
|
if (flags & (ES_SCROLL_X_DRAG | ES_SCROLL_Y_DRAG)) {
|
|
parent->StartAnimating();
|
|
dragScrolling = true;
|
|
}
|
|
} else if (message->type == ES_MSG_MOUSE_LEFT_UP || message->type == ES_MSG_MOUSE_RIGHT_UP || message->type == ES_MSG_MOUSE_MIDDLE_UP) {
|
|
dragScrolling = false;
|
|
} else if (message->type == ES_MSG_ANIMATE) {
|
|
if (dragScrolling) {
|
|
EsPoint point = EsMouseGetPosition(parent);
|
|
EsRectangle bounds = parent->GetBounds();
|
|
double distanceX = point.x < bounds.l ? point.x - bounds.l : point.x >= bounds.r ? point.x - bounds.r + 1 : 0;
|
|
double distanceY = point.y < bounds.t ? point.y - bounds.t : point.y >= bounds.b ? point.y - bounds.b + 1 : 0;
|
|
double deltaX = message->animate.deltaMs * distanceX / 300.0;
|
|
double deltaY = message->animate.deltaMs * distanceY / 300.0;
|
|
if (deltaX && (flags & ES_SCROLL_X_DRAG)) SetX(position[0] + deltaX, true);
|
|
if (deltaY && (flags & ES_SCROLL_Y_DRAG)) SetY(position[1] + deltaY, true);
|
|
message->animate.complete = false;
|
|
}
|
|
} else if (message->type == ES_MSG_GET_HEIGHT && !message->measure.internalMeasurement) {
|
|
int width = message->measure.width;
|
|
|
|
// If there is a prescribed internal width, we would need to subtract the width of the vertical scroll bar.
|
|
// But we are measuring the external height here, so if this height value gets used then
|
|
// the vertical scroll bar can only show if it is in the fixed mode.
|
|
if (width && mode[1] == ES_SCROLL_MODE_FIXED) {
|
|
width -= bar[1]->style->preferredWidth;
|
|
}
|
|
|
|
// Get the internal measurement on this axis.
|
|
EsMessage m = {};
|
|
m.type = ES_MSG_GET_HEIGHT;
|
|
m.measure.width = mode[0] ? 0 : width; // If the opposite axis is being scrolled, then ignore a prescribed measurement -- that will be an external measurement.
|
|
m.measure.internalMeasurement = true;
|
|
EsMessageSend(parent, &m); // Send it to ourself for internal measurement.
|
|
message->measure.height = m.measure.height;
|
|
|
|
bool horizontalScrollBarWillShow = false;
|
|
|
|
if (mode[0] == ES_SCROLL_MODE_FIXED) {
|
|
horizontalScrollBarWillShow = true;
|
|
} else if (mode[0] == ES_SCROLL_MODE_AUTO) {
|
|
if (!width) {
|
|
// If there is no prescribed external width,
|
|
// then we have no way to determine whether the scroll bar will be shown in this case.
|
|
} else {
|
|
EsMessage m = {};
|
|
m.type = ES_MSG_GET_WIDTH;
|
|
EsMessageSend(parent, &m);
|
|
horizontalScrollBarWillShow = m.measure.width + fixedViewport[0] > width;
|
|
}
|
|
}
|
|
|
|
// Add the height of the horizontal scroll bar, if it will be shown, to calculate the external height.
|
|
if (horizontalScrollBarWillShow) message->measure.height += bar[0]->style->preferredHeight;
|
|
|
|
return ES_HANDLED;
|
|
} else if (message->type == ES_MSG_GET_WIDTH && !message->measure.internalMeasurement) {
|
|
// Algorithm copied from above, in the GET_HEIGHT case.
|
|
int height = message->measure.height;
|
|
if (height && mode[0] == ES_SCROLL_MODE_FIXED) height -= bar[0]->style->preferredHeight;
|
|
EsMessage m = { .type = ES_MSG_GET_WIDTH, .measure = { .height = mode[1] ? 0 : height, .internalMeasurement = true } };
|
|
EsMessageSend(parent, &m);
|
|
message->measure.width = m.measure.width;
|
|
bool verticalScrollBarWillShow = mode[1] == ES_SCROLL_MODE_FIXED;
|
|
|
|
if (mode[1] == ES_SCROLL_MODE_AUTO && height) {
|
|
EsMessage m = { .type = ES_MSG_GET_HEIGHT };
|
|
EsMessageSend(parent, &m);
|
|
verticalScrollBarWillShow = m.measure.height + fixedViewport[1] > height;
|
|
}
|
|
|
|
if (verticalScrollBarWillShow) message->measure.width += bar[1]->style->preferredWidth;
|
|
return ES_HANDLED;
|
|
} else if (message->type == ES_MSG_SCROLL_WHEEL) {
|
|
double oldPosition0 = position[0];
|
|
double oldPosition1 = position[1];
|
|
SetPosition(0, position[0] + 60 * message->scrollWheel.dx / ES_SCROLL_WHEEL_NOTCH, true);
|
|
SetPosition(1, position[1] - 60 * message->scrollWheel.dy / ES_SCROLL_WHEEL_NOTCH, true);
|
|
if (oldPosition0 != position[0] || oldPosition1 != position[1]) return ES_HANDLED;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void ScrollPane::SetPosition(int axis, double newScroll, bool sendMovedMessage) {
|
|
if (mode[axis] == ES_SCROLL_MODE_NONE) return;
|
|
if (newScroll < 0) newScroll = 0;
|
|
else if (newScroll > limit[axis]) newScroll = limit[axis];
|
|
if (newScroll == position[axis]) return;
|
|
|
|
double previous = position[axis];
|
|
position[axis] = newScroll;
|
|
if (bar[axis]) ScrollbarSetPosition(bar[axis], position[axis], false, false);
|
|
|
|
if (sendMovedMessage) {
|
|
EsMessage m = {};
|
|
m.type = axis ? ES_MSG_SCROLL_Y : ES_MSG_SCROLL_X;
|
|
m.scroll.scroll = position[axis];
|
|
m.scroll.previous = previous;
|
|
EsMessageSend(parent, &m);
|
|
}
|
|
}
|
|
|
|
bool ScrollPane::RefreshLimit(int axis, int64_t *contentSize) {
|
|
if (mode[axis] != ES_SCROLL_MODE_NONE) {
|
|
uint8_t *internalOffset = axis ? &parent->internalOffsetRight : &parent->internalOffsetBottom;
|
|
EsRectangle bounds = parent->GetBounds();
|
|
|
|
EsMessage m = {};
|
|
m.type = axis ? ES_MSG_GET_HEIGHT : ES_MSG_GET_WIDTH;
|
|
m.measure.internalMeasurement = true;
|
|
if (axis) m.measure.width = bounds.r;
|
|
else m.measure.height = bounds.b;
|
|
EsMessageSend(parent, &m);
|
|
|
|
*contentSize = axis ? m.measure.height : m.measure.width;
|
|
limit[axis] = *contentSize - (axis ? bounds.b : bounds.r);
|
|
if (limit[axis] < 0) limit[axis] = 0;
|
|
|
|
if (parent->state & UI_STATE_INSPECTING) {
|
|
InspectorNotifyElementEvent(parent, "scroll", "New %c limit: %d. (Measured content %d with other axis %d.)\n",
|
|
axis + 'X', limit[axis], *contentSize, axis ? bounds.r : bounds.b);
|
|
}
|
|
|
|
if (mode[axis] == ES_SCROLL_MODE_AUTO && limit[axis] > 0 && !(*internalOffset)) {
|
|
*internalOffset = axis ? bar[axis]->style->preferredWidth : bar[axis]->style->preferredHeight;
|
|
return true;
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void ScrollPane::Refresh() {
|
|
if (parent->state & UI_STATE_INSPECTING) {
|
|
InspectorNotifyElementEvent(parent, "scroll", "Refreshing scroll pane...\n");
|
|
}
|
|
|
|
parent->internalOffsetRight = mode[1] == ES_SCROLL_MODE_FIXED ? bar[1]->style->preferredWidth : 0;
|
|
parent->internalOffsetBottom = mode[0] == ES_SCROLL_MODE_FIXED ? bar[0]->style->preferredHeight : 0;
|
|
|
|
int64_t contentWidth = 0, contentHeight = 0;
|
|
|
|
bool recalculateLimits1 = RefreshLimit(0, &contentWidth);
|
|
bool recalculateLimits2 = RefreshLimit(1, &contentHeight);
|
|
|
|
if (recalculateLimits1 || recalculateLimits2) {
|
|
RefreshLimit(0, &contentWidth);
|
|
RefreshLimit(1, &contentHeight);
|
|
}
|
|
|
|
EsRectangle bounds = parent->GetBounds();
|
|
|
|
if (bar[0]) ScrollbarSetMeasurements(bar[0], bounds.r - fixedViewport[0], contentWidth - fixedViewport[0]);
|
|
if (bar[1]) ScrollbarSetMeasurements(bar[1], bounds.b - fixedViewport[1], contentHeight - fixedViewport[1]);
|
|
|
|
if (~flags & ES_SCROLL_MANUAL) {
|
|
SetPosition(0, position[0], true);
|
|
SetPosition(1, position[1], true);
|
|
}
|
|
|
|
EsRectangle border = parent->style->borders;
|
|
|
|
bool previousEnabled[2] = { enabled[0], enabled[1] };
|
|
|
|
if (bar[0]) {
|
|
bar[0]->InternalMove(parent->width - parent->internalOffsetRight - border.r - border.l, bar[0]->style->preferredHeight,
|
|
border.l, parent->height - parent->internalOffsetBottom - border.b);
|
|
enabled[0] = ~bar[0]->flags & ES_ELEMENT_DISABLED;
|
|
}
|
|
|
|
if (bar[1]) {
|
|
bar[1]->InternalMove(bar[1]->style->preferredWidth, parent->height - parent->internalOffsetBottom - border.b - border.t,
|
|
parent->width - parent->internalOffsetRight - border.r, border.t);
|
|
enabled[1] = ~bar[1]->flags & ES_ELEMENT_DISABLED;
|
|
}
|
|
|
|
if (pad) {
|
|
pad->InternalMove(parent->internalOffsetRight, parent->internalOffsetBottom,
|
|
parent->width - parent->internalOffsetRight - border.r, parent->height - parent->internalOffsetBottom - border.b);
|
|
}
|
|
|
|
if ((bar[0] && previousEnabled[0] != enabled[0]) || (bar[1] && previousEnabled[1] != enabled[1])) {
|
|
// The scroll bars have moved, and so the internal offsets have changed.
|
|
// Therefore we need to tell the element to relayout.
|
|
EsElementRelayout(parent);
|
|
}
|
|
}
|
|
|
|
struct EsScrollView : EsElement { ScrollPane scroll; };
|
|
void EsScrollViewSetup(EsScrollView *view, uint8_t xMode, uint8_t yMode, uint16_t flags) { view->scroll.Setup(view, xMode, yMode, flags); }
|
|
void EsScrollViewSetPosition(EsScrollView *view, int axis, double newPosition, bool notify) { view->scroll.SetPosition(axis, newPosition, notify); }
|
|
void EsScrollViewRefresh(EsScrollView *view) { view->scroll.Refresh(); }
|
|
int EsScrollViewReceivedMessage(EsScrollView *view, EsMessage *message) { return view->scroll.ReceivedMessage(message); }
|
|
int64_t EsScrollViewGetPosition(EsScrollView *view, int axis) { return view->scroll.position[axis]; }
|
|
int64_t EsScrollViewGetLimit(EsScrollView *view, int axis) { return view->scroll.limit[axis]; }
|
|
void EsScrollViewSetFixedViewport(EsScrollView *view, int axis, int32_t value) { view->scroll.fixedViewport[axis] = value; }
|
|
bool EsScrollViewIsBarEnabled(EsScrollView *view, int axis) { return view->scroll.enabled[axis]; }
|
|
bool EsScrollViewIsInDragScroll(EsScrollView *view) { return view->scroll.dragScrolling; }
|
|
|
|
EsScrollView *EsCustomScrollViewCreate(EsElement *parent, uint64_t flags, EsStyleID style) {
|
|
EsScrollView *element = (EsScrollView *) EsHeapAllocate(sizeof(EsScrollView), true);
|
|
if (!element) return nullptr;
|
|
element->Initialise(parent, flags, nullptr, style);
|
|
element->cName = "custom scroll view";
|
|
return element;
|
|
}
|
|
|
|
// --------------------------------- Textboxes.
|
|
|
|
#include "textbox.cpp"
|
|
|
|
// --------------------------------- List views.
|
|
|
|
#include "list_view.cpp"
|
|
|
|
// --------------------------------- Panels.
|
|
|
|
void PanelSwitcherTransitionComplete(EsPanel *panel) {
|
|
if (panel->switchedFrom) {
|
|
if (panel->destroyPreviousAfterTransitionCompletes) {
|
|
panel->switchedFrom->Destroy();
|
|
} else {
|
|
EsElementSetHidden(panel->switchedFrom, true);
|
|
}
|
|
|
|
panel->switchedFrom = nullptr;
|
|
}
|
|
|
|
panel->transitionType = ES_TRANSITION_NONE;
|
|
}
|
|
|
|
void PanelTableSetChildCell(EsPanel *panel, EsElement *child) {
|
|
uintptr_t index = panel->tableIndex++;
|
|
TableCell cell = {};
|
|
|
|
if (panel->flags & ES_PANEL_HORIZONTAL) {
|
|
cell.from[0] = cell.to[0] = index % panel->bandCount[0];
|
|
cell.from[1] = cell.to[1] = index / panel->bandCount[0];
|
|
|
|
if (panel->bandCount[1] <= cell.from[1] && !panel->bands[1]) {
|
|
panel->bandCount[1] = cell.from[1] + 1;
|
|
}
|
|
} else {
|
|
cell.from[0] = cell.to[0] = index / panel->bandCount[1];
|
|
cell.from[1] = cell.to[1] = index % panel->bandCount[1];
|
|
|
|
if (panel->bandCount[0] <= cell.from[0] && !panel->bands[0]) {
|
|
panel->bandCount[0] = cell.from[0] + 1;
|
|
}
|
|
}
|
|
|
|
child->tableCell = cell;
|
|
|
|
EsHeapFree(panel->tableMemoryBase);
|
|
panel->tableMemoryBase = nullptr;
|
|
}
|
|
|
|
int ProcessPanelMessage(EsElement *element, EsMessage *message) {
|
|
EsPanel *panel = (EsPanel *) element;
|
|
EsRectangle bounds = panel->GetBounds();
|
|
|
|
int response = panel->scroll.ReceivedMessage(message);
|
|
if (response) return response;
|
|
|
|
if (message->type == ES_MSG_LAYOUT) {
|
|
if (panel->flags & ES_PANEL_TABLE) {
|
|
LayoutTable(panel, message);
|
|
} else if (panel->flags & ES_PANEL_SWITCHER) {
|
|
EsRectangle insets = panel->GetInsets();
|
|
|
|
if (panel->switchedFrom) {
|
|
EsElementMove(panel->switchedFrom, bounds.l + insets.l, bounds.t + insets.t,
|
|
bounds.r - bounds.l - insets.r - insets.l, bounds.b - bounds.t - insets.b - insets.t);
|
|
}
|
|
|
|
if (panel->switchedTo) {
|
|
EsElementMove(panel->switchedTo, bounds.l + insets.l, bounds.t + insets.t,
|
|
bounds.r - bounds.l - insets.r - insets.l, bounds.b - bounds.t - insets.b - insets.t);
|
|
}
|
|
} else if (panel->flags & ES_PANEL_Z_STACK) {
|
|
EsRectangle insets = panel->GetInsets();
|
|
|
|
for (uintptr_t i = 0; i < element->GetChildCount(); i++) {
|
|
EsElement *child = element->GetChild(i);
|
|
if (child->flags & (ES_ELEMENT_HIDDEN | ES_ELEMENT_NON_CLIENT)) continue;
|
|
PanelMoveChild(child, bounds.r - bounds.l - insets.r - insets.l,
|
|
bounds.b - bounds.t - insets.b - insets.t,
|
|
bounds.l + insets.l, bounds.t + insets.t);
|
|
}
|
|
} else {
|
|
LayoutStack(panel, message);
|
|
}
|
|
} else if (message->type == ES_MSG_PAINT) {
|
|
uint8_t *memory = panel->tableMemoryBase;
|
|
EsRectangle client = EsPainterBoundsClient(message->painter);
|
|
|
|
if (memory) {
|
|
EsPanelBand *calculatedProperties[2];
|
|
calculatedProperties[0] = (EsPanelBand *) memory; memory += sizeof(EsPanelBand) * panel->bandCount[0];
|
|
calculatedProperties[1] = (EsPanelBand *) memory; memory += sizeof(EsPanelBand) * panel->bandCount[1];
|
|
|
|
for (uintptr_t i = 0; i < panel->bandDecorators.Length(); i++) {
|
|
EsPanelBandDecorator decorator = panel->bandDecorators[i];
|
|
|
|
for (uintptr_t j = decorator.index; j < panel->bandCount[decorator.axis]; j += decorator.repeatEvery) {
|
|
EsRectangle bounds;
|
|
|
|
if (decorator.axis) {
|
|
bounds.l = client.l + panel->style->insets.l;
|
|
bounds.r = client.r - panel->style->insets.r;
|
|
bounds.t = client.t + calculatedProperties[1][j].maximumSize;
|
|
bounds.b = client.t + calculatedProperties[1][j].maximumSize + calculatedProperties[1][j].preferredSize;
|
|
} else {
|
|
bounds.l = client.l + calculatedProperties[0][j].maximumSize;
|
|
bounds.r = client.l + calculatedProperties[0][j].maximumSize + calculatedProperties[0][j].preferredSize;
|
|
bounds.t = client.t + panel->style->insets.t;
|
|
bounds.b = client.b - panel->style->insets.b;
|
|
}
|
|
|
|
EsDrawRectangle(message->painter, bounds, decorator.appearance.backgroundColor,
|
|
decorator.appearance.borderColor, decorator.appearance.borderSize);
|
|
if (!decorator.repeatEvery) break;
|
|
}
|
|
}
|
|
}
|
|
} else if (message->type == ES_MSG_PAINT_CHILDREN) {
|
|
if ((panel->flags & ES_PANEL_SWITCHER) && panel->transitionType != ES_TRANSITION_NONE) {
|
|
double progress = SmoothAnimationTimeSharp((double) panel->transitionTimeMs / (double) panel->transitionLengthMs);
|
|
EsRectangle bounds = EsPainterBoundsClient(message->painter);
|
|
int width = Width(bounds), height = Height(bounds);
|
|
EsPaintTarget target;
|
|
|
|
if (EsPaintTargetTake(&target, width, height)) {
|
|
EsPainter painter = { .clip = ES_RECT_4(0, width, 0, height), .width = width, .height = height, .target = &target };
|
|
|
|
// TODO 'Clip'-style transitions. ES_TRANSITION_REVEAL_UP/ES_TRANSITION_REVEAL_DOWN.
|
|
|
|
if (panel->switchedFrom) {
|
|
panel->switchedFrom->InternalPaint(&painter, PAINT_SHADOW);
|
|
panel->switchedFrom->InternalPaint(&painter, ES_FLAGS_DEFAULT);
|
|
panel->switchedFrom->InternalPaint(&painter, PAINT_OVERLAY);
|
|
UIDrawTransitionEffect(message->painter, &target, bounds, (EsTransitionType) panel->transitionType, progress, false);
|
|
EsPaintTargetClear(&target);
|
|
}
|
|
|
|
if (panel->switchedTo) {
|
|
panel->switchedTo->InternalPaint(&painter, PAINT_SHADOW);
|
|
panel->switchedTo->InternalPaint(&painter, ES_FLAGS_DEFAULT);
|
|
panel->switchedTo->InternalPaint(&painter, PAINT_OVERLAY);
|
|
UIDrawTransitionEffect(message->painter, &target, bounds, (EsTransitionType) panel->transitionType, progress, true);
|
|
}
|
|
|
|
EsPaintTargetReturn(&target);
|
|
} else {
|
|
// Not enough memory to get a paint target.
|
|
return 0;
|
|
}
|
|
} else {
|
|
return 0;
|
|
}
|
|
} else if (message->type == ES_MSG_GET_WIDTH) {
|
|
if (!panel->measurementCache.Get(message, &panel->state)) {
|
|
if (panel->flags & ES_PANEL_TABLE) {
|
|
LayoutTable(panel, message);
|
|
} else if (panel->flags & (ES_PANEL_Z_STACK | ES_PANEL_SWITCHER)) {
|
|
int maximum = 0;
|
|
|
|
for (uintptr_t i = 0; i < element->GetChildCount(); i++) {
|
|
EsElement *child = element->GetChild(i);
|
|
if (child->flags & ES_ELEMENT_NON_CLIENT) continue;
|
|
if ((child->flags & ES_ELEMENT_HIDDEN) && (~panel->flags & ES_PANEL_SWITCHER_MEASURE_LARGEST)) continue;
|
|
int size = child->GetWidth(message->measure.height);
|
|
if (size > maximum) maximum = size;
|
|
}
|
|
|
|
message->measure.width = maximum + panel->GetInsetWidth();
|
|
} else {
|
|
LayoutStack(panel, message);
|
|
}
|
|
|
|
panel->measurementCache.Store(message);
|
|
}
|
|
} else if (message->type == ES_MSG_GET_HEIGHT) {
|
|
if (!panel->measurementCache.Get(message, &panel->state)) {
|
|
if (panel->flags & ES_PANEL_TABLE) {
|
|
LayoutTable(panel, message);
|
|
} else if (panel->flags & (ES_PANEL_Z_STACK | ES_PANEL_SWITCHER)) {
|
|
int maximum = 0;
|
|
|
|
for (uintptr_t i = 0; i < element->GetChildCount(); i++) {
|
|
EsElement *child = element->GetChild(i);
|
|
if (child->flags & ES_ELEMENT_NON_CLIENT) continue;
|
|
if ((child->flags & ES_ELEMENT_HIDDEN) && (~panel->flags & ES_PANEL_SWITCHER_MEASURE_LARGEST)) continue;
|
|
int size = child->GetHeight(message->measure.width);
|
|
if (size > maximum) maximum = size;
|
|
}
|
|
|
|
message->measure.height = maximum + panel->GetInsetHeight();
|
|
} else {
|
|
LayoutStack(panel, message);
|
|
}
|
|
panel->measurementCache.Store(message);
|
|
}
|
|
} else if (message->type == ES_MSG_ENSURE_VISIBLE) {
|
|
if (panel->scroll.enabled[0] || panel->scroll.enabled[1]) {
|
|
EsElement *child = message->ensureVisible.descendent, *e = child;
|
|
int offsetX = 0, offsetY = 0;
|
|
while (e != element) offsetX += e->offsetX, offsetY += e->offsetY, e = e->parent;
|
|
EsRectangle bounds = panel->GetBounds();
|
|
|
|
if (message->ensureVisible.center || !EsRectangleContainsAll(bounds, ES_RECT_4PD(offsetX, offsetY, child->width, child->height))) {
|
|
panel->scroll.SetX(offsetX + panel->scroll.position[0] + child->width / 2 - bounds.r / 2);
|
|
panel->scroll.SetY(offsetY + panel->scroll.position[1] + child->height / 2 - bounds.b / 2);
|
|
}
|
|
|
|
return ES_HANDLED;
|
|
} else {
|
|
// This is not a scroll container, so don't update the child element being made visible.
|
|
return 0;
|
|
}
|
|
} else if (message->type == ES_MSG_PRE_ADD_CHILD) {
|
|
if (!panel->addingSeparator && panel->separatorStylePart && panel->GetChildCount()) {
|
|
panel->addingSeparator = true;
|
|
EsCustomElementCreate(panel, panel->separatorFlags, panel->separatorStylePart)->cName = "panel separator";
|
|
panel->addingSeparator = false;
|
|
}
|
|
} else if (message->type == ES_MSG_ADD_CHILD) {
|
|
if (panel->flags & ES_PANEL_TABLE) {
|
|
if (!panel->bandCount[0] && !panel->bandCount[1]) {
|
|
// The application has not yet set the number of columns/rows,
|
|
// so we can't perform automatical element placement.
|
|
// The application will need to call EsPanelTableSetChildCells.
|
|
} else {
|
|
PanelTableSetChildCell(panel, (EsElement *) message->child);
|
|
}
|
|
} else if (panel->flags & ES_PANEL_SWITCHER) {
|
|
EsElement *child = (EsElement *) message->child;
|
|
child->state |= UI_STATE_BLOCK_INTERACTION;
|
|
child->flags |= ES_ELEMENT_HIDDEN;
|
|
}
|
|
} else if (message->type == ES_MSG_SCROLL_X || message->type == ES_MSG_SCROLL_Y) {
|
|
int delta = message->scroll.scroll - message->scroll.previous;
|
|
int deltaX = message->type == ES_MSG_SCROLL_X ? delta : 0;
|
|
int deltaY = message->type == ES_MSG_SCROLL_Y ? delta : 0;
|
|
|
|
for (uintptr_t i = 0; i < panel->GetChildCount(); i++) {
|
|
EsElement *child = panel->GetChild(i);
|
|
if (child->flags & (ES_ELEMENT_HIDDEN | ES_ELEMENT_NON_CLIENT)) continue;
|
|
child->InternalMove(child->width, child->height, child->offsetX - deltaX, child->offsetY - deltaY);
|
|
}
|
|
} else if (message->type == ES_MSG_ANIMATE) {
|
|
panel->transitionTimeMs += message->animate.deltaMs;
|
|
message->animate.complete = panel->transitionTimeMs >= panel->transitionLengthMs;
|
|
|
|
if (panel->flags & ES_PANEL_SWITCHER) {
|
|
panel->Repaint(true);
|
|
|
|
if (message->animate.complete) {
|
|
PanelSwitcherTransitionComplete(panel);
|
|
}
|
|
} else if (panel->movementItems.Length()) {
|
|
EsElementRelayout(panel);
|
|
|
|
if (message->animate.complete) {
|
|
panel->movementItems.Free();
|
|
}
|
|
}
|
|
} else if (message->type == ES_MSG_DESTROY_CONTENTS) {
|
|
if ((panel->flags & ES_PANEL_TABLE)) {
|
|
panel->tableIndex = 0;
|
|
panel->bandCount[(panel->flags & ES_PANEL_HORIZONTAL) ? 1 : 0] = 0;
|
|
}
|
|
} else if (message->type == ES_MSG_DESTROY) {
|
|
if ((panel->flags & ES_PANEL_TABLE)) {
|
|
EsHeapFree(panel->bands[0]);
|
|
EsHeapFree(panel->bands[1]);
|
|
EsHeapFree(panel->tableMemoryBase);
|
|
}
|
|
|
|
panel->bandDecorators.Free();
|
|
panel->movementItems.Free();
|
|
} else if (message->type == ES_MSG_KEY_DOWN) {
|
|
if (!(panel->flags & (ES_PANEL_TABLE | ES_PANEL_SWITCHER))
|
|
&& panel->window->focused && panel->window->focused->parent == panel) {
|
|
bool reverse = panel->flags & ES_PANEL_REVERSE;
|
|
bool left = message->keyboard.scancode == ES_SCANCODE_LEFT_ARROW;
|
|
bool right = message->keyboard.scancode == ES_SCANCODE_RIGHT_ARROW;
|
|
bool up = message->keyboard.scancode == ES_SCANCODE_UP_ARROW;
|
|
bool down = message->keyboard.scancode == ES_SCANCODE_DOWN_ARROW;
|
|
|
|
if (~panel->flags & ES_PANEL_HORIZONTAL) {
|
|
left = up;
|
|
right = down;
|
|
}
|
|
|
|
EsElement *focus = nullptr;
|
|
|
|
if ((left && !reverse) || (right && reverse)) {
|
|
for (uintptr_t i = 0; i < panel->GetChildCount(); i++) {
|
|
EsElement *child = panel->GetChild(i);
|
|
|
|
if (child->flags & (ES_ELEMENT_HIDDEN | ES_ELEMENT_NON_CLIENT)) {
|
|
continue;
|
|
}
|
|
|
|
if (child == panel->window->focused) {
|
|
break;
|
|
} else if (child->IsFocusable()) {
|
|
focus = child;
|
|
}
|
|
}
|
|
} else if ((left && reverse) || (right && !reverse)) {
|
|
for (uintptr_t i = panel->GetChildCount(); i > 0; i--) {
|
|
EsElement *child = panel->GetChild(i - 1);
|
|
|
|
if (child->flags & (ES_ELEMENT_HIDDEN | ES_ELEMENT_NON_CLIENT)) {
|
|
continue;
|
|
}
|
|
|
|
if (child == panel->window->focused) {
|
|
break;
|
|
} else if (child->IsFocusable()) {
|
|
focus = child;
|
|
}
|
|
}
|
|
} else {
|
|
return 0;
|
|
}
|
|
|
|
if (focus) {
|
|
EsElementFocus(focus);
|
|
|
|
if (panel->flags & ES_PANEL_RADIO_GROUP) {
|
|
EsMessage m = { .type = ES_MSG_MOUSE_LEFT_CLICK };
|
|
EsMessageSend(focus, &m);
|
|
}
|
|
}
|
|
} else {
|
|
return 0;
|
|
}
|
|
} else if (message->type == ES_MSG_GET_INSPECTOR_INFORMATION) {
|
|
EsBuffer *buffer = message->getContent.buffer;
|
|
|
|
if (panel->flags & ES_PANEL_Z_STACK) {
|
|
EsBufferFormat(buffer, "z-stack");
|
|
} else if (panel->flags & ES_PANEL_SWITCHER) {
|
|
EsBufferFormat(buffer, "switcher");
|
|
} else if (panel->flags & ES_PANEL_TABLE) {
|
|
EsBufferFormat(buffer, "table");
|
|
} else {
|
|
EsBufferFormat(buffer, "%z%z stack",
|
|
(panel->flags & ES_PANEL_REVERSE) ? "reverse " : "",
|
|
(panel->flags & ES_PANEL_HORIZONTAL) ? "horizontal" : "vertical");
|
|
}
|
|
} else if (message->type == ES_MSG_BEFORE_Z_ORDER) {
|
|
bool isStack = !(panel->flags & (ES_PANEL_TABLE | ES_PANEL_SWITCHER | ES_PANEL_Z_STACK));
|
|
|
|
if (isStack && panel->children.Length() > 100) {
|
|
// Count the number of client children.
|
|
|
|
size_t childCount = panel->children.Length();
|
|
|
|
while (childCount) {
|
|
if (panel->children[childCount - 1]->flags & ES_ELEMENT_NON_CLIENT) {
|
|
childCount--;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
if (childCount < 100) {
|
|
return 0; // Don't bother if there are only a small number of child elements.
|
|
}
|
|
|
|
message->beforeZOrder.nonClient = childCount;
|
|
|
|
// Binary search for an early visible child.
|
|
|
|
bool found = false;
|
|
uintptr_t position = 0;
|
|
|
|
if (panel->flags & ES_PANEL_HORIZONTAL) {
|
|
ES_MACRO_SEARCH(childCount, result = message->beforeZOrder.clip.l - panel->children[index]->offsetX;, position, found);
|
|
} else {
|
|
ES_MACRO_SEARCH(childCount, result = message->beforeZOrder.clip.t - panel->children[index]->offsetY;, position, found);
|
|
}
|
|
|
|
if (!found) {
|
|
position = 0;
|
|
}
|
|
|
|
// Search back until we find the first.
|
|
// Assumption: children with paint outsets do not extend beyond the next child.
|
|
|
|
while (position) {
|
|
if (position < childCount) {
|
|
EsElement *child = panel->children[position];
|
|
|
|
if (panel->flags & ES_PANEL_HORIZONTAL) {
|
|
if (child->offsetX + child->width + child->style->paintOutsets.r < message->beforeZOrder.clip.l) {
|
|
break;
|
|
}
|
|
} else {
|
|
if (child->offsetY + child->height + child->style->paintOutsets.b < message->beforeZOrder.clip.t) {
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
position--;
|
|
}
|
|
|
|
message->beforeZOrder.start = position;
|
|
|
|
// Search forward until we find the last visible child.
|
|
|
|
while (position < childCount) {
|
|
EsElement *child = panel->children[position];
|
|
|
|
if (panel->flags & ES_PANEL_HORIZONTAL) {
|
|
if (child->offsetX - child->style->paintOutsets.l > message->beforeZOrder.clip.r) {
|
|
break;
|
|
}
|
|
} else {
|
|
if (child->offsetY - child->style->paintOutsets.t > message->beforeZOrder.clip.b) {
|
|
break;
|
|
}
|
|
}
|
|
|
|
position++;
|
|
}
|
|
|
|
message->beforeZOrder.end = position;
|
|
}
|
|
} else {
|
|
return 0;
|
|
}
|
|
|
|
return ES_HANDLED;
|
|
}
|
|
|
|
EsPanel *EsPanelCreate(EsElement *parent, uint64_t flags, EsStyleID style) {
|
|
EsPanel *panel = (EsPanel *) EsHeapAllocate(sizeof(EsPanel), true);
|
|
if (!panel) return nullptr;
|
|
|
|
panel->Initialise(parent, flags, ProcessPanelMessage, style);
|
|
panel->cName = "panel";
|
|
|
|
if (flags & ES_PANEL_Z_STACK) panel->state |= UI_STATE_Z_STACK;
|
|
if (flags & ES_PANEL_RADIO_GROUP) panel->state |= UI_STATE_RADIO_GROUP, panel->flags |= ES_ELEMENT_FOCUSABLE;
|
|
if (flags & ES_PANEL_HORIZONTAL) panel->flags |= ES_ELEMENT_LAYOUT_HINT_HORIZONTAL;
|
|
if (flags & ES_PANEL_REVERSE) panel->flags |= ES_ELEMENT_LAYOUT_HINT_REVERSE;
|
|
|
|
panel->scroll.Setup(panel,
|
|
((flags & ES_PANEL_H_SCROLL_FIXED) ? ES_SCROLL_MODE_FIXED : (flags & ES_PANEL_H_SCROLL_AUTO) ? ES_SCROLL_MODE_AUTO : ES_SCROLL_MODE_NONE),
|
|
((flags & ES_PANEL_V_SCROLL_FIXED) ? ES_SCROLL_MODE_FIXED : (flags & ES_PANEL_V_SCROLL_AUTO) ? ES_SCROLL_MODE_AUTO : ES_SCROLL_MODE_NONE),
|
|
ES_FLAGS_DEFAULT);
|
|
|
|
return panel;
|
|
}
|
|
|
|
struct EsSpacer : EsElement {
|
|
int width, height;
|
|
};
|
|
|
|
int ProcessSpacerMessage(EsElement *element, EsMessage *message) {
|
|
EsSpacer *spacer = (EsSpacer *) element;
|
|
|
|
if (message->type == ES_MSG_GET_WIDTH) {
|
|
message->measure.width = spacer->width * spacer->style->scale;
|
|
} else if (message->type == ES_MSG_GET_HEIGHT) {
|
|
message->measure.height = spacer->height * spacer->style->scale;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
EsSpacer *EsSpacerCreate(EsElement *panel, uint64_t flags, EsStyleID style, int width, int height) {
|
|
EsSpacer *spacer = (EsSpacer *) EsHeapAllocate(sizeof(EsSpacer), true);
|
|
if (!spacer) return nullptr;
|
|
spacer->Initialise(panel, flags, ProcessSpacerMessage, style);
|
|
spacer->cName = "spacer";
|
|
spacer->width = width == -1 ? 4 : width;
|
|
spacer->height = height == -1 ? 4 : height;
|
|
return spacer;
|
|
}
|
|
|
|
void EsSpacerChangeStyle(EsSpacer *spacer, EsStyleID style) {
|
|
EsMessageMutexCheck();
|
|
EsAssert(spacer->messageClass == ProcessSpacerMessage);
|
|
spacer->SetStyle(style);
|
|
}
|
|
|
|
EsElement *EsCustomElementCreate(EsElement *parent, uint64_t flags, EsStyleID style) {
|
|
EsElement *element = (EsElement *) EsHeapAllocate(sizeof(EsElement), true);
|
|
if (!element) return nullptr;
|
|
element->Initialise(parent, flags, nullptr, style);
|
|
element->cName = "custom element";
|
|
return element;
|
|
}
|
|
|
|
void EsElementSetCellRange(EsElement *element, int xFrom, int yFrom, int xTo, int yTo) {
|
|
EsMessageMutexCheck();
|
|
|
|
if (xFrom == -1) xFrom = element->tableCell.from[0];
|
|
if (yFrom == -1) yFrom = element->tableCell.from[1];
|
|
if (xTo == -1) xTo = xFrom;
|
|
if (yTo == -1) yTo = yFrom;
|
|
|
|
EsPanel *panel = (EsPanel *) element->parent;
|
|
EsAssert(panel->messageClass == ProcessPanelMessage && panel->flags & ES_PANEL_TABLE); // Invalid parent for SetCellRange.
|
|
|
|
TableCell cell = {};
|
|
cell.from[0] = xFrom, cell.from[1] = yFrom;
|
|
cell.to[0] = xTo, cell.to[1] = yTo;
|
|
element->tableCell = cell;
|
|
}
|
|
|
|
void EsPanelSetBands(EsPanel *panel, size_t columnCount, size_t rowCount, const EsPanelBand *columns, const EsPanelBand *rows) {
|
|
EsMessageMutexCheck();
|
|
EsAssert(panel->flags & ES_PANEL_TABLE); // Cannot set the bands layout for a non-table panel.
|
|
EsHeapFree(panel->bands[0]);
|
|
EsHeapFree(panel->bands[1]);
|
|
panel->bands[0] = nullptr;
|
|
panel->bands[1] = nullptr;
|
|
panel->bandCount[0] = columnCount;
|
|
panel->bandCount[1] = rowCount;
|
|
panel->bands[0] = columns ? (EsPanelBand *) EsHeapAllocate(columnCount * sizeof(EsPanelBand), false) : nullptr;
|
|
panel->bands[1] = rows ? (EsPanelBand *) EsHeapAllocate(rowCount * sizeof(EsPanelBand), false) : nullptr;
|
|
if (columns && panel->bands[0]) EsMemoryCopy(panel->bands[0], columns, columnCount * sizeof(EsPanelBand));
|
|
if (rows && panel->bands[1]) EsMemoryCopy(panel->bands[1], rows, rowCount * sizeof(EsPanelBand));
|
|
EsHeapFree(panel->tableMemoryBase);
|
|
panel->tableMemoryBase = nullptr;
|
|
}
|
|
|
|
void EsPanelSetBandsAll(EsPanel *panel, const EsPanelBand *column, const EsPanelBand *row) {
|
|
EsMessageMutexCheck();
|
|
EsAssert(panel->flags & ES_PANEL_TABLE); // Cannot set the bands layout for a non-table panel.
|
|
|
|
const EsPanelBand *templates[2] = { column, row };
|
|
|
|
for (uintptr_t axis = 0; axis < 2; axis++) {
|
|
if (!templates[axis]) continue;
|
|
|
|
if (!panel->bands[axis]) {
|
|
panel->bands[axis] = (EsPanelBand *) EsHeapAllocate(panel->bandCount[axis] * sizeof(EsPanelBand), false);
|
|
}
|
|
|
|
if (panel->bands[axis]) {
|
|
for (uintptr_t i = 0; i < panel->bandCount[axis]; i++) {
|
|
panel->bands[axis][i] = *templates[axis];
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void EsPanelTableSetChildCells(EsPanel *panel) {
|
|
EsMessageMutexCheck();
|
|
EsAssert(panel->flags & ES_PANEL_TABLE);
|
|
|
|
// The number of columns/rows should have been set by the time this function is called.
|
|
EsAssert(panel->bandCount[0] || panel->bandCount[1]);
|
|
|
|
panel->tableIndex = 0;
|
|
panel->bandCount[(panel->flags & ES_PANEL_HORIZONTAL) ? 1 : 0] = 0;
|
|
|
|
for (uintptr_t i = 0; i < panel->GetChildCount(); i++) {
|
|
EsElement *child = panel->GetChild(i);
|
|
if (child->flags & ES_ELEMENT_NON_CLIENT) continue;
|
|
PanelTableSetChildCell(panel, child);
|
|
}
|
|
|
|
EsHeapFree(panel->tableMemoryBase);
|
|
panel->tableMemoryBase = nullptr;
|
|
}
|
|
|
|
void EsPanelTableAddBandDecorator(EsPanel *panel, EsPanelBandDecorator decorator) {
|
|
EsMessageMutexCheck();
|
|
EsAssert(panel->flags & ES_PANEL_TABLE);
|
|
EsAssert(decorator.axis == 0 || decorator.axis == 1);
|
|
panel->bandDecorators.Add(decorator);
|
|
}
|
|
|
|
void EsPanelSwitchTo(EsPanel *panel, EsElement *targetChild, EsTransitionType transitionType, uint32_t flags, float timeMultiplier) {
|
|
EsMessageMutexCheck();
|
|
EsAssert(targetChild->parent == panel);
|
|
EsAssert(panel->flags & ES_PANEL_SWITCHER); // Cannot switch element for a non-switcher panel.
|
|
uint32_t timeMs = timeMultiplier * GetConstantNumber("transitionTime") * api.global->animationTimeMultiplier;
|
|
|
|
if (targetChild == panel->switchedTo) {
|
|
return;
|
|
}
|
|
|
|
if (panel->switchedFrom) {
|
|
// We're interrupting the previous transition.
|
|
PanelSwitcherTransitionComplete(panel);
|
|
}
|
|
|
|
panel->transitionType = transitionType;
|
|
panel->transitionTimeMs = 0;
|
|
panel->transitionLengthMs = timeMs;
|
|
panel->switchedFrom = panel->switchedTo;
|
|
panel->switchedTo = targetChild;
|
|
panel->destroyPreviousAfterTransitionCompletes = flags & ES_PANEL_SWITCHER_DESTROY_PREVIOUS_AFTER_TRANSITION;
|
|
|
|
if (panel->switchedTo) {
|
|
EsElementSetHidden(panel->switchedTo, false);
|
|
panel->switchedTo->state &= ~UI_STATE_BLOCK_INTERACTION;
|
|
panel->switchedTo->BringToFront();
|
|
}
|
|
|
|
if (panel->switchedFrom) {
|
|
panel->switchedFrom->state |= UI_STATE_BLOCK_INTERACTION;
|
|
UIMaybeRemoveFocusedElement(panel->window);
|
|
}
|
|
|
|
if (transitionType == ES_TRANSITION_NONE || panel->switchedFrom == panel->switchedTo || !panel->transitionLengthMs) {
|
|
PanelSwitcherTransitionComplete(panel);
|
|
} else {
|
|
panel->StartAnimating();
|
|
}
|
|
|
|
EsElementRelayout(panel);
|
|
}
|
|
|
|
void EsPanelStartMovementAnimation(EsPanel *panel, float timeMultiplier) {
|
|
// TODO Custom smoothing functions.
|
|
|
|
uint32_t timeMs = timeMultiplier * GetConstantNumber("transitionTime") * api.global->animationTimeMultiplier;
|
|
if (!timeMs) return;
|
|
EsMessageMutexCheck();
|
|
EsAssert(~panel->flags & ES_PANEL_SWITCHER); // Use EsPanelSwitchTo!
|
|
panel->transitionTimeMs = 0;
|
|
panel->transitionLengthMs = timeMs;
|
|
panel->StartAnimating();
|
|
panel->movementItems.Free();
|
|
|
|
for (uintptr_t i = 0; i < panel->GetChildCount(); i++) {
|
|
EsElement *element = panel->GetChild(i);
|
|
|
|
if (element->flags & ES_ELEMENT_NON_CLIENT) {
|
|
continue;
|
|
}
|
|
|
|
PanelMovementItem item = {};
|
|
item.element = element;
|
|
item.oldBounds = ES_RECT_4(element->offsetX, element->offsetX + element->width,
|
|
element->offsetY, element->offsetY + element->height);
|
|
item.wasHidden = element->flags & ES_ELEMENT_HIDDEN;
|
|
panel->movementItems.Add(item);
|
|
}
|
|
|
|
EsElementRelayout(panel);
|
|
}
|
|
|
|
EsButton *EsPanelRadioGroupGetChecked(EsPanel *panel) {
|
|
EsMessageMutexCheck();
|
|
EsAssert(panel->state & UI_STATE_RADIO_GROUP);
|
|
EsAssert(panel->GetChildCount());
|
|
|
|
for (uintptr_t i = 0; i < panel->GetChildCount(); i++) {
|
|
if (ES_CHECK_CHECKED == EsButtonGetCheck((EsButton *) panel->GetChild(i))) {
|
|
return (EsButton *) panel->GetChild(i);
|
|
}
|
|
}
|
|
|
|
return (EsButton *) panel->GetChild(0);
|
|
}
|
|
|
|
// --------------------------------- Dialogs.
|
|
|
|
struct EsDialog {
|
|
EsElement *mainPanel;
|
|
EsElement *buttonArea;
|
|
EsElement *contentArea;
|
|
EsButton *cancelButton;
|
|
};
|
|
|
|
void DialogDismissAll(EsWindow *window) {
|
|
for (intptr_t i = window->dialogs.Length() - 1; i >= 0; i--) {
|
|
EsButton *button = window->dialogs[i]->cancelButton;
|
|
|
|
if (button && button->onCommand) {
|
|
button->onCommand(button->instance, button, button->command);
|
|
}
|
|
}
|
|
}
|
|
|
|
int DialogClosingMessage(EsElement *element, EsMessage *message) {
|
|
if (message->type == ES_MSG_TRANSITION_COMPLETE) {
|
|
// Destroy the dialog and its wrapper.
|
|
EsElementDestroy(EsElementGetLayoutParent(element));
|
|
}
|
|
|
|
return ProcessPanelMessage(element, message);
|
|
}
|
|
|
|
void EsDialogClose(EsDialog *dialog) {
|
|
EsMessageMutexCheck();
|
|
|
|
EsWindow *window = dialog->mainPanel->window;
|
|
bool isTop = window->dialogs.Last() == dialog;
|
|
window->dialogs.FindAndDelete(dialog, true);
|
|
|
|
EsAssert(dialog->mainPanel->messageClass == ProcessPanelMessage);
|
|
dialog->mainPanel->messageClass = DialogClosingMessage;
|
|
dialog->mainPanel->state |= UI_STATE_BLOCK_INTERACTION;
|
|
EsElementStartTransition(dialog->mainPanel, ES_TRANSITION_ZOOM_OUT_LIGHT, ES_ELEMENT_TRANSITION_EXIT, 1.0f);
|
|
|
|
if (!isTop) {
|
|
} else if (!window->dialogs.Length()) {
|
|
window->children[0]->children[0]->state &= ~UI_STATE_BLOCK_INTERACTION;
|
|
window->children[1]->state &= ~UI_STATE_BLOCK_INTERACTION;
|
|
|
|
if (window->inactiveFocus) {
|
|
EsElementFocus(window->inactiveFocus, false);
|
|
window->inactiveFocus->Repaint(true);
|
|
window->inactiveFocus = nullptr;
|
|
}
|
|
} else {
|
|
window->dialogs.Last()->mainPanel->state &= ~UI_STATE_BLOCK_INTERACTION;
|
|
}
|
|
}
|
|
|
|
EsDialog *EsDialogShow(EsWindow *window, const char *title, ptrdiff_t titleBytes,
|
|
const char *content, ptrdiff_t contentBytes, uint32_t iconID, uint32_t flags) {
|
|
// TODO Show on a separate window?
|
|
// TODO Support dialogs owned by other processes.
|
|
|
|
EsDialog *dialog = (EsDialog *) EsHeapAllocate(sizeof(EsDialog), true);
|
|
if (!dialog) return nullptr;
|
|
|
|
EsAssert(window->windowStyle == ES_WINDOW_NORMAL); // Can only show dialogs on normal windows.
|
|
|
|
if (window->focused) {
|
|
window->inactiveFocus = window->focused;
|
|
window->inactiveFocus->Repaint(true);
|
|
window->focused = nullptr;
|
|
UIRemoveFocusFromElement(window->focused);
|
|
}
|
|
|
|
EsElement *mainStack = window->children[0];
|
|
|
|
if (!window->dialogs.Length()) {
|
|
mainStack->children[0]->state |= UI_STATE_BLOCK_INTERACTION; // Main content.
|
|
window->children[1]->state |= UI_STATE_BLOCK_INTERACTION; // Toolbar.
|
|
} else {
|
|
window->dialogs.Last()->mainPanel->state |= UI_STATE_BLOCK_INTERACTION;
|
|
}
|
|
|
|
EsElement *wrapper = EsPanelCreate(mainStack, ES_PANEL_VERTICAL | ES_CELL_FILL, ES_STYLE_DIALOG_WRAPPER);
|
|
wrapper->cName = "dialog wrapper";
|
|
dialog->mainPanel = EsPanelCreate(wrapper, ES_PANEL_VERTICAL | ES_CELL_SHRINK | ES_ELEMENT_AUTO_GROUP, ES_STYLE_DIALOG_SHADOW);
|
|
dialog->mainPanel->cName = "dialog";
|
|
EsElementStartTransition(dialog->mainPanel, ES_TRANSITION_ZOOM_OUT_LIGHT, ES_FLAGS_DEFAULT, 1.0f);
|
|
window->dialogs.Add(dialog);
|
|
|
|
EsElement *mainPanel = dialog->mainPanel;
|
|
EsPanel *heading = EsPanelCreate(mainPanel, ES_CELL_H_FILL | ES_PANEL_HORIZONTAL, ES_STYLE_DIALOG_HEADING);
|
|
|
|
if (iconID) {
|
|
EsIconDisplayCreate(heading, ES_FLAGS_DEFAULT, 0, iconID);
|
|
}
|
|
|
|
EsTextDisplayCreate(heading, ES_CELL_H_FILL | ES_CELL_V_CENTER, ES_STYLE_TEXT_HEADING2, title, titleBytes)->cName = "dialog heading";
|
|
|
|
dialog->contentArea = EsPanelCreate(mainPanel, ES_CELL_H_FILL | ES_PANEL_VERTICAL, ES_STYLE_DIALOG_CONTENT);
|
|
EsTextDisplayCreate(dialog->contentArea, ES_CELL_H_FILL, ES_STYLE_TEXT_PARAGRAPH, content, contentBytes)->cName = "dialog contents";
|
|
|
|
EsPanel *buttonArea = EsPanelCreate(mainPanel, ES_CELL_H_FILL | ES_PANEL_HORIZONTAL | ES_PANEL_REVERSE, ES_STYLE_DIALOG_BUTTON_AREA);
|
|
dialog->buttonArea = buttonArea;
|
|
|
|
if (flags & ES_DIALOG_ALERT_OK_BUTTON) {
|
|
EsButton *button = EsButtonCreate(buttonArea, ES_BUTTON_DEFAULT | ES_BUTTON_CANCEL, 0, INTERFACE_STRING(CommonOK));
|
|
EsElementFocus(button);
|
|
|
|
EsButtonOnCommand(button, [] (EsInstance *instance, EsElement *, EsCommand *) {
|
|
EsDialogClose(instance->window->dialogs.Last());
|
|
});
|
|
}
|
|
|
|
return dialog;
|
|
}
|
|
|
|
EsButton *EsDialogAddButton(EsDialog *dialog, uint64_t flags, EsStyleID style, const char *label, ptrdiff_t labelBytes, EsCommandCallback callback) {
|
|
EsButton *button = EsButtonCreate(dialog->buttonArea, flags, style, label, labelBytes);
|
|
|
|
if (button) {
|
|
if (flags & ES_BUTTON_CANCEL) {
|
|
dialog->cancelButton = button;
|
|
}
|
|
|
|
EsButtonOnCommand(button, callback);
|
|
}
|
|
|
|
return button;
|
|
}
|
|
|
|
EsElement *EsDialogGetContentArea(EsDialog *dialog) {
|
|
return dialog->contentArea;
|
|
}
|
|
|
|
// --------------------------------- Canvas panes.
|
|
|
|
struct EsCanvasPane : EsElement {
|
|
double panX, panY, zoom;
|
|
bool zoomFit, contentsChanged, center;
|
|
int previousWidth, previousHeight;
|
|
EsPoint lastPanPoint;
|
|
ScrollPane scroll;
|
|
};
|
|
|
|
EsElement *CanvasPaneGetCanvas(EsElement *element) {
|
|
for (uintptr_t i = 0; i < element->GetChildCount(); i++) {
|
|
if (~element->GetChild(i)->flags & ES_ELEMENT_NON_CLIENT) {
|
|
return element->GetChild(i);
|
|
}
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
int ProcessCanvasPaneMessage(EsElement *element, EsMessage *message) {
|
|
EsCanvasPane *pane = (EsCanvasPane *) element;
|
|
|
|
int response = pane->scroll.ReceivedMessage(message);
|
|
if (response) return response;
|
|
|
|
if (message->type == ES_MSG_LAYOUT) {
|
|
EsElement *canvas = CanvasPaneGetCanvas(element);
|
|
if (!canvas) return 0;
|
|
|
|
EsRectangle bounds = element->GetBounds();
|
|
EsRectangle insets = element->style->insets;
|
|
bounds.l += insets.l, bounds.r -= insets.r;
|
|
bounds.t += insets.t, bounds.b -= insets.b;
|
|
|
|
pane->panX -= (Width(bounds) - pane->previousWidth) / 2 / pane->zoom;
|
|
pane->panY -= (Height(bounds) - pane->previousHeight) / 2 / pane->zoom;
|
|
pane->previousWidth = Width(bounds), pane->previousHeight = Height(bounds);
|
|
|
|
int width = canvas->GetWidth(0), height = canvas->GetHeight(0);
|
|
|
|
double minimumZoomX = 1, minimumZoomY = 1;
|
|
if (width > Width(bounds)) minimumZoomX = (double) Width(bounds) / width;
|
|
if (height > Height(bounds)) minimumZoomY = (double) Height(bounds) / height;
|
|
double minimumZoom = minimumZoomX < minimumZoomY ? minimumZoomX : minimumZoomY;
|
|
|
|
if (pane->zoom < minimumZoom || pane->contentsChanged || pane->zoomFit) {
|
|
pane->zoom = minimumZoom;
|
|
pane->zoomFit = true;
|
|
}
|
|
|
|
pane->contentsChanged = false;
|
|
|
|
if (pane->panX < 0) pane->panX = 0;
|
|
if (pane->panX > width - Width(bounds) / pane->zoom) pane->panX = width - Width(bounds) / pane->zoom;
|
|
if (pane->panY < 0) pane->panY = 0;
|
|
if (pane->panY > height - Height(bounds) / pane->zoom) pane->panY = height - Height(bounds) / pane->zoom;
|
|
|
|
bool widthFits = width * pane->zoom <= Width(bounds);
|
|
bool heightFits = height * pane->zoom <= Height(bounds);
|
|
|
|
if (pane->center) {
|
|
pane->panX = width / 2 - Width(bounds) / pane->zoom / 2;
|
|
pane->panY = height / 2 - Height(bounds) / pane->zoom / 2;
|
|
}
|
|
|
|
if (widthFits) {
|
|
uint64_t cellH = canvas->flags & (ES_CELL_H_LEFT | ES_CELL_H_RIGHT);
|
|
if (cellH == ES_CELL_H_LEFT) pane->panX = 0;
|
|
if (cellH == ES_CELL_H_CENTER) pane->panX = width / 2 - Width(bounds) / pane->zoom / 2;
|
|
if (cellH == ES_CELL_H_RIGHT) pane->panX = width - Width(bounds) / pane->zoom;
|
|
}
|
|
|
|
if (heightFits) {
|
|
uint64_t cellV = canvas->flags & (ES_CELL_V_TOP | ES_CELL_V_BOTTOM);
|
|
if (cellV == ES_CELL_V_TOP) pane->panY = 0;
|
|
if (cellV == ES_CELL_V_CENTER) pane->panY = height / 2 - Height(bounds) / pane->zoom / 2;
|
|
if (cellV == ES_CELL_V_BOTTOM) pane->panY = height - Height(bounds) / pane->zoom;
|
|
}
|
|
|
|
pane->scroll.position[0] = pane->panX;
|
|
pane->scroll.position[1] = pane->panY;
|
|
ScrollbarSetPosition(pane->scroll.bar[0], pane->panX, false, false);
|
|
ScrollbarSetPosition(pane->scroll.bar[1], pane->panY, false, false);
|
|
|
|
pane->center = false;
|
|
|
|
int x = (int) (0.5f + LinearMap(pane->panX, pane->panX + Width(bounds) / pane->zoom, 0, Width(bounds), 0));
|
|
int y = (int) (0.5f + LinearMap(pane->panY, pane->panY + Height(bounds) / pane->zoom, 0, Height(bounds), 0));
|
|
canvas->InternalMove(width, height, x + bounds.l, y + bounds.t);
|
|
} else if (message->type == ES_MSG_PAINT) {
|
|
EsElement *canvas = CanvasPaneGetCanvas(element);
|
|
if (!canvas) return 0;
|
|
|
|
if (element->flags & ES_CANVAS_PANE_SHOW_SHADOW) {
|
|
UIStyle *style = GetStyle(MakeStyleKey(ES_STYLE_CANVAS_SHADOW, 0), true);
|
|
EsRectangle shadow = ES_RECT_4PD(canvas->offsetX, canvas->offsetY, canvas->width, canvas->height);
|
|
style->PaintLayers(message->painter, shadow, THEME_CHILD_TYPE_ONLY, ES_FLAGS_DEFAULT);
|
|
}
|
|
} else if (message->type == ES_MSG_MOUSE_MIDDLE_DOWN) {
|
|
pane->lastPanPoint = EsMouseGetPosition(pane);
|
|
} else if (message->type == ES_MSG_MOUSE_MIDDLE_DRAG) {
|
|
EsPoint point = EsMouseGetPosition(pane);
|
|
pane->zoomFit = false;
|
|
pane->panX -= (point.x - pane->lastPanPoint.x) / pane->zoom;
|
|
pane->panY -= (point.y - pane->lastPanPoint.y) / pane->zoom;
|
|
pane->lastPanPoint = point;
|
|
EsElementRelayout(pane);
|
|
} else if (message->type == ES_MSG_GET_CURSOR && pane->window->dragged == pane) {
|
|
message->cursorStyle = ES_CURSOR_HAND_DRAG;
|
|
} else if (message->type == ES_MSG_GET_WIDTH) {
|
|
EsElement *canvas = CanvasPaneGetCanvas(element);
|
|
message->measure.width = canvas ? canvas->GetWidth(message->measure.height) : 0;
|
|
} else if (message->type == ES_MSG_GET_HEIGHT) {
|
|
EsElement *canvas = CanvasPaneGetCanvas(element);
|
|
message->measure.height = canvas ? canvas->GetHeight(message->measure.width) : 0;
|
|
} else if (message->type == ES_MSG_SCROLL_X || message->type == ES_MSG_SCROLL_Y) {
|
|
pane->panX = pane->scroll.position[0];
|
|
pane->panY = pane->scroll.position[1];
|
|
EsElementRelayout(pane);
|
|
} else {
|
|
return 0;
|
|
}
|
|
|
|
return ES_HANDLED;
|
|
}
|
|
|
|
EsCanvasPane *EsCanvasPaneCreate(EsElement *parent, uint64_t flags, EsStyleID style) {
|
|
EsCanvasPane *pane = (EsCanvasPane *) EsHeapAllocate(sizeof(EsCanvasPane), true);
|
|
if (!pane) return nullptr;
|
|
pane->Initialise(parent, flags, ProcessCanvasPaneMessage, style);
|
|
pane->cName = "canvas pane";
|
|
pane->zoom = 1.0;
|
|
pane->scroll.Setup(pane, ES_SCROLL_MODE_AUTO, ES_SCROLL_MODE_AUTO, ES_SCROLL_MANUAL);
|
|
return pane;
|
|
}
|
|
|
|
// --------------------------------- Text displays.
|
|
|
|
// TODO Inline images and icons.
|
|
// TODO Links.
|
|
// TODO Inline backgrounds.
|
|
|
|
void TextDisplayFreeRuns(EsTextDisplay *display) {
|
|
if (display->plan) {
|
|
EsTextPlanDestroy(display->plan);
|
|
display->plan = nullptr;
|
|
}
|
|
|
|
if (display->usingSyntaxHighlighting) {
|
|
Array<EsTextRun> textRuns = { display->textRuns };
|
|
textRuns.Free();
|
|
} else {
|
|
EsHeapFree(display->textRuns);
|
|
}
|
|
}
|
|
|
|
int ProcessTextDisplayMessage(EsElement *element, EsMessage *message) {
|
|
EsTextDisplay *display = (EsTextDisplay *) element;
|
|
|
|
if (message->type == ES_MSG_PAINT) {
|
|
EsRectangle textBounds = EsPainterBoundsInset(message->painter);
|
|
|
|
if (!display->plan || display->planWidth != textBounds.r - textBounds.l || display->planHeight != textBounds.b - textBounds.t) {
|
|
if (display->plan) EsTextPlanDestroy(display->plan);
|
|
display->properties.flags = display->style->textAlign;
|
|
if (~display->flags & ES_TEXT_DISPLAY_PREFORMATTED) display->properties.flags |= ES_TEXT_PLAN_TRIM_SPACES;
|
|
if (display->flags & ES_TEXT_DISPLAY_NO_FONT_SUBSTITUTION) display->properties.flags |= ES_TEXT_PLAN_NO_FONT_SUBSTITUTION;
|
|
display->plan = EsTextPlanCreate(element, &display->properties, textBounds, display->contents, display->textRuns, display->textRunCount);
|
|
display->planWidth = textBounds.r - textBounds.l;
|
|
display->planHeight = textBounds.b - textBounds.t;
|
|
}
|
|
|
|
if (display->plan) {
|
|
EsDrawTextLayers(message->painter, display->plan, EsPainterBoundsInset(message->painter));
|
|
}
|
|
} else if (message->type == ES_MSG_GET_WIDTH || message->type == ES_MSG_GET_HEIGHT) {
|
|
if (!display->measurementCache.Get(message, &display->state)) {
|
|
EsRectangle insets = EsElementGetInsets(element);
|
|
|
|
if ((~display->style->textAlign & ES_TEXT_WRAP) && display->plan) {
|
|
// The text is not wrapped, so the input bounds cannot change the measured size.
|
|
// Therefore there is no need to recreate the plan.
|
|
// TODO Double-check that this is correct.
|
|
} else {
|
|
if (display->plan) EsTextPlanDestroy(display->plan);
|
|
display->properties.flags = display->style->textAlign | ((display->flags & ES_TEXT_DISPLAY_PREFORMATTED) ? 0 : ES_TEXT_PLAN_TRIM_SPACES);
|
|
display->planWidth = message->type == ES_MSG_GET_HEIGHT && message->measure.width
|
|
? (message->measure.width - insets.l - insets.r) : 0;
|
|
display->planHeight = 0;
|
|
display->plan = EsTextPlanCreate(element, &display->properties,
|
|
ES_RECT_4(0, display->planWidth, 0, 0),
|
|
display->contents, display->textRuns, display->textRunCount);
|
|
}
|
|
|
|
if (!display->plan) {
|
|
message->measure.width = message->measure.height = 0;
|
|
} else {
|
|
if (message->type == ES_MSG_GET_WIDTH) {
|
|
message->measure.width = EsTextPlanGetWidth(display->plan) + insets.l + insets.r;
|
|
} else {
|
|
message->measure.height = EsTextPlanGetHeight(display->plan) + insets.t + insets.b;
|
|
}
|
|
}
|
|
|
|
display->measurementCache.Store(message);
|
|
}
|
|
} else if (message->type == ES_MSG_DESTROY) {
|
|
TextDisplayFreeRuns(display);
|
|
EsHeapFree(display->contents);
|
|
} else if (message->type == ES_MSG_GET_INSPECTOR_INFORMATION) {
|
|
EsBufferFormat(message->getContent.buffer, "'%s'", display->textRuns[display->textRunCount].offset, display->contents);
|
|
} else if (message->type == ES_MSG_UI_SCALE_CHANGED) {
|
|
if (display->plan) {
|
|
EsTextPlanDestroy(display->plan);
|
|
display->plan = nullptr;
|
|
}
|
|
} else {
|
|
return 0;
|
|
}
|
|
|
|
return ES_HANDLED;
|
|
}
|
|
|
|
void EsTextDisplaySetStyledContents(EsTextDisplay *display, const char *string, EsTextRun *runs, size_t runCount) {
|
|
TextDisplayFreeRuns(display);
|
|
|
|
display->textRuns = (EsTextRun *) EsHeapAllocate(sizeof(EsTextRun) * (runCount + 1), true);
|
|
display->textRunCount = runCount;
|
|
|
|
size_t outBytes;
|
|
HeapDuplicate((void **) &display->contents, &outBytes, string, runs[runCount].offset);
|
|
|
|
if (outBytes != runs[runCount].offset) {
|
|
// TODO Handle allocation failure.
|
|
}
|
|
|
|
EsMemoryCopy(display->textRuns, runs, sizeof(EsTextRun) * (runCount + 1));
|
|
|
|
display->usingSyntaxHighlighting = false;
|
|
EsElementUpdateContentSize(display);
|
|
InspectorNotifyElementContentChanged(display);
|
|
}
|
|
|
|
void EsTextDisplaySetContents(EsTextDisplay *display, const char *string, ptrdiff_t stringBytes) {
|
|
if (stringBytes == -1) stringBytes = EsCStringLength(string);
|
|
|
|
TextDisplayFreeRuns(display);
|
|
|
|
if (display->flags & ES_TEXT_DISPLAY_RICH_TEXT) {
|
|
EsHeapFree(display->contents);
|
|
EsTextStyle baseStyle = {};
|
|
display->style->GetTextStyle(&baseStyle);
|
|
EsRichTextParse(string, stringBytes, &display->contents, &display->textRuns, &display->textRunCount, &baseStyle);
|
|
} else {
|
|
HeapDuplicate((void **) &display->contents, (size_t *) &stringBytes, string, stringBytes);
|
|
display->textRuns = (EsTextRun *) EsHeapAllocate(sizeof(EsTextRun) * 2, true);
|
|
display->style->GetTextStyle(&display->textRuns[0].style);
|
|
display->textRuns[1].offset = stringBytes;
|
|
display->textRunCount = 1;
|
|
}
|
|
|
|
display->usingSyntaxHighlighting = false;
|
|
EsElementUpdateContentSize(display);
|
|
InspectorNotifyElementContentChanged(display);
|
|
}
|
|
|
|
EsTextDisplay *EsTextDisplayCreate(EsElement *parent, uint64_t flags, EsStyleID style, const char *label, ptrdiff_t labelBytes) {
|
|
EsTextDisplay *display = (EsTextDisplay *) EsHeapAllocate(sizeof(EsTextDisplay), true);
|
|
if (!display) return nullptr;
|
|
display->Initialise(parent, flags, ProcessTextDisplayMessage, style ?: UIGetDefaultStyleVariant(ES_STYLE_TEXT_LABEL, parent));
|
|
display->cName = "text display";
|
|
if (labelBytes == -1) labelBytes = EsCStringLength(label);
|
|
EsTextDisplaySetContents(display, label, labelBytes);
|
|
return display;
|
|
}
|
|
|
|
void EsTextDisplaySetupSyntaxHighlighting(EsTextDisplay *display, uint32_t language, const uint32_t *customColors, size_t customColorCount) {
|
|
// Copied from EsTextboxSetupSyntaxHighlighting.
|
|
uint32_t colors[8];
|
|
colors[0] = 0x04000000; // Highlighted line.
|
|
colors[1] = 0xFF000000; // Default.
|
|
colors[2] = 0xFFA11F20; // Comment.
|
|
colors[3] = 0xFF037E01; // String.
|
|
colors[4] = 0xFF213EF1; // Number.
|
|
colors[5] = 0xFF7F0480; // Operator.
|
|
colors[6] = 0xFF545D70; // Preprocessor.
|
|
colors[7] = 0xFF17546D; // Keyword.
|
|
|
|
if (customColorCount > sizeof(colors) / sizeof(uint32_t)) customColorCount = sizeof(colors) / sizeof(uint32_t);
|
|
EsMemoryCopy(colors, customColors, customColorCount * sizeof(uint32_t));
|
|
|
|
EsTextStyle textStyle = {};
|
|
display->style->GetTextStyle(&textStyle);
|
|
|
|
EsTextRun *newRuns = TextApplySyntaxHighlighting(&textStyle, language, colors, {},
|
|
display->contents, display->textRuns[display->textRunCount].offset).array;
|
|
TextDisplayFreeRuns(display);
|
|
display->textRuns = newRuns;
|
|
display->textRunCount = ArrayLength(display->textRuns) - 1;
|
|
display->usingSyntaxHighlighting = true;
|
|
display->Repaint(true);
|
|
}
|
|
|
|
// --------------------------------- List displays.
|
|
|
|
struct EsListDisplay : EsElement {
|
|
uintptr_t itemCount, startIndex;
|
|
EsListDisplay *previous;
|
|
};
|
|
|
|
int ProcessListDisplayMessage(EsElement *element, EsMessage *message) {
|
|
EsListDisplay *display = (EsListDisplay *) element;
|
|
|
|
if (message->type == ES_MSG_GET_HEIGHT) {
|
|
int32_t height = 0;
|
|
int32_t margin = element->style->insets.l + element->style->insets.r + element->style->gapMinor;
|
|
uintptr_t itemCount = 0;
|
|
|
|
for (uintptr_t i = 0; i < element->GetChildCount(); i++) {
|
|
EsElement *child = element->GetChild(i);
|
|
if (child->flags & ES_ELEMENT_NON_CLIENT) continue;
|
|
height += child->GetHeight(message->measure.width - margin);
|
|
itemCount++;
|
|
}
|
|
|
|
if (itemCount) {
|
|
height += (itemCount - 1) * element->style->gapMajor;
|
|
}
|
|
|
|
message->measure.height = height + element->style->insets.t + element->style->insets.b;
|
|
} else if (message->type == ES_MSG_LAYOUT) {
|
|
int32_t position = element->style->insets.t;
|
|
int32_t margin = element->style->insets.l + element->style->gapMinor;
|
|
int32_t width = element->width - margin - element->style->insets.r;
|
|
|
|
for (uintptr_t i = 0; i < element->GetChildCount(); i++) {
|
|
EsElement *child = element->GetChild(i);
|
|
if (child->flags & ES_ELEMENT_NON_CLIENT) continue;
|
|
int height = child->GetHeight(width);
|
|
EsElementMove(child, margin, position, width, height);
|
|
position += height + element->style->gapMajor;
|
|
}
|
|
} else if (message->type == ES_MSG_PAINT) {
|
|
char buffer[64];
|
|
EsTextPlanProperties properties = {};
|
|
properties.flags = ES_TEXT_H_RIGHT | ES_TEXT_V_TOP | ES_TEXT_PLAN_SINGLE_USE;
|
|
EsTextRun textRun[2] = {};
|
|
|
|
EsRectangle bounds = EsPainterBoundsClient(message->painter);
|
|
bounds.r = bounds.l + element->style->insets.l;
|
|
|
|
uintptr_t counter = display->previous ? display->previous->itemCount : display->startIndex;
|
|
uint8_t markerType = element->flags & ES_LIST_DISPLAY_MARKER_TYPE_MASK;
|
|
|
|
EsMessage m = {};
|
|
m.type = ES_MSG_LIST_DISPLAY_GET_MARKER;
|
|
EsBuffer buffer2 = { .out = (uint8_t *) buffer, .bytes = sizeof(buffer) };
|
|
m.getContent.buffer = &buffer2;
|
|
|
|
for (uintptr_t i = 0; i < element->GetChildCount(); i++) {
|
|
EsElement *child = element->GetChild(i);
|
|
if (child->flags & ES_ELEMENT_NON_CLIENT) continue;
|
|
|
|
if (markerType == ES_LIST_DISPLAY_BULLETED) {
|
|
EsMemoryCopy(buffer, "\xE2\x80\xA2", (textRun[1].offset = 3));
|
|
} else if (markerType == ES_LIST_DISPLAY_NUMBERED) {
|
|
textRun[1].offset = EsStringFormat(buffer, sizeof(buffer), "%d.", counter + 1);
|
|
} else if (markerType == ES_LIST_DISPLAY_LOWER_ALPHA) {
|
|
textRun[1].offset = EsStringFormat(buffer, sizeof(buffer), "(%c)", counter + 'a');
|
|
} else if (markerType == ES_LIST_DISPLAY_CUSTOM_MARKER) {
|
|
m.getContent.index = counter;
|
|
EsMessageSend(element, &m);
|
|
textRun[1].offset = buffer2.position;
|
|
} else {
|
|
EsAssert(false);
|
|
}
|
|
|
|
child->style->GetTextStyle(&textRun[0].style);
|
|
textRun[0].style.figures = ES_TEXT_FIGURE_TABULAR;
|
|
bounds.t += child->offsetY;
|
|
bounds.b = bounds.t + child->height;
|
|
EsTextPlan *plan = EsTextPlanCreate(element, &properties, bounds, buffer, textRun, 1);
|
|
if (plan) EsDrawText(message->painter, plan, bounds);
|
|
bounds.t -= child->offsetY;
|
|
counter++;
|
|
}
|
|
} else if (message->type == ES_MSG_ADD_CHILD) {
|
|
display->itemCount++;
|
|
} else if (message->type == ES_MSG_REMOVE_CHILD) {
|
|
display->itemCount--;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
EsListDisplay *EsListDisplayCreate(EsElement *parent, uint64_t flags, EsStyleID style) {
|
|
EsListDisplay *display = (EsListDisplay *) EsHeapAllocate(sizeof(EsListDisplay), true);
|
|
if (!display) return nullptr;
|
|
display->Initialise(parent, flags, ProcessListDisplayMessage, style ?: ES_STYLE_LIST_DISPLAY_DEFAULT);
|
|
display->cName = "list display";
|
|
return display;
|
|
}
|
|
|
|
void EsListDisplaySetCounterContinuation(EsListDisplay *display, EsListDisplay *previous) {
|
|
display->previous = previous;
|
|
EsElementRepaint(display);
|
|
}
|
|
|
|
void EsListDisplaySetCounterStart(EsListDisplay *display, uintptr_t index) {
|
|
display->startIndex = index;
|
|
display->previous = nullptr;
|
|
EsElementRepaint(display);
|
|
}
|
|
|
|
// --------------------------------- Announcements.
|
|
|
|
// TODO Different colored messages for info/warning/error.
|
|
// TODO Different hold times.
|
|
|
|
int AnnouncementMessage(EsElement *element, EsMessage *message) {
|
|
EsWindow *window = (EsWindow *) element;
|
|
|
|
if (message->type == ES_MSG_ANIMATE) {
|
|
window->animationTime += message->animate.deltaMs;
|
|
|
|
double progress = window->animationTime / GetConstantNumber("announcementDuration");
|
|
|
|
if (progress > 1) {
|
|
EsElementDestroy(window);
|
|
return 0;
|
|
}
|
|
|
|
progress = 2 * progress - 1;
|
|
progress = (1 + progress * progress * progress * progress * progress) * 0.5;
|
|
|
|
double inOnly = 2 * (progress < 0.5 ? progress : 0.5);
|
|
double inOut = progress < 0.5 ? progress * 2 : (2 - progress * 2);
|
|
|
|
EsRectangle bounds = EsWindowGetBounds(window);
|
|
int32_t height = Height(bounds);
|
|
bounds.t = window->announcementBase.y - inOnly * GetConstantNumber("announcementMovement");
|
|
bounds.b = bounds.t + height;
|
|
|
|
EsSyscall(ES_SYSCALL_WINDOW_SET_PROPERTY, window->handle, 0xFF * inOut, 0, ES_WINDOW_PROPERTY_ALPHA);
|
|
EsSyscall(ES_SYSCALL_WINDOW_MOVE, window->handle, (uintptr_t) &bounds, 0,
|
|
ES_WINDOW_MOVE_ADJUST_TO_FIT_SCREEN | ES_WINDOW_MOVE_ALWAYS_ON_TOP | ES_WINDOW_MOVE_UPDATE_SCREEN);
|
|
|
|
message->animate.complete = false;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void EsAnnouncementShow(EsWindow *parent, uint64_t flags, int32_t x, int32_t y, const char *text, ptrdiff_t textBytes) {
|
|
(void) flags;
|
|
|
|
if (x == -1 && y == -1) {
|
|
EsRectangle bounds = EsElementGetWindowBounds((EsElement *) parent->toolbarSwitcher ?: parent);
|
|
x = (bounds.l + bounds.r) / 2;
|
|
y = bounds.t + 40 * theming.scale;
|
|
}
|
|
|
|
EsWindow *window = EsWindowCreate(nullptr, ES_WINDOW_TIP);
|
|
if (!window) return;
|
|
window->messageUser = AnnouncementMessage;
|
|
|
|
EsTextDisplay *display = EsTextDisplayCreate(window, ES_CELL_FILL, ES_STYLE_ANNOUNCEMENT, text, textBytes);
|
|
int32_t width = display ? display->GetWidth(0) : 0;
|
|
int32_t height = display ? display->GetHeight(width) : 0;
|
|
|
|
EsRectangle parentBounds = {};
|
|
if (parent) parentBounds = EsWindowGetBounds(parent);
|
|
EsRectangle bounds = ES_RECT_4PD(x - width / 2 + parentBounds.l, y - height + parentBounds.t, width, height);
|
|
EsSyscall(ES_SYSCALL_WINDOW_SET_PROPERTY, window->handle, 0x00, 0, ES_WINDOW_PROPERTY_ALPHA);
|
|
EsSyscall(ES_SYSCALL_WINDOW_MOVE, window->handle, (uintptr_t) &bounds, 0, ES_WINDOW_MOVE_ADJUST_TO_FIT_SCREEN | ES_WINDOW_MOVE_ALWAYS_ON_TOP);
|
|
window->announcementBase.y = EsWindowGetBounds(window).t;
|
|
window->StartAnimating();
|
|
}
|
|
|
|
// --------------------------------- Buttons.
|
|
|
|
int ProcessButtonMessage(EsElement *element, EsMessage *message) {
|
|
EsButton *button = (EsButton *) element;
|
|
|
|
if (message->type == ES_MSG_PAINT) {
|
|
EsDrawContent(message->painter, element,
|
|
ES_RECT_2S(message->painter->width, message->painter->height),
|
|
button->label, button->labelBytes, button->iconID,
|
|
((button->flags & ES_BUTTON_DROPDOWN) ? ES_DRAW_CONTENT_MARKER_DOWN_ARROW : 0));
|
|
} else if (message->type == ES_MSG_PAINT_ICON) {
|
|
if (button->imageDisplay) {
|
|
EsRectangle imageSize = ES_RECT_2S(button->imageDisplay->width, button->imageDisplay->height);
|
|
EsRectangle bounds = EsRectangleFit(EsPainterBoundsClient(message->painter), imageSize, true);
|
|
EsImageDisplayPaint(button->imageDisplay, message->painter, bounds);
|
|
} else {
|
|
return 0;
|
|
}
|
|
} else if (message->type == ES_MSG_GET_WIDTH) {
|
|
if (!button->measurementCache.Get(message, &button->state)) {
|
|
EsTextStyle textStyle;
|
|
button->style->GetTextStyle(&textStyle);
|
|
int stringWidth = TextGetStringWidth(button, &textStyle, button->label, button->labelBytes);
|
|
int iconWidth = button->iconID ? button->style->metrics->iconSize : 0;
|
|
int contentWidth = stringWidth + iconWidth + ((stringWidth && iconWidth) ? button->style->gapMinor : 0)
|
|
+ button->style->insets.l + button->style->insets.r;
|
|
|
|
if (button->flags & ES_BUTTON_DROPDOWN) {
|
|
int64_t width = 0;
|
|
GetPreferredSizeFromStylePart(ES_STYLE_MARKER_DOWN_ARROW, &width, nullptr);
|
|
contentWidth += width + button->style->gapMinor;
|
|
}
|
|
|
|
int minimumReportedWidth = GetConstantNumber("pushButtonMinimumReportedWidth");
|
|
if (button->flags & ES_BUTTON_MENU_ITEM) minimumReportedWidth = GetConstantNumber("menuItemMinimumReportedWidth");
|
|
if (!stringWidth || (button->flags & ES_BUTTON_COMPACT)) minimumReportedWidth = 0;
|
|
|
|
message->measure.width = minimumReportedWidth > contentWidth ? minimumReportedWidth : contentWidth;
|
|
|
|
button->measurementCache.Store(message);
|
|
}
|
|
} else if (message->type == ES_MSG_DESTROY) {
|
|
EsHeapFree(button->label);
|
|
|
|
if (button->command) {
|
|
Array<EsElement *> elements = { button->command->elements };
|
|
elements.FindAndDeleteSwap(button, true);
|
|
button->command->elements = elements.array;
|
|
}
|
|
|
|
if (button->imageDisplay) {
|
|
EsElementDestroy(button->imageDisplay);
|
|
button->imageDisplay = nullptr;
|
|
}
|
|
} else if (message->type == ES_MSG_MOUSE_LEFT_DOWN) {
|
|
} else if (message->type == ES_MSG_MOUSE_LEFT_CLICK) {
|
|
if (button->flags & ES_BUTTON_CHECKBOX) {
|
|
button->customStyleState &= ~THEME_STATE_INDETERMINATE;
|
|
button->customStyleState ^= THEME_STATE_CHECKED;
|
|
} else if (button->flags & ES_BUTTON_RADIOBOX) {
|
|
button->customStyleState |= THEME_STATE_CHECKED;
|
|
|
|
EsMessage m = { ES_MSG_RADIO_GROUP_UPDATED };
|
|
|
|
for (uintptr_t i = 0; i < button->parent->GetChildCount(); i++) {
|
|
if (button->parent->GetChild(i) != button) {
|
|
EsMessageSend(button->parent->GetChild(i), &m);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (button->checkBuddy) {
|
|
EsElementSetDisabled(button->checkBuddy, !(button->customStyleState & (THEME_STATE_CHECKED | THEME_STATE_INDETERMINATE)));
|
|
}
|
|
|
|
if (button->onCommand) {
|
|
button->onCommand(button->instance, button, button->command);
|
|
}
|
|
|
|
if (button->flags & ES_BUTTON_MENU_ITEM) {
|
|
EsAssert(button->window->windowStyle == ES_WINDOW_MENU);
|
|
EsMenuClose((EsMenu *) button->window);
|
|
} else {
|
|
button->MaybeRefreshStyle();
|
|
}
|
|
} else if (message->type == ES_MSG_GET_ACCESS_KEY_HINT_BOUNDS && (button->flags & (ES_BUTTON_RADIOBOX | ES_BUTTON_CHECKBOX))) {
|
|
EsRectangle bounds = element->GetWindowBounds();
|
|
int width = Width(*message->accessKeyHintBounds), height = Height(*message->accessKeyHintBounds);
|
|
int x = bounds.l - 3 * width / 4, y = (bounds.t + bounds.b) / 2 - height / 4;
|
|
*message->accessKeyHintBounds = ES_RECT_4(x - width / 2, x + width / 2, y - height / 4, y + 3 * height / 4);
|
|
} else if (message->type == ES_MSG_RADIO_GROUP_UPDATED && (button->flags & ES_BUTTON_RADIOBOX)) {
|
|
EsButtonSetCheck(button, ES_CHECK_UNCHECKED);
|
|
} else if (message->type == ES_MSG_FOCUSED_START) {
|
|
if (button->window->defaultEnterButton && (button->flags & ES_BUTTON_PUSH)) {
|
|
button->window->enterButton->customStyleState &= ~THEME_STATE_DEFAULT_BUTTON;
|
|
button->window->enterButton->MaybeRefreshStyle();
|
|
button->customStyleState |= THEME_STATE_DEFAULT_BUTTON;
|
|
button->window->enterButton = button;
|
|
}
|
|
} else if (message->type == ES_MSG_FOCUSED_END) {
|
|
if (button->window->enterButton == button) {
|
|
button->customStyleState &= ~THEME_STATE_DEFAULT_BUTTON;
|
|
button->window->enterButton = button->window->defaultEnterButton;
|
|
|
|
if (button->window->enterButton) {
|
|
button->window->enterButton->customStyleState |= THEME_STATE_DEFAULT_BUTTON;
|
|
button->window->enterButton->MaybeRefreshStyle();
|
|
}
|
|
}
|
|
} else if (message->type == ES_MSG_GET_INSPECTOR_INFORMATION) {
|
|
EsBufferFormat(message->getContent.buffer, "'%s'", button->labelBytes, button->label);
|
|
} else {
|
|
return 0;
|
|
}
|
|
|
|
return ES_HANDLED;
|
|
}
|
|
|
|
EsButton *EsButtonCreate(EsElement *parent, uint64_t flags, EsStyleID style, const char *label, ptrdiff_t labelBytes) {
|
|
EsButton *button = (EsButton *) EsHeapAllocate(sizeof(EsButton), true);
|
|
if (!button) return button;
|
|
|
|
if (!style) {
|
|
if (flags & ES_BUTTON_MENU_ITEM) {
|
|
if (flags & ES_MENU_ITEM_HEADER) {
|
|
style = ES_STYLE_MENU_ITEM_HEADER;
|
|
} else {
|
|
style = ES_STYLE_MENU_ITEM_NORMAL;
|
|
}
|
|
} else if (flags & ES_BUTTON_TOOLBAR) {
|
|
style = ES_STYLE_PUSH_BUTTON_TOOLBAR;
|
|
} else if (flags & ES_BUTTON_CHECKBOX) {
|
|
style = ES_STYLE_CHECKBOX_NORMAL;
|
|
} else if (flags & ES_BUTTON_RADIOBOX) {
|
|
style = ES_STYLE_CHECKBOX_RADIOBOX;
|
|
} else if (flags & ES_BUTTON_DEFAULT) {
|
|
style = ES_STYLE_PUSH_BUTTON_NORMAL;
|
|
} else {
|
|
style = ES_STYLE_PUSH_BUTTON_NORMAL;
|
|
}
|
|
|
|
style = UIGetDefaultStyleVariant(style, parent);
|
|
}
|
|
|
|
if (style == ES_STYLE_PUSH_BUTTON_NORMAL || style == ES_STYLE_PUSH_BUTTON_DANGEROUS) {
|
|
flags |= ES_BUTTON_PUSH;
|
|
} else if (style == ES_STYLE_PUSH_BUTTON_TOOLBAR
|
|
|| style == ES_STYLE_PUSH_BUTTON_TOOLBAR_BIG || style == ES_STYLE_PUSH_BUTTON_STATUS_BAR) {
|
|
flags |= ES_BUTTON_COMPACT | ES_BUTTON_NOT_FOCUSABLE;
|
|
} else if (style == ES_STYLE_CHECKBOX_NORMAL || style == ES_STYLE_CHECKBOX_RADIOBOX) {
|
|
flags |= ES_BUTTON_COMPACT;
|
|
}
|
|
|
|
if (~flags & ES_BUTTON_NOT_FOCUSABLE) {
|
|
flags |= ES_ELEMENT_FOCUSABLE;
|
|
}
|
|
|
|
button->Initialise(parent, flags, ProcessButtonMessage, style);
|
|
button->cName = "button";
|
|
|
|
if (flags & ES_BUTTON_DEFAULT) {
|
|
button->window->defaultEnterButton = button;
|
|
button->window->enterButton = button;
|
|
button->customStyleState |= THEME_STATE_DEFAULT_BUTTON;
|
|
}
|
|
|
|
if (flags & ES_BUTTON_CANCEL) {
|
|
button->window->escapeButton = button;
|
|
}
|
|
|
|
if (labelBytes == -1) labelBytes = EsCStringLength(label);
|
|
HeapDuplicate((void **) &button->label, &button->labelBytes, label, labelBytes);
|
|
|
|
if ((flags & ES_BUTTON_MENU_ITEM) && (flags & ES_MENU_ITEM_HEADER)) {
|
|
EsElementSetDisabled(button, true);
|
|
}
|
|
|
|
EsButtonSetCheck(button, (EsCheckState) (flags & 3), false);
|
|
button->MaybeRefreshStyle();
|
|
return button;
|
|
}
|
|
|
|
void EsButtonSetLabel(EsButton *button, const char *label, ptrdiff_t labelBytes) {
|
|
EsMessageMutexCheck();
|
|
if (labelBytes == -1) labelBytes = EsCStringLength(label);
|
|
HeapDuplicate((void **) &button->label, &button->labelBytes, label, labelBytes);
|
|
button->Repaint(true);
|
|
button->state &= ~UI_STATE_USE_MEASUREMENT_CACHE;
|
|
EsElementUpdateContentSize(button->parent);
|
|
}
|
|
|
|
void EsButtonSetIcon(EsButton *button, uint32_t iconID) {
|
|
EsMessageMutexCheck();
|
|
|
|
if (button->imageDisplay) {
|
|
EsElementDestroy(button->imageDisplay);
|
|
button->imageDisplay = nullptr;
|
|
}
|
|
|
|
button->iconID = iconID;
|
|
button->Repaint(true);
|
|
}
|
|
|
|
void EsButtonSetIconFromBits(EsButton *button, const uint32_t *bits, size_t width, size_t height, size_t stride) {
|
|
EsMessageMutexCheck();
|
|
|
|
if (!button->imageDisplay) {
|
|
button->imageDisplay = EsImageDisplayCreate(button);
|
|
}
|
|
|
|
if (button->imageDisplay) {
|
|
EsImageDisplayLoadBits(button->imageDisplay, bits, width, height, stride);
|
|
button->Repaint(true);
|
|
}
|
|
}
|
|
|
|
void EsButtonOnCommand(EsButton *button, EsCommandCallback onCommand) {
|
|
EsMessageMutexCheck();
|
|
button->onCommand = onCommand;
|
|
}
|
|
|
|
void EsButtonSetCheckBuddy(EsButton *button, EsElement *checkBuddy) {
|
|
EsMessageMutexCheck();
|
|
|
|
button->checkBuddy = checkBuddy;
|
|
EsElementSetDisabled(button->checkBuddy, !(button->customStyleState & (THEME_STATE_CHECKED | THEME_STATE_INDETERMINATE)));
|
|
}
|
|
|
|
EsElement *EsButtonGetCheckBuddy(EsButton *button) {
|
|
EsMessageMutexCheck();
|
|
|
|
return button->checkBuddy;
|
|
}
|
|
|
|
EsCheckState EsButtonGetCheck(EsButton *button) {
|
|
EsMessageMutexCheck();
|
|
|
|
if (button->customStyleState & THEME_STATE_CHECKED) return ES_CHECK_CHECKED;
|
|
if (button->customStyleState & THEME_STATE_INDETERMINATE) return ES_CHECK_INDETERMINATE;
|
|
return ES_CHECK_UNCHECKED;
|
|
}
|
|
|
|
void EsButtonSetCheck(EsButton *button, EsCheckState checkState, bool sendUpdatedMessage) {
|
|
if (checkState == EsButtonGetCheck(button)) {
|
|
return;
|
|
}
|
|
|
|
button->customStyleState &= ~(THEME_STATE_CHECKED | THEME_STATE_INDETERMINATE);
|
|
|
|
if (checkState == ES_CHECK_CHECKED) button->customStyleState |= THEME_STATE_CHECKED;
|
|
if (checkState == ES_CHECK_INDETERMINATE) button->customStyleState |= THEME_STATE_INDETERMINATE;
|
|
|
|
if (sendUpdatedMessage) {
|
|
EsMessage m = { ES_MSG_CHECK_UPDATED };
|
|
m.checkState = checkState;
|
|
EsMessageSend(button, &m);
|
|
|
|
if (button->onCommand) {
|
|
button->onCommand(button->instance, button, button->command);
|
|
}
|
|
}
|
|
|
|
if (button->checkBuddy) {
|
|
EsElementSetDisabled(button->checkBuddy, !(button->customStyleState & (THEME_STATE_CHECKED | THEME_STATE_INDETERMINATE)));
|
|
}
|
|
|
|
button->MaybeRefreshStyle();
|
|
}
|
|
|
|
void MenuItemGetKeyboardShortcutString(EsCommand *command, EsBuffer *buffer) {
|
|
if (!command) {
|
|
return;
|
|
}
|
|
|
|
const char *input = command->cKeyboardShortcut;
|
|
|
|
if (!input) {
|
|
return;
|
|
}
|
|
|
|
while (true) {
|
|
if (input[0] == 'C' && input[1] == 't' && input[2] == 'r' && input[3] == 'l' && input[4] == '+') {
|
|
EsBufferFormat(buffer, "%c", 0x2303);
|
|
input += 5;
|
|
} else if (input[0] == 'S' && input[1] == 'h' && input[2] == 'i' && input[3] == 'f' && input[4] == 't' && input[5] == '+') {
|
|
EsBufferFormat(buffer, "%c", 0x21E7);
|
|
input += 6;
|
|
} else if (input[0] == 'A' && input[1] == 'l' && input[2] == 't' && input[3] == '+') {
|
|
EsBufferFormat(buffer, "%c", 0x2325);
|
|
input += 4;
|
|
} else {
|
|
break;
|
|
}
|
|
}
|
|
|
|
while (*input != 0 && *input != '|') {
|
|
EsBufferWrite(buffer, input++, 1);
|
|
}
|
|
}
|
|
|
|
int ProcessMenuItemMessage(EsElement *element, EsMessage *message) {
|
|
MenuItem *button = (MenuItem *) element;
|
|
|
|
if (message->type == ES_MSG_PAINT) {
|
|
// Draw the label.
|
|
|
|
EsDrawContent(message->painter, element,
|
|
ES_RECT_2S(message->painter->width, message->painter->height),
|
|
button->label, button->labelBytes, 0, ES_FLAGS_DEFAULT);
|
|
|
|
// Draw the keyboard shortcut.
|
|
// TODO If activated by the keyboard, show access keys instead.
|
|
|
|
uint8_t _buffer[64];
|
|
EsBuffer buffer = { .out = _buffer, .bytes = sizeof(_buffer) };
|
|
MenuItemGetKeyboardShortcutString(button->command, &buffer);
|
|
|
|
EsDrawContent(message->painter, element,
|
|
ES_RECT_2S(message->painter->width, message->painter->height),
|
|
(const char *) _buffer, buffer.position, 0, ES_TEXT_H_RIGHT);
|
|
} else if (message->type == ES_MSG_GET_WIDTH) {
|
|
uint8_t _buffer[64];
|
|
EsBuffer buffer = { .out = _buffer, .bytes = sizeof(_buffer) };
|
|
MenuItemGetKeyboardShortcutString(button->command, &buffer);
|
|
|
|
EsTextStyle textStyle;
|
|
button->style->GetTextStyle(&textStyle);
|
|
|
|
int stringWidth = TextGetStringWidth(button, &textStyle, button->label, button->labelBytes);
|
|
int keyboardShortcutWidth = TextGetStringWidth(button, &textStyle, (const char *) _buffer, buffer.position);
|
|
int contentWidth = stringWidth + button->style->insets.l + button->style->insets.r
|
|
+ (keyboardShortcutWidth ? (keyboardShortcutWidth + button->style->gapMinor) : 0);
|
|
message->measure.width = MaximumInteger(GetConstantNumber("menuItemMinimumReportedWidth"), contentWidth);
|
|
} else if (message->type == ES_MSG_DESTROY) {
|
|
EsHeapFree(button->label);
|
|
|
|
if (button->command) {
|
|
Array<EsElement *> elements = { button->command->elements };
|
|
elements.FindAndDeleteSwap(button, true);
|
|
button->command->elements = elements.array;
|
|
}
|
|
} else if (message->type == ES_MSG_MOUSE_LEFT_DOWN) {
|
|
} else if (message->type == ES_MSG_MOUSE_LEFT_CLICK) {
|
|
if (~element->flags & ES_ELEMENT_DISABLED) {
|
|
EsMenuCallback callback = (EsMenuCallback) element->userData.p;
|
|
|
|
if (button->onCommand) {
|
|
button->onCommand(button->instance, button, button->command);
|
|
} else if (callback) {
|
|
callback((EsMenu *) element->window, button->menuItemContext);
|
|
}
|
|
|
|
EsAssert(button->window->windowStyle == ES_WINDOW_MENU);
|
|
EsMenuClose((EsMenu *) button->window);
|
|
}
|
|
} else if (message->type == ES_MSG_GET_INSPECTOR_INFORMATION) {
|
|
EsBufferFormat(message->getContent.buffer, "'%s'", button->labelBytes, button->label);
|
|
} else {
|
|
return 0;
|
|
}
|
|
|
|
return ES_HANDLED;
|
|
}
|
|
|
|
MenuItem *MenuItemCreate(EsMenu *menu, uint64_t flags, const char *label, ptrdiff_t labelBytes) {
|
|
MenuItem *button = (MenuItem *) EsHeapAllocate(sizeof(MenuItem), true);
|
|
if (!button) return nullptr;
|
|
EsStyleID style = (flags & ES_MENU_ITEM_HEADER) ? ES_STYLE_MENU_ITEM_HEADER : ES_STYLE_MENU_ITEM_NORMAL;
|
|
if (flags & ES_MENU_ITEM_HEADER) flags |= ES_ELEMENT_DISABLED;
|
|
button->Initialise(menu, flags | ES_CELL_H_FILL, ProcessMenuItemMessage, style);
|
|
button->cName = "menu item";
|
|
if (labelBytes == -1) labelBytes = EsCStringLength(label);
|
|
HeapDuplicate((void **) &button->label, &button->labelBytes, label, labelBytes);
|
|
return button;
|
|
}
|
|
|
|
void EsMenuAddItem(EsMenu *menu, uint64_t flags, const char *label, ptrdiff_t labelBytes, EsMenuCallback callback, EsGeneric context) {
|
|
MenuItem *button = MenuItemCreate(menu, flags, label, labelBytes);
|
|
if (!button) return;
|
|
EsButtonSetCheck(button, (EsCheckState) (flags & 3), false);
|
|
button->MaybeRefreshStyle();
|
|
button->userData = (void *) callback;
|
|
button->menuItemContext = context;
|
|
}
|
|
|
|
void EsMenuAddCommand(EsMenu *menu, uint64_t flags, const char *label, ptrdiff_t labelBytes, EsCommand *command) {
|
|
MenuItem *button = MenuItemCreate(menu, flags, label, labelBytes);
|
|
if (button) EsCommandAddButton(command, button);
|
|
}
|
|
|
|
// --------------------------------- Color wells and pickers.
|
|
|
|
struct EsColorWell : EsElement {
|
|
uint32_t color;
|
|
struct ColorPicker *picker;
|
|
bool indeterminate;
|
|
};
|
|
|
|
int ProcessColorWellMessage(EsElement *element, EsMessage *message);
|
|
|
|
struct ColorPicker {
|
|
uint32_t color;
|
|
float hue, saturation, value, opacity;
|
|
float dragStartHue, dragStartSaturation;
|
|
int dragComponent;
|
|
bool indeterminateBeforeEyedrop;
|
|
bool modified;
|
|
|
|
ColorPickerHost host;
|
|
EsPanel *panel;
|
|
EsElement *circle, *slider, *circlePoint, *sliderPoint, *opacitySlider, *opacitySliderPoint;
|
|
EsTextbox *textbox;
|
|
|
|
uint32_t GetColorForHost() {
|
|
return color | (host.hasOpacity ? ((uint32_t) (255.0f * opacity) << 24) : 0);
|
|
}
|
|
|
|
void Sync(EsElement *excluding) {
|
|
if (excluding != textbox && textbox) {
|
|
char string[16];
|
|
size_t length;
|
|
|
|
if (host.indeterminate && *host.indeterminate) {
|
|
string[0] = '#';
|
|
length = 1;
|
|
} else {
|
|
const char *hexChars = "0123456789ABCDEF";
|
|
|
|
if (host.hasOpacity) {
|
|
uint8_t alpha = (uint8_t) (opacity * 0xFF);
|
|
length = EsStringFormat(string, sizeof(string), "#%c%c%c%c%c%c%c%c",
|
|
hexChars[(alpha >> 4) & 0xF], hexChars[(alpha >> 0) & 0xF], hexChars[(color >> 20) & 0xF], hexChars[(color >> 16) & 0xF],
|
|
hexChars[(color >> 12) & 0xF], hexChars[(color >> 8) & 0xF], hexChars[(color >> 4) & 0xF], hexChars[(color >> 0) & 0xF]);
|
|
} else {
|
|
length = EsStringFormat(string, sizeof(string), "#%c%c%c%c%c%c",
|
|
hexChars[(color >> 20) & 0xF], hexChars[(color >> 16) & 0xF], hexChars[(color >> 12) & 0xF],
|
|
hexChars[(color >> 8) & 0xF], hexChars[(color >> 4) & 0xF], hexChars[(color >> 0) & 0xF]);
|
|
}
|
|
}
|
|
|
|
EsTextboxSelectAll(textbox);
|
|
EsTextboxInsert(textbox, string, length, false);
|
|
}
|
|
|
|
if (excluding != circle) circle->Repaint(true);
|
|
if (excluding != slider) slider->Repaint(true);
|
|
if (excluding != opacitySlider && opacitySlider) opacitySlider->Repaint(true);
|
|
|
|
if (excluding != circlePoint) {
|
|
if (host.indeterminate && *host.indeterminate) {
|
|
EsElementSetHidden(circlePoint, true);
|
|
} else {
|
|
EsElementSetHidden(circlePoint, false);
|
|
float x = saturation * EsCRTcosf((hue - 3) * 1.047197551) * 0.5f + 0.5f;
|
|
float y = saturation * EsCRTsinf((hue - 3) * 1.047197551) * 0.5f + 0.5f;
|
|
EsRectangle pointSize = EsElementGetPreferredSize(circlePoint), circleSize = EsElementGetPreferredSize(circle);
|
|
int x2 = x * circleSize.r - pointSize.r / 2, y2 = y * circleSize.b - pointSize.b / 2;
|
|
EsElementMove(circlePoint, x2, y2, pointSize.r, pointSize.b, true);
|
|
circlePoint->Repaint(true);
|
|
}
|
|
}
|
|
|
|
if (excluding != sliderPoint) {
|
|
if (host.indeterminate && *host.indeterminate) {
|
|
EsElementSetHidden(sliderPoint, true);
|
|
} else {
|
|
EsElementSetHidden(sliderPoint, false);
|
|
float x = 0.5f, y = 1.0f - EsCRTpowf(value, 1.333f);
|
|
EsRectangle pointSize = EsElementGetPreferredSize(sliderPoint), sliderSize = EsElementGetPreferredSize(slider);
|
|
int x2 = x * sliderSize.r - pointSize.r / 2, y2 = y * sliderSize.b - pointSize.b / 2;
|
|
EsElementMove(sliderPoint, x2, y2, pointSize.r, pointSize.b, true);
|
|
sliderPoint->Repaint(true);
|
|
}
|
|
}
|
|
|
|
if (excluding != opacitySliderPoint && opacitySliderPoint) {
|
|
if (host.indeterminate && *host.indeterminate) {
|
|
EsElementSetHidden(opacitySliderPoint, true);
|
|
} else {
|
|
EsElementSetHidden(opacitySliderPoint, false);
|
|
float x = 0.5f, y = 1.0f - opacity;
|
|
EsRectangle pointSize = EsElementGetPreferredSize(opacitySliderPoint), sliderSize = EsElementGetPreferredSize(opacitySlider);
|
|
int x2 = x * sliderSize.r - pointSize.r / 2, y2 = y * sliderSize.b - pointSize.b / 2;
|
|
EsElementMove(opacitySliderPoint, x2, y2, pointSize.r, pointSize.b, true);
|
|
opacitySliderPoint->Repaint(true);
|
|
}
|
|
}
|
|
|
|
if (excluding != host.well && host.well) {
|
|
if (host.well->messageClass == ProcessColorWellMessage) {
|
|
((EsColorWell *) host.well)->color = GetColorForHost();
|
|
host.well->Repaint(true);
|
|
}
|
|
|
|
EsMessage m = { ES_MSG_COLOR_CHANGED };
|
|
m.colorChanged.newColor = GetColorForHost();
|
|
m.colorChanged.pickerClosed = false;
|
|
EsMessageSend(host.well, &m);
|
|
|
|
modified = true;
|
|
}
|
|
}
|
|
|
|
void PositionOnCircleToColor(int _x, int _y) {
|
|
EsRectangle size = EsElementGetInsetSize(circle);
|
|
float x = (float) _x / (float) (size.r - 1) * 2.0f - 1.0f;
|
|
float y = (float) _y / (float) (size.b - 1) * 2.0f - 1.0f;
|
|
float newSaturation = EsCRTsqrtf(x * x + y * y), newHue = EsCRTatan2f(y, x) * 0.954929659f + 3;
|
|
if (!EsKeyboardIsAltHeld() && newSaturation < 0.1f) newSaturation = 0;
|
|
if (newSaturation > 1) newSaturation = 1;
|
|
if (newHue >= 6) newHue -= 6;
|
|
if (newHue < 0 || newHue >= 6) newHue = 0;
|
|
|
|
if (EsKeyboardIsShiftHeld()) {
|
|
float deltaHue = dragStartHue - newHue;
|
|
float deltaSaturation = EsCRTfabs(dragStartSaturation - newSaturation);
|
|
|
|
if (-3 < deltaHue && deltaHue < 3) deltaHue = EsCRTfabs(deltaHue);
|
|
if (deltaHue < -3) deltaHue += 6;
|
|
if (deltaHue > 3) deltaHue = -deltaHue + 6;
|
|
deltaHue /= 2;
|
|
|
|
if (deltaHue < deltaSaturation) {
|
|
newHue = dragStartHue;
|
|
} else {
|
|
newSaturation = dragStartSaturation;
|
|
}
|
|
}
|
|
|
|
uint32_t newColor = EsColorConvertToRGB(newHue, newSaturation, value);
|
|
hue = newHue, color = newColor, saturation = newSaturation;
|
|
if (host.indeterminate) *host.indeterminate = false;
|
|
Sync(circle);
|
|
}
|
|
|
|
void PositionOnSliderToColor(int _x, int _y) {
|
|
(void) _x;
|
|
EsRectangle size = EsElementGetInsetSize(slider);
|
|
float y = 1 - (float) _y / (float) (size.b - 1);
|
|
if (y < 0) y = 0;
|
|
y = EsCRTsqrtf(y) * EsCRTsqrtf(EsCRTsqrtf(y));
|
|
if (y > 1) y = 1;
|
|
if (y < 0) y = 0;
|
|
uint32_t newColor = EsColorConvertToRGB(hue, saturation, y);
|
|
color = newColor;
|
|
value = y;
|
|
if (host.indeterminate) *host.indeterminate = false;
|
|
Sync(slider);
|
|
}
|
|
|
|
void PositionOnOpacitySliderToColor(int _x, int _y) {
|
|
(void) _x;
|
|
EsRectangle size = EsElementGetInsetSize(opacitySlider);
|
|
float y = 1 - (float) _y / (float) (size.b - 1);
|
|
if (y > 1) y = 1;
|
|
if (y < 0) y = 0;
|
|
opacity = y;
|
|
if (host.indeterminate) *host.indeterminate = false;
|
|
Sync(opacitySlider);
|
|
}
|
|
};
|
|
|
|
struct StyledBox {
|
|
EsRectangle bounds;
|
|
uint32_t backgroundColor, backgroundColor2, borderColor;
|
|
uint32_t cornerRadius, borderSize;
|
|
EsFragmentShaderCallback fragmentShader;
|
|
};
|
|
|
|
void DrawStyledBox(EsPainter *painter, StyledBox box) {
|
|
ThemeLayer layer = {};
|
|
ThemeLayerBox layerBox = {};
|
|
EsBuffer data = {};
|
|
|
|
layerBox.borders = { (int8_t) box.borderSize, (int8_t) box.borderSize, (int8_t) box.borderSize, (int8_t) box.borderSize };
|
|
layerBox.corners = { (int8_t) box.cornerRadius, (int8_t) box.cornerRadius, (int8_t) box.cornerRadius, (int8_t) box.cornerRadius };
|
|
layerBox.mainPaintType = THEME_PAINT_SOLID;
|
|
layerBox.borderPaintType = THEME_PAINT_SOLID;
|
|
|
|
uint8_t info[sizeof(ThemeLayerBox) + sizeof(ThemePaintCustom) + sizeof(ThemePaintSolid) * 2] = {};
|
|
|
|
if (box.fragmentShader) {
|
|
ThemeLayerBox *infoBox = (ThemeLayerBox *) info;
|
|
ThemePaintCustom *infoMain = (ThemePaintCustom *) (infoBox + 1);
|
|
ThemePaintSolid *infoBorder = (ThemePaintSolid *) (infoMain + 1);
|
|
|
|
*infoBox = layerBox;
|
|
infoBox->mainPaintType = THEME_PAINT_CUSTOM;
|
|
infoMain->callback = box.fragmentShader;
|
|
infoBorder->color = box.borderColor;
|
|
|
|
data.in = (const uint8_t *) &info;
|
|
data.bytes = sizeof(info);
|
|
data.context = &box;
|
|
} else {
|
|
ThemeLayerBox *infoBox = (ThemeLayerBox *) info;
|
|
ThemePaintSolid *infoMain = (ThemePaintSolid *) (infoBox + 1);
|
|
ThemePaintSolid *infoBorder = (ThemePaintSolid *) (infoMain + 1);
|
|
|
|
*infoBox = layerBox;
|
|
infoMain->color = box.backgroundColor;
|
|
infoBorder->color = box.borderColor;
|
|
|
|
data.in = (const uint8_t *) &info;
|
|
data.bytes = sizeof(info);
|
|
}
|
|
|
|
ThemeDrawBox(painter, box.bounds, &data, 1, &layer, {}, THEME_CHILD_TYPE_ONLY);
|
|
}
|
|
|
|
int ProcessColorChosenPointMessage(EsElement *element, EsMessage *message) {
|
|
ColorPicker *picker = (ColorPicker *) element->userData.p;
|
|
|
|
if (message->type == ES_MSG_PAINT) {
|
|
EsRectangle bounds = EsPainterBoundsInset(message->painter);
|
|
StyledBox box = {};
|
|
box.bounds = bounds;
|
|
box.borderColor = 0xFFFFFFFF;
|
|
box.backgroundColor = picker->color | 0xFF000000;
|
|
box.backgroundColor2 = picker->color | ((uint32_t) (255.0f * picker->opacity) << 24);
|
|
box.borderSize = 2;
|
|
box.cornerRadius = Width(box.bounds) / 2;
|
|
|
|
if (picker->opacity < 1 && picker->host.hasOpacity) {
|
|
box.fragmentShader = [] (int x, int y, StyledBox *box) -> uint32_t {
|
|
// TODO Move the alpha background as the chosen point moves.
|
|
return EsColorBlend(((((x - 2) >> 3) ^ ((y + 5) >> 3)) & 1) ? 0xFFFFFFFF : 0xFFC0C0C0,
|
|
box->backgroundColor2, false);
|
|
};
|
|
}
|
|
|
|
DrawStyledBox(message->painter, box);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void ColorPickerCreate(EsElement *parent, ColorPickerHost host, uint32_t initialColor, bool showTextbox) {
|
|
ColorPicker *picker = (ColorPicker *) EsHeapAllocate(sizeof(ColorPicker), true);
|
|
if (!picker) return;
|
|
picker->host = host;
|
|
picker->color = initialColor & 0xFFFFFF;
|
|
picker->opacity = (float) (initialColor >> 24) / 255.0f;
|
|
if (host.well && host.well->messageClass == ProcessColorWellMessage) ((EsColorWell *) host.well)->picker = picker;
|
|
EsColorConvertToHSV(picker->color, &picker->hue, &picker->saturation, &picker->value);
|
|
|
|
picker->panel = EsPanelCreate(parent, ES_PANEL_HORIZONTAL | ES_PANEL_TABLE, ES_STYLE_COLOR_PICKER_MAIN_PANEL);
|
|
|
|
picker->panel->userData = picker;
|
|
|
|
picker->panel->messageUser = [] (EsElement *element, EsMessage *message) {
|
|
ColorPicker *picker = (ColorPicker *) element->userData.p;
|
|
|
|
if (message->type == ES_MSG_DESTROY) {
|
|
if (picker->host.well && picker->modified) {
|
|
EsMessage m = { ES_MSG_COLOR_CHANGED };
|
|
m.colorChanged.newColor = picker->GetColorForHost();
|
|
m.colorChanged.pickerClosed = true;
|
|
EsMessageSend(picker->host.well, &m);
|
|
}
|
|
|
|
if (picker->host.well && picker->host.well->messageClass == ProcessColorWellMessage) {
|
|
((EsColorWell *) picker->host.well)->picker = nullptr;
|
|
}
|
|
|
|
EsHeapFree(picker);
|
|
}
|
|
|
|
return 0;
|
|
};
|
|
|
|
bool hasOpacity = picker->host.hasOpacity;
|
|
|
|
EsPanelSetBands(picker->panel, hasOpacity ? 3 : 2, showTextbox ? 2 : 1);
|
|
|
|
picker->circle = EsCustomElementCreate(picker->panel, ES_ELEMENT_FOCUSABLE | ES_ELEMENT_NOT_TAB_TRAVERSABLE, ES_STYLE_COLOR_CIRCLE);
|
|
|
|
picker->circle->cName = "hue-saturation wheel";
|
|
picker->circle->userData = picker;
|
|
|
|
picker->circle->messageUser = [] (EsElement *element, EsMessage *message) {
|
|
ColorPicker *picker = (ColorPicker *) element->userData.p;
|
|
|
|
if (message->type == ES_MSG_PAINT) {
|
|
// EsPerformanceTimerPush();
|
|
|
|
EsPainter *painter = message->painter;
|
|
EsRectangle bounds = EsPainterBoundsInset(painter);
|
|
EsRectangle clip = painter->clip;
|
|
EsRectangleClip(clip, bounds, &clip);
|
|
uint32_t stride = painter->target->stride;
|
|
uint32_t *bitmap = (uint32_t *) painter->target->bits;
|
|
float epsilon = 1.0f / (bounds.b - bounds.t - 1);
|
|
|
|
for (int j = clip.t; j < clip.b; j++) {
|
|
for (int i = clip.l; i < clip.r; i++) {
|
|
float x = (float) (i - bounds.l) / (float) (bounds.r - bounds.l - 1) * 2.0f - 1.0f;
|
|
float y = (float) (j - bounds.t) / (float) (bounds.b - bounds.t - 1) * 2.0f - 1.0f;
|
|
float radius = EsCRTsqrtf(x * x + y * y), hue = EsCRTatan2f(y, x) * 0.954929659f + 3;
|
|
if (hue >= 6) hue -= 6;
|
|
|
|
if (radius > 1.0f + epsilon) {
|
|
// Outside the circle.
|
|
} else if (radius > 1.0f - epsilon) {
|
|
// On the edge.
|
|
uint32_t over = EsColorConvertToRGB(hue, 1, picker->value);
|
|
// float opacity = (1.0f - ((radius - (1.0f - epsilon)) / epsilon * 0.5f));
|
|
float opacity = 0.5f - (radius - 1.0f) / epsilon * 0.5f;
|
|
uint32_t alpha = (((uint32_t) (255.0f * opacity)) & 0xFF) << 24;
|
|
uint32_t *under = &bitmap[i + j * (stride >> 2)];
|
|
*under = EsColorBlend(*under, over | alpha, true);
|
|
} else {
|
|
// Inside the circle.
|
|
uint32_t over = EsColorConvertToRGB(hue, radius, picker->value);
|
|
bitmap[i + j * (stride >> 2)] = over | 0xFF000000;
|
|
}
|
|
}
|
|
}
|
|
|
|
// EsPrint("Rendered color circle in %*Fms.\n", 3, 1000 * EsPerformanceTimerPop());
|
|
} else if (message->type == ES_MSG_HIT_TEST) {
|
|
EsRectangle size = EsElementGetInsetSize(element);
|
|
float x = (float) message->hitTest.x / (float) (size.r - 1) * 2.0f - 1.0f;
|
|
float y = (float) message->hitTest.y / (float) (size.b - 1) * 2.0f - 1.0f;
|
|
message->hitTest.inside = x * x + y * y <= 1;
|
|
} else if (message->type == ES_MSG_MOUSE_LEFT_DRAG) {
|
|
picker->PositionOnCircleToColor(message->mouseDragged.newPositionX, message->mouseDragged.newPositionY);
|
|
} else if (message->type == ES_MSG_KEY_DOWN) {
|
|
if ((message->keyboard.scancode == ES_SCANCODE_LEFT_SHIFT || message->keyboard.scancode == ES_SCANCODE_RIGHT_SHIFT) && !message->keyboard.repeat) {
|
|
picker->dragStartHue = picker->hue;
|
|
picker->dragStartSaturation = picker->saturation;
|
|
} else {
|
|
return 0;
|
|
}
|
|
} else if (message->type == ES_MSG_MOUSE_LEFT_DOWN) {
|
|
picker->PositionOnCircleToColor(message->mouseDown.positionX, message->mouseDown.positionY);
|
|
picker->dragStartHue = picker->hue;
|
|
picker->dragStartSaturation = picker->saturation;
|
|
} else {
|
|
return 0;
|
|
}
|
|
|
|
return ES_HANDLED;
|
|
};
|
|
|
|
picker->circlePoint = EsCustomElementCreate(picker->circle, ES_ELEMENT_NO_HOVER, ES_STYLE_COLOR_CHOSEN_POINT);
|
|
picker->circlePoint->messageUser = ProcessColorChosenPointMessage;
|
|
picker->circlePoint->cName = "selected hue-saturation";
|
|
picker->circlePoint->userData = picker;
|
|
|
|
picker->slider = EsCustomElementCreate(picker->panel, ES_ELEMENT_FOCUSABLE | ES_ELEMENT_NOT_TAB_TRAVERSABLE, ES_STYLE_COLOR_SLIDER);
|
|
picker->slider->cName = "value slider";
|
|
picker->slider->userData = picker;
|
|
|
|
picker->slider->messageUser = [] (EsElement *element, EsMessage *message) {
|
|
ColorPicker *picker = (ColorPicker *) element->userData.p;
|
|
|
|
if (message->type == ES_MSG_PAINT) {
|
|
// EsPerformanceTimerPush();
|
|
|
|
EsPainter *painter = message->painter;
|
|
EsRectangle bounds = EsPainterBoundsInset(painter);
|
|
EsRectangle clip = painter->clip;
|
|
EsRectangleClip(clip, bounds, &clip);
|
|
uint32_t stride = painter->target->stride;
|
|
uint32_t *bitmap = (uint32_t *) painter->target->bits;
|
|
|
|
float valueIncrement = -1.0f / (bounds.b - bounds.t - 1), value = 1.0f;
|
|
|
|
for (int j = clip.t; j < clip.b; j++, value += valueIncrement) {
|
|
// float valueSqrt = EsCRTsqrtf(value);
|
|
// uint32_t color = EsColorConvertToRGB(picker->hue, picker->saturation, valueSqrt * EsCRTsqrtf(valueSqrt));
|
|
|
|
for (int i = clip.l; i < clip.r; i++) {
|
|
float i2 = (float) (i - ((bounds.l + bounds.r) >> 1)) / (float) (bounds.r - bounds.l);
|
|
uint32_t color = EsColorConvertToRGB(picker->hue, picker->saturation, EsCRTpowf(value, 0.75f + i2 * i2 * 0.3f));
|
|
bitmap[i + j * (stride >> 2)] = 0xFF000000 | color;
|
|
}
|
|
}
|
|
|
|
// EsPrint("Rendered color slider in %*Fms.\n", 3, 1000 * EsPerformanceTimerPop());
|
|
} else if (message->type == ES_MSG_MOUSE_LEFT_DRAG) {
|
|
picker->PositionOnSliderToColor(message->mouseDragged.newPositionX, message->mouseDragged.newPositionY);
|
|
} else if (message->type == ES_MSG_MOUSE_LEFT_DOWN) {
|
|
picker->PositionOnSliderToColor(message->mouseDown.positionX, message->mouseDown.positionY);
|
|
} else {
|
|
return 0;
|
|
}
|
|
|
|
return ES_HANDLED;
|
|
};
|
|
|
|
picker->sliderPoint = EsCustomElementCreate(picker->slider, ES_ELEMENT_NO_HOVER, ES_STYLE_COLOR_CHOSEN_POINT);
|
|
picker->sliderPoint->messageUser = ProcessColorChosenPointMessage;
|
|
picker->sliderPoint->cName = "selected value";
|
|
picker->sliderPoint->userData = picker;
|
|
|
|
if (hasOpacity) {
|
|
picker->opacitySlider = EsCustomElementCreate(picker->panel, ES_ELEMENT_FOCUSABLE | ES_ELEMENT_NOT_TAB_TRAVERSABLE, ES_STYLE_COLOR_SLIDER);
|
|
picker->opacitySlider->cName = "opacity slider";
|
|
picker->opacitySlider->userData = picker;
|
|
|
|
picker->opacitySlider->messageUser = [] (EsElement *element, EsMessage *message) {
|
|
ColorPicker *picker = (ColorPicker *) element->userData.p;
|
|
|
|
if (message->type == ES_MSG_PAINT) {
|
|
// EsPerformanceTimerPush();
|
|
|
|
EsPainter *painter = message->painter;
|
|
EsRectangle bounds = EsPainterBoundsInset(painter);
|
|
EsRectangle clip = painter->clip;
|
|
EsRectangleClip(clip, bounds, &clip);
|
|
uint32_t stride = painter->target->stride;
|
|
uint32_t *bitmap = (uint32_t *) painter->target->bits;
|
|
|
|
float opacityIncrement = -1.0f / (bounds.b - bounds.t - 1), opacity = 1.0f;
|
|
|
|
for (int j = clip.t; j < clip.b; j++, opacity += opacityIncrement) {
|
|
uint32_t alpha = (uint32_t) (opacity * 255.0f) << 24;
|
|
|
|
for (int i = clip.l; i < clip.r; i++) {
|
|
bitmap[i + j * (stride >> 2)]
|
|
= EsColorBlend(((((i - bounds.l + 1) >> 3) ^ ((j - bounds.t + 2) >> 3)) & 1) ? 0xFFFFFFFF : 0xFFC0C0C0,
|
|
alpha | (picker->color & 0xFFFFFF), false);
|
|
}
|
|
}
|
|
|
|
// EsPrint("Rendered opacity slider in %*Fms.\n", 3, 1000 * EsPerformanceTimerPop());
|
|
} else if (message->type == ES_MSG_MOUSE_LEFT_DRAG) {
|
|
picker->PositionOnOpacitySliderToColor(message->mouseDragged.newPositionX, message->mouseDragged.newPositionY);
|
|
} else if (message->type == ES_MSG_MOUSE_LEFT_DOWN) {
|
|
picker->PositionOnOpacitySliderToColor(message->mouseDown.positionX, message->mouseDown.positionY);
|
|
} else {
|
|
return 0;
|
|
}
|
|
|
|
return ES_HANDLED;
|
|
};
|
|
|
|
picker->opacitySliderPoint = EsCustomElementCreate(picker->opacitySlider, ES_ELEMENT_NO_HOVER, ES_STYLE_COLOR_CHOSEN_POINT);
|
|
picker->opacitySliderPoint->messageUser = ProcessColorChosenPointMessage;
|
|
picker->opacitySliderPoint->cName = "selected opacity";
|
|
picker->opacitySliderPoint->userData = picker;
|
|
}
|
|
|
|
if (showTextbox) {
|
|
picker->textbox = EsTextboxCreate(picker->panel, ES_TEXTBOX_EDIT_BASED | ES_CELL_EXPAND | ES_TEXTBOX_NO_SMART_CONTEXT_MENUS, ES_STYLE_COLOR_HEX_TEXTBOX);
|
|
picker->textbox->userData = picker;
|
|
|
|
picker->textbox->messageUser = [] (EsElement *element, EsMessage *message) {
|
|
ColorPicker *picker = (ColorPicker *) element->userData.p;
|
|
|
|
if (message->type == ES_MSG_TEXTBOX_UPDATED) {
|
|
size_t bytes;
|
|
char *string = EsTextboxGetContents(picker->textbox, &bytes);
|
|
uint32_t color = EsColorParse(string, bytes);
|
|
picker->opacity = (float) (color >> 24) / 255.0f;
|
|
color &= 0xFFFFFF;
|
|
EsColorConvertToHSV(color, &picker->hue, &picker->saturation, &picker->value);
|
|
picker->color = color;
|
|
if (picker->host.indeterminate) *picker->host.indeterminate = false;
|
|
picker->Sync(picker->textbox);
|
|
EsHeapFree(string);
|
|
} else if (message->type == ES_MSG_TEXTBOX_EDIT_START) {
|
|
EsTextboxSetSelection((EsTextbox *) element, 0, 1, 0, -1);
|
|
} else if (message->type == ES_MSG_TEXTBOX_EDIT_END) {
|
|
picker->Sync(nullptr);
|
|
} else if (message->type == ES_MSG_TEXTBOX_NUMBER_DRAG_DELTA) {
|
|
int componentCount = picker->host.hasOpacity ? 4 : 3;
|
|
int componentIndex = (message->numberDragDelta.hoverCharacter - 1) / 2;
|
|
if (componentIndex < 0) componentIndex = 0;
|
|
if (componentIndex >= componentCount) componentIndex = componentCount - 1;
|
|
componentIndex = componentCount - componentIndex - 1;
|
|
picker->dragComponent = componentIndex;
|
|
|
|
if (componentIndex == 3) {
|
|
picker->opacity += message->numberDragDelta.delta / 255.0f;
|
|
if (picker->opacity < 0) picker->opacity = 0;
|
|
if (picker->opacity > 1) picker->opacity = 1;
|
|
} else {
|
|
int32_t componentValue = (picker->color >> (componentIndex << 3)) & 0xFF;
|
|
componentValue += message->numberDragDelta.delta;
|
|
if (componentValue < 0) componentValue = 0;
|
|
if (componentValue > 255) componentValue = 255;
|
|
picker->color &= ~(0xFF << (componentIndex << 3));
|
|
picker->color |= (uint32_t) componentValue << (componentIndex << 3);
|
|
EsColorConvertToHSV(picker->color, &picker->hue, &picker->saturation, &picker->value);
|
|
}
|
|
|
|
picker->Sync(nullptr);
|
|
} else {
|
|
return 0;
|
|
}
|
|
|
|
return ES_HANDLED;
|
|
};
|
|
|
|
EsTextboxUseNumberOverlay(picker->textbox, false);
|
|
|
|
EsButton *eyedropperButton = EsButtonCreate(picker->panel, ES_CELL_EXPAND, 0);
|
|
eyedropperButton->userData = picker;
|
|
|
|
eyedropperButton->messageUser = [] (EsElement *element, EsMessage *message) {
|
|
ColorPicker *picker = (ColorPicker *) element->userData.p;
|
|
|
|
if (message->type == ES_MSG_MOUSE_LEFT_CLICK) {
|
|
picker->indeterminateBeforeEyedrop = picker->host.indeterminate && *picker->host.indeterminate;
|
|
EsSyscall(ES_SYSCALL_EYEDROP_START, (uintptr_t) element, picker->circle->window->handle, picker->color, 0);
|
|
} else if (message->type == ES_MSG_EYEDROP_REPORT) {
|
|
if (message->eyedrop.cancelled && picker->indeterminateBeforeEyedrop) {
|
|
if (picker->host.well && picker->host.well->messageClass == ProcessColorWellMessage) {
|
|
EsColorWellSetIndeterminate((EsColorWell *) picker->host.well);
|
|
}
|
|
} else {
|
|
picker->color = message->eyedrop.color;
|
|
EsColorConvertToHSV(picker->color, &picker->hue, &picker->saturation, &picker->value);
|
|
if (picker->host.indeterminate) *picker->host.indeterminate = false;
|
|
picker->Sync(nullptr);
|
|
}
|
|
} else {
|
|
return 0;
|
|
}
|
|
|
|
return ES_HANDLED;
|
|
};
|
|
|
|
EsButtonSetIcon(eyedropperButton, ES_ICON_COLOR_SELECT_SYMBOLIC);
|
|
|
|
if (hasOpacity) {
|
|
EsElementSetCellRange(eyedropperButton, 1, 1, 2, 1);
|
|
}
|
|
}
|
|
|
|
picker->Sync(picker->host.well);
|
|
if (picker->textbox) EsElementFocus(picker->textbox);
|
|
}
|
|
|
|
uint32_t EsColorWellGetRGB(EsColorWell *well) {
|
|
EsMessageMutexCheck();
|
|
|
|
return well->color & ((well->flags & ES_COLOR_WELL_HAS_OPACITY) ? 0xFFFFFFFF : 0x00FFFFFF);
|
|
}
|
|
|
|
void EsColorWellSetRGB(EsColorWell *well, uint32_t color, bool sendChangedMessage) {
|
|
EsMessageMutexCheck();
|
|
|
|
well->color = color;
|
|
well->indeterminate = false;
|
|
well->Repaint(true);
|
|
|
|
if (sendChangedMessage) {
|
|
EsMessage m = { ES_MSG_COLOR_CHANGED };
|
|
m.colorChanged.newColor = color;
|
|
m.colorChanged.pickerClosed = true;
|
|
EsMessageSend(well, &m);
|
|
}
|
|
|
|
if (well->picker) {
|
|
well->picker->color = color & 0xFFFFFF;
|
|
well->picker->opacity = (color >> 24) / 255.0f;
|
|
EsColorConvertToHSV(well->picker->color, &well->picker->hue, &well->picker->saturation, &well->picker->value);
|
|
well->picker->Sync(well);
|
|
}
|
|
}
|
|
|
|
void EsColorWellSetIndeterminate(EsColorWell *well) {
|
|
EsMessageMutexCheck();
|
|
|
|
well->color = 0xFFFFFFFF;
|
|
well->indeterminate = true;
|
|
well->Repaint(true);
|
|
|
|
if (well->picker) {
|
|
well->picker->color = 0xFFFFFF;
|
|
well->picker->opacity = 1.0f;
|
|
EsColorConvertToHSV(well->picker->color, &well->picker->hue, &well->picker->saturation, &well->picker->value);
|
|
well->picker->Sync(well);
|
|
}
|
|
}
|
|
|
|
int ProcessColorWellMessage(EsElement *element, EsMessage *message) {
|
|
EsColorWell *well = (EsColorWell *) element;
|
|
|
|
if (message->type == ES_MSG_PAINT) {
|
|
EsRectangle bounds = EsPainterBoundsInset(message->painter);
|
|
StyledBox box = {};
|
|
box.bounds = bounds;
|
|
box.borderSize = 1;
|
|
|
|
if (well->indeterminate) {
|
|
box.backgroundColor = 0;
|
|
box.borderColor = 0x40000000;
|
|
} else {
|
|
box.backgroundColor = well->color;
|
|
if (~well->flags & ES_COLOR_WELL_HAS_OPACITY) box.backgroundColor |= 0xFF000000;
|
|
box.borderColor = EsColorBlend(well->color | 0xFF000000, 0x40000000, false);
|
|
|
|
if ((well->flags & ES_COLOR_WELL_HAS_OPACITY) && ((well->color & 0xFF000000) != 0xFF000000)) {
|
|
box.fragmentShader = [] (int x, int y, StyledBox *box) -> uint32_t {
|
|
return EsColorBlend(((((x - box->bounds.l - 4) >> 3) ^ ((y - box->bounds.t + 2) >> 3)) & 1)
|
|
? 0xFFFFFFFF : 0xFFC0C0C0, box->backgroundColor, false);
|
|
};
|
|
}
|
|
}
|
|
|
|
DrawStyledBox(message->painter, box);
|
|
} else if (message->type == ES_MSG_MOUSE_LEFT_CLICK) {
|
|
EsMenu *menu = EsMenuCreate(well, ES_FLAGS_DEFAULT);
|
|
if (!menu) return ES_HANDLED;
|
|
ColorPickerHost host = { well, &well->indeterminate, (well->flags & ES_COLOR_WELL_HAS_OPACITY) ? true : false };
|
|
ColorPickerCreate((EsElement *) menu, host, well->color, true);
|
|
EsMenuShow(menu);
|
|
} else {
|
|
return 0;
|
|
}
|
|
|
|
return ES_HANDLED;
|
|
}
|
|
|
|
EsColorWell *EsColorWellCreate(EsElement *parent, uint64_t flags, uint32_t initialColor) {
|
|
EsColorWell *well = (EsColorWell *) EsHeapAllocate(sizeof(EsColorWell), true);
|
|
if (!well) return nullptr;
|
|
well->color = initialColor;
|
|
well->Initialise(parent, flags | ES_ELEMENT_FOCUSABLE, ProcessColorWellMessage, ES_STYLE_PUSH_BUTTON_NORMAL_COLOR_WELL);
|
|
well->cName = "color well";
|
|
return well;
|
|
}
|
|
|
|
// --------------------------------- Splitters.
|
|
|
|
// TODO With dockable UI, show split bars at the start and end of the splitter as drop targets.
|
|
// The root splitter will also need two split bars at the start and end of the other axis.
|
|
// Split bars should also be enlarged when actings as drop targets.
|
|
// When dropping on an existing non-splitter panel, you can either form a tab group,
|
|
// or create a new split on the other axis, on one of the two sides.
|
|
|
|
struct EsSplitter : EsElement {
|
|
bool horizontal;
|
|
bool addingSplitBar;
|
|
int previousSize;
|
|
float previousScale;
|
|
Array<int64_t> resizeStartSizes;
|
|
bool calculatedInitialSize;
|
|
};
|
|
|
|
struct SplitBar : EsElement {
|
|
int position, dragStartPosition;
|
|
|
|
void Move(int newPosition, bool fromKeyboard) {
|
|
EsSplitter *splitter = (EsSplitter *) parent;
|
|
EsElement *panelBefore = nullptr, *panelAfter = nullptr;
|
|
int barBefore = 0, barAfter;
|
|
if (splitter->horizontal) barAfter = EsRectangleAddBorder(splitter->GetBounds(), splitter->style->borders).r - style->preferredWidth;
|
|
else barAfter = EsRectangleAddBorder(splitter->GetBounds(), splitter->style->borders).b - style->preferredHeight;
|
|
int preferredSize = splitter->horizontal ? style->preferredWidth : style->preferredHeight;
|
|
splitter->resizeStartSizes.Free();
|
|
|
|
for (uintptr_t i = 0; i < splitter->GetChildCount(); i++) {
|
|
if (splitter->GetChild(i) == this) {
|
|
EsAssert(i & 1); // Expected split bars between each EsSplitter child.
|
|
panelBefore = splitter->GetChild(i - 1);
|
|
panelAfter = splitter->GetChild(i + 1);
|
|
|
|
if (i != 1) {
|
|
barBefore = ((SplitBar *) splitter->GetChild(i - 2))->position + preferredSize;
|
|
}
|
|
|
|
if (i != splitter->GetChildCount() - 2) {
|
|
barAfter = ((SplitBar *) splitter->GetChild(i + 2))->position - preferredSize;
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
|
|
EsAssert(panelBefore && panelAfter); // Could not find split bar in parent.
|
|
|
|
barBefore -= splitter->horizontal ? style->borders.l : style->borders.t;
|
|
barAfter += splitter->horizontal ? style->borders.r : style->borders.b;
|
|
|
|
int minimumPosition, maximumPosition, minimumPosition1, maximumPosition1, minimumPosition2, maximumPosition2;
|
|
|
|
if (splitter->horizontal) {
|
|
minimumPosition1 = barBefore + panelBefore->style->metrics->minimumWidth;
|
|
maximumPosition1 = barAfter - panelAfter ->style->metrics->minimumWidth;
|
|
minimumPosition2 = barAfter - panelAfter ->style->metrics->maximumWidth;
|
|
maximumPosition2 = barBefore + panelBefore->style->metrics->maximumWidth;
|
|
if (!panelAfter ->style->metrics->maximumWidth) minimumPosition2 = INT_MIN;
|
|
if (!panelBefore->style->metrics->maximumWidth) maximumPosition2 = INT_MAX;
|
|
} else {
|
|
minimumPosition1 = barBefore + panelBefore->style->metrics->minimumHeight;
|
|
maximumPosition1 = barAfter - panelAfter ->style->metrics->minimumHeight;
|
|
minimumPosition2 = barAfter - panelAfter ->style->metrics->maximumHeight;
|
|
maximumPosition2 = barBefore + panelBefore->style->metrics->maximumHeight;
|
|
if (!panelAfter ->style->metrics->maximumHeight) minimumPosition2 = INT_MIN;
|
|
if (!panelBefore->style->metrics->maximumHeight) maximumPosition2 = INT_MAX;
|
|
}
|
|
|
|
minimumPosition = minimumPosition1 > minimumPosition2 ? minimumPosition1 : minimumPosition2;
|
|
maximumPosition = maximumPosition1 < maximumPosition2 ? maximumPosition1 : maximumPosition2;
|
|
|
|
if (minimumPosition < maximumPosition) {
|
|
int oldPosition = position;
|
|
|
|
if (newPosition < minimumPosition) {
|
|
if (newPosition > minimumPosition2
|
|
&& (fromKeyboard || newPosition < (barBefore + minimumPosition1) / 2)
|
|
&& (!fromKeyboard || newPosition < position)
|
|
&& panelBefore->flags & ES_CELL_COLLAPSABLE) {
|
|
position = barBefore > minimumPosition2 ? barBefore : minimumPosition2;
|
|
} else {
|
|
position = minimumPosition;
|
|
}
|
|
} else if (newPosition > maximumPosition) {
|
|
if (newPosition < maximumPosition2
|
|
&& (fromKeyboard || newPosition > (barAfter + maximumPosition1) / 2)
|
|
&& (!fromKeyboard || newPosition > position)
|
|
&& panelAfter->flags & ES_CELL_COLLAPSABLE) {
|
|
position = barAfter < maximumPosition2 ? barAfter : maximumPosition2;
|
|
} else {
|
|
position = maximumPosition;
|
|
}
|
|
} else {
|
|
position = newPosition;
|
|
}
|
|
|
|
if (oldPosition != position) {
|
|
EsElementRelayout(splitter);
|
|
}
|
|
}
|
|
}
|
|
};
|
|
|
|
int ProcessSplitBarMessage(EsElement *element, EsMessage *message) {
|
|
SplitBar *bar = (SplitBar *) element;
|
|
EsSplitter *splitter = (EsSplitter *) bar->parent;
|
|
|
|
if (message->type == ES_MSG_MOUSE_LEFT_DOWN) {
|
|
bar->dragStartPosition = bar->position;
|
|
|
|
if (!bar->window->focused || bar->window->focused->messageClass != ProcessSplitBarMessage) {
|
|
// Don't take focus.
|
|
return ES_REJECTED;
|
|
}
|
|
} else if (message->type == ES_MSG_MOUSE_LEFT_DRAG) {
|
|
if (splitter->horizontal) {
|
|
bar->Move(message->mouseDragged.newPositionX - message->mouseDragged.originalPositionX + bar->dragStartPosition, false);
|
|
} else {
|
|
bar->Move(message->mouseDragged.newPositionY - message->mouseDragged.originalPositionY + bar->dragStartPosition, false);
|
|
}
|
|
} else if (message->type == ES_MSG_KEY_TYPED) {
|
|
if (message->keyboard.scancode == (splitter->horizontal ? ES_SCANCODE_LEFT_ARROW : ES_SCANCODE_UP_ARROW)) {
|
|
bar->Move(bar->position - GetConstantNumber("splitBarKeyboardMovementAmount"), true);
|
|
} else if (message->keyboard.scancode == (splitter->horizontal ? ES_SCANCODE_RIGHT_ARROW : ES_SCANCODE_DOWN_ARROW)) {
|
|
bar->Move(bar->position + GetConstantNumber("splitBarKeyboardMovementAmount"), true);
|
|
} else {
|
|
return 0;
|
|
}
|
|
} else if (message->type == ES_MSG_GET_ACCESS_KEY_HINT_BOUNDS) {
|
|
AccessKeysCenterHint(element, message);
|
|
} else {
|
|
return 0;
|
|
}
|
|
|
|
return ES_HANDLED;
|
|
}
|
|
|
|
int ProcessSplitterMessage(EsElement *element, EsMessage *message) {
|
|
EsSplitter *splitter = (EsSplitter *) element;
|
|
|
|
if (message->type == ES_MSG_LAYOUT && splitter->GetChildCount()) {
|
|
EsRectangle client = splitter->GetBounds();
|
|
EsRectangle bounds = EsRectangleAddBorder(client, splitter->style->insets);
|
|
|
|
size_t childCount = splitter->GetChildCount();
|
|
EsAssert(childCount & 1); // Expected split bars between each EsSplitter child.
|
|
uint64_t pushFlag = splitter->horizontal ? ES_CELL_H_PUSH : ES_CELL_V_PUSH;
|
|
|
|
if (!splitter->calculatedInitialSize) {
|
|
for (uintptr_t i = 0; i < childCount; i += 2) {
|
|
EsElement *child = splitter->GetChild(i);
|
|
|
|
if (~child->flags & pushFlag) {
|
|
int width = child->GetWidth(bounds.b - bounds.t);
|
|
int height = child->GetHeight(width);
|
|
splitter->resizeStartSizes.Add(splitter->horizontal ? width : height);
|
|
} else {
|
|
splitter->resizeStartSizes.Add(0);
|
|
}
|
|
}
|
|
}
|
|
|
|
int64_t newSize = splitter->horizontal ? (bounds.r - bounds.l) : (bounds.b - bounds.t);
|
|
|
|
if (newSize != splitter->previousSize && childCount > 1) {
|
|
// Step 1: Make a list of current sizes.
|
|
|
|
int64_t barSize = splitter->horizontal ? splitter->GetChild(1)->style->preferredWidth : splitter->GetChild(1)->style->preferredHeight;
|
|
int64_t previousPosition = 0;
|
|
|
|
if (!splitter->resizeStartSizes.Length()) {
|
|
for (uintptr_t i = 1; i < childCount; i += 2) {
|
|
int64_t position = ((SplitBar *) splitter->GetChild(i))->position;
|
|
splitter->resizeStartSizes.Add(position - previousPosition);
|
|
previousPosition = position + barSize;
|
|
}
|
|
|
|
splitter->resizeStartSizes.Add(splitter->previousSize - previousPosition);
|
|
}
|
|
|
|
Array<int64_t> currentSizes = {};
|
|
|
|
for (uintptr_t i = 0; i < splitter->resizeStartSizes.Length(); i++) {
|
|
currentSizes.Add(splitter->resizeStartSizes[i]);
|
|
}
|
|
|
|
if (currentSizes.Length() != childCount / 2 + 1) {
|
|
currentSizes.Free();
|
|
return ES_HANDLED;
|
|
}
|
|
|
|
// Step 2: Calculate the fixed size, and total weight.
|
|
|
|
int64_t fixedSize = 0, totalWeight = 0;
|
|
|
|
for (uintptr_t i = 0; i < childCount; i += 2) {
|
|
EsElement *child = splitter->GetChild(i);
|
|
|
|
if (~child->flags & pushFlag) {
|
|
fixedSize += currentSizes[i >> 1];
|
|
} else {
|
|
if (currentSizes[i >> 1] < 1) currentSizes[i >> 1] = 1;
|
|
totalWeight += currentSizes[i >> 1];
|
|
}
|
|
}
|
|
|
|
EsAssert(totalWeight); // Splitter must have at least one child with a PUSH flag for its orientation.
|
|
|
|
// Step 3: Calculate the new weighted sizes.
|
|
|
|
int64_t availableSpace = newSize - fixedSize - barSize * (childCount >> 1);
|
|
|
|
if (availableSpace >= 0) {
|
|
for (uintptr_t i = 0; i < childCount; i += 2) {
|
|
EsElement *child = splitter->GetChild(i);
|
|
|
|
if (child->flags & pushFlag) {
|
|
currentSizes[i >> 1] = availableSpace * currentSizes[i >> 1] / totalWeight;
|
|
}
|
|
}
|
|
} else {
|
|
availableSpace += fixedSize;
|
|
if (availableSpace < 0) availableSpace = 0;
|
|
|
|
for (uintptr_t i = 0; i < childCount; i += 2) {
|
|
EsElement *child = splitter->GetChild(i);
|
|
|
|
if (child->flags & pushFlag) {
|
|
currentSizes[i >> 1] = 0;
|
|
} else {
|
|
currentSizes[i >> 1] = availableSpace * currentSizes[i >> 1] / fixedSize;
|
|
}
|
|
}
|
|
}
|
|
|
|
// Step 4: Update the positions.
|
|
|
|
previousPosition = 0;
|
|
|
|
for (uintptr_t i = 1; i < childCount; i += 2) {
|
|
SplitBar *bar = (SplitBar *) splitter->GetChild(i);
|
|
bar->position = previousPosition + currentSizes[i >> 1];
|
|
previousPosition = bar->position + barSize - (splitter->horizontal ? bar->style->borders.l : bar->style->borders.t);
|
|
|
|
if (bar->position == 0) {
|
|
bar->position -= splitter->horizontal ? bar->style->borders.l : bar->style->borders.t;
|
|
} else if (bar->position == newSize - barSize) {
|
|
bar->position += splitter->horizontal ? bar->style->borders.r : bar->style->borders.b;
|
|
}
|
|
}
|
|
|
|
currentSizes.Free();
|
|
}
|
|
|
|
splitter->calculatedInitialSize = true;
|
|
splitter->previousSize = newSize;
|
|
splitter->previousScale = theming.scale;
|
|
|
|
int position = splitter->horizontal ? bounds.l : bounds.t;
|
|
|
|
for (uintptr_t i = 0; i < childCount; i++) {
|
|
EsElement *child = splitter->GetChild(i);
|
|
|
|
if (i & 1) {
|
|
if (splitter->horizontal) {
|
|
int size = child->style->preferredWidth;
|
|
EsElementMove(child, position, client.t, size, client.b - client.t);
|
|
position += size;
|
|
} else {
|
|
int size = child->style->preferredHeight;
|
|
EsElementMove(child, client.l, position, client.r - client.l, size);
|
|
position += size;
|
|
}
|
|
} else if (i == childCount - 1) {
|
|
if (splitter->horizontal) {
|
|
EsElementMove(child, position, bounds.t, bounds.r - position, bounds.b - bounds.t);
|
|
} else {
|
|
EsElementMove(child, bounds.l, position, bounds.r - bounds.l, bounds.b - position);
|
|
}
|
|
} else {
|
|
SplitBar *bar = (SplitBar *) splitter->GetChild(i + 1);
|
|
int size = bar->position - position;
|
|
|
|
if (splitter->horizontal) {
|
|
EsElementMove(child, position, bounds.t, size, bounds.b - bounds.t);
|
|
} else {
|
|
EsElementMove(child, bounds.l, position, bounds.r - bounds.l, size);
|
|
}
|
|
|
|
position += size;
|
|
}
|
|
}
|
|
} else if ((message->type == ES_MSG_GET_WIDTH && splitter->horizontal)
|
|
|| (message->type == ES_MSG_GET_HEIGHT && !splitter->horizontal)) {
|
|
int size = 0;
|
|
|
|
for (uintptr_t i = 0; i < splitter->GetChildCount(); i++) {
|
|
EsElement *child = splitter->GetChild(i);
|
|
size += splitter->horizontal ? child->GetWidth(message->measure.height) : child->GetHeight(message->measure.width);
|
|
}
|
|
|
|
if (splitter->horizontal) {
|
|
message->measure.width = size;
|
|
} else {
|
|
message->measure.height = size;
|
|
}
|
|
} else if (message->type == ES_MSG_PRE_ADD_CHILD && !splitter->addingSplitBar && splitter->GetChildCount()) {
|
|
splitter->addingSplitBar = true;
|
|
SplitBar *bar = (SplitBar *) EsHeapAllocate(sizeof(SplitBar), true);
|
|
|
|
bar->Initialise(splitter, ES_ELEMENT_FOCUSABLE | ES_ELEMENT_NOT_TAB_TRAVERSABLE | ES_CELL_EXPAND,
|
|
ProcessSplitBarMessage, splitter->horizontal ? ES_STYLE_SPLIT_BAR_VERTICAL : ES_STYLE_SPLIT_BAR_HORIZONTAL);
|
|
|
|
bar->cName = "split bar";
|
|
bar->accessKey = 'Q';
|
|
|
|
splitter->addingSplitBar = false;
|
|
} else if (message->type == ES_MSG_REMOVE_CHILD && ((EsElement *) message->child)->messageClass != ProcessSplitBarMessage) {
|
|
for (uintptr_t i = 0; i < splitter->GetChildCount(); i++) {
|
|
if (splitter->GetChild(i) == message->child) {
|
|
// Remove the corresponding split bar.
|
|
|
|
if (i) {
|
|
splitter->GetChild(i - 1)->Destroy();
|
|
} else {
|
|
splitter->GetChild(i + 1)->Destroy();
|
|
}
|
|
|
|
break;
|
|
}
|
|
}
|
|
} else if (message->type == ES_MSG_UI_SCALE_CHANGED) {
|
|
float changeFactor = theming.scale / splitter->previousScale;
|
|
splitter->previousScale = theming.scale;
|
|
|
|
for (uintptr_t i = 1; i < splitter->GetChildCount(); i += 2) {
|
|
SplitBar *bar = (SplitBar *) splitter->GetChild(i);
|
|
bar->position *= changeFactor;
|
|
}
|
|
|
|
for (uintptr_t i = 0; i < splitter->resizeStartSizes.Length(); i++) {
|
|
splitter->resizeStartSizes[i] *= changeFactor;
|
|
}
|
|
} else if (message->type == ES_MSG_DESTROY) {
|
|
splitter->resizeStartSizes.Free();
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
EsSplitter *EsSplitterCreate(EsElement *parent, uint64_t flags, EsStyleID style) {
|
|
EsSplitter *splitter = (EsSplitter *) EsHeapAllocate(sizeof(EsSplitter), true);
|
|
if (!splitter) return nullptr;
|
|
splitter->horizontal = flags & ES_SPLITTER_HORIZONTAL;
|
|
splitter->Initialise(parent, flags | ES_ELEMENT_NO_CLIP, ProcessSplitterMessage,
|
|
style ?: ES_STYLE_PANEL_WINDOW_BACKGROUND);
|
|
splitter->cName = "splitter";
|
|
return splitter;
|
|
}
|
|
|
|
// --------------------------------- Image displays.
|
|
|
|
// TODO
|
|
// asynchronous/synchronous load/decode from file/memory
|
|
// unloading image when not visible
|
|
// aspect ratio; sizing
|
|
// upscale/downscale quality
|
|
// subregion, transformations
|
|
// transparency, IsRegionCompletelyOpaque
|
|
// image sets, DPI; SVG scaling
|
|
// embedding in TextDisplay
|
|
// merge with IconDisplay
|
|
// decode in separate process for security?
|
|
// clipboard
|
|
// zoom/pan
|
|
|
|
void EsImageDisplayPaint(EsImageDisplay *display, EsPainter *painter, EsRectangle bounds) {
|
|
if (!display->bits && !display->source) {
|
|
return;
|
|
}
|
|
|
|
if (!display->bits && display->source) {
|
|
uint32_t width, height;
|
|
uint8_t *bits = EsImageLoad((uint8_t *) display->source, display->sourceBytes, &width, &height, 4);
|
|
|
|
if (bits) {
|
|
display->bits = (uint32_t *) bits;
|
|
display->width = width;
|
|
display->height = height;
|
|
display->stride = width * 4;
|
|
}
|
|
|
|
if (~display->flags & UI_STATE_CHECK_VISIBLE) {
|
|
if (display->window->checkVisible.Add(display)) {
|
|
display->state |= UI_STATE_CHECK_VISIBLE;
|
|
}
|
|
}
|
|
}
|
|
|
|
EsPaintTarget source = {};
|
|
source.bits = display->bits;
|
|
source.width = display->width;
|
|
source.height = display->height;
|
|
source.stride = display->stride;
|
|
source.fullAlpha = ~display->flags & ES_IMAGE_DISPLAY_FULLY_OPAQUE;
|
|
EsDrawPaintTarget(painter, &source, bounds, ES_RECT_4(0, display->width, 0, display->height), 0xFF);
|
|
}
|
|
|
|
int ProcessImageDisplayMessage(EsElement *element, EsMessage *message) {
|
|
EsImageDisplay *display = (EsImageDisplay *) element;
|
|
|
|
if (message->type == ES_MSG_PAINT) {
|
|
EsImageDisplayPaint(display, message->painter, EsPainterBoundsInset(message->painter));
|
|
} else if (message->type == ES_MSG_GET_WIDTH) {
|
|
message->measure.width = display->width;
|
|
} else if (message->type == ES_MSG_GET_HEIGHT) {
|
|
message->measure.height = display->height;
|
|
} else if (message->type == ES_MSG_DESTROY) {
|
|
EsHeapFree(display->bits);
|
|
EsHeapFree(display->source);
|
|
} else if (message->type == ES_MSG_NOT_VISIBLE) {
|
|
EsHeapFree(display->bits);
|
|
display->bits = nullptr;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
uint32_t EsImageDisplayGetImageWidth(EsImageDisplay *display) {
|
|
return display->width;
|
|
}
|
|
|
|
uint32_t EsImageDisplayGetImageHeight(EsImageDisplay *display) {
|
|
return display->height;
|
|
}
|
|
|
|
EsImageDisplay *EsImageDisplayCreate(EsElement *parent, uint64_t flags, EsStyleID style) {
|
|
EsImageDisplay *display = (EsImageDisplay *) EsHeapAllocate(sizeof(EsImageDisplay), true);
|
|
if (!display) return nullptr;
|
|
display->Initialise(parent, flags, ProcessImageDisplayMessage, style);
|
|
display->cName = "image";
|
|
return display;
|
|
}
|
|
|
|
void EsImageDisplayLoadBits(EsImageDisplay *display, const uint32_t *bits, size_t width, size_t height, size_t stride) {
|
|
EsHeapFree(display->bits);
|
|
display->bits = (uint32_t *) EsHeapAllocate(stride * height, false);
|
|
|
|
if (!display->bits) {
|
|
display->width = display->height = display->stride = 0;
|
|
} else {
|
|
display->width = width;
|
|
display->height = height;
|
|
display->stride = stride;
|
|
EsMemoryCopy(display->bits, bits, stride * height);
|
|
}
|
|
}
|
|
|
|
void EsImageDisplayLoadFromMemory(EsImageDisplay *display, const void *buffer, size_t bufferBytes) {
|
|
if (display->flags & ES_IMAGE_DISPLAY_DECODE_WHEN_NEEDED) {
|
|
EsHeapFree(display->source);
|
|
EsHeapFree(display->bits);
|
|
display->bits = nullptr;
|
|
display->source = EsHeapAllocate(bufferBytes, false);
|
|
if (!display->source) return;
|
|
EsMemoryCopy(display->source, buffer, bufferBytes);
|
|
display->sourceBytes = bufferBytes;
|
|
|
|
if (~display->flags & ES_IMAGE_DISPLAY_MANUAL_SIZE) {
|
|
// TODO Make a version of EsImageLoad that doesn't load the full image, but only gets the size.
|
|
uint32_t width, height;
|
|
uint8_t *bits = EsImageLoad((uint8_t *) buffer, bufferBytes, &width, &height, 4);
|
|
EsHeapFree(bits);
|
|
display->width = width;
|
|
display->height = height;
|
|
}
|
|
} else {
|
|
uint32_t width, height;
|
|
uint8_t *bits = EsImageLoad((uint8_t *) buffer, bufferBytes, &width, &height, 4);
|
|
if (!bits) return;
|
|
EsHeapFree(display->bits);
|
|
display->bits = (uint32_t *) bits;
|
|
display->width = width;
|
|
display->height = height;
|
|
display->stride = width * 4;
|
|
}
|
|
}
|
|
|
|
struct EsIconDisplay : EsElement {
|
|
uint32_t iconID;
|
|
};
|
|
|
|
int ProcessIconDisplayMessage(EsElement *element, EsMessage *message) {
|
|
EsIconDisplay *display = (EsIconDisplay *) element;
|
|
|
|
if (message->type == ES_MSG_PAINT) {
|
|
EsDrawContent(message->painter, element, ES_RECT_2S(message->painter->width, message->painter->height), "", 0, display->iconID);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
EsIconDisplay *EsIconDisplayCreate(EsElement *parent, uint64_t flags, EsStyleID style, uint32_t iconID) {
|
|
EsIconDisplay *display = (EsIconDisplay *) EsHeapAllocate(sizeof(EsIconDisplay), true);
|
|
if (!display) return nullptr;
|
|
display->Initialise(parent, flags, ProcessIconDisplayMessage, style ?: ES_STYLE_ICON_DISPLAY);
|
|
display->cName = "icon";
|
|
display->iconID = iconID;
|
|
return display;
|
|
}
|
|
|
|
void EsIconDisplaySetIcon(EsIconDisplay *display, uint32_t iconID) {
|
|
display->iconID = iconID;
|
|
EsElementRepaint(display);
|
|
}
|
|
|
|
// --------------------------------- Sliders.
|
|
|
|
struct EsSlider : EsElement {
|
|
EsElement *point;
|
|
double value;
|
|
uint32_t steps;
|
|
int32_t dragOffset;
|
|
bool inDrag, endDrag;
|
|
};
|
|
|
|
int ProcessSliderPointMessage(EsElement *element, EsMessage *message) {
|
|
EsSlider *slider = (EsSlider *) EsElementGetLayoutParent(element);
|
|
|
|
if (message->type == ES_MSG_MOUSE_LEFT_DRAG) {
|
|
double range = slider->width - slider->point->style->preferredWidth;
|
|
slider->inDrag = true;
|
|
EsSliderSetValue(slider, (message->mouseDragged.newPositionX + element->offsetX - slider->dragOffset) / range);
|
|
} else if (message->type == ES_MSG_MOUSE_LEFT_UP && slider->inDrag) {
|
|
slider->inDrag = false;
|
|
slider->endDrag = true; // Force sending the update message.
|
|
EsSliderSetValue(slider, slider->value);
|
|
slider->endDrag = false;
|
|
} else if (message->type == ES_MSG_MOUSE_LEFT_DOWN) {
|
|
slider->dragOffset = message->mouseDown.positionX;
|
|
EsElementFocus(slider);
|
|
} else {
|
|
return 0;
|
|
}
|
|
|
|
return ES_HANDLED;
|
|
}
|
|
|
|
int ProcessSliderMessage(EsElement *element, EsMessage *message) {
|
|
EsSlider *slider = (EsSlider *) element;
|
|
|
|
if (message->type == ES_MSG_LAYOUT) {
|
|
int pointWidth = slider->point->style->preferredWidth;
|
|
int pointHeight = slider->point->style->preferredHeight;
|
|
slider->point->InternalMove(pointWidth, pointHeight, (slider->width - pointWidth) * slider->value, (slider->height - pointHeight) / 2);
|
|
} else if (message->type == ES_MSG_FOCUSED_START) {
|
|
slider->point->customStyleState |= THEME_STATE_FOCUSED_ITEM;
|
|
slider->point->MaybeRefreshStyle();
|
|
} else if (message->type == ES_MSG_FOCUSED_END) {
|
|
slider->point->customStyleState &= ~THEME_STATE_FOCUSED_ITEM;
|
|
slider->point->MaybeRefreshStyle();
|
|
} else if (message->type == ES_MSG_KEY_TYPED && message->keyboard.scancode == ES_SCANCODE_LEFT_ARROW) {
|
|
EsSliderSetValue(slider, slider->value - (slider->steps ? 1.0 / slider->steps : 0.02));
|
|
} else if (message->type == ES_MSG_KEY_TYPED && message->keyboard.scancode == ES_SCANCODE_RIGHT_ARROW) {
|
|
EsSliderSetValue(slider, slider->value + (slider->steps ? 1.0 / slider->steps : 0.02));
|
|
} else if (message->type == ES_MSG_KEY_TYPED && message->keyboard.scancode == ES_SCANCODE_HOME) {
|
|
EsSliderSetValue(slider, 0.0);
|
|
} else if (message->type == ES_MSG_KEY_TYPED && message->keyboard.scancode == ES_SCANCODE_END) {
|
|
EsSliderSetValue(slider, 1.0);
|
|
} else if (message->type == ES_MSG_PAINT) {
|
|
// TODO Draw ticks.
|
|
} else {
|
|
return 0;
|
|
}
|
|
|
|
return ES_HANDLED;
|
|
}
|
|
|
|
double EsSliderGetValue(EsSlider *slider) {
|
|
return slider->value;
|
|
}
|
|
|
|
void EsSliderSetValue(EsSlider *slider, double newValue, bool sendUpdatedMessage) {
|
|
newValue = ClampDouble(0.0, 1.0, newValue);
|
|
|
|
if (slider->steps) {
|
|
newValue = EsCRTfloor((slider->steps - 1) * newValue + 0.5) / (slider->steps - 1);
|
|
}
|
|
|
|
double previous = slider->value;
|
|
if (previous == newValue && !slider->endDrag) return;
|
|
slider->value = newValue;
|
|
EsElementRelayout(slider);
|
|
|
|
if (sendUpdatedMessage) {
|
|
EsMessage m = { ES_MSG_SLIDER_MOVED, .sliderMoved = { .value = newValue, .previous = previous, .inDrag = slider->inDrag } };
|
|
EsMessageSend(slider, &m);
|
|
}
|
|
}
|
|
|
|
EsSlider *EsSliderCreate(EsElement *parent, uint64_t flags, EsStyleID style, double value, uint32_t steps) {
|
|
EsSlider *slider = (EsSlider *) EsHeapAllocate(sizeof(EsSlider), true);
|
|
if (!slider) return nullptr;
|
|
slider->Initialise(parent, flags | ES_ELEMENT_FOCUSABLE, ProcessSliderMessage, style ?: ES_STYLE_SLIDER_TRACK);
|
|
slider->cName = "slider";
|
|
slider->point = EsCustomElementCreate(slider, ES_FLAGS_DEFAULT, ES_STYLE_SLIDER_POINT);
|
|
slider->point->messageUser = ProcessSliderPointMessage;
|
|
slider->steps = steps;
|
|
EsSliderSetValue(slider, value, false);
|
|
return slider;
|
|
}
|
|
|
|
// --------------------------------- File menu.
|
|
|
|
const EsStyle styleFileMenuDocumentInformationPanel1 = {
|
|
.metrics = {
|
|
.mask = ES_THEME_METRICS_INSETS | ES_THEME_METRICS_GAP_MAJOR | ES_THEME_METRICS_PREFERRED_WIDTH,
|
|
.insets = ES_RECT_4(10, 10, 5, 5),
|
|
.preferredWidth = 230,
|
|
.gapMajor = 5,
|
|
},
|
|
};
|
|
|
|
const EsStyle styleFileMenuDocumentInformationPanel2 = {
|
|
.metrics = {
|
|
.mask = ES_THEME_METRICS_GAP_MAJOR,
|
|
.gapMajor = 5,
|
|
},
|
|
};
|
|
|
|
const EsStyle styleFileMenuNameTextbox = {
|
|
.inherit = ES_STYLE_TEXTBOX_TRANSPARENT,
|
|
|
|
.metrics = {
|
|
.mask = ES_THEME_METRICS_PREFERRED_WIDTH,
|
|
.preferredWidth = 0,
|
|
},
|
|
};
|
|
|
|
void InstanceRenameFromTextbox(EsWindow *window, APIInstance *instance, EsTextbox *textbox) {
|
|
size_t newNameBytes;
|
|
char *newName = EsTextboxGetContents(textbox, &newNameBytes);
|
|
uint8_t *buffer = (uint8_t *) EsHeapAllocate(1 + newNameBytes, false);
|
|
buffer[0] = DESKTOP_MSG_RENAME;
|
|
EsMemoryCopy(buffer + 1, newName, newNameBytes);
|
|
MessageDesktop(buffer, 1 + newNameBytes, window->handle);
|
|
EsHeapFree(buffer);
|
|
EsHeapFree(instance->newName);
|
|
instance->newName = newName;
|
|
instance->newNameBytes = newNameBytes;
|
|
}
|
|
|
|
int FileMenuNameTextboxMessage(EsElement *element, EsMessage *message) {
|
|
if (message->type == ES_MSG_TEXTBOX_EDIT_END) {
|
|
APIInstance *instance = (APIInstance *) element->instance->_private;
|
|
|
|
if (!message->endEdit.rejected && !message->endEdit.unchanged) {
|
|
InstanceRenameFromTextbox(element->instance->window, instance, instance->fileMenuNameTextbox);
|
|
EsMenuClose((EsMenu *) element->window);
|
|
} else {
|
|
EsPanelSwitchTo(instance->fileMenuNameSwitcher, instance->fileMenuNamePanel, ES_TRANSITION_SLIDE_DOWN);
|
|
}
|
|
|
|
return ES_HANDLED;
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void TextboxSelectSectionBeforeFileExtension(EsTextbox *textbox, const char *name, ptrdiff_t nameBytes) {
|
|
uintptr_t extensionOffset = 0;
|
|
|
|
if (nameBytes == -1) {
|
|
nameBytes = EsCStringLength(name);
|
|
}
|
|
|
|
for (intptr_t i = 1; i < nameBytes; i++) {
|
|
if (name[i] == '.') {
|
|
extensionOffset = i;
|
|
}
|
|
}
|
|
|
|
if (extensionOffset) {
|
|
EsTextboxSetSelection(textbox, 0, 0, 0, extensionOffset);
|
|
}
|
|
}
|
|
|
|
void FileMenuRename(EsInstance *_instance, EsElement *, EsCommand *) {
|
|
APIInstance *instance = (APIInstance *) _instance->_private;
|
|
EsTextboxClear(instance->fileMenuNameTextbox, false);
|
|
|
|
const char *initialName = nullptr;
|
|
ptrdiff_t initialNameBytes = 0;
|
|
|
|
if (instance->startupInformation && instance->startupInformation->filePathBytes) {
|
|
PathGetName(instance->startupInformation->filePath, instance->startupInformation->filePathBytes, &initialName, &initialNameBytes);
|
|
} else {
|
|
EsInstanceClassEditorSettings *editorSettings = &instance->editorSettings;
|
|
initialName = editorSettings->newDocumentFileName;
|
|
initialNameBytes = editorSettings->newDocumentFileNameBytes;
|
|
}
|
|
|
|
if (initialNameBytes == -1) {
|
|
initialNameBytes = EsCStringLength(initialName);
|
|
}
|
|
|
|
EsTextboxInsert(instance->fileMenuNameTextbox, initialName, initialNameBytes, false);
|
|
EsPanelSwitchTo(instance->fileMenuNameSwitcher, instance->fileMenuNameTextbox, ES_TRANSITION_SLIDE_UP);
|
|
EsElementFocus(instance->fileMenuNameTextbox);
|
|
EsTextboxStartEdit(instance->fileMenuNameTextbox);
|
|
TextboxSelectSectionBeforeFileExtension(instance->fileMenuNameTextbox, initialName, initialNameBytes);
|
|
instance->fileMenuNameTextbox->messageUser = FileMenuNameTextboxMessage;
|
|
}
|
|
|
|
void EsFileMenuCreate(EsInstance *_instance, EsElement *element, uint32_t menuFlags) {
|
|
// TODO Make this user-customizable?
|
|
|
|
// const EsFileMenuSettings *settings = (const EsFileMenuSettings *) element->userData.p;
|
|
APIInstance *instance = (APIInstance *) _instance->_private;
|
|
EsAssert(instance->instanceClass == ES_INSTANCE_CLASS_EDITOR);
|
|
EsInstanceClassEditorSettings *editorSettings = &instance->editorSettings;
|
|
bool newDocument = !instance->startupInformation || !instance->startupInformation->filePath;
|
|
|
|
EsMenu *menu = EsMenuCreate(element, menuFlags);
|
|
if (!menu) return;
|
|
EsPanel *panel1 = EsPanelCreate(menu, ES_PANEL_HORIZONTAL | ES_CELL_H_LEFT, EsStyleIntern(&styleFileMenuDocumentInformationPanel1));
|
|
if (!panel1) goto show;
|
|
|
|
{
|
|
// TODO Get this icon from the file type database?
|
|
// We'll probably need Desktop to send this via _EsApplicationStartupInformation and when the file is renamed.
|
|
|
|
EsIconDisplayCreate(panel1, ES_CELL_V_TOP, 0, editorSettings->documentIconID);
|
|
EsSpacerCreate(panel1, ES_FLAGS_DEFAULT, 0, 5, 0);
|
|
|
|
EsPanel *panel2 = EsPanelCreate(panel1, ES_CELL_H_FILL, EsStyleIntern(&styleFileMenuDocumentInformationPanel2));
|
|
if (!panel2) goto show;
|
|
EsPanel *switcher = EsPanelCreate(panel2, ES_CELL_H_FILL | ES_PANEL_SWITCHER | ES_PANEL_SWITCHER_MEASURE_LARGEST);
|
|
if (!switcher) goto show;
|
|
EsPanel *panel3 = EsPanelCreate(switcher, ES_PANEL_HORIZONTAL | ES_CELL_H_FILL, EsStyleIntern(&styleFileMenuDocumentInformationPanel2));
|
|
if (!panel3) goto show;
|
|
|
|
instance->fileMenuNameTextbox = EsTextboxCreate(switcher, ES_CELL_H_FILL | ES_TEXTBOX_EDIT_BASED, EsStyleIntern(&styleFileMenuNameTextbox));
|
|
|
|
instance->fileMenuNameSwitcher = switcher;
|
|
instance->fileMenuNamePanel = panel3;
|
|
EsPanelSwitchTo(instance->fileMenuNameSwitcher, instance->fileMenuNamePanel, ES_TRANSITION_NONE);
|
|
|
|
if (newDocument) {
|
|
EsTextDisplayCreate(panel3, ES_CELL_H_FILL, ES_STYLE_TEXT_LABEL,
|
|
editorSettings->newDocumentTitle, editorSettings->newDocumentTitleBytes);
|
|
} else {
|
|
const char *name;
|
|
ptrdiff_t nameBytes;
|
|
PathGetName(instance->startupInformation->filePath, instance->startupInformation->filePathBytes, &name, &nameBytes);
|
|
EsTextDisplayCreate(panel3, ES_CELL_H_FILL, ES_STYLE_TEXT_LABEL, name, nameBytes);
|
|
}
|
|
|
|
EsButton *renameButton = EsButtonCreate(panel3, ES_BUTTON_TOOLBAR);
|
|
if (!renameButton) goto show;
|
|
EsButtonSetIcon(renameButton, ES_ICON_DOCUMENT_EDIT_SYMBOLIC);
|
|
EsButtonOnCommand(renameButton, FileMenuRename);
|
|
|
|
if (!newDocument) {
|
|
EsPanel *panel4 = EsPanelCreate(panel2, ES_PANEL_TABLE | ES_PANEL_HORIZONTAL | ES_CELL_H_LEFT, EsStyleIntern(&styleFileMenuDocumentInformationPanel2));
|
|
if (!panel4) goto show;
|
|
EsPanelSetBands(panel4, 2 /* columns */);
|
|
|
|
EsTextDisplayCreate(panel4, ES_CELL_H_RIGHT, ES_STYLE_TEXT_LABEL_SECONDARY, INTERFACE_STRING(CommonFileMenuFileLocation));
|
|
EsTextDisplayCreate(panel4, ES_CELL_H_LEFT, ES_STYLE_TEXT_LABEL, instance->startupInformation->containingFolder,
|
|
instance->startupInformation->containingFolderBytes);
|
|
|
|
char buffer[64];
|
|
size_t bytes = EsStringFormat(buffer, sizeof(buffer), "%D", EsFileStoreGetSize(instance->fileStore));
|
|
EsTextDisplayCreate(panel4, ES_CELL_H_RIGHT, ES_STYLE_TEXT_LABEL_SECONDARY, INTERFACE_STRING(CommonFileMenuFileSize));
|
|
EsTextDisplayCreate(panel4, ES_CELL_H_LEFT, ES_STYLE_TEXT_LABEL, buffer, bytes);
|
|
|
|
// TODO Modification date, author, etc.
|
|
}
|
|
}
|
|
|
|
if (instance->instanceClass == ES_INSTANCE_CLASS_EDITOR) {
|
|
EsMenuAddSeparator(menu);
|
|
|
|
if (!instance->commandSave.enabled && !newDocument) {
|
|
EsMenuAddItem(menu, ES_ELEMENT_DISABLED, INTERFACE_STRING(CommonFileUnchanged));
|
|
} else {
|
|
EsMenuAddCommand(menu, ES_FLAGS_DEFAULT, INTERFACE_STRING(CommonFileSave), &instance->commandSave);
|
|
}
|
|
|
|
// EsMenuAddItem(menu, newDocument ? ES_ELEMENT_DISABLED : ES_FLAGS_DEFAULT, INTERFACE_STRING(CommonFileMakeCopy)); // TODO.
|
|
// EsMenuAddSeparator(menu);
|
|
|
|
// EsMenuAddItem(menu, newDocument ? ES_ELEMENT_DISABLED : ES_FLAGS_DEFAULT, INTERFACE_STRING(CommonFileShare)); // TODO.
|
|
// EsMenuAddItem(menu, newDocument ? ES_ELEMENT_DISABLED : ES_FLAGS_DEFAULT, INTERFACE_STRING(CommonFileVersionHistory)); // TODO.
|
|
EsMenuAddCommand(menu, newDocument ? ES_ELEMENT_DISABLED : ES_FLAGS_DEFAULT, INTERFACE_STRING(CommonFileShowInFileManager), &instance->commandShowInFileManager);
|
|
}
|
|
|
|
show: EsMenuShow(menu);
|
|
}
|
|
|
|
void FileMenuCreate(EsInstance *_instance, EsElement *element, EsCommand *) {
|
|
EsFileMenuCreate(_instance, element);
|
|
}
|
|
|
|
void EsFileMenuAddToToolbar(EsElement *element, const EsFileMenuSettings *settings) {
|
|
EsPanel *buttonGroup = EsPanelCreate(element, ES_PANEL_HORIZONTAL);
|
|
EsButton *button = EsButtonCreate(buttonGroup, ES_BUTTON_DROPDOWN, 0, INTERFACE_STRING(CommonFileMenu));
|
|
if (!button) return;
|
|
button->accessKey = 'F';
|
|
button->userData = (void *) settings;
|
|
EsButtonOnCommand(button, FileMenuCreate);
|
|
}
|
|
|
|
// --------------------------------- Message loop and core UI infrastructure.
|
|
|
|
void EsElement::PrintTree(int depth) {
|
|
const char *tabs = "\t\t\t\t\t\t\t\t\t\t\t\t\t\t";
|
|
EsPrint("%s%z c:%d, %z\n", depth, tabs, cName, children.Length(), state & UI_STATE_DESTROYING ? "DESTROYING" : "");
|
|
|
|
for (uintptr_t i = 0; i < children.Length(); i++) {
|
|
children[i]->PrintTree(depth + 1);
|
|
}
|
|
}
|
|
|
|
void EsElement::Destroy(bool manual) {
|
|
if (state & UI_STATE_DESTROYING) {
|
|
return;
|
|
}
|
|
|
|
if (manual) {
|
|
EsElement *ancestor = parent;
|
|
|
|
while (ancestor && (~ancestor->state & UI_STATE_DESTROYING_CHILD)) {
|
|
ancestor->state |= UI_STATE_DESTROYING_CHILD;
|
|
ancestor = ancestor->parent;
|
|
}
|
|
}
|
|
|
|
if (state & UI_STATE_MENU_SOURCE) {
|
|
for (uintptr_t i = 0; i < gui.allWindows.Length(); i++) {
|
|
if (gui.allWindows[i]->source == this) {
|
|
// Close the menu attached to this element.
|
|
EsMenuClose((EsMenu *) gui.allWindows[i]);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
state |= UI_STATE_DESTROYING | UI_STATE_DESTROYING_CHILD | UI_STATE_BLOCK_INTERACTION;
|
|
|
|
if (parent) {
|
|
EsMessage m = { ES_MSG_REMOVE_CHILD };
|
|
m.child = this;
|
|
EsMessageSend(parent, &m);
|
|
}
|
|
|
|
if (window->hovered == this) {
|
|
window->hovered = window;
|
|
EsMessage m = {};
|
|
m.type = ES_MSG_HOVERED_START;
|
|
EsMessageSend(window, &m);
|
|
}
|
|
|
|
EsMessage m = { ES_MSG_DESTROY };
|
|
if (messageUser) messageUser(this, &m);
|
|
messageUser = nullptr;
|
|
if (messageClass) messageClass(this, &m);
|
|
messageClass = nullptr;
|
|
|
|
if (window->inactiveFocus == this) window->inactiveFocus = nullptr;
|
|
if (window->pressed == this) window->pressed = nullptr;
|
|
if (window->focused == this) window->focused = nullptr;
|
|
if (window->defaultEnterButton == this) window->defaultEnterButton = nullptr;
|
|
if (window->enterButton == this) window->enterButton = window->defaultEnterButton;
|
|
if (window->escapeButton == this) window->escapeButton = nullptr;
|
|
if (window->dragged == this) window->dragged = nullptr;
|
|
if (gui.clickChainElement == this) gui.clickChainElement = nullptr;
|
|
|
|
if (parent) EsElementUpdateContentSize(parent);
|
|
|
|
UIWindowNeedsUpdate(window);
|
|
}
|
|
|
|
bool EsElement::InternalDestroy() {
|
|
if (~state & UI_STATE_DESTROYING_CHILD) {
|
|
return false;
|
|
}
|
|
|
|
for (uintptr_t i = 0; i < children.Length(); i++) {
|
|
if (state & UI_STATE_DESTROYING) {
|
|
children[i]->Destroy(false);
|
|
}
|
|
|
|
if (children[i]->InternalDestroy() && (~state & UI_STATE_DESTROYING)) {
|
|
children.Delete(i);
|
|
i--;
|
|
}
|
|
}
|
|
|
|
state &= ~UI_STATE_DESTROYING_CHILD;
|
|
|
|
if (~state & UI_STATE_DESTROYING) {
|
|
return false;
|
|
}
|
|
|
|
children.Free();
|
|
|
|
InspectorNotifyElementDestroyed(this);
|
|
|
|
if (state & UI_STATE_ANIMATING) {
|
|
for (uintptr_t i = 0; i < gui.animatingElements.Length(); i++) {
|
|
if (gui.animatingElements[i] == this) {
|
|
gui.animatingElements.DeleteSwap(i);
|
|
break;
|
|
}
|
|
}
|
|
|
|
state &= ~UI_STATE_ANIMATING;
|
|
}
|
|
|
|
if (state & UI_STATE_CHECK_VISIBLE) {
|
|
window->checkVisible.FindAndDeleteSwap(this, true);
|
|
}
|
|
|
|
if (flags & ES_ELEMENT_FREE_USER_DATA) {
|
|
EsHeapFree(userData.p);
|
|
}
|
|
|
|
if (style) style->CloseReference();
|
|
if (previousTransitionFrame) EsPaintTargetDestroy(previousTransitionFrame);
|
|
ThemeAnimationDestroy(&animation);
|
|
if (window == this) UIWindowDestroy(window); // Windows are deallocated after receiving ES_MSG_WINDOW_DESTROYED.
|
|
else EsHeapFree(this);
|
|
|
|
return true;
|
|
}
|
|
|
|
EsElement *EsWindowGetToolbar(EsWindow *window, bool createNew) {
|
|
if (createNew || !window->toolbar) {
|
|
bool first = !window->toolbar;
|
|
window->toolbar = EsPanelCreate(window->toolbarSwitcher, ES_PANEL_HORIZONTAL | ES_CELL_FILL, ES_STYLE_PANEL_TOOLBAR);
|
|
|
|
if (!window->toolbar) {
|
|
return nullptr;
|
|
}
|
|
|
|
window->toolbar->cName = "toolbar";
|
|
EsAssert(window->toolbar->messageClass == ProcessPanelMessage);
|
|
|
|
window->toolbar->messageClass = [] (EsElement *element, EsMessage *message) {
|
|
if (message->type == ES_MSG_GET_CHILD_STYLE_VARIANT) {
|
|
if (message->childStyleVariant == ES_STYLE_TEXT_LABEL) {
|
|
message->childStyleVariant = ES_STYLE_TEXT_TOOLBAR;
|
|
return ES_HANDLED;
|
|
} else if (message->childStyleVariant == ES_STYLE_PUSH_BUTTON_NORMAL
|
|
|| message->childStyleVariant == ES_STYLE_CHECKBOX_NORMAL
|
|
|| message->childStyleVariant == ES_STYLE_CHECKBOX_RADIOBOX) {
|
|
message->childStyleVariant = ES_STYLE_PUSH_BUTTON_TOOLBAR;
|
|
return ES_HANDLED;
|
|
}
|
|
}
|
|
|
|
return ProcessPanelMessage(element, message);
|
|
};
|
|
|
|
if (first) EsPanelSwitchTo(window->toolbarSwitcher, window->toolbar, ES_TRANSITION_NONE);
|
|
}
|
|
|
|
return window->toolbar;
|
|
}
|
|
|
|
void EsWindowSwitchToolbar(EsWindow *window, EsElement *toolbar, EsTransitionType transitionType) {
|
|
EsPanelSwitchTo(window->toolbarSwitcher, toolbar, transitionType);
|
|
}
|
|
|
|
EsRectangle EsWindowGetBounds(EsWindow *window) {
|
|
EsRectangle bounds;
|
|
EsSyscall(ES_SYSCALL_WINDOW_GET_BOUNDS, window->handle, (uintptr_t) &bounds, 0, 0);
|
|
return bounds;
|
|
}
|
|
|
|
EsRectangle EsElementGetInsetBounds(EsElement *element) {
|
|
EsMessageMutexCheck();
|
|
EsRectangle insets = element->style->insets;
|
|
return ES_RECT_4(insets.l, element->width - insets.r,
|
|
insets.t, element->height - insets.b);
|
|
}
|
|
|
|
EsRectangle EsElementGetInsetSize(EsElement *element) {
|
|
EsMessageMutexCheck();
|
|
|
|
EsRectangle insets = element->style->insets;
|
|
return ES_RECT_4(0, element->width - insets.l - insets.r,
|
|
0, element->height - insets.t - insets.b);
|
|
}
|
|
|
|
void EsWindowSetIcon(EsWindow *window, uint32_t iconID) {
|
|
EsMessageMutexCheck();
|
|
|
|
char buffer[5];
|
|
buffer[0] = DESKTOP_MSG_SET_ICON;
|
|
EsMemoryCopy(buffer + 1, &iconID, sizeof(uint32_t));
|
|
MessageDesktop(buffer, sizeof(buffer), window->handle);
|
|
}
|
|
|
|
void EsWindowSetTitle(EsWindow *window, const char *title, ptrdiff_t titleBytes) {
|
|
EsMessageMutexCheck();
|
|
|
|
APIInstance *instance = window->instance ? ((APIInstance *) window->instance->_private) : nullptr;
|
|
const char *applicationName = instance ? instance->applicationName : nullptr;
|
|
size_t applicationNameBytes = instance ? instance->applicationNameBytes : 0;
|
|
|
|
if (!applicationNameBytes && !titleBytes) {
|
|
return;
|
|
}
|
|
|
|
if (titleBytes == -1) {
|
|
titleBytes = EsCStringLength(title);
|
|
}
|
|
|
|
if (titleBytes) {
|
|
applicationNameBytes = 0;
|
|
}
|
|
|
|
char buffer[4096];
|
|
size_t bytes = EsStringFormat(buffer, 4096, "%c%s%s", DESKTOP_MSG_SET_TITLE, titleBytes, title, applicationNameBytes, applicationName);
|
|
MessageDesktop(buffer, bytes, window->handle);
|
|
}
|
|
|
|
EsHandle _EsWindowGetHandle(EsWindow *window) {
|
|
return window->handle;
|
|
}
|
|
|
|
EsError EsMouseSetPosition(EsWindow *relativeWindow, int x, int y) {
|
|
if (relativeWindow) {
|
|
EsRectangle bounds = EsWindowGetBounds(relativeWindow);
|
|
x += bounds.l;
|
|
y += bounds.t;
|
|
}
|
|
|
|
return EsSyscall(ES_SYSCALL_CURSOR_POSITION_SET, x, y, 0, 0);
|
|
}
|
|
|
|
EsPoint EsMouseGetPosition(EsElement *relativeElement) {
|
|
if (relativeElement) {
|
|
EsPoint position = relativeElement->window->mousePosition;
|
|
EsRectangle bounds = relativeElement->GetWindowBounds();
|
|
position.x -= bounds.l;
|
|
position.y -= bounds.t;
|
|
return position;
|
|
} else {
|
|
EsPoint position;
|
|
EsSyscall(ES_SYSCALL_CURSOR_POSITION_GET, (uintptr_t) &position, 0, 0, 0);
|
|
return position;
|
|
}
|
|
}
|
|
|
|
EsStyleID UIGetDefaultStyleVariant(EsStyleID style, EsElement *parent) {
|
|
EsMessage m = { .type = ES_MSG_GET_CHILD_STYLE_VARIANT, .childStyleVariant = style };
|
|
EsElement *ancestor = parent;
|
|
|
|
while (ancestor) {
|
|
if (ES_HANDLED == EsMessageSend(ancestor, &m)) {
|
|
break;
|
|
}
|
|
|
|
ancestor = ancestor->parent;
|
|
}
|
|
|
|
return m.childStyleVariant;
|
|
}
|
|
|
|
EsElement *UIFindHoverElementRecursively2(EsElement *element, int offsetX, int offsetY, EsPoint position, uintptr_t i) {
|
|
EsElement *child = element->GetChildByZ(i - 1);
|
|
|
|
if (!child) return nullptr;
|
|
if (child->flags & ES_ELEMENT_HIDDEN) return nullptr;
|
|
if (child->state & UI_STATE_DESTROYING) return nullptr;
|
|
if (child->state & UI_STATE_BLOCK_INTERACTION) return nullptr;
|
|
|
|
if (!EsRectangleContains(ES_RECT_4(offsetX + child->offsetX, offsetX + child->offsetX + child->width,
|
|
offsetY + child->offsetY, offsetY + child->offsetY + child->height),
|
|
position.x, position.y)) {
|
|
return nullptr;
|
|
}
|
|
|
|
EsMessage m = { ES_MSG_HIT_TEST };
|
|
m.hitTest.x = position.x - offsetX - child->offsetX - child->internalOffsetLeft;
|
|
m.hitTest.y = position.y - offsetY - child->offsetY - child->internalOffsetTop;
|
|
m.hitTest.inside = true;
|
|
int response = EsMessageSend(child, &m);
|
|
|
|
if ((response != ES_HANDLED || m.hitTest.inside) && response != ES_REJECTED) {
|
|
return UIFindHoverElementRecursively(child, offsetX, offsetY, position);
|
|
}
|
|
|
|
return nullptr;
|
|
}
|
|
|
|
EsElement *UIFindHoverElementRecursively(EsElement *element, int offsetX, int offsetY, EsPoint position) {
|
|
offsetX += element->offsetX;
|
|
offsetY += element->offsetY;
|
|
|
|
EsMessage zOrder = { ES_MSG_BEFORE_Z_ORDER };
|
|
zOrder.beforeZOrder.nonClient = zOrder.beforeZOrder.end = element->children.Length();
|
|
zOrder.beforeZOrder.clip = Translate(ES_RECT_4(0, element->width, 0, element->height), -offsetX, -offsetY);
|
|
EsMessageSend(element, &zOrder);
|
|
|
|
EsElement *result = nullptr;
|
|
|
|
if (~element->flags & ES_ELEMENT_NO_HOVER_DESCENDENTS) {
|
|
for (uintptr_t i = element->children.Length(); !result && i > zOrder.beforeZOrder.nonClient; i--) {
|
|
result = UIFindHoverElementRecursively2(element, offsetX, offsetY, position, i);
|
|
}
|
|
|
|
for (uintptr_t i = zOrder.beforeZOrder.end; !result && i > zOrder.beforeZOrder.start; i--) {
|
|
result = UIFindHoverElementRecursively2(element, offsetX, offsetY, position, i);
|
|
}
|
|
}
|
|
|
|
zOrder.type = ES_MSG_AFTER_Z_ORDER;
|
|
EsMessageSend(element, &zOrder);
|
|
|
|
return result ? result : (element->flags & ES_ELEMENT_NO_HOVER) ? nullptr : element;
|
|
}
|
|
|
|
void UIFindHoverElement(EsWindow *window) {
|
|
// TS("Finding element under cursor\n");
|
|
|
|
EsPoint position = EsMouseGetPosition(window);
|
|
|
|
EsElement *element;
|
|
|
|
if (position.x < 0 || position.y < 0 || position.x >= window->width || position.y >= window->height || !window->hovering) {
|
|
element = window;
|
|
} else {
|
|
element = UIFindHoverElementRecursively(window, 0, 0, position);
|
|
}
|
|
|
|
if (window->hovered != element) {
|
|
EsElement *previous = window->hovered;
|
|
window->hovered = element;
|
|
|
|
EsMessage m = {};
|
|
m.type = ES_MSG_HOVERED_END;
|
|
EsMessageSend(previous, &m);
|
|
m.type = ES_MSG_HOVERED_START;
|
|
EsMessageSend(element, &m);
|
|
}
|
|
}
|
|
|
|
void UIRemoveFocusFromElement(EsElement *oldFocus) {
|
|
if (!oldFocus) return;
|
|
EsMessage m = {};
|
|
|
|
if (~oldFocus->state & UI_STATE_LOST_STRONG_FOCUS) {
|
|
m.type = ES_MSG_STRONG_FOCUS_END;
|
|
oldFocus->state |= UI_STATE_LOST_STRONG_FOCUS;
|
|
EsMessageSend(oldFocus, &m);
|
|
}
|
|
|
|
m.type = ES_MSG_FOCUSED_END;
|
|
oldFocus->state &= ~(UI_STATE_FOCUSED | UI_STATE_LOST_STRONG_FOCUS);
|
|
EsMessageSend(oldFocus, &m);
|
|
}
|
|
|
|
void UIMaybeRemoveFocusedElement(EsWindow *window) {
|
|
if (window->focused && !window->focused->IsFocusable()) {
|
|
EsElement *oldFocus = window->focused;
|
|
window->focused = nullptr;
|
|
UIRemoveFocusFromElement(oldFocus);
|
|
}
|
|
}
|
|
|
|
bool EsElementIsFocused(EsElement *element) {
|
|
return element->window->focused == element;
|
|
}
|
|
|
|
void UISendEnsureVisibleMessage(EsElement *element, EsGeneric) {
|
|
EsElement *child = element, *e = element;
|
|
bool center = element->state & UI_STATE_ENSURE_VISIBLE_CENTER;
|
|
EsAssert(element->state & UI_STATE_QUEUED_ENSURE_VISIBLE);
|
|
element->state &= ~(UI_STATE_QUEUED_ENSURE_VISIBLE | UI_STATE_ENSURE_VISIBLE_CENTER);
|
|
|
|
while (e->parent) {
|
|
EsMessage m = { ES_MSG_ENSURE_VISIBLE };
|
|
m.ensureVisible.descendent = child;
|
|
m.ensureVisible.center = center;
|
|
e = e->parent;
|
|
|
|
if (ES_HANDLED == EsMessageSend(e, &m)) {
|
|
child = e;
|
|
}
|
|
}
|
|
|
|
EsAssert(~element->state & UI_STATE_QUEUED_ENSURE_VISIBLE);
|
|
}
|
|
|
|
void UIQueueEnsureVisibleMessage(EsElement *element, bool center) {
|
|
if (center) {
|
|
element->state |= UI_STATE_ENSURE_VISIBLE_CENTER;
|
|
}
|
|
|
|
if (~element->state & UI_STATE_QUEUED_ENSURE_VISIBLE) {
|
|
element->state |= UI_STATE_QUEUED_ENSURE_VISIBLE;
|
|
UpdateAction action = {};
|
|
action.element = element;
|
|
action.callback = UISendEnsureVisibleMessage;
|
|
element->window->updateActions.Add(action);
|
|
}
|
|
}
|
|
|
|
void EsElementFocus(EsElement *element, uint32_t flags) {
|
|
EsMessageMutexCheck();
|
|
|
|
EsWindow *window = element->window;
|
|
EsMessage m;
|
|
|
|
// If this element is not focusable or if the window doesn't allow focused elements, ignore the request.
|
|
|
|
if (!element->IsFocusable() || (window->windowStyle == ES_WINDOW_CONTAINER)) return;
|
|
|
|
// If the element is already focused, then don't resend it any messages.
|
|
|
|
if (window->focused == element) return;
|
|
|
|
// If an element is already focused and the request is only to focus the element if there is no focused element, ignore the request.
|
|
|
|
if ((flags & ES_ELEMENT_FOCUS_ONLY_IF_NO_FOCUSED_ELEMENT) && window->focused) return;
|
|
|
|
// Tell the previously focused element it's no longer focused.
|
|
|
|
EsElement *oldFocus = window->focused;
|
|
window->focused = element;
|
|
UIRemoveFocusFromElement(oldFocus);
|
|
|
|
// Tell any parents of the previously focused element that aren't parents of the newly focused element that they no longer has focus-within,
|
|
// and the parents of the newly focused element that they have focus-within.
|
|
|
|
EsElement *parent = element->parent;
|
|
|
|
while (parent) {
|
|
parent->state |= UI_STATE_TEMP;
|
|
parent = parent->parent;
|
|
}
|
|
|
|
if (oldFocus) {
|
|
parent = oldFocus->parent;
|
|
|
|
while (parent) {
|
|
if (~parent->state & UI_STATE_TEMP) {
|
|
parent->state &= ~UI_STATE_FOCUS_WITHIN;
|
|
m.type = ES_MSG_FOCUS_WITHIN_END;
|
|
EsMessageSend(parent, &m);
|
|
}
|
|
|
|
parent = parent->parent;
|
|
}
|
|
}
|
|
|
|
parent = element->parent;
|
|
window->focused = element;
|
|
|
|
while (parent) {
|
|
if (~parent->state & UI_STATE_FOCUS_WITHIN) {
|
|
parent->state |= UI_STATE_FOCUS_WITHIN;
|
|
m.type = ES_MSG_FOCUS_WITHIN_START;
|
|
EsMessageSend(parent, &m);
|
|
|
|
EsAssert(window->focused == element); // Cannot change window focus from FOCUS_WITHIN_START message.
|
|
}
|
|
|
|
parent->state &= ~UI_STATE_TEMP;
|
|
parent = parent->parent;
|
|
}
|
|
|
|
// Tell the newly focused element it's focused.
|
|
|
|
m.type = ES_MSG_FOCUSED_START;
|
|
m.focus.flags = flags;
|
|
window->focused->state |= UI_STATE_FOCUSED;
|
|
EsMessageSend(element, &m);
|
|
EsAssert(window->focused == element); // Cannot change window focus from FOCUSED_START message.
|
|
|
|
// Ensure the element is visible.
|
|
|
|
if ((flags & ES_ELEMENT_FOCUS_ENSURE_VISIBLE) && element) {
|
|
UIQueueEnsureVisibleMessage(element, true);
|
|
}
|
|
}
|
|
|
|
EsRectangle EsElementGetWindowBounds(EsElement *element, bool client) {
|
|
EsElement *e = element;
|
|
int x = 0, y = 0;
|
|
|
|
while (element) {
|
|
x += element->offsetX, y += element->offsetY;
|
|
element = element->parent;
|
|
}
|
|
|
|
int cw = e->width - (client ? (e->internalOffsetLeft + e->internalOffsetRight) : 0);
|
|
int ch = e->height - (client ? (e->internalOffsetTop + e->internalOffsetBottom) : 0);
|
|
|
|
return ES_RECT_4(x, x + cw, y, y + ch);
|
|
}
|
|
|
|
EsRectangle EsElementGetScreenBounds(EsElement *element, bool client) {
|
|
EsRectangle elementBoundsInWindow = EsElementGetWindowBounds(element, client);
|
|
EsRectangle windowBoundsInScreen = EsWindowGetBounds(element->window);
|
|
return Translate(elementBoundsInWindow, windowBoundsInScreen.l, windowBoundsInScreen.t);
|
|
}
|
|
|
|
void EsElementInsertAfter(EsElement *element) {
|
|
EsAssert(!gui.insertAfter);
|
|
gui.insertAfter = element;
|
|
}
|
|
|
|
void EsElement::Initialise(EsElement *_parent, uint64_t _flags, EsElementCallback _classCallback, EsStyleID _style) {
|
|
EsMessageMutexCheck();
|
|
|
|
// EsPrint("New element '%z' %x with parent %x.\n", _debugName, this, _parent);
|
|
|
|
messageClass = _classCallback;
|
|
flags = _flags;
|
|
|
|
if (gui.insertAfter) {
|
|
if (_parent) {
|
|
EsAssert(_parent == gui.insertAfter || _parent == gui.insertAfter->parent);
|
|
} else {
|
|
_parent = gui.insertAfter->parent;
|
|
}
|
|
}
|
|
|
|
if (_parent) {
|
|
if (!_parent->parent && (~_flags & ES_ELEMENT_NON_CLIENT)) {
|
|
_parent = WindowGetMainPanel((EsWindow *) _parent);
|
|
}
|
|
|
|
parent = _parent;
|
|
window = parent->window;
|
|
instance = window->instance;
|
|
|
|
if (parent->flags & ES_ELEMENT_DISABLED) flags |= ES_ELEMENT_DISABLED;
|
|
|
|
if (~flags & ES_ELEMENT_NON_CLIENT) {
|
|
EsMessage m = {};
|
|
m.type = ES_MSG_PRE_ADD_CHILD;
|
|
m.child = this;
|
|
EsMessageSend(parent, &m);
|
|
}
|
|
|
|
if (gui.insertAfter == _parent) {
|
|
parent->children.Insert(this, 0);
|
|
gui.insertAfter = nullptr;
|
|
} else if (gui.insertAfter) {
|
|
uintptr_t i = parent->children.Find(gui.insertAfter, true);
|
|
parent->children.Insert(this, i + 1);
|
|
gui.insertAfter = nullptr;
|
|
} else if (flags & ES_ELEMENT_NON_CLIENT) {
|
|
parent->children.Add(this);
|
|
} else {
|
|
for (uintptr_t i = parent->children.Length();; i--) {
|
|
if (i == 0 || (~parent->children[i - 1]->flags & ES_ELEMENT_NON_CLIENT)) {
|
|
parent->children.Insert(this, i);
|
|
break;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (~flags & ES_ELEMENT_NON_CLIENT) {
|
|
EsMessage m = {};
|
|
m.type = ES_MSG_ADD_CHILD;
|
|
m.child = this;
|
|
EsMessageSend(parent, &m);
|
|
}
|
|
|
|
EsElementUpdateContentSize(parent);
|
|
}
|
|
|
|
SetStyle(_style, false);
|
|
RefreshStyle();
|
|
InspectorNotifyElementCreated(this);
|
|
}
|
|
|
|
EsRectangle EsElementGetInsets(EsElement *element) {
|
|
EsMessageMutexCheck();
|
|
return element->style->insets;
|
|
}
|
|
|
|
EsThemeMetrics EsElementGetMetrics(EsElement *element) {
|
|
EsMessageMutexCheck();
|
|
EsThemeMetrics m = {};
|
|
ThemeMetrics *metrics = element->style->metrics;
|
|
#define RECTANGLE_8_TO_ES_RECTANGLE(x) { (int32_t) (x).l, (int32_t) (x).r, (int32_t) (x).t, (int32_t) (x).b }
|
|
m.insets = RECTANGLE_8_TO_ES_RECTANGLE(metrics->insets);
|
|
m.clipInsets = RECTANGLE_8_TO_ES_RECTANGLE(metrics->clipInsets);
|
|
m.clipEnabled = metrics->clipEnabled;
|
|
m.cursor = (EsCursorStyle) metrics->cursor;
|
|
m.preferredWidth = metrics->preferredWidth;
|
|
m.preferredHeight = metrics->preferredHeight;
|
|
m.minimumWidth = metrics->minimumWidth;
|
|
m.minimumHeight = metrics->minimumHeight;
|
|
m.maximumWidth = metrics->maximumWidth;
|
|
m.maximumHeight = metrics->maximumHeight;
|
|
m.gapMajor = metrics->gapMajor;
|
|
m.gapMinor = metrics->gapMinor;
|
|
m.gapWrap = metrics->gapWrap;
|
|
m.textColor = metrics->textColor;
|
|
m.selectedBackground = metrics->selectedBackground;
|
|
m.selectedText = metrics->selectedText;
|
|
m.iconColor = metrics->iconColor;
|
|
m.textAlign = metrics->textAlign;
|
|
m.textSize = metrics->textSize;
|
|
m.fontFamily = metrics->fontFamily;
|
|
m.fontWeight = metrics->fontWeight;
|
|
m.iconSize = metrics->iconSize;
|
|
m.isItalic = metrics->isItalic;
|
|
return m;
|
|
}
|
|
|
|
float EsElementGetScaleFactor(EsElement *element) {
|
|
EsAssert(element);
|
|
return theming.scale;
|
|
}
|
|
|
|
void EsElementSetCallback(EsElement *element, EsElementCallback callback) {
|
|
EsMessageMutexCheck();
|
|
element->messageUser = callback;
|
|
}
|
|
|
|
void EsElementSetHidden(EsElement *element, bool hidden) {
|
|
EsMessageMutexCheck();
|
|
|
|
bool old = element->flags & ES_ELEMENT_HIDDEN;
|
|
if (old == hidden) return;
|
|
|
|
if (hidden) {
|
|
element->flags |= ES_ELEMENT_HIDDEN;
|
|
} else {
|
|
element->flags &= ~ES_ELEMENT_HIDDEN;
|
|
}
|
|
|
|
EsElementUpdateContentSize(element->parent);
|
|
UIMaybeRemoveFocusedElement(element->window);
|
|
}
|
|
|
|
bool EsElementIsHidden(EsElement *element) {
|
|
EsMessageMutexCheck();
|
|
return element->flags & ES_ELEMENT_HIDDEN;
|
|
}
|
|
|
|
void EsElementSetDisabled(EsElement *element, bool disabled) {
|
|
EsMessageMutexCheck();
|
|
|
|
if (element->window->focused == element) {
|
|
element->window->focused = nullptr;
|
|
UIRemoveFocusFromElement(element);
|
|
}
|
|
|
|
for (uintptr_t i = 0; i < element->GetChildCount(); i++) {
|
|
if (element->GetChild(i)->flags & ES_ELEMENT_NON_CLIENT) continue;
|
|
EsElementSetDisabled(element->GetChild(i), disabled);
|
|
}
|
|
|
|
if ((element->flags & ES_ELEMENT_DISABLED) && disabled) return;
|
|
if ((~element->flags & ES_ELEMENT_DISABLED) && !disabled) return;
|
|
|
|
if (disabled) element->flags |= ES_ELEMENT_DISABLED;
|
|
else element->flags &= ~ES_ELEMENT_DISABLED;
|
|
|
|
element->MaybeRefreshStyle();
|
|
}
|
|
|
|
void EsElementDestroy(EsElement *element) {
|
|
EsMessageMutexCheck();
|
|
element->Destroy();
|
|
}
|
|
|
|
void EsElementDestroyContents(EsElement *element) {
|
|
EsMessageMutexCheck();
|
|
|
|
for (uintptr_t i = 0; i < element->GetChildCount(); i++) {
|
|
if (element->GetChild(i)->flags & ES_ELEMENT_NON_CLIENT) continue;
|
|
element->GetChild(i)->Destroy();
|
|
}
|
|
|
|
EsMessage m = {};
|
|
m.type = ES_MSG_DESTROY_CONTENTS;
|
|
EsMessageSend(element, &m);
|
|
}
|
|
|
|
EsElement *UITabTraversalGetChild(EsElement *element, uintptr_t index) {
|
|
if (element->flags & ES_ELEMENT_LAYOUT_HINT_REVERSE) {
|
|
return element->children[element->children.Length() - index - 1];
|
|
} else {
|
|
return element->children[index];
|
|
}
|
|
}
|
|
|
|
EsElement *UITabTraversalDo(EsElement *element, bool shift) {
|
|
if (shift) {
|
|
if (element->parent && UITabTraversalGetChild(element->parent, 0) != element) {
|
|
for (uintptr_t i = 1; i < element->parent->children.Length(); i++) {
|
|
if (UITabTraversalGetChild(element->parent, i) == element) {
|
|
element = UITabTraversalGetChild(element->parent, i - 1);
|
|
|
|
while (element->children.Length()) {
|
|
element = UITabTraversalGetChild(element, element->children.Length() - 1);
|
|
}
|
|
|
|
return element;
|
|
}
|
|
}
|
|
} else if (element->parent) {
|
|
return element->parent;
|
|
} else {
|
|
while (element->children.Length()) {
|
|
element = UITabTraversalGetChild(element, element->children.Length() - 1);
|
|
}
|
|
|
|
return element;
|
|
}
|
|
} else {
|
|
if (element->children.Length()) {
|
|
return UITabTraversalGetChild(element, 0);
|
|
} else while (element->parent) {
|
|
EsElement *child = element;
|
|
element = element->parent;
|
|
|
|
for (uintptr_t i = 0; i < element->children.Length() - 1u; i++) {
|
|
if (UITabTraversalGetChild(element, i) == child) {
|
|
element = UITabTraversalGetChild(element, i + 1);
|
|
return element;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return element;
|
|
}
|
|
|
|
uint8_t EsKeyboardGetModifiers() {
|
|
return gui.leftModifiers | gui.rightModifiers;
|
|
}
|
|
|
|
bool EsMouseIsLeftHeld() { return gui.mouseButtonDown && gui.lastClickButton == ES_MSG_MOUSE_LEFT_DOWN; }
|
|
bool EsMouseIsRightHeld() { return gui.mouseButtonDown && gui.lastClickButton == ES_MSG_MOUSE_RIGHT_DOWN; }
|
|
bool EsMouseIsMiddleHeld() { return gui.mouseButtonDown && gui.lastClickButton == ES_MSG_MOUSE_MIDDLE_DOWN; }
|
|
|
|
void UIScaleChanged(EsElement *element, EsMessage *message) {
|
|
EsMessageMutexCheck();
|
|
|
|
element->RefreshStyle(nullptr, false, true);
|
|
element->offsetX = element->offsetY = element->width = element->height = 0;
|
|
EsMessageSend(element, message);
|
|
|
|
for (uintptr_t i = 0; i < element->children.Length(); i++) {
|
|
UIScaleChanged(element->children[i], message);
|
|
}
|
|
}
|
|
|
|
void _EsUISetFont(EsFontFamily id) {
|
|
fontManagement.sans = id;
|
|
EsMessage m = { ES_MSG_UI_SCALE_CHANGED };
|
|
|
|
for (uintptr_t i = 0; i < gui.allWindows.Length(); i++) {
|
|
UIScaleChanged(gui.allWindows[i], &m);
|
|
EsElementRelayout(gui.allWindows[i]);
|
|
}
|
|
}
|
|
|
|
void UIMaybeRefreshStyleAll(EsElement *element) {
|
|
element->MaybeRefreshStyle();
|
|
|
|
for (uintptr_t i = 0; i < element->children.Length(); i++) {
|
|
UIMaybeRefreshStyleAll(element->children[i]);
|
|
}
|
|
}
|
|
|
|
void EsElementGetSize(EsElement *element, int *width, int *height) {
|
|
EsMessageMutexCheck();
|
|
|
|
EsRectangle bounds = element->GetBounds();
|
|
*width = bounds.r;
|
|
*height = bounds.b;
|
|
}
|
|
|
|
void EsElementGetTextStyle(EsElement *element, EsTextStyle *style) {
|
|
element->style->GetTextStyle(style);
|
|
}
|
|
|
|
void EsElementRepaint(EsElement *element, const EsRectangle *region) {
|
|
EsMessageMutexCheck();
|
|
if (region) element->Repaint(false, *region);
|
|
else element->Repaint(true /* repaint all */);
|
|
}
|
|
|
|
void EsElementRepaintForScroll(EsElement *element, EsMessage *message, EsRectangle border) {
|
|
// TODO Improved fast scroll:
|
|
// - Set a scroll rectangle in the window.
|
|
// - If one is already marked, then don't attempt a fast scroll.
|
|
// - When painting, exclude anything containing in the scroll rectangle.
|
|
// - When sending bits to the WM, give it the scroll rectangle to apply first.
|
|
|
|
EsElementRepaint(element);
|
|
|
|
(void) message;
|
|
(void) border;
|
|
}
|
|
|
|
bool EsElementStartAnimating(EsElement *element) {
|
|
EsMessageMutexCheck();
|
|
|
|
return element->StartAnimating();
|
|
}
|
|
|
|
EsElement *EsElementGetLayoutParent(EsElement *element) {
|
|
EsMessageMutexCheck();
|
|
|
|
return element->parent;
|
|
}
|
|
|
|
void EsElementDraw(EsElement *element, EsPainter *painter) {
|
|
element->InternalPaint(painter, PAINT_SHADOW);
|
|
element->InternalPaint(painter, ES_FLAGS_DEFAULT);
|
|
element->InternalPaint(painter, PAINT_OVERLAY);
|
|
}
|
|
|
|
int UIMessageSendPropagateToAncestors(EsElement *element, EsMessage *message, EsElement **handler = nullptr) {
|
|
while (element) {
|
|
int response = EsMessageSend(element, message);
|
|
|
|
if (response) {
|
|
if (handler && (~element->state & UI_STATE_DESTROYING)) {
|
|
*handler = element;
|
|
}
|
|
|
|
return response;
|
|
}
|
|
|
|
element = element->parent;
|
|
}
|
|
|
|
if (handler) *handler = nullptr;
|
|
return 0;
|
|
}
|
|
|
|
void UIMouseDown(EsWindow *window, EsMessage *message) {
|
|
window->mousePosition.x = message->mouseDown.positionX;
|
|
window->mousePosition.y = message->mouseDown.positionY;
|
|
|
|
AccessKeyModeExit();
|
|
|
|
double timeStampMs = EsTimeStampMs();
|
|
|
|
if (gui.clickChainStartMs + api.global->clickChainTimeoutMs < timeStampMs
|
|
|| window->hovered != gui.clickChainElement) {
|
|
// Start a new click chain.
|
|
gui.clickChainStartMs = timeStampMs;
|
|
gui.clickChainCount = 1;
|
|
gui.clickChainElement = window->hovered;
|
|
} else {
|
|
gui.clickChainStartMs = timeStampMs;
|
|
gui.clickChainCount++;
|
|
}
|
|
|
|
message->mouseDown.clickChainCount = gui.clickChainCount;
|
|
|
|
gui.lastClickX = message->mouseDown.positionX;
|
|
gui.lastClickY = message->mouseDown.positionY;
|
|
gui.lastClickButton = message->type;
|
|
gui.mouseButtonDown = true;
|
|
|
|
if ((~window->hovered->flags & ES_ELEMENT_DISABLED) && (~window->hovered->state & UI_STATE_BLOCK_INTERACTION)) {
|
|
// If the hovered element is destroyed in response to one of these messages,
|
|
// window->hovered will be set to nullptr, so save the element here.
|
|
EsElement *element = window->hovered;
|
|
|
|
window->pressed = element;
|
|
EsMessage m = { ES_MSG_PRESSED_START };
|
|
EsMessageSend(element, &m);
|
|
|
|
EsRectangle bounds = element->GetWindowBounds();
|
|
message->mouseDown.positionX -= bounds.l;
|
|
message->mouseDown.positionY -= bounds.t;
|
|
|
|
if (ES_REJECTED != UIMessageSendPropagateToAncestors(element, message, &window->dragged)) {
|
|
if (window->dragged && (~window->dragged->flags & ES_ELEMENT_NO_FOCUS_ON_CLICK)) {
|
|
EsElementFocus(window->dragged, false);
|
|
}
|
|
}
|
|
}
|
|
|
|
if (window->hovered != window->focused && window->focused && (~window->focused->state & UI_STATE_LOST_STRONG_FOCUS)) {
|
|
EsMessage m = { ES_MSG_STRONG_FOCUS_END };
|
|
window->focused->state |= UI_STATE_LOST_STRONG_FOCUS;
|
|
EsMessageSend(window->focused, &m);
|
|
}
|
|
}
|
|
|
|
void UIMouseUp(EsWindow *window, EsMessage *message, bool sendClick) {
|
|
gui.mouseButtonDown = false;
|
|
window->dragged = nullptr;
|
|
|
|
if (window->pressed) {
|
|
EsElement *pressed = window->pressed;
|
|
window->pressed = nullptr;
|
|
|
|
if (message) {
|
|
EsRectangle bounds = pressed->GetWindowBounds();
|
|
message->mouseDown.positionX -= bounds.l;
|
|
message->mouseDown.positionY -= bounds.t;
|
|
EsMessageSend(pressed, message);
|
|
} else {
|
|
EsMessage m = {};
|
|
m.type = (EsMessageType) (gui.lastClickButton + 1);
|
|
EsMessageSend(pressed, &m);
|
|
}
|
|
|
|
EsMessage m = { ES_MSG_PRESSED_END };
|
|
EsMessageSend(pressed, &m);
|
|
|
|
if (message && window->hovered == pressed && !gui.draggingStarted && sendClick) {
|
|
if (message->type == ES_MSG_MOUSE_LEFT_UP) {
|
|
m.type = ES_MSG_MOUSE_LEFT_CLICK;
|
|
EsMessageSend(pressed, &m);
|
|
} else if (message->type == ES_MSG_MOUSE_RIGHT_UP) {
|
|
m.type = ES_MSG_MOUSE_RIGHT_CLICK;
|
|
EsMessageSend(pressed, &m);
|
|
} else if (message->type == ES_MSG_MOUSE_MIDDLE_UP) {
|
|
m.type = ES_MSG_MOUSE_MIDDLE_CLICK;
|
|
EsMessageSend(pressed, &m);
|
|
}
|
|
}
|
|
|
|
if (window->hovered) window->hovered->MaybeRefreshStyle();
|
|
}
|
|
|
|
gui.draggingStarted = false;
|
|
}
|
|
|
|
void UIInitialiseKeyboardShortcutNamesTable() {
|
|
#define ADD_KEYBOARD_SHORTCUT_NAME(a, b) HashTablePutShort(&gui.keyboardShortcutNames, a, (void *) b)
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_A, "A");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_B, "B");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_C, "C");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_D, "D");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_E, "E");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_F, "F");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_G, "G");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_H, "H");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_I, "I");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_J, "J");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_K, "K");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_L, "L");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_M, "M");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_N, "N");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_O, "O");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_P, "P");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_Q, "Q");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_R, "R");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_S, "S");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_T, "T");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_U, "U");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_V, "V");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_W, "W");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_X, "X");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_Y, "Y");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_Z, "Z");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_0, "0");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_1, "1");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_2, "2");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_3, "3");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_4, "4");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_5, "5");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_6, "6");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_7, "7");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_8, "8");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_9, "9");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_BACKSPACE, "Backspace");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_ESCAPE, "Esc");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_INSERT, "Ins");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_HOME, "Home");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_PAGE_UP, "PgUp");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_DELETE, "Del");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_END, "End");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_PAGE_DOWN, "PgDn");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_UP_ARROW, "Up");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_LEFT_ARROW, "Left");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_DOWN_ARROW, "Down");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_RIGHT_ARROW, "Right");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_SPACE, "Space");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_TAB, "Tab");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_ENTER, "Enter");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_F1, "F1");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_F2, "F2");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_F3, "F3");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_F4, "F4");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_F5, "F5");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_F6, "F6");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_F7, "F7");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_F8, "F8");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_F9, "F9");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_F10, "F10");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_F11, "F11");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_F12, "F12");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_MM_NEXT, "Next Track");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_MM_PREVIOUS, "Previous Track");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_MM_STOP, "Stop Media");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_MM_PAUSE, "Play/Pause");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_MM_MUTE, "Mute");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_MM_QUIETER, "Quieter");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_MM_LOUDER, "Louder");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_MM_SELECT, "Open Media");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_WWW_SEARCH, "Search");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_WWW_HOME, "Homepage");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_WWW_BACK, "Back");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_WWW_FORWARD, "Forward");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_WWW_STOP, "Stop");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_WWW_REFRESH, "Refresh");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_WWW_STARRED, "Bookmark");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_SLASH, "/");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_PUNCTUATION_1, "\\");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_LEFT_BRACE, "(");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_RIGHT_BRACE, ")");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_EQUALS, "=");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_PUNCTUATION_5, "`");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_HYPHEN, "-");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_PUNCTUATION_3, ";");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_PUNCTUATION_4, "'");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_COMMA, ",");
|
|
ADD_KEYBOARD_SHORTCUT_NAME(ES_SCANCODE_PERIOD, ".");
|
|
}
|
|
|
|
void AccessKeysCenterHint(EsElement *element, EsMessage *message) {
|
|
EsRectangle bounds = element->GetWindowBounds();
|
|
UIStyle *style = gui.accessKeys.hintStyle;
|
|
int x = (bounds.l + bounds.r) / 2, y = (bounds.t + bounds.b) / 2 - style->preferredHeight / 4;
|
|
*message->accessKeyHintBounds = ES_RECT_4(x - style->preferredWidth / 2, x + style->preferredWidth / 2,
|
|
y - style->preferredHeight / 4, y + 3 * style->preferredHeight / 4);
|
|
}
|
|
|
|
void AccessKeysGather(EsElement *element) {
|
|
if (element->flags & ES_ELEMENT_BLOCK_FOCUS) return;
|
|
if (element->state & UI_STATE_BLOCK_INTERACTION) return;
|
|
if (element->flags & ES_ELEMENT_HIDDEN) return;
|
|
|
|
for (uintptr_t i = 0; i < element->children.Length(); i++) {
|
|
AccessKeysGather(element->children[i]);
|
|
}
|
|
|
|
if (!element->accessKey) return;
|
|
if (element->state & UI_STATE_DESTROYING) return;
|
|
if (element->flags & ES_ELEMENT_DISABLED) return;
|
|
|
|
AccessKeyEntry entry = {};
|
|
entry.character = element->accessKey;
|
|
entry.number = gui.accessKeys.numbers[entry.character - 'A'];
|
|
entry.element = element;
|
|
|
|
if (entry.number >= 10) return;
|
|
|
|
EsRectangle bounds = element->GetWindowBounds();
|
|
UIStyle *style = gui.accessKeys.hintStyle;
|
|
int x = (bounds.l + bounds.r) / 2, y = bounds.b;
|
|
EsRectangle hintBounds = ES_RECT_4(x - style->preferredWidth / 2, x + style->preferredWidth / 2,
|
|
y - style->preferredHeight / 4, y + 3 * style->preferredHeight / 4);
|
|
|
|
EsMessage m = {};
|
|
m.type = ES_MSG_GET_ACCESS_KEY_HINT_BOUNDS;
|
|
m.accessKeyHintBounds = &hintBounds;
|
|
EsMessageSend(element, &m);
|
|
|
|
if (hintBounds.r > (int32_t) gui.accessKeys.window->windowWidth) {
|
|
hintBounds.l = gui.accessKeys.window->windowWidth - style->preferredWidth;
|
|
hintBounds.r = hintBounds.l + style->preferredWidth;
|
|
}
|
|
|
|
if (hintBounds.l < 0) {
|
|
hintBounds.l = 0;
|
|
hintBounds.r = hintBounds.l + style->preferredWidth;
|
|
}
|
|
|
|
if (hintBounds.b > (int32_t) gui.accessKeys.window->windowHeight) {
|
|
hintBounds.t = gui.accessKeys.window->windowHeight - style->preferredHeight;
|
|
hintBounds.b = hintBounds.t + style->preferredHeight;
|
|
}
|
|
|
|
if (hintBounds.t < 0) {
|
|
hintBounds.t = 0;
|
|
hintBounds.b = hintBounds.t + style->preferredHeight;
|
|
}
|
|
|
|
entry.bounds = hintBounds;
|
|
|
|
if (gui.accessKeys.entries.Add(entry)) {
|
|
gui.accessKeys.numbers[entry.character - 'A']++;
|
|
}
|
|
}
|
|
|
|
void AccessKeyHintsShow(EsPainter *painter) {
|
|
for (uintptr_t i = 0; i < gui.accessKeys.entries.Length(); i++) {
|
|
AccessKeyEntry *entry = &gui.accessKeys.entries[i];
|
|
UIStyle *style = gui.accessKeys.hintStyle;
|
|
|
|
if (gui.accessKeys.typedCharacter && entry->character != gui.accessKeys.typedCharacter) {
|
|
continue;
|
|
}
|
|
|
|
style->PaintLayers(painter, entry->bounds, 0, THEME_LAYER_MODE_BACKGROUND);
|
|
char c = gui.accessKeys.typedCharacter ? entry->number + '0' : entry->character;
|
|
style->PaintText(painter, gui.accessKeys.window, entry->bounds, &c, 1, 0, ES_FLAGS_DEFAULT);
|
|
}
|
|
}
|
|
|
|
int AccessKeyLayerMessage(EsElement *element, EsMessage *message) {
|
|
if (message->type == ES_MSG_PAINT && gui.accessKeyMode && gui.accessKeys.window == element->window) {
|
|
AccessKeyHintsShow(message->painter);
|
|
}
|
|
|
|
return 0;
|
|
}
|
|
|
|
void AccessKeyModeEnter(EsWindow *window) {
|
|
if (window->dialogs.Length() || gui.menuMode || gui.accessKeyMode || window->windowStyle != ES_WINDOW_NORMAL) {
|
|
return;
|
|
}
|
|
|
|
if (!gui.accessKeys.hintStyle) {
|
|
gui.accessKeys.hintStyle = GetStyle(MakeStyleKey(ES_STYLE_ACCESS_KEY_HINT, 0), true);
|
|
}
|
|
|
|
gui.accessKeyMode = true;
|
|
gui.accessKeys.window = window;
|
|
AccessKeysGather(window);
|
|
|
|
for (uintptr_t i = 0; i < gui.accessKeys.entries.Length(); i++) {
|
|
if (gui.accessKeys.numbers[gui.accessKeys.entries[i].character - 'A'] == 1) {
|
|
gui.accessKeys.entries[i].number = -1;
|
|
}
|
|
}
|
|
|
|
window->Repaint(true);
|
|
}
|
|
|
|
void AccessKeyModeExit() {
|
|
if (!gui.accessKeyMode) {
|
|
return;
|
|
}
|
|
|
|
gui.accessKeys.entries.Free();
|
|
gui.accessKeys.window->Repaint(true);
|
|
EsMemoryZero(gui.accessKeys.numbers, sizeof(gui.accessKeys.numbers));
|
|
gui.accessKeys.typedCharacter = 0;
|
|
gui.accessKeys.window = nullptr;
|
|
gui.accessKeyMode = false;
|
|
}
|
|
|
|
void AccessKeyModeHandleKeyPress(EsMessage *message) {
|
|
if (message->type == ES_MSG_KEY_UP || message->keyboard.scancode == ES_SCANCODE_LEFT_ALT) {
|
|
return;
|
|
}
|
|
|
|
EsWindow *window = gui.accessKeys.window;
|
|
|
|
const char *inputString = KeyboardLayoutLookup(message->keyboard.scancode,
|
|
message->keyboard.modifiers & ES_MODIFIER_SHIFT, message->keyboard.modifiers & ES_MODIFIER_ALT_GR,
|
|
false, false);
|
|
int ic = EsCRTtoupper(inputString ? *inputString : 0);
|
|
|
|
bool keepAccessKeyModeActive = false;
|
|
bool regatherKeys = false;
|
|
|
|
if (ic >= 'A' && ic <= 'Z' && !gui.accessKeys.typedCharacter) {
|
|
if (gui.accessKeys.numbers[ic - 'A'] > 1) {
|
|
keepAccessKeyModeActive = true;
|
|
gui.accessKeys.typedCharacter = ic;
|
|
} else if (gui.accessKeys.numbers[ic - 'A'] == 1) {
|
|
for (uintptr_t i = 0; i < gui.accessKeys.entries.Length(); i++) {
|
|
AccessKeyEntry *entry = &gui.accessKeys.entries[i];
|
|
|
|
if (entry->character == ic) {
|
|
EsMessage m = { ES_MSG_MOUSE_LEFT_CLICK };
|
|
EsMessageSend(entry->element, &m);
|
|
EsElementFocus(entry->element, ES_ELEMENT_FOCUS_ENSURE_VISIBLE | ES_ELEMENT_FOCUS_FROM_KEYBOARD);
|
|
|
|
keepAccessKeyModeActive = entry->element->flags & ES_ELEMENT_STICKY_ACCESS_KEY;
|
|
regatherKeys = true;
|
|
}
|
|
}
|
|
}
|
|
} else if (ic >= '0' && ic <= '9' && gui.accessKeys.typedCharacter) {
|
|
for (uintptr_t i = 0; i < gui.accessKeys.entries.Length(); i++) {
|
|
AccessKeyEntry *entry = &gui.accessKeys.entries[i];
|
|
|
|
if (entry->character == gui.accessKeys.typedCharacter && entry->number == ic - '0') {
|
|
EsMessage m = { ES_MSG_MOUSE_LEFT_CLICK };
|
|
EsMessageSend(entry->element, &m);
|
|
EsElementFocus(entry->element, ES_ELEMENT_FOCUS_ENSURE_VISIBLE | ES_ELEMENT_FOCUS_FROM_KEYBOARD);
|
|
|
|
keepAccessKeyModeActive = entry->element->flags & ES_ELEMENT_STICKY_ACCESS_KEY;
|
|
regatherKeys = true;
|
|
}
|
|
}
|
|
}
|
|
|
|
if (!keepAccessKeyModeActive) {
|
|
AccessKeyModeExit();
|
|
} else if (regatherKeys) {
|
|
AccessKeyModeExit();
|
|
UIWindowLayoutNow(window, nullptr);
|
|
AccessKeyModeEnter(window);
|
|
} else {
|
|
window->Repaint(true);
|
|
}
|
|
}
|
|
|
|
void UIRefreshPrimaryClipboard(EsWindow *window) {
|
|
if (window->focused) {
|
|
EsMessage m = {};
|
|
m.type = ES_MSG_PRIMARY_CLIPBOARD_UPDATED;
|
|
EsMessageSend(window->focused, &m);
|
|
}
|
|
}
|
|
|
|
bool UIHandleKeyMessage(EsWindow *window, EsMessage *message) {
|
|
if (message->type == ES_MSG_KEY_UP) {
|
|
if (message->keyboard.scancode == ES_SCANCODE_LEFT_CTRL ) gui.leftModifiers &= ~ES_MODIFIER_CTRL;
|
|
if (message->keyboard.scancode == ES_SCANCODE_LEFT_ALT ) gui.leftModifiers &= ~ES_MODIFIER_ALT;
|
|
if (message->keyboard.scancode == ES_SCANCODE_LEFT_SHIFT ) gui.leftModifiers &= ~ES_MODIFIER_SHIFT;
|
|
if (message->keyboard.scancode == ES_SCANCODE_LEFT_FLAG ) gui.leftModifiers &= ~ES_MODIFIER_FLAG;
|
|
if (message->keyboard.scancode == ES_SCANCODE_RIGHT_CTRL ) gui.rightModifiers &= ~ES_MODIFIER_CTRL;
|
|
if (message->keyboard.scancode == ES_SCANCODE_RIGHT_ALT ) gui.rightModifiers &= ~ES_MODIFIER_ALT_GR;
|
|
if (message->keyboard.scancode == ES_SCANCODE_RIGHT_SHIFT) gui.rightModifiers &= ~ES_MODIFIER_SHIFT;
|
|
if (message->keyboard.scancode == ES_SCANCODE_RIGHT_FLAG ) gui.rightModifiers &= ~ES_MODIFIER_FLAG;
|
|
|
|
if (message->keyboard.scancode == ES_SCANCODE_LEFT_ALT && message->keyboard.single) {
|
|
AccessKeyModeEnter(window);
|
|
return true;
|
|
} else if (window->focused) {
|
|
return ES_HANDLED == EsMessageSend(window->focused, message);
|
|
} else {
|
|
return ES_HANDLED == EsMessageSend(window, message);
|
|
}
|
|
}
|
|
|
|
if (window->targetMenu) {
|
|
window = window->targetMenu;
|
|
}
|
|
|
|
if (message->keyboard.scancode == ES_SCANCODE_F2 && message->keyboard.modifiers == ES_MODIFIER_ALT) {
|
|
EnterDebugger();
|
|
EsPrint("[Alt-F2]\n");
|
|
}
|
|
|
|
if (message->keyboard.scancode == ES_SCANCODE_LEFT_CTRL ) gui.leftModifiers |= ES_MODIFIER_CTRL;
|
|
if (message->keyboard.scancode == ES_SCANCODE_LEFT_ALT ) gui.leftModifiers |= ES_MODIFIER_ALT;
|
|
if (message->keyboard.scancode == ES_SCANCODE_LEFT_SHIFT ) gui.leftModifiers |= ES_MODIFIER_SHIFT;
|
|
if (message->keyboard.scancode == ES_SCANCODE_LEFT_FLAG ) gui.leftModifiers |= ES_MODIFIER_FLAG;
|
|
if (message->keyboard.scancode == ES_SCANCODE_RIGHT_CTRL ) gui.rightModifiers |= ES_MODIFIER_CTRL;
|
|
if (message->keyboard.scancode == ES_SCANCODE_RIGHT_ALT ) gui.rightModifiers |= ES_MODIFIER_ALT_GR;
|
|
if (message->keyboard.scancode == ES_SCANCODE_RIGHT_SHIFT) gui.rightModifiers |= ES_MODIFIER_SHIFT;
|
|
if (message->keyboard.scancode == ES_SCANCODE_RIGHT_FLAG ) gui.rightModifiers |= ES_MODIFIER_FLAG;
|
|
|
|
if (window->windowStyle == ES_WINDOW_MENU && message->keyboard.scancode == ES_SCANCODE_ESCAPE) {
|
|
EsMenuClose((EsMenu *) window);
|
|
return true;
|
|
}
|
|
|
|
if (gui.menuMode) {
|
|
// TODO Check the window is the one that enabled menu mode.
|
|
// TODO Escape to close/exit menu mode.
|
|
// TODO Left/right to navigate columns/cycle menubar and open/close submenus.
|
|
// TODO Up/down to traverse menu.
|
|
// TODO Enter to open submenu/invoke item.
|
|
return true;
|
|
} else if (gui.accessKeyMode && gui.accessKeys.window == window) {
|
|
AccessKeyModeHandleKeyPress(message);
|
|
return true;
|
|
}
|
|
|
|
if (window->pressed) {
|
|
if (message->keyboard.scancode == ES_SCANCODE_ESCAPE) {
|
|
UIMouseUp(window, nullptr, false);
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (window->focused) {
|
|
message->type = ES_MSG_KEY_TYPED;
|
|
|
|
if (EsMessageSend(window->focused, message) == ES_HANDLED /* allow messageUser to reject input */) {
|
|
return true;
|
|
}
|
|
|
|
EsElement *element = window->focused;
|
|
message->type = ES_MSG_KEY_DOWN;
|
|
|
|
if (UIMessageSendPropagateToAncestors(element, message)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
if (message->keyboard.scancode == ES_SCANCODE_TAB && (message->keyboard.modifiers & ~ES_MODIFIER_SHIFT) == 0) {
|
|
EsElement *element = window->focused ?: window;
|
|
EsElement *start = element;
|
|
bool backwards = message->keyboard.modifiers & ES_MODIFIER_SHIFT;
|
|
|
|
tryAgain:
|
|
element = UITabTraversalDo(element, backwards);
|
|
if (!element->IsTabTraversable() && element != start) goto tryAgain;
|
|
|
|
if (element->state & UI_STATE_RADIO_GROUP) {
|
|
if (backwards && start->parent == element) {
|
|
goto tryAgain;
|
|
} else {
|
|
element = EsPanelRadioGroupGetChecked((EsPanel *) element);
|
|
}
|
|
}
|
|
|
|
EsElementFocus(element, ES_ELEMENT_FOCUS_ENSURE_VISIBLE | ES_ELEMENT_FOCUS_FROM_KEYBOARD);
|
|
return true;
|
|
}
|
|
|
|
if (window->focused) {
|
|
if (message->keyboard.scancode == ES_SCANCODE_SPACE && message->keyboard.modifiers == 0) {
|
|
EsMessage m = { ES_MSG_MOUSE_LEFT_CLICK };
|
|
EsMessageSend(window->focused, &m);
|
|
return true;
|
|
}
|
|
} else {
|
|
if (ES_HANDLED == EsMessageSend(window, message)) {
|
|
return true;
|
|
}
|
|
}
|
|
|
|
// TODO Radio group navigation.
|
|
|
|
if (window->enterButton && message->keyboard.scancode == ES_SCANCODE_ENTER && !message->keyboard.modifiers
|
|
&& window->enterButton->onCommand && (~window->enterButton->flags & ES_ELEMENT_DISABLED)) {
|
|
window->enterButton->onCommand(window->instance, window->enterButton, window->enterButton->command);
|
|
return true;
|
|
} else if (window->escapeButton && message->keyboard.scancode == ES_SCANCODE_ESCAPE && !message->keyboard.modifiers
|
|
&& window->escapeButton->onCommand && (~window->escapeButton->flags & ES_ELEMENT_DISABLED)) {
|
|
window->escapeButton->onCommand(window->instance, window->escapeButton, window->escapeButton->command);
|
|
return true;
|
|
}
|
|
|
|
if (!window->dialogs.Length()) {
|
|
// TODO Sort out what commands can be used from within dialogs and menus.
|
|
|
|
if (!gui.keyboardShortcutNames.itemCount) UIInitialiseKeyboardShortcutNamesTable();
|
|
const char *shortcutName = (const char *) HashTableGetShort(&gui.keyboardShortcutNames, ScancodeMapToLabel(message->keyboard.scancode));
|
|
|
|
if (shortcutName && window->instance && window->instance->_private) {
|
|
APIInstance *instance = (APIInstance *) window->instance->_private;
|
|
|
|
char keyboardShortcutString[128];
|
|
size_t bytes = EsStringFormat(keyboardShortcutString, 128, "%z%z%z%z%c",
|
|
(message->keyboard.modifiers & ES_MODIFIER_CTRL) ? "Ctrl+" : "",
|
|
(message->keyboard.modifiers & ES_MODIFIER_SHIFT) ? "Shift+" : "",
|
|
(message->keyboard.modifiers & ES_MODIFIER_ALT) ? "Alt+" : "",
|
|
shortcutName, 0) - 1;
|
|
|
|
for (uintptr_t i = 0; i < instance->commands.Count(); i++) {
|
|
EsCommand *command = instance->commands[i];
|
|
if (!command->cKeyboardShortcut || !command->enabled) continue;
|
|
const char *position = EsCRTstrstr(command->cKeyboardShortcut, keyboardShortcutString);
|
|
if (!position) continue;
|
|
|
|
if ((position[bytes] == 0 || position[bytes] == '|') && (position == command->cKeyboardShortcut || position[-1] == '|')) {
|
|
if (command->callback) {
|
|
command->callback(window->instance, nullptr, command);
|
|
}
|
|
|
|
return true;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
void UIWindowPaintNow(EsWindow *window, ProcessMessageTiming *timing, bool afterResize) {
|
|
if (window->doNotPaint) {
|
|
return;
|
|
}
|
|
|
|
// Calculate the regions to repaint, and then perform painting.
|
|
|
|
if (timing) timing->startPaint = EsTimeStampMs();
|
|
|
|
EsRectangle updateRegion = window->updateRegion;
|
|
|
|
EsRectangle bounds = ES_RECT_4(0, window->windowWidth, 0, window->windowHeight);
|
|
EsRectangleClip(updateRegion, bounds, &updateRegion);
|
|
|
|
if (THEME_RECT_VALID(updateRegion)) {
|
|
EsPainter painter = {};
|
|
EsPaintTarget target = {};
|
|
|
|
target.fullAlpha = window->windowStyle != ES_WINDOW_NORMAL;
|
|
target.width = Width(updateRegion);
|
|
target.height = Height(updateRegion);
|
|
target.stride = target.width * 4;
|
|
target.bits = EsHeapAllocate(target.stride * target.height, false);
|
|
target.forWindowManager = true;
|
|
|
|
if (!target.bits) {
|
|
return; // Insufficient memory for painting.
|
|
}
|
|
|
|
EsMemoryFaultRange(target.bits, target.stride * target.height);
|
|
painter.offsetX -= updateRegion.l;
|
|
painter.offsetY -= updateRegion.t;
|
|
painter.clip = ES_RECT_4(0, target.width, 0, target.height);
|
|
painter.target = ⌖
|
|
|
|
window->updateRegionInProgress = updateRegion;
|
|
window->InternalPaint(&painter, ES_FLAGS_DEFAULT);
|
|
window->updateRegionInProgress = {};
|
|
if (timing) timing->endPaint = EsTimeStampMs();
|
|
|
|
if (window->visualizeRepaints) {
|
|
EsDrawRectangle(&painter, painter.clip, 0, EsRandomU64(), ES_RECT_1(3));
|
|
}
|
|
|
|
// Update the screen.
|
|
if (timing) timing->startUpdate = EsTimeStampMs();
|
|
EsSyscall(ES_SYSCALL_WINDOW_SET_BITS, window->handle, (uintptr_t) &updateRegion, (uintptr_t) target.bits,
|
|
afterResize ? WINDOW_SET_BITS_AFTER_RESIZE : WINDOW_SET_BITS_NORMAL);
|
|
if (timing) timing->endUpdate = EsTimeStampMs();
|
|
|
|
EsHeapFree(target.bits);
|
|
}
|
|
|
|
window->updateRegion = ES_RECT_4(window->windowWidth, 0, window->windowHeight, 0);
|
|
}
|
|
|
|
void UIWindowLayoutNow(EsWindow *window, ProcessMessageTiming *timing) {
|
|
if (timing) timing->startLayout = EsTimeStampMs();
|
|
|
|
window->InternalMove(window->windowWidth, window->windowHeight, 0, 0);
|
|
|
|
while (window->updateActions.Length()) {
|
|
// TODO Preventing/detecting infinite cycles?
|
|
UpdateAction action = window->updateActions[0];
|
|
window->updateActions.DeleteSwap(0);
|
|
|
|
if (~action.element->state & UI_STATE_DESTROYING) {
|
|
action.callback(action.element, action.context);
|
|
}
|
|
}
|
|
|
|
if (window->processCheckVisible) {
|
|
for (uintptr_t i = 0; i < window->checkVisible.Length(); i++) {
|
|
EsElement *element = window->checkVisible[i];
|
|
EsAssert(element->state & UI_STATE_CHECK_VISIBLE);
|
|
EsRectangle bounds = element->GetWindowBounds();
|
|
bool offScreen = bounds.r < 0 || bounds.b < 0 || bounds.l >= element->window->width || bounds.t >= element->window->height;
|
|
if (!offScreen) continue;
|
|
element->state &= ~UI_STATE_CHECK_VISIBLE;
|
|
window->checkVisible.Delete(i);
|
|
i--;
|
|
EsMessage m = { ES_MSG_NOT_VISIBLE };
|
|
EsMessageSend(element, &m);
|
|
}
|
|
|
|
window->processCheckVisible = false;
|
|
}
|
|
|
|
if (timing) timing->endLayout = EsTimeStampMs();
|
|
}
|
|
|
|
bool UISetCursor(EsWindow *window) {
|
|
ThemeInitialise();
|
|
|
|
EsCursorStyle cursorStyle = ES_CURSOR_NORMAL;
|
|
EsElement *element = window->dragged ?: window->pressed ?: window->hovered;
|
|
|
|
if (element) {
|
|
EsMessage m = { ES_MSG_GET_CURSOR };
|
|
|
|
if (ES_HANDLED == EsMessageSend(element, &m)) {
|
|
cursorStyle = m.cursorStyle;
|
|
} else {
|
|
cursorStyle = (EsCursorStyle) element->style->metrics->cursor;
|
|
}
|
|
}
|
|
|
|
// TODO Make these configurable in the theme file!
|
|
|
|
int x, y, ox, oy, w, h;
|
|
|
|
#define CURSOR(_constant, _x, _y, _ox, _oy, _w, _h) _constant: { x = _x; y = _y; ox = -_ox; oy = -_oy; w = _w; h = _h; } break
|
|
|
|
switch (cursorStyle) {
|
|
CURSOR(case ES_CURSOR_TEXT, 84, 60, 4, 10, 11, 22);
|
|
CURSOR(case ES_CURSOR_RESIZE_VERTICAL, 44, 12, 4, 11, 11, 24);
|
|
CURSOR(case ES_CURSOR_RESIZE_HORIZONTAL, 68, 15, 11, 4, 24, 11);
|
|
CURSOR(case ES_CURSOR_RESIZE_DIAGONAL_1, 55, 35, 8, 8, 19, 19);
|
|
CURSOR(case ES_CURSOR_RESIZE_DIAGONAL_2, 82, 35, 8, 8, 19, 19);
|
|
CURSOR(case ES_CURSOR_SPLIT_VERTICAL, 37, 55, 4, 10, 12, 24);
|
|
CURSOR(case ES_CURSOR_SPLIT_HORIZONTAL, 56, 60, 10, 4, 24, 12);
|
|
CURSOR(case ES_CURSOR_HAND_HOVER, 131, 14, 8, 1, 21, 26);
|
|
CURSOR(case ES_CURSOR_HAND_DRAG, 107, 18, 7, 1, 20, 22);
|
|
CURSOR(case ES_CURSOR_HAND_POINT, 159, 14, 5, 1, 21, 26);
|
|
CURSOR(case ES_CURSOR_SCROLL_UP_LEFT, 217, 89, 9, 9, 16, 16);
|
|
CURSOR(case ES_CURSOR_SCROLL_UP, 193, 87, 5, 11, 13, 18);
|
|
CURSOR(case ES_CURSOR_SCROLL_UP_RIGHT, 234, 89, 4, 9, 16, 16);
|
|
CURSOR(case ES_CURSOR_SCROLL_LEFT, 175, 93, 11, 5, 18, 13);
|
|
CURSOR(case ES_CURSOR_SCROLL_CENTER, 165, 51, 12, 12, 28, 28);
|
|
CURSOR(case ES_CURSOR_SCROLL_RIGHT, 194, 106, 4, 4, 18, 13);
|
|
CURSOR(case ES_CURSOR_SCROLL_DOWN_LEFT, 216, 106, 10, 4, 16, 16);
|
|
CURSOR(case ES_CURSOR_SCROLL_DOWN, 182, 107, 4, 3, 12, 16);
|
|
CURSOR(case ES_CURSOR_SCROLL_DOWN_RIGHT, 234, 106, 4, 4, 16, 16);
|
|
CURSOR(case ES_CURSOR_SELECT_LINES, 7, 19, 10, 0, 16, 23);
|
|
CURSOR(case ES_CURSOR_DROP_TEXT, 11, 48, 1, 1, 21, 28);
|
|
CURSOR(case ES_CURSOR_CROSS_HAIR_PICK, 104, 56, 11, 11, 26, 26);
|
|
CURSOR(case ES_CURSOR_CROSS_HAIR_RESIZE, 134, 53, 11, 11, 26, 26);
|
|
CURSOR(case ES_CURSOR_MOVE_HOVER, 226, 13, 10, 10, 24, 32);
|
|
CURSOR(case ES_CURSOR_MOVE_DRAG, 197, 13, 10, 10, 24, 24);
|
|
CURSOR(case ES_CURSOR_ROTATE_HOVER, 228, 49, 9, 10, 24, 32);
|
|
CURSOR(case ES_CURSOR_ROTATE_DRAG, 198, 49, 9, 10, 22, 22);
|
|
CURSOR(case ES_CURSOR_BLANK, 0, 0, 0, 0, 1, 1);
|
|
CURSOR(default, 24, 19, 0, 0, 11, 20);
|
|
}
|
|
|
|
bool shadow = cursorStyle != ES_CURSOR_TEXT && api.global->showCursorShadow;
|
|
|
|
return EsSyscall(ES_SYSCALL_WINDOW_SET_CURSOR, window->handle,
|
|
(uintptr_t) theming.cursors.bits + x * 4 + y * theming.cursors.stride,
|
|
((0xFF & ox) << 0) | ((0xFF & oy) << 8) | ((0xFF & w) << 16) | ((0xFF & h) << 24),
|
|
theming.cursors.stride | ((uint32_t) shadow << 30));
|
|
}
|
|
|
|
void UIProcessWindowManagerMessage(EsWindow *window, EsMessage *message, ProcessMessageTiming *timing) {
|
|
// Check if the window has been destroyed.
|
|
|
|
if (message->type == ES_MSG_WINDOW_DESTROYED) {
|
|
if (window->instance) {
|
|
if (window->instance->window == window) {
|
|
window->instance->window = nullptr;
|
|
}
|
|
|
|
EsInstanceCloseReference(window->instance);
|
|
}
|
|
|
|
EsAssert(window->handle == ES_INVALID_HANDLE);
|
|
EsHeapFree(window);
|
|
return;
|
|
} else if (window->handle == ES_INVALID_HANDLE) {
|
|
return;
|
|
}
|
|
|
|
// Begin update.
|
|
|
|
window->willUpdate = true;
|
|
|
|
// Make sure any elements marked to be destroyed in between updates are actually destroyed,
|
|
// before we begin message processing.
|
|
|
|
if (window->InternalDestroy()) {
|
|
// The window has been destroyed.
|
|
return;
|
|
}
|
|
|
|
ProcessAnimations();
|
|
UIFindHoverElement(window);
|
|
|
|
// Process input message.
|
|
|
|
if (timing) timing->startLogic = EsTimeStampMs();
|
|
|
|
if (api.global->swapLeftAndRightButtons) {
|
|
if (message->type == ES_MSG_MOUSE_LEFT_DOWN ) message->type = ES_MSG_MOUSE_RIGHT_DOWN;
|
|
else if (message->type == ES_MSG_MOUSE_RIGHT_DOWN) message->type = ES_MSG_MOUSE_LEFT_DOWN;
|
|
else if (message->type == ES_MSG_MOUSE_LEFT_UP ) message->type = ES_MSG_MOUSE_RIGHT_UP;
|
|
else if (message->type == ES_MSG_MOUSE_RIGHT_UP ) message->type = ES_MSG_MOUSE_LEFT_UP;
|
|
}
|
|
|
|
if (message->type == ES_MSG_MOUSE_MOVED) {
|
|
window->mousePosition.x = message->mouseMoved.newPositionX;
|
|
window->mousePosition.y = message->mouseMoved.newPositionY;
|
|
|
|
if ((!window->activated || window->targetMenu) && window->windowStyle == ES_WINDOW_NORMAL) {
|
|
window->hovering = false;
|
|
goto doneInputMessage;
|
|
} else if (window->dragged) {
|
|
if (gui.draggingStarted || DistanceSquared(message->mouseMoved.newPositionX - gui.lastClickX,
|
|
message->mouseMoved.newPositionY - gui.lastClickY) >= GetConstantNumber("dragThreshold")) {
|
|
EsRectangle bounds = window->dragged->GetWindowBounds();
|
|
message->type = gui.lastClickButton == ES_MSG_MOUSE_LEFT_DOWN ? ES_MSG_MOUSE_LEFT_DRAG
|
|
: gui.lastClickButton == ES_MSG_MOUSE_RIGHT_DOWN ? ES_MSG_MOUSE_RIGHT_DRAG
|
|
: gui.lastClickButton == ES_MSG_MOUSE_MIDDLE_DOWN ? ES_MSG_MOUSE_MIDDLE_DRAG : (EsMessageType) 0;
|
|
EsAssert(message->type);
|
|
message->mouseDragged.originalPositionX = gui.lastClickX;
|
|
message->mouseDragged.originalPositionY = gui.lastClickY;
|
|
message->mouseDragged.newPositionX -= bounds.l;
|
|
message->mouseDragged.newPositionY -= bounds.t;
|
|
message->mouseDragged.originalPositionX -= bounds.l;
|
|
message->mouseDragged.originalPositionY -= bounds.t;
|
|
|
|
if (ES_HANDLED == EsMessageSend(window->dragged, message)) {
|
|
gui.draggingStarted = true;
|
|
}
|
|
}
|
|
} else {
|
|
EsRectangle bounds = window->hovered->GetWindowBounds();
|
|
message->mouseMoved.newPositionX -= bounds.l;
|
|
message->mouseMoved.newPositionY -= bounds.t;
|
|
EsMessageSend(window->hovered, message);
|
|
}
|
|
|
|
window->hovering = true;
|
|
} else if (message->type == ES_MSG_MOUSE_EXIT) {
|
|
window->hovering = false;
|
|
} else if (message->type == ES_MSG_MOUSE_LEFT_DOWN || message->type == ES_MSG_MOUSE_RIGHT_DOWN || message->type == ES_MSG_MOUSE_MIDDLE_DOWN) {
|
|
if (gui.mouseButtonDown || window->targetMenu) {
|
|
goto doneInputMessage;
|
|
}
|
|
|
|
UIMouseDown(window, message);
|
|
} else if (message->type == ES_MSG_MOUSE_LEFT_UP || message->type == ES_MSG_MOUSE_RIGHT_UP || message->type == ES_MSG_MOUSE_MIDDLE_UP) {
|
|
AccessKeyModeExit();
|
|
|
|
if (gui.mouseButtonDown && gui.lastClickButton == message->type - 1) {
|
|
UIMouseUp(window, message, true);
|
|
}
|
|
} else if (message->type == ES_MSG_KEY_UP || message->type == ES_MSG_KEY_DOWN) {
|
|
if (UIHandleKeyMessage(window, message)) {
|
|
goto doneInputMessage;
|
|
}
|
|
|
|
// If this key was on the numpad, translate it to the normal key.
|
|
|
|
int numpad = 0, nshift = 0;
|
|
uint32_t scancode = message->keyboard.scancode;
|
|
bool allowShift = false;
|
|
|
|
if (scancode == ES_SCANCODE_NUM_DIVIDE ) { numpad = ES_SCANCODE_SLASH; }
|
|
if (scancode == ES_SCANCODE_NUM_MULTIPLY) { numpad = ES_SCANCODE_8; nshift = 1; }
|
|
if (scancode == ES_SCANCODE_NUM_SUBTRACT) { numpad = ES_SCANCODE_HYPHEN; }
|
|
if (scancode == ES_SCANCODE_NUM_ADD ) { numpad = ES_SCANCODE_EQUALS; nshift = 1; }
|
|
if (scancode == ES_SCANCODE_NUM_ENTER ) { numpad = ES_SCANCODE_ENTER; }
|
|
|
|
if (message->keyboard.numlock) {
|
|
if (scancode == ES_SCANCODE_NUM_POINT) { numpad = ES_SCANCODE_PERIOD; }
|
|
if (scancode == ES_SCANCODE_NUM_0 ) { numpad = ES_SCANCODE_0; }
|
|
if (scancode == ES_SCANCODE_NUM_1 ) { numpad = ES_SCANCODE_1; }
|
|
if (scancode == ES_SCANCODE_NUM_2 ) { numpad = ES_SCANCODE_2; }
|
|
if (scancode == ES_SCANCODE_NUM_3 ) { numpad = ES_SCANCODE_3; }
|
|
if (scancode == ES_SCANCODE_NUM_4 ) { numpad = ES_SCANCODE_4; }
|
|
if (scancode == ES_SCANCODE_NUM_5 ) { numpad = ES_SCANCODE_5; }
|
|
if (scancode == ES_SCANCODE_NUM_6 ) { numpad = ES_SCANCODE_6; }
|
|
if (scancode == ES_SCANCODE_NUM_7 ) { numpad = ES_SCANCODE_7; }
|
|
if (scancode == ES_SCANCODE_NUM_8 ) { numpad = ES_SCANCODE_8; }
|
|
if (scancode == ES_SCANCODE_NUM_9 ) { numpad = ES_SCANCODE_9; }
|
|
} else {
|
|
if (scancode == ES_SCANCODE_NUM_POINT) { numpad = ES_SCANCODE_DELETE; allowShift = true; }
|
|
if (scancode == ES_SCANCODE_NUM_0 ) { numpad = ES_SCANCODE_INSERT; allowShift = true; }
|
|
if (scancode == ES_SCANCODE_NUM_1 ) { numpad = ES_SCANCODE_END; allowShift = true; }
|
|
if (scancode == ES_SCANCODE_NUM_2 ) { numpad = ES_SCANCODE_DOWN_ARROW; allowShift = true; }
|
|
if (scancode == ES_SCANCODE_NUM_3 ) { numpad = ES_SCANCODE_PAGE_DOWN; allowShift = true; }
|
|
if (scancode == ES_SCANCODE_NUM_4 ) { numpad = ES_SCANCODE_LEFT_ARROW; allowShift = true; }
|
|
if (scancode == ES_SCANCODE_NUM_6 ) { numpad = ES_SCANCODE_RIGHT_ARROW; allowShift = true; }
|
|
if (scancode == ES_SCANCODE_NUM_7 ) { numpad = ES_SCANCODE_HOME; allowShift = true; }
|
|
if (scancode == ES_SCANCODE_NUM_8 ) { numpad = ES_SCANCODE_UP_ARROW; allowShift = true; }
|
|
if (scancode == ES_SCANCODE_NUM_9 ) { numpad = ES_SCANCODE_PAGE_UP; allowShift = true; }
|
|
}
|
|
|
|
if (numpad && ((~message->keyboard.modifiers & ES_MODIFIER_SHIFT) || allowShift)) {
|
|
EsMessage m = *message;
|
|
m.type = message->type;
|
|
m.keyboard.modifiers = message->keyboard.modifiers | (nshift ? ES_MODIFIER_SHIFT : 0);
|
|
m.keyboard.scancode = numpad;
|
|
m.keyboard.numpad = true;
|
|
|
|
if (UIHandleKeyMessage(window, &m)) {
|
|
goto doneInputMessage;
|
|
}
|
|
}
|
|
} else if (message->type == ES_MSG_SCROLL_WHEEL) {
|
|
EsElement *element = window->dragged ?: window->pressed ?: window->hovered;
|
|
|
|
if (element && (~element->state & UI_STATE_BLOCK_INTERACTION) && !gui.accessKeyMode) {
|
|
UIMessageSendPropagateToAncestors(element, message);
|
|
}
|
|
} else if (message->type == ES_MSG_WINDOW_RESIZED) {
|
|
AccessKeyModeExit();
|
|
|
|
gui.leftModifiers = gui.rightModifiers = 0;
|
|
gui.clickChainStartMs = 0;
|
|
|
|
window->receivedFirstResize = true;
|
|
|
|
if (!window->width || !window->height) {
|
|
UIRefreshPrimaryClipboard(window); // Embedded window activated.
|
|
}
|
|
|
|
window->windowWidth = message->windowResized.content.r;
|
|
window->windowHeight = message->windowResized.content.b;
|
|
|
|
if (window->windowStyle == ES_WINDOW_CONTAINER) {
|
|
EsRectangle opaqueBounds = ES_RECT_4(CONTAINER_OPAQUE_C, window->windowWidth - CONTAINER_OPAQUE_C,
|
|
CONTAINER_OPAQUE_T, window->windowHeight - CONTAINER_OPAQUE_B);
|
|
EsSyscall(ES_SYSCALL_WINDOW_SET_PROPERTY, window->handle, (uintptr_t) &opaqueBounds, 0, ES_WINDOW_PROPERTY_OPAQUE_BOUNDS);
|
|
} else if (window->windowStyle == ES_WINDOW_INSPECTOR) {
|
|
EsRectangle opaqueBounds = ES_RECT_2S(window->windowWidth, window->windowHeight);
|
|
EsSyscall(ES_SYSCALL_WINDOW_SET_PROPERTY, window->handle, (uintptr_t) &opaqueBounds, 0, ES_WINDOW_PROPERTY_OPAQUE_BOUNDS);
|
|
}
|
|
|
|
if (!message->windowResized.hidden) {
|
|
EsElementRelayout(window);
|
|
window->Repaint(true);
|
|
}
|
|
|
|
EsMessageSend(window, message);
|
|
|
|
for (uintptr_t i = 0; i < window->sizeAlternatives.Length(); i++) {
|
|
SizeAlternative *alternative = &window->sizeAlternatives[i];
|
|
bool belowThreshold = window->windowWidth < alternative->widthThreshold * theming.scale
|
|
|| window->windowHeight < alternative->heightThreshold * theming.scale;
|
|
EsElementSetHidden(alternative->small, !belowThreshold);
|
|
EsElementSetHidden(alternative->big, belowThreshold);
|
|
}
|
|
|
|
// The mouse position gets reset to (0,0) on deactivation, so get the correct position here.
|
|
EsSyscall(ES_SYSCALL_CURSOR_POSITION_GET, (uintptr_t) &window->mousePosition, 0, 0, 0);
|
|
EsRectangle windowBounds = EsWindowGetBounds(window);
|
|
window->mousePosition.x -= windowBounds.l, window->mousePosition.y -= windowBounds.t;
|
|
window->hovering = true;
|
|
} else if (message->type == ES_MSG_WINDOW_DEACTIVATED) {
|
|
if (window->activated) {
|
|
if (window->pressed) {
|
|
UIMouseUp(window, nullptr, false);
|
|
}
|
|
|
|
AccessKeyModeExit();
|
|
|
|
if (window->windowStyle == ES_WINDOW_MENU) {
|
|
EsMenuClose((EsMenu *) window);
|
|
}
|
|
|
|
window->activated = false;
|
|
window->hovering = false;
|
|
|
|
if (window->focused) {
|
|
window->inactiveFocus = window->focused;
|
|
window->inactiveFocus->Repaint(true);
|
|
window->focused = nullptr;
|
|
UIRemoveFocusFromElement(window->inactiveFocus);
|
|
}
|
|
|
|
EsMessageSend(window, message);
|
|
UIMaybeRefreshStyleAll(window);
|
|
}
|
|
} else if (message->type == ES_MSG_WINDOW_ACTIVATED) {
|
|
gui.leftModifiers = message->windowActivated.leftModifiers;
|
|
gui.rightModifiers = message->windowActivated.rightModifiers;
|
|
|
|
if (!window->activated) {
|
|
AccessKeyModeExit();
|
|
|
|
gui.clickChainStartMs = 0;
|
|
window->activated = true;
|
|
|
|
EsMessageSend(window, message);
|
|
|
|
if (!window->focused && window->inactiveFocus) {
|
|
EsElementFocus(window->inactiveFocus, false);
|
|
window->inactiveFocus->Repaint(true);
|
|
window->inactiveFocus = nullptr;
|
|
}
|
|
|
|
UIRefreshPrimaryClipboard(window);
|
|
UIMaybeRefreshStyleAll(window);
|
|
}
|
|
}
|
|
|
|
doneInputMessage:;
|
|
|
|
if (timing) timing->endLogic = EsTimeStampMs();
|
|
|
|
// Destroy, relayout, and repaint elements as necessary.
|
|
|
|
if (window->InternalDestroy()) {
|
|
// The window has been destroyed.
|
|
return;
|
|
}
|
|
|
|
if (window->receivedFirstResize /* don't try to layout an embedded window until its size is known */
|
|
|| window->windowStyle != ES_WINDOW_NORMAL) {
|
|
UIWindowLayoutNow(window, timing);
|
|
}
|
|
|
|
UIFindHoverElement(window);
|
|
bool changedCursor = UISetCursor(window);
|
|
|
|
if (window->width == (int) window->windowWidth && window->height == (int) window->windowHeight
|
|
&& THEME_RECT_VALID(window->updateRegion) && !window->doNotPaint) {
|
|
UIWindowPaintNow(window, timing, message->type == ES_MSG_WINDOW_RESIZED);
|
|
} else if (changedCursor) {
|
|
EsSyscall(ES_SYSCALL_SCREEN_FORCE_UPDATE, 0, 0, 0, 0);
|
|
}
|
|
|
|
if (window->instance) {
|
|
APIInstance *instance = (APIInstance *) window->instance->_private;
|
|
EsUndoEndGroup(instance->activeUndoManager);
|
|
}
|
|
|
|
// Free any unused styles.
|
|
|
|
FreeUnusedStyles(false);
|
|
|
|
// Finish update.
|
|
|
|
window->willUpdate = false;
|
|
}
|