essence-os/util/designer2.cpp

1591 lines
57 KiB
C++

#define UI_IMPLEMENTATION
#define ES_CRT_WITHOUT_PREFIX
#include "luigi.h"
#ifndef OS_ESSENCE
#include <stdio.h>
#include <math.h>
#endif
#include "hsluv.h"
// x86_64-w64-mingw32-gcc -O3 -o bin/designer2.exe -D UI_WINDOWS util/designer2.cpp -DUNICODE -lgdi32 -luser32 -lkernel32 -Wl,--subsystem,windows -fno-exceptions -fno-rtti
// Needed to replace the old designer:
// TODO Layers containing arrays.
// TODO Export.
// TODO Proper rendering using theme.cpp.
// TODO Implement remaining objects.
// TODO Import and reorganize old theming data.
// Additional features:
// TODO Selecting multiple objects.
// TODO Resizing objects?
// TODO Find object in graph by name.
// TODO Prototyping display. (Multiple instances of each object can be placed, resized and interacted with).
//////////////////////////////////////////////////////////////
#ifndef OS_ESSENCE
#define ES_TEXT_H_LEFT (1 << 0)
#define ES_TEXT_H_CENTER (1 << 1)
#define ES_TEXT_H_RIGHT (1 << 2)
#define ES_TEXT_V_TOP (1 << 3)
#define ES_TEXT_V_CENTER (1 << 4)
#define ES_TEXT_V_BOTTOM (1 << 5)
#define ES_TEXT_ELLIPSIS (1 << 6)
#define ES_TEXT_WRAP (1 << 7)
#define ES_FONT_SANS (0xFFFF)
#define ES_FONT_SERIF (0xFFFE)
#define ES_FONT_MONOSPACED (0xFFFD)
#define EsHeap void
#define EsHeapAllocate(a, b, c) calloc(1, (a))
#define EsHeapFree(a, b, c) free((a))
#define EsMemoryZero(a, b) memset((a), 0, (b))
#define EsMemoryCopy memcpy
#define EsMemoryCopyReverse memmove
#define EsPanic(...) UI_ASSERT(false)
#define ES_MEMORY_MOVE_BACKWARDS -
void *EsHeapReallocate(void *oldAddress, size_t newAllocationSize, bool zeroNewSpace, EsHeap *) {
UI_ASSERT(!zeroNewSpace);
return realloc(oldAddress, newAllocationSize);
}
void EsMemoryMove(void *_start, void *_end, intptr_t amount, bool zeroEmptySpace) {
uint8_t *start = (uint8_t *) _start;
uint8_t *end = (uint8_t *) _end;
if (end < start) return;
if (amount > 0) {
EsMemoryCopyReverse(start + amount, start, end - start);
if (zeroEmptySpace) EsMemoryZero(start, amount);
} else if (amount < 0) {
EsMemoryCopy(start + amount, start, end - start);
if (zeroEmptySpace) EsMemoryZero(end + amount, -amount);
}
}
#endif
const char *cursorStrings[] = {
"Normal",
"Text",
"Resize vertical",
"Resize horizontal",
"Diagonal 1",
"Diagonal 2",
"Split vertical",
"Split horizontal",
"Hand hover",
"Hand drag",
"Hand point",
"Scroll up-left",
"Scroll up",
"Scroll up-right",
"Scroll left",
"Scroll center",
"Scroll right",
"Scroll down-left",
"Scroll down",
"Scroll down-right",
"Select lines",
"Drop text",
"Cross hair pick",
"Cross hair resize",
"Move hover",
"Move drag",
"Rotate hover",
"Rotate drag",
"Blank",
};
void BlendPixel(uint32_t *destinationPixel, uint32_t modified, bool fullAlpha) {
if ((modified & 0xFF000000) == 0xFF000000) {
*destinationPixel = modified;
} else if ((modified & 0xFF000000) == 0x00000000) {
} else if ((*destinationPixel & 0xFF000000) != 0xFF000000 && fullAlpha) {
uint32_t original = *destinationPixel;
uint32_t alpha1 = (modified & 0xFF000000) >> 24;
uint32_t alpha2 = 255 - alpha1;
uint32_t alphaD = (original & 0xFF000000) >> 24;
uint32_t alphaD2 = alphaD * alpha2;
uint32_t alphaOut = alpha1 + (alphaD2 >> 8);
if (alphaOut) {
uint32_t m2 = alphaD2 / alphaOut;
uint32_t m1 = (alpha1 << 8) / alphaOut;
if (m2 == 0x100) m2--;
if (m1 == 0x100) m1--;
uint32_t r2 = m2 * ((original & 0x000000FF) >> 0);
uint32_t g2 = m2 * ((original & 0x0000FF00) >> 8);
uint32_t b2 = m2 * ((original & 0x00FF0000) >> 16);
uint32_t r1 = m1 * ((modified & 0x000000FF) >> 0);
uint32_t g1 = m1 * ((modified & 0x0000FF00) >> 8);
uint32_t b1 = m1 * ((modified & 0x00FF0000) >> 16);
uint32_t result = (alphaOut << 24)
| (0x00FF0000 & ((b1 + b2) << 8))
| (0x0000FF00 & ((g1 + g2) << 0))
| (0x000000FF & ((r1 + r2) >> 8));
*destinationPixel = result;
}
} else {
uint32_t original = *destinationPixel;
uint32_t alpha1 = (modified & 0xFF000000) >> 24;
uint32_t alpha2 = 255 - alpha1;
uint32_t r2 = alpha2 * ((original & 0x000000FF) >> 0);
uint32_t g2 = alpha2 * ((original & 0x0000FF00) >> 8);
uint32_t b2 = alpha2 * ((original & 0x00FF0000) >> 16);
uint32_t r1 = alpha1 * ((modified & 0x000000FF) >> 0);
uint32_t g1 = alpha1 * ((modified & 0x0000FF00) >> 8);
uint32_t b1 = alpha1 * ((modified & 0x00FF0000) >> 16);
uint32_t result = 0xFF000000 | (0x00FF0000 & ((b1 + b2) << 8))
| (0x0000FF00 & ((g1 + g2) << 0))
| (0x000000FF & ((r1 + r2) >> 8));
*destinationPixel = result;
}
}
#include "../shared/array.cpp"
//////////////////////////////////////////////////////////////
#define UI_SIZE_CHECKBOX_BOX (14)
#define UI_SIZE_CHECKBOX_GAP (8)
typedef struct UICheckbox {
#define UI_CHECKBOX_ALLOW_INDETERMINATE (1 << 0)
UIElement e;
#define UI_CHECK_UNCHECKED (0)
#define UI_CHECK_CHECKED (1)
#define UI_CHECK_INDETERMINATE (2)
uint8_t check;
char *label;
ptrdiff_t labelBytes;
void (*invoke)(void *cp);
} UICheckbox;
UICheckbox *UICheckboxCreate(UIElement *parent, uint32_t flags, const char *label, ptrdiff_t labelBytes);
int _UICheckboxMessage(UIElement *element, UIMessage message, int di, void *dp) {
UICheckbox *box = (UICheckbox *) element;
if (message == UI_MSG_GET_HEIGHT) {
return UI_SIZE_BUTTON_HEIGHT * element->window->scale;
} else if (message == UI_MSG_GET_WIDTH) {
int labelSize = UIMeasureStringWidth(box->label, box->labelBytes);
return (labelSize + UI_SIZE_CHECKBOX_BOX + UI_SIZE_CHECKBOX_GAP) * element->window->scale;
} else if (message == UI_MSG_PAINT) {
UIPainter *painter = (UIPainter *) dp;
uint32_t color, textColor;
_UIButtonCalculateColors(element, &color, &textColor);
int midY = (element->bounds.t + element->bounds.b) / 2;
UIRectangle boxBounds = UI_RECT_4(element->bounds.l, element->bounds.l + UI_SIZE_CHECKBOX_BOX,
midY - UI_SIZE_CHECKBOX_BOX / 2, midY + UI_SIZE_CHECKBOX_BOX / 2);
UIDrawRectangle(painter, boxBounds, color, ui.theme.border, UI_RECT_1(1));
UIDrawString(painter, UIRectangleAdd(boxBounds, UI_RECT_4(1, 0, 0, 0)),
box->check == UI_CHECK_CHECKED ? "*" : box->check == UI_CHECK_INDETERMINATE ? "-" : " ", -1,
textColor, UI_ALIGN_CENTER, NULL);
UIDrawString(painter, UIRectangleAdd(element->bounds, UI_RECT_4(UI_SIZE_CHECKBOX_BOX + UI_SIZE_CHECKBOX_GAP, 0, 0, 0)),
box->label, box->labelBytes, textColor, UI_ALIGN_LEFT, NULL);
} else if (message == UI_MSG_UPDATE) {
UIElementRepaint(element, NULL);
} else if (message == UI_MSG_DESTROY) {
UI_FREE(box->label);
} else if (message == UI_MSG_KEY_TYPED) {
UIKeyTyped *m = (UIKeyTyped *) dp;
if (m->textBytes == 1 && m->text[0] == ' ') {
UIElementMessage(element, UI_MSG_CLICKED, 0, 0);
UIElementRepaint(element, NULL);
}
} else if (message == UI_MSG_CLICKED) {
box->check = (box->check + 1) % ((element->flags & UI_CHECKBOX_ALLOW_INDETERMINATE) ? 3 : 2);
UIElementRepaint(element, NULL);
if (box->invoke) box->invoke(element->cp);
}
return 0;
}
UICheckbox *UICheckboxCreate(UIElement *parent, uint32_t flags, const char *label, ptrdiff_t labelBytes) {
UICheckbox *box = (UICheckbox *) UIElementCreate(sizeof(UICheckbox), parent, flags | UI_ELEMENT_TAB_STOP, _UICheckboxMessage, "Checkbox");
box->label = UIStringCopy(label, (box->labelBytes = labelBytes));
return box;
}
//////////////////////////////////////////////////////////////
#ifdef OS_ESSENCE
EsFileStore *fileStore;
const EsInstanceClassEditorSettings instanceClassEditorSettings = {
"untitled.designer", -1,
"New design", -1,
ES_ICON_TEXT_CSS,
};
#endif
UIWindow *window;
UIElement *canvas;
UIElement *inspector;
UIElement *canvasControls;
UILabel *labelMessage;
uint64_t selectedObjectID;
void InspectorAnnouncePropertyChanged(uint64_t objectID, const char *cPropertyName);
void InspectorPopulate();
void InspectorPickTargetEnd();
void CanvasSelectObject(struct Object *object);
//////////////////////////////////////////////////////////////
enum PropertyType : uint8_t {
PROP_NONE,
PROP_COLOR,
PROP_INT,
PROP_OBJECT,
};
struct Property {
PropertyType type;
#define PROPERTY_NAME_SIZE (31)
char cName[PROPERTY_NAME_SIZE];
union {
int32_t integer;
uint64_t object;
};
};
enum ObjectType : uint8_t {
OBJ_NONE,
OBJ_STYLE,
OBJ_VAR_COLOR = 0x40,
OBJ_VAR_INT,
OBJ_VAR_TEXT_STYLE,
OBJ_VAR_ICON_STYLE,
OBJ_PAINT_OVERWRITE = 0x60,
// OBJ_PAINT_LINEAR_GRADIENT,
// OBJ_PAINT_RADIAL_GRADIENT,
OBJ_LAYER_BOX = 0x80,
OBJ_LAYER_METRICS,
OBJ_LAYER_TEXT,
// OBJ_LAYER_PATH,
// OBJ_LAYER_GROUP,
// OBJ_LAYER_SEQUENCE,
// OBJ_LAYER_SELECTOR,
OBJ_MOD_COLOR = 0xC0,
OBJ_MOD_MULTIPLY,
};
struct Object {
ObjectType type;
#define OBJECT_NAME_SIZE (47)
char cName[OBJECT_NAME_SIZE];
uint64_t id;
Array<Property> properties;
};
enum StepType : uint8_t {
STEP_GROUP_MARKER,
STEP_MODIFY_PROPERTY,
STEP_RENAME_OBJECT,
STEP_ADD_OBJECT,
STEP_DELETE_OBJECT,
};
enum StepApplyMode {
STEP_APPLY_NORMAL,
STEP_APPLY_GROUPED,
STEP_APPLY_UNDO,
STEP_APPLY_REDO,
};
struct Step {
StepType type;
uint64_t objectID;
union {
Property property;
char cName[OBJECT_NAME_SIZE];
Object object;
};
};
Array<Step> undoStack;
Array<Step> redoStack;
bool documentModified;
// Document state:
Array<Object> objects;
uint64_t objectIDAllocator;
Object *ObjectFind(uint64_t id) {
// TODO Use a hash table.
for (uintptr_t i = 0; i < objects.Length(); i++) {
if (objects[i].id == id) {
return &objects[i];
}
}
return nullptr;
}
Property *PropertyFind(Object *object, const char *cName, uint8_t type = 0) {
if (object) {
for (uintptr_t i = 0; i < object->properties.Length(); i++) {
if (0 == strcmp(object->properties[i].cName, cName)) {
if (type && object->properties[i].type != type) {
return nullptr;
} else {
return &object->properties[i];
}
}
}
}
return nullptr;
}
Property *PropertyFindOrInherit(Object *object, const char *cName, uint8_t type = 0) {
while (object) {
Property *property = PropertyFind(object, cName);
if (property) {
return type && property->type != type ? nullptr : property;
}
property = PropertyFind(object, "_parent", PROP_OBJECT);
object = ObjectFind(property ? property->object : 0);
}
return nullptr;
}
int32_t PropertyReadInt32(Object *object, const char *cName, int32_t defaultValue = 0) {
Property *property = PropertyFind(object, cName);
return !property || property->type == PROP_OBJECT ? defaultValue : property->integer;
}
void DocumentSave(void *) {
#ifdef OS_ESSENCE
EsBuffer buffer = { .canGrow = 1 };
#define fwrite(a, b, c, d) EsBufferWrite(&buffer, (a), (b) * (c))
#else
FILE *f = fopen("bin/designer2.dat", "wb");
#endif
uint32_t version = 1;
fwrite(&version, 1, sizeof(uint32_t), f);
uint32_t objectCount = objects.Length();
fwrite(&objectCount, 1, sizeof(uint32_t), f);
fwrite(&objectIDAllocator, 1, sizeof(uint64_t), f);
for (uintptr_t i = 0; i < objects.Length(); i++) {
Object copy = objects[i];
uint32_t propertyCount = copy.properties.Length();
copy.properties.array = nullptr;
fwrite(&copy, 1, sizeof(Object), f);
fwrite(&propertyCount, 1, sizeof(uint32_t), f);
fwrite(objects[i].properties.array, 1, sizeof(Property) * propertyCount, f);
}
#ifdef OS_ESSENCE
EsFileStoreWriteAll(fileStore, buffer.out, buffer.position);
EsHeapFree(buffer.out);
#else
fclose(f);
#endif
documentModified = false;
}
void DocumentLoad() {
#ifdef OS_ESSENCE
EsBuffer buffer = {};
buffer.out = (uint8_t *) EsFileStoreReadAll(fileStore, &buffer.bytes);
#define fread(a, b, c, d) EsBufferReadInto(&buffer, (a), (b) * (c))
#else
FILE *f = fopen("bin/designer2.dat", "rb");
if (!f) return;
#endif
uint32_t version = 1;
fread(&version, 1, sizeof(uint32_t), f);
uint32_t objectCount = 0;
fread(&objectCount, 1, sizeof(uint32_t), f);
fread(&objectIDAllocator, 1, sizeof(uint64_t), f);
for (uintptr_t i = 0; i < objectCount; i++) {
Object object = {};
fread(&object, 1, sizeof(Object), f);
uint32_t propertyCount = 0;
fread(&propertyCount, 1, sizeof(uint32_t), f);
object.properties.InsertMany(0, propertyCount);
fread(object.properties.array, 1, sizeof(Property) * propertyCount, f);
objects.Add(object);
}
#ifdef OS_ESSENCE
EsHeapFree(buffer.out);
#else
fclose(f);
#endif
}
void DocumentFree() {
for (uintptr_t i = 0; i < objects.Length(); i++) {
objects[i].properties.Free();
}
objects.Free();
}
void DocumentApplyStep(Step step, StepApplyMode mode = STEP_APPLY_NORMAL) {
bool allowMerge = false;
if (step.type == STEP_GROUP_MARKER) {
} else if (step.type == STEP_MODIFY_PROPERTY) {
Object *object = ObjectFind(step.objectID);
UI_ASSERT(object);
Property *property = PropertyFind(object, step.property.cName);
if (property) {
Property oldValue = *property;
if (step.property.type != PROP_NONE) {
// Update the property.
*property = step.property;
allowMerge = true;
} else {
// Remove the property.
for (uintptr_t i = 0; i < object->properties.Length(); i++) {
if (0 == strcmp(object->properties[i].cName, step.property.cName)) {
object->properties.DeleteSwap(i);
break;
}
}
}
step.property = oldValue;
} else {
if (step.property.type != PROP_NONE) {
// Add the property.
object->properties.Add(step.property);
step.property.type = PROP_NONE;
} else {
// Asking to remove a property that does not exist.
// Probably from a remove broadcast.
}
}
UIElementRepaint(canvas, nullptr);
InspectorAnnouncePropertyChanged(step.objectID, step.property.cName);
} else if (step.type == STEP_RENAME_OBJECT) {
Object *object = ObjectFind(step.objectID);
UI_ASSERT(object);
char oldName[OBJECT_NAME_SIZE];
strcpy(oldName, object->cName);
strcpy(object->cName, step.cName);
strcpy(step.cName, oldName);
UIElementRepaint(canvas, nullptr);
InspectorPopulate();
} else if (step.type == STEP_ADD_OBJECT) {
objects.Add(step.object);
selectedObjectID = step.object.id;
UIElementRepaint(canvas, nullptr);
step.objectID = step.object.id;
step.type = STEP_DELETE_OBJECT;
InspectorPopulate();
} else if (step.type == STEP_DELETE_OBJECT) {
if (selectedObjectID == step.objectID) selectedObjectID = 0;
step.type = STEP_ADD_OBJECT;
for (uintptr_t i = 0; i < objects.Length(); i++) {
if (objects[i].id == step.objectID) {
step.object = objects[i];
objects.DeleteSwap(i);
break;
}
}
UIElementRepaint(canvas, nullptr);
InspectorPopulate();
} else {
UI_ASSERT(false);
}
if (mode == STEP_APPLY_NORMAL || mode == STEP_APPLY_GROUPED) {
bool merge = false;
if (allowMerge && undoStack.Length() > 2 && !redoStack.Length()) {
Step last = undoStack[undoStack.Length() - 2];
if (step.type == STEP_MODIFY_PROPERTY && last.type == STEP_MODIFY_PROPERTY
&& last.objectID == step.objectID && 0 == strcmp(last.property.cName, step.property.cName)) {
merge = true;
}
}
if (!merge) {
undoStack.Add(step);
for (uintptr_t i = 0; i < redoStack.Length(); i++) {
if (redoStack[i].type == STEP_ADD_OBJECT) {
redoStack[i].object.properties.Free();
}
}
redoStack.Free();
if (mode != STEP_APPLY_GROUPED) {
Step step = {};
step.type = STEP_GROUP_MARKER;
undoStack.Add(step);
}
}
} else if (mode == STEP_APPLY_UNDO) {
redoStack.Add(step);
} else if (mode == STEP_APPLY_REDO) {
undoStack.Add(step);
}
documentModified = true;
InspectorPickTargetEnd();
#ifdef UI_ESSENCE
EsInstanceSetModified(ui.instance, true);
#endif
}
void DocumentUndoStep(void *) {
if (!undoStack.Length()) return;
Step marker = undoStack.Pop();
UI_ASSERT(marker.type == STEP_GROUP_MARKER);
do {
DocumentApplyStep(undoStack.Pop(), STEP_APPLY_UNDO);
} while (undoStack.Length() && undoStack.Last().type != STEP_GROUP_MARKER);
redoStack.Add(marker);
}
void DocumentRedoStep(void *) {
if (!redoStack.Length()) return;
Step marker = redoStack.Pop();
UI_ASSERT(marker.type == STEP_GROUP_MARKER);
do {
DocumentApplyStep(redoStack.Pop(), STEP_APPLY_REDO);
} while (redoStack.Length() && redoStack.Last().type != STEP_GROUP_MARKER);
undoStack.Add(marker);
}
//////////////////////////////////////////////////////////////
enum InspectorElementType {
INSPECTOR_INVALID_ELEMENT,
INSPECTOR_REMOVE_BUTTON,
INSPECTOR_REMOVE_BUTTON_BROADCAST,
INSPECTOR_COLOR_PICKER,
INSPECTOR_COLOR_TEXTBOX,
INSPECTOR_INTEGER_TEXTBOX,
INSPECTOR_INTEGER_TEXTBOX_BROADCAST,
INSPECTOR_LINK,
INSPECTOR_LINK_BROADCAST,
INSPECTOR_BOOLEAN_TOGGLE,
INSPECTOR_RADIO_SWITCH,
INSPECTOR_CURSOR_DROP_DOWN,
};
struct InspectorBindingData {
UIElement *element;
uint64_t objectID;
const char *cPropertyName;
const char *cEnablePropertyName;
InspectorElementType elementType;
int32_t radioValue;
};
Array<UIElement *> inspectorBoundElements;
UIElement *inspectorActivePropertyStepElement;
InspectorBindingData *inspectorMenuData;
InspectorBindingData *inspectorPickData;
void InspectorPickTargetEnd() {
if (inspectorPickData) {
inspectorPickData = nullptr;
UILabelSetContent(labelMessage, "", -1);
UIElementRefresh(&labelMessage->e);
UIElementRepaint(canvas, nullptr);
}
}
void InspectorUpdateSingleElementEnable(InspectorBindingData *data) {
UI_ASSERT(data->cEnablePropertyName);
bool enabled = PropertyReadInt32(ObjectFind(data->objectID), data->cEnablePropertyName);
if (enabled) data->element->flags &= ~UI_ELEMENT_DISABLED;
else data->element->flags |= UI_ELEMENT_DISABLED;
UIElementRefresh(data->element);
}
void InspectorUpdateSingleElement(InspectorBindingData *data) {
if (data->elementType == INSPECTOR_REMOVE_BUTTON || data->elementType == INSPECTOR_REMOVE_BUTTON_BROADCAST) {
UIButton *button = (UIButton *) data->element;
Property *property = PropertyFind(ObjectFind(data->objectID), data->cPropertyName);
if (property) button->e.flags &= ~UI_ELEMENT_DISABLED;
else button->e.flags |= UI_ELEMENT_DISABLED;
button->label[0] = property ? 'X' : '-';
UIElementRefresh(&button->e);
} else if (data->elementType == INSPECTOR_BOOLEAN_TOGGLE) {
UICheckbox *box = (UICheckbox *) data->element;
box->check = PropertyReadInt32(ObjectFind(data->objectID), data->cPropertyName, 2);
UIElementRefresh(&box->e);
} else if (data->elementType == INSPECTOR_RADIO_SWITCH) {
UIButton *button = (UIButton *) data->element;
int32_t value = PropertyReadInt32(ObjectFind(data->objectID), data->cPropertyName);
if (value == data->radioValue) button->e.flags |= UI_BUTTON_CHECKED;
else button->e.flags &= ~UI_BUTTON_CHECKED;
UIElementRefresh(&button->e);
} else if (data->elementType == INSPECTOR_CURSOR_DROP_DOWN) {
UIButton *button = (UIButton *) data->element;
Property *property = PropertyFind(ObjectFind(data->objectID), data->cPropertyName, PROP_INT);
UI_FREE(button->label);
button->label = UIStringCopy(property ? cursorStrings[property->integer] : "---", (button->labelBytes = -1));
UIElementRefresh(&button->e);
} else if (data->elementType == INSPECTOR_COLOR_PICKER) {
UIColorPicker *colorPicker = (UIColorPicker *) data->element;
Property *property = PropertyFind(ObjectFind(data->objectID), data->cPropertyName, PROP_COLOR);
uint32_t color = property ? property->integer : 0xFFFFFFFF;
colorPicker->opacity = (color >> 24) / 255.0f;
UIColorToHSV(color, &colorPicker->hue, &colorPicker->saturation, &colorPicker->value);
UIElementRefresh(&colorPicker->e);
} else if (data->elementType == INSPECTOR_COLOR_TEXTBOX) {
UITextbox *textbox = (UITextbox *) data->element;
Property *property = PropertyFind(ObjectFind(data->objectID), data->cPropertyName, PROP_COLOR);
char buffer[32] = "";
if (property) snprintf(buffer, sizeof(buffer), "%.8X", (uint32_t) property->integer);
UITextboxClear(textbox, false);
UITextboxReplace(textbox, buffer, -1, false);
UIElementRefresh(&textbox->e);
} else if (data->elementType == INSPECTOR_INTEGER_TEXTBOX || data->elementType == INSPECTOR_INTEGER_TEXTBOX_BROADCAST) {
UITextbox *textbox = (UITextbox *) data->element;
Property *property = PropertyFind(ObjectFind(data->objectID), data->cPropertyName, PROP_INT);
char buffer[32] = "";
if (property) snprintf(buffer, sizeof(buffer), "%d", property->integer);
UITextboxClear(textbox, false);
UITextboxReplace(textbox, buffer, -1, false);
UIElementRefresh(&textbox->e);
} else if (data->elementType == INSPECTOR_LINK || data->elementType == INSPECTOR_LINK_BROADCAST) {
UIButton *button = (UIButton *) data->element;
Property *property = PropertyFind(ObjectFind(data->objectID), data->cPropertyName, PROP_OBJECT);
Object *target = ObjectFind(property ? property->object : 0);
const char *string = target ? target->cName : "---";
UI_FREE(button->label);
button->label = UIStringCopy(string, (button->labelBytes = -1));
UIElementRefresh(&button->e);
} else {
UI_ASSERT(false);
}
}
void InspectorAnnouncePropertyChanged(uint64_t objectID, const char *cPropertyName) {
for (uintptr_t i = 0; i < inspectorBoundElements.Length(); i++) {
InspectorBindingData *data = (InspectorBindingData *) inspectorBoundElements[i]->cp;
if (data->element == inspectorActivePropertyStepElement) continue;
if (data->objectID != objectID) continue;
if (data->cEnablePropertyName && 0 == strcmp(data->cEnablePropertyName, cPropertyName)) {
InspectorUpdateSingleElementEnable(data);
}
if (0 == strcmp(data->cPropertyName, cPropertyName)) {
InspectorUpdateSingleElement(data);
}
}
}
void InspectorBroadcastStep(Step step, InspectorBindingData *data) {
if (data->elementType == INSPECTOR_INTEGER_TEXTBOX_BROADCAST || data->elementType == INSPECTOR_LINK_BROADCAST
|| data->elementType == INSPECTOR_REMOVE_BUTTON_BROADCAST) {
for (char i = '1'; i <= '3'; i++) {
step.property.cName[strlen(step.property.cName) - 1] = i;
DocumentApplyStep(step, STEP_APPLY_GROUPED);
}
strcpy(step.property.cName, data->cPropertyName);
}
}
void InspectorCursorDropDownMenuInvoke(void *cp) {
intptr_t index = (intptr_t) cp;
Step step = {};
step.type = STEP_MODIFY_PROPERTY;
step.objectID = inspectorMenuData->objectID;
strcpy(step.property.cName, inspectorMenuData->cPropertyName);
step.property.type = index == -1 ? PROP_NONE : PROP_INT;
step.property.integer = index;
DocumentApplyStep(step);
inspectorMenuData = nullptr;
}
int InspectorBoundMessage(UIElement *element, UIMessage message, int di, void *dp) {
InspectorBindingData *data = (InspectorBindingData *) element->cp;
if (message == UI_MSG_DESTROY) {
inspectorBoundElements.FindAndDeleteSwap(element, true);
free(data);
data = nullptr;
} else if (message == UI_MSG_VALUE_CHANGED) {
Step step = {};
step.type = STEP_MODIFY_PROPERTY;
step.objectID = data->objectID;
strcpy(step.property.cName, data->cPropertyName);
if (data->elementType == INSPECTOR_COLOR_PICKER) {
UIColorPicker *colorPicker = (UIColorPicker *) element;
uint32_t color;
UIColorToRGB(colorPicker->hue, colorPicker->saturation, colorPicker->value, &color);
color |= (uint32_t) (colorPicker->opacity * 255.0f) << 24;
step.property.type = PROP_COLOR;
step.property.integer = (int32_t) color;
} else if (data->elementType == INSPECTOR_COLOR_TEXTBOX) {
UITextbox *textbox = (UITextbox *) element;
char buffer[32];
int length = 31 > textbox->bytes ? textbox->bytes : 31;
memcpy(buffer, textbox->string, length);
buffer[length] = 0;
step.property.type = PROP_COLOR;
step.property.integer = (int32_t) strtoul(buffer, nullptr, 16);
} else if (data->elementType == INSPECTOR_INTEGER_TEXTBOX
|| data->elementType == INSPECTOR_INTEGER_TEXTBOX_BROADCAST) {
UITextbox *textbox = (UITextbox *) element;
char buffer[32];
int length = 31 > textbox->bytes ? textbox->bytes : 31;
memcpy(buffer, textbox->string, length);
buffer[length] = 0;
step.property.type = PROP_INT;
step.property.integer = (int32_t) strtol(buffer, nullptr, 0);
InspectorBroadcastStep(step, data);
} else {
UI_ASSERT(false);
}
inspectorActivePropertyStepElement = element; // Don't tell this element about the step.
DocumentApplyStep(step);
inspectorActivePropertyStepElement = nullptr;
} else if (message == UI_MSG_CLICKED) {
Step step = {};
step.type = STEP_MODIFY_PROPERTY;
step.objectID = data->objectID;
strcpy(step.property.cName, data->cPropertyName);
if (data->elementType == INSPECTOR_REMOVE_BUTTON || data->elementType == INSPECTOR_REMOVE_BUTTON_BROADCAST) {
step.property.type = PROP_NONE; // Remove the property.
InspectorBroadcastStep(step, data);
DocumentApplyStep(step);
} else if (data->elementType == INSPECTOR_LINK || data->elementType == INSPECTOR_LINK_BROADCAST) {
char *name = nullptr;
const char *dialog = "Enter the name of the new link target object:\n%t\n\n%l\n\n%f%b%b";
const char *result = UIDialogShow(window, 0, dialog, &name, "OK", "Cancel");
if (0 == strcmp(result, "OK")) {
uint64_t id = 0;
for (uintptr_t i = 0; i < objects.Length(); i++) {
if (0 == strcmp(objects[i].cName, name)) {
id = objects[i].id;
break;
}
}
if (!id) {
UIDialogShow(window, 0, "Error: The object was not found.\n%f%b", "OK");
} else {
step.property.type = PROP_OBJECT;
step.property.object = id;
InspectorBroadcastStep(step, data);
DocumentApplyStep(step);
}
}
free(name);
} else if (data->elementType == INSPECTOR_RADIO_SWITCH) {
step.property.type = PROP_INT;
step.property.integer = data->radioValue;
DocumentApplyStep(step);
} else if (data->elementType == INSPECTOR_BOOLEAN_TOGGLE) {
UICheckbox *box = (UICheckbox *) element;
step.property.type = (box->check + 1) == UI_CHECK_INDETERMINATE ? PROP_NONE : PROP_INT;
step.property.integer = (box->check + 1) % 3;
DocumentApplyStep(step);
return 1; // InspectorUpdateSingleElement will update the check.
} else if (data->elementType == INSPECTOR_CURSOR_DROP_DOWN) {
UIMenu *menu = UIMenuCreate(window->pressed, UI_MENU_NO_SCROLL);
UIMenuAddItem(menu, 0, "Inherit", -1, InspectorCursorDropDownMenuInvoke, (void *) (intptr_t) -1);
for (uintptr_t i = 0; i < sizeof(cursorStrings) / sizeof(cursorStrings[0]); i++) {
UIMenuAddItem(menu, 0, cursorStrings[i], -1, InspectorCursorDropDownMenuInvoke, (void *) (intptr_t) i);
}
inspectorMenuData = data;
UIMenuShow(menu);
}
} else if (message == UI_MSG_UPDATE) {
if (di == UI_UPDATE_FOCUSED && element->window->focused == element
&& (data->elementType == INSPECTOR_COLOR_TEXTBOX || data->elementType == INSPECTOR_INTEGER_TEXTBOX
|| data->elementType == INSPECTOR_INTEGER_TEXTBOX_BROADCAST)) {
UITextbox *textbox = (UITextbox *) element;
textbox->carets[0] = 0;
textbox->carets[1] = textbox->bytes;
}
}
return 0;
}
InspectorBindingData *InspectorBind(UIElement *element, uint64_t objectID, const char *cPropertyName, InspectorElementType elementType,
int32_t radioValue = 0, const char *cEnablePropertyName = nullptr) {
InspectorBindingData *data = (InspectorBindingData *) calloc(1, sizeof(InspectorBindingData));
data->element = element;
data->objectID = objectID;
data->cPropertyName = cPropertyName;
data->elementType = elementType;
data->radioValue = radioValue;
data->cEnablePropertyName = cEnablePropertyName;
element->cp = data;
element->messageUser = InspectorBoundMessage;
inspectorBoundElements.Add(element);
InspectorUpdateSingleElement(data);
if (cEnablePropertyName) InspectorUpdateSingleElementEnable(data);
return data;
}
void InspectorRenameObject(void *) {
Step step = {};
step.type = STEP_RENAME_OBJECT;
step.objectID = selectedObjectID;
char *name = nullptr;
const char *result = UIDialogShow(window, 0, "Enter the new name for the object:\n%t\n\n%l\n\n%f%b%b", &name, "OK", "Cancel");
if (0 == strcmp(result, "OK")) {
if (!name || strlen(name) >= sizeof(step.cName) - 1) {
UIDialogShow(window, 0, "Error: Name must be between 1 and 46 characters.\n%f%b", "OK");
} else {
strcpy(step.cName, name);
DocumentApplyStep(step);
}
}
free(name);
}
void InspectorPickTargetCommand(void *cp) {
if (inspectorPickData) {
InspectorPickTargetEnd();
return;
}
inspectorPickData = (InspectorBindingData *) cp;
UILabelSetContent(labelMessage, "** Click an object to link it. **", -1);
UIElementRefresh(&labelMessage->e);
UIElementRepaint(canvas, nullptr);
}
void InspectorFindTargetCommand(void *cp) {
InspectorBindingData *data = (InspectorBindingData *) cp;
Property *property = PropertyFind(ObjectFind(data->objectID), data->cPropertyName, PROP_OBJECT);
Object *target = ObjectFind(property ? property->object : 0);
if (target) {
CanvasSelectObject(target);
} else {
UIDialogShow(window, 0, "Error: The object does not exist.\n%f%b", "OK");
}
}
int InspectorTabPaneMessage(UIElement *element, UIMessage message, int di, void *dp) {
if (message == UI_MSG_LEFT_DOWN) {
element->messageClass(element, message, di, dp);
UIElementRefresh(inspector);
return 1;
}
return 0;
}
void InspectorAddLink(Object *object, const char *cLabel, const char *cPropertyName,
bool broadcast = false, const char *cEnablePropertyName = nullptr) {
if (cLabel) UILabelCreate(0, 0, cLabel, -1);
UIElement *row = &UIPanelCreate(0, UI_PANEL_HORIZONTAL)->e;
InspectorBindingData *data = InspectorBind(&UIButtonCreate(row, UI_ELEMENT_H_FILL, 0, 0)->e,
object->id, cPropertyName, broadcast ? INSPECTOR_LINK_BROADCAST : INSPECTOR_LINK, 0, cEnablePropertyName);
UIButton *pickTarget = UIButtonCreate(row, UI_BUTTON_SMALL, "Pick", -1);
pickTarget->e.cp = data;
pickTarget->invoke = InspectorPickTargetCommand;
UIButton *findTarget = UIButtonCreate(row, UI_BUTTON_SMALL, "Find", -1);
findTarget->e.cp = data;
findTarget->invoke = InspectorFindTargetCommand;
if (!broadcast) InspectorBind(&UIButtonCreate(row, UI_BUTTON_SMALL, "X", 1)->e, object->id, cPropertyName, INSPECTOR_REMOVE_BUTTON);
}
void InspectorAddIntegerTextbox(Object *object, const char *cLabel, const char *cPropertyName, bool broadcast = false, const char *cEnablePropertyName = nullptr) {
if (cLabel) UILabelCreate(0, 0, cLabel, -1);
InspectorBind(&UITextboxCreate(0, UI_ELEMENT_H_FILL)->e, object->id, cPropertyName,
broadcast ? INSPECTOR_INTEGER_TEXTBOX_BROADCAST : INSPECTOR_INTEGER_TEXTBOX, 0, cEnablePropertyName);
}
void InspectorAddInteger(Object *object, const char *cLabel, const char *cPropertyName) {
if (cLabel) UILabelCreate(0, 0, cLabel, -1);
UIPanelCreate(0, UI_ELEMENT_PARENT_PUSH | UI_ELEMENT_H_FILL | UI_PANEL_HORIZONTAL);
UITabPane *tabPane = UITabPaneCreate(0, UI_ELEMENT_PARENT_PUSH | UI_ELEMENT_H_FILL, "Direct\tLink");
tabPane->e.messageUser = InspectorTabPaneMessage;
InspectorAddIntegerTextbox(object, nullptr, cPropertyName);
InspectorAddLink(object, nullptr, cPropertyName);
UIParentPop();
Property *property = PropertyFind(object, cPropertyName);
if (property && property->type == PROP_OBJECT) tabPane->active = 1;
InspectorBind(&UIButtonCreate(0, UI_BUTTON_SMALL, "X", 1)->e, object->id, cPropertyName, INSPECTOR_REMOVE_BUTTON);
UIParentPop();
}
void InspectorAddFourGroup(Object *object, const char *cLabel, const char *cPropertyName0, const char *cPropertyName1,
const char *cPropertyName2, const char *cPropertyName3, const char *cEnablePropertyName = nullptr) {
if (cLabel) UILabelCreate(0, 0, cLabel, -1);
UIPanelCreate(0, UI_ELEMENT_PARENT_PUSH | UI_ELEMENT_H_FILL | UI_PANEL_HORIZONTAL);
UITabPane *tabPane = UITabPaneCreate(0, UI_ELEMENT_PARENT_PUSH | UI_ELEMENT_H_FILL, "Single\tIndividual\tLink");
tabPane->e.messageUser = InspectorTabPaneMessage;
InspectorAddIntegerTextbox(object, nullptr, cPropertyName0, true /* broadcast */, cEnablePropertyName);
UIElement *row = &UIPanelCreate(0, UI_PANEL_HORIZONTAL)->e;
InspectorBind(&UITextboxCreate(row, UI_ELEMENT_H_FILL)->e, object->id, cPropertyName0, INSPECTOR_INTEGER_TEXTBOX, 0, cEnablePropertyName);
InspectorBind(&UITextboxCreate(row, UI_ELEMENT_H_FILL)->e, object->id, cPropertyName1, INSPECTOR_INTEGER_TEXTBOX, 0, cEnablePropertyName);
InspectorBind(&UITextboxCreate(row, UI_ELEMENT_H_FILL)->e, object->id, cPropertyName2, INSPECTOR_INTEGER_TEXTBOX, 0, cEnablePropertyName);
InspectorBind(&UITextboxCreate(row, UI_ELEMENT_H_FILL)->e, object->id, cPropertyName3, INSPECTOR_INTEGER_TEXTBOX, 0, cEnablePropertyName);
InspectorAddLink(object, nullptr, cPropertyName0, true /* broadcast */, cEnablePropertyName);
UIParentPop();
Property *property = PropertyFind(object, cPropertyName0);
int32_t b0 = PropertyReadInt32(object, cPropertyName0);
int32_t b1 = PropertyReadInt32(object, cPropertyName1);
int32_t b2 = PropertyReadInt32(object, cPropertyName2);
int32_t b3 = PropertyReadInt32(object, cPropertyName3);
if (property && property->type == PROP_OBJECT) tabPane->active = 2;
else if (b0 != b1 || b1 != b2 || b2 != b3) tabPane->active = 1;
InspectorBind(&UIButtonCreate(0, UI_BUTTON_SMALL, "X", 1)->e, object->id, cPropertyName0, INSPECTOR_REMOVE_BUTTON_BROADCAST);
UIParentPop();
}
void InspectorAddBooleanToggle(Object *object, const char *cLabel, const char *cPropertyName) {
InspectorBind(&UICheckboxCreate(0, UI_CHECKBOX_ALLOW_INDETERMINATE, cLabel, -1)->e, object->id, cPropertyName, INSPECTOR_BOOLEAN_TOGGLE);
}
void InspectorAddRadioSwitch(Object *object, const char *cLabel, const char *cPropertyName, int32_t radioValue) {
InspectorBind(&UIButtonCreate(0, UI_BUTTON_SMALL, cLabel, -1)->e, object->id, cPropertyName, INSPECTOR_RADIO_SWITCH, radioValue);
}
void InspectorPopulate() {
UIElementDestroyDescendents(inspector);
UIParentPush(inspector);
Object *object = ObjectFind(selectedObjectID);
if (object) {
UIPanelCreate(0, UI_ELEMENT_PARENT_PUSH | UI_PANEL_BORDER | UI_PANEL_MEDIUM_SPACING | UI_PANEL_EXPAND);
UIPanelCreate(0, UI_ELEMENT_PARENT_PUSH | UI_PANEL_HORIZONTAL);
char buffer[256];
snprintf(buffer, sizeof(buffer), "%s (%lu)", object->cName, object->id);
UILabelCreate(0, 0, buffer, -1);
UISpacerCreate(0, UI_ELEMENT_H_FILL, 0, 0);
UIButtonCreate(0, 0, "Rename", -1)->invoke = InspectorRenameObject;
UIParentPop();
if (object->type != OBJ_VAR_COLOR && object->type != OBJ_VAR_INT
&& object->type != OBJ_MOD_COLOR && object->type != OBJ_MOD_MULTIPLY) {
InspectorAddLink(object, "Inherit from:", "_parent");
}
UIParentPop();
UISpacerCreate(0, 0, 0, 10);
if (object->type == OBJ_STYLE) {
InspectorAddLink(object, "Appearance:", "appearance");
InspectorAddLink(object, "Metrics:", "metrics");
InspectorAddLink(object, "Text style:", "textStyle");
InspectorAddLink(object, "Icon style:", "iconStyle");
InspectorAddBooleanToggle(object, "Public style", "isPublic");
} else if (object->type == OBJ_VAR_COLOR) {
InspectorBind(&UIColorPickerCreate(&UIPanelCreate(0, 0)->e, UI_COLOR_PICKER_HAS_OPACITY)->e, object->id, "color", INSPECTOR_COLOR_PICKER);
InspectorBind(&UITextboxCreate(0, 0)->e, object->id, "color", INSPECTOR_COLOR_TEXTBOX);
InspectorAddBooleanToggle(object, "Export to theme file", "isExported");
} else if (object->type == OBJ_VAR_INT) {
InspectorBind(&UITextboxCreate(0, 0)->e, object->id, "value", INSPECTOR_INTEGER_TEXTBOX);
InspectorAddBooleanToggle(object, "Export to theme file", "isExported");
InspectorAddBooleanToggle(object, "Apply UI scaling factor", "isScaled");
} else if (object->type == OBJ_LAYER_METRICS) {
UILabelCreate(0, 0, "General options:", -1);
UIPanelCreate(0, UI_ELEMENT_PARENT_PUSH | UI_PANEL_HORIZONTAL);
InspectorAddBooleanToggle(object, "Enable clipping", "clipEnabled");
InspectorAddBooleanToggle(object, "Wrap text", "wrapText");
InspectorAddBooleanToggle(object, "Ellipsis", "ellipsis");
UIParentPop();
InspectorAddFourGroup(object, "Insets:", "insets0", "insets1", "insets2", "insets3");
InspectorAddFourGroup(object, "Clip insets:", "clipInsets0", "clipInsets1", "clipInsets2", "clipInsets3", "clipEnabled");
UILabelCreate(0, 0, "Preferred size:", -1);
UIPanelCreate(0, UI_ELEMENT_PARENT_PUSH | UI_PANEL_HORIZONTAL);
InspectorAddInteger(object, nullptr, "preferredWidth");
InspectorAddInteger(object, nullptr, "preferredHeight");
UIParentPop();
UILabelCreate(0, 0, "Minimum size:", -1);
UIPanelCreate(0, UI_ELEMENT_PARENT_PUSH | UI_PANEL_HORIZONTAL);
InspectorAddInteger(object, nullptr, "minimumWidth");
InspectorAddInteger(object, nullptr, "minimumHeight");
UIParentPop();
UILabelCreate(0, 0, "Maximum size:", -1);
UIPanelCreate(0, UI_ELEMENT_PARENT_PUSH | UI_PANEL_HORIZONTAL);
InspectorAddInteger(object, nullptr, "maximumWidth");
InspectorAddInteger(object, nullptr, "maximumHeight");
UIParentPop();
UILabelCreate(0, 0, "Gaps:", -1);
UIPanelCreate(0, UI_ELEMENT_PARENT_PUSH | UI_PANEL_HORIZONTAL);
InspectorAddInteger(object, nullptr, "gapMajor");
InspectorAddInteger(object, nullptr, "gapMinor");
InspectorAddInteger(object, nullptr, "gapWrap");
UIParentPop();
UILabelCreate(0, 0, "Horizontal text alignment:", -1);
UIPanelCreate(0, UI_ELEMENT_PARENT_PUSH | UI_PANEL_HORIZONTAL);
InspectorAddRadioSwitch(object, "Left", "horizontalTextAlign", 1);
InspectorAddRadioSwitch(object, "Center", "horizontalTextAlign", 2);
InspectorAddRadioSwitch(object, "Right", "horizontalTextAlign", 3);
InspectorBind(&UIButtonCreate(0, UI_BUTTON_SMALL, "X", 1)->e, object->id, "horizontalTextAlign", INSPECTOR_REMOVE_BUTTON);
UIParentPop();
UILabelCreate(0, 0, "Vertical text alignment:", -1);
UIPanelCreate(0, UI_ELEMENT_PARENT_PUSH | UI_PANEL_HORIZONTAL);
InspectorAddRadioSwitch(object, "Top", "verticalTextAlign", 1);
InspectorAddRadioSwitch(object, "Center", "verticalTextAlign", 2);
InspectorAddRadioSwitch(object, "Bottom", "verticalTextAlign", 3);
InspectorBind(&UIButtonCreate(0, UI_BUTTON_SMALL, "X", 1)->e, object->id, "verticalTextAlign", INSPECTOR_REMOVE_BUTTON);
UIParentPop();
UILabelCreate(0, 0, "Cursor style:", -1);
InspectorBind(&UIButtonCreate(0, UI_BUTTON_DROP_DOWN, 0, 0)->e, object->id, "cursor", INSPECTOR_CURSOR_DROP_DOWN);
} else if (object->type == OBJ_VAR_TEXT_STYLE) {
InspectorAddLink(object, "Text color:", "textColor");
InspectorAddLink(object, "Selection background color:", "selectedBackground");
InspectorAddLink(object, "Selection text color:", "selectedText");
InspectorAddInteger(object, "Text size:", "textSize");
InspectorAddInteger(object, "Font weight:", "fontWeight");
UILabelCreate(0, 0, "Font options:", -1);
UIPanelCreate(0, UI_ELEMENT_PARENT_PUSH | UI_PANEL_HORIZONTAL);
InspectorAddBooleanToggle(object, "Italic", "isItalic");
UIParentPop();
UILabelCreate(0, 0, "Font family:", -1);
UIPanelCreate(0, UI_ELEMENT_PARENT_PUSH | UI_PANEL_HORIZONTAL);
InspectorAddRadioSwitch(object, "Sans-serif", "fontFamily", ES_FONT_SANS);
InspectorAddRadioSwitch(object, "Serif", "fontFamily", ES_FONT_SERIF);
InspectorAddRadioSwitch(object, "Monospaced", "fontFamily", ES_FONT_MONOSPACED);
InspectorBind(&UIButtonCreate(0, UI_BUTTON_SMALL, "X", 1)->e, object->id, "fontFamily", INSPECTOR_REMOVE_BUTTON);
UIParentPop();
} else if (object->type == OBJ_VAR_ICON_STYLE) {
InspectorAddLink(object, "Icon color:", "iconColor");
InspectorAddInteger(object, "Icon size:", "iconSize");
} else if (object->type == OBJ_LAYER_BOX) {
InspectorAddFourGroup(object, "Border sizes:", "borders0", "borders1", "borders2", "borders3");
InspectorAddLink(object, "Fill paint:", "mainPaint");
InspectorAddLink(object, "Border paint:", "borderPaint");
} else if (object->type == OBJ_LAYER_TEXT) {
InspectorAddLink(object, "Text color:", "color");
InspectorAddInteger(object, "Blur radius:", "blur");
} else if (object->type == OBJ_PAINT_OVERWRITE) {
InspectorAddLink(object, "Color:", "color");
} else if (object->type == OBJ_MOD_COLOR) {
InspectorAddLink(object, "Base color:", "base");
UILabelCreate(0, 0, "Brightness (%):", -1);
InspectorBind(&UITextboxCreate(0, UI_ELEMENT_H_FILL)->e, object->id, "brightness", INSPECTOR_INTEGER_TEXTBOX);
UILabelCreate(0, 0, "Hue shift (deg):", -1);
InspectorBind(&UITextboxCreate(0, UI_ELEMENT_H_FILL)->e, object->id, "hueShift", INSPECTOR_INTEGER_TEXTBOX);
} else if (object->type == OBJ_MOD_MULTIPLY) {
InspectorAddLink(object, "Base integer:", "base");
UILabelCreate(0, 0, "Factor (%):", -1);
InspectorBind(&UITextboxCreate(0, UI_ELEMENT_H_FILL)->e, object->id, "factor", INSPECTOR_INTEGER_TEXTBOX);
} else {
// TODO.
}
} else {
UILabelCreate(0, 0, "Select an object to inspect.", -1);
}
UIParentPop();
UIElementRefresh(inspector);
}
//////////////////////////////////////////////////////////////
#define CANVAS_ALIGN (20)
float canvasPanX, canvasPanY;
float canvasLastPanPointX, canvasLastPanPointY;
bool canvasDragging, canvasCanDrag;
int32_t canvasDragNewX, canvasDragNewY;
int32_t canvasDragOffsetX, canvasDragOffsetY;
int32_t canvasLeftDownX, canvasLeftDownY;
bool canvasShowArrows;
UIRectangle CanvasGetObjectBounds(Object *object) {
int32_t x = PropertyReadInt32(object, "_graphX") - canvasPanX + canvas->bounds.l;
int32_t y = PropertyReadInt32(object, "_graphY") - canvasPanY + canvas->bounds.t;
int32_t w = PropertyReadInt32(object, "_graphW");
int32_t h = PropertyReadInt32(object, "_graphH");
if (object->id == selectedObjectID && canvasDragging) {
x = canvasDragNewX - canvasPanX + canvas->bounds.l;
y = canvasDragNewY - canvasPanY + canvas->bounds.t;
}
return UI_RECT_4(x, x + w, y, y + h);
}
void CanvasSelectObject(Object *object) {
UIRectangle bounds = CanvasGetObjectBounds(object);
canvasPanX += bounds.l - UI_RECT_WIDTH(canvas->bounds) / 2;
canvasPanY += bounds.t - UI_RECT_HEIGHT(canvas->bounds) / 2;
selectedObjectID = object->id;
UIElementRepaint(canvas, nullptr);
InspectorPopulate();
}
void CanvasDrawColorSwatch(UIPainter *painter, UIRectangle bounds, uint32_t color) {
uint32_t colors[2] = { 0xFFFFFFFF, 0xFFC0C0C0 };
BlendPixel(&colors[0], color, false);
BlendPixel(&colors[1], color, false);
int x = bounds.l, y = bounds.t;
bounds = UIRectangleIntersection(bounds, painter->clip);
if (UI_RECT_VALID(bounds)) {
for (int32_t j = bounds.t; j < bounds.b; j++) {
for (int32_t i = bounds.l; i < bounds.r; i++) {
painter->bits[j * painter->width + i] = colors[(((j - y) >> 3) ^ ((i - x) >> 3)) & 1];
}
}
}
}
void CanvasDrawArrow(UIPainter *painter, int x0, int y0, int x1, int y1, uint32_t color) {
if (!UIDrawLine(painter, x0, y0, x1, y1, color)) return;
float angle = atan2f(y1 - y0, x1 - x0);
UIDrawLine(painter, x0, y0, x0 + cosf(angle + 0.5f) * 15, y0 + sinf(angle + 0.5f) * 15, color);
UIDrawLine(painter, x0, y0, x0 + cosf(angle - 0.5f) * 15, y0 + sinf(angle - 0.5f) * 15, color);
}
int32_t CanvasGetInteger(Object *object, int depth = 0) {
if (!object || depth == 10) {
return 0;
}
if (object->type == OBJ_VAR_INT) {
return PropertyReadInt32(object, "value");
} else if (object->type == OBJ_MOD_MULTIPLY) {
Property *property = PropertyFind(object, "base", PROP_OBJECT);
int32_t base = CanvasGetInteger(ObjectFind(property ? property->object : 0), depth + 1);
int32_t factor = PropertyReadInt32(object, "factor");
return base * factor / 100;
} else {
return 0;
}
}
int32_t CanvasGetIntegerFromProperty(Property *property) {
if (!property) {
return 0;
} else if (property->type == PROP_INT) {
return property->integer;
} else if (property->type == PROP_OBJECT) {
return CanvasGetInteger(ObjectFind(property->object));
} else {
return 0;
}
}
uint32_t CanvasGetColorFromPaint(Object *object, int depth = 0) {
if (!object || depth == 10) {
return 0;
}
if (object->type == OBJ_VAR_COLOR) {
return PropertyReadInt32(object, "color");
} else if (object->type == OBJ_MOD_COLOR) {
Property *property = PropertyFind(object, "base", PROP_OBJECT);
uint32_t base = CanvasGetColorFromPaint(ObjectFind(property ? property->object : 0), depth + 1);
uint32_t alpha = base & 0xFF000000;
int32_t brightness = PropertyReadInt32(object, "brightness");
int32_t hueShift = PropertyReadInt32(object, "hueShift");
double hue, saturation, luminosity, red, green, blue;
rgb2hsluv(UI_COLOR_RED_F(base), UI_COLOR_GREEN_F(base), UI_COLOR_BLUE_F(base), &hue, &saturation, &luminosity);
luminosity += luminosity * brightness / 100.0f;
hue = fmod(hue + hueShift, 360.0);
if (luminosity < 0.0) luminosity = 0.0;
if (luminosity > 100.0) luminosity = 100.0;
hsluv2rgb(hue, saturation, luminosity, &red, &green, &blue);
return UI_COLOR_FROM_FLOAT(red, green, blue) | alpha;
} else {
return 0;
}
}
int CanvasMessage(UIElement *element, UIMessage message, int di, void *dp) {
if (message == UI_MSG_PAINT) {
UIPainter *painter = (UIPainter *) dp;
UIDrawBlock(painter, element->bounds, 0xFFC0C0C0);
for (uintptr_t i = 0; i < objects.Length(); i++) {
Object *object = &objects[i];
UIRectangle bounds = CanvasGetObjectBounds(object);
if (bounds.r < element->bounds.l || bounds.l > element->bounds.r
|| bounds.b < element->bounds.t || bounds.t > element->bounds.b) {
continue;
}
if ((object->id == selectedObjectID) == (inspectorPickData == nullptr)) {
UIDrawBorder(painter, UIRectangleAdd(bounds, UI_RECT_1I(-3)), 0xFF4092FF, UI_RECT_1(3));
}
UIDrawString(painter, UI_RECT_4(bounds.l, element->bounds.r, bounds.t - ui.glyphHeight, bounds.t),
object->cName, -1, 0xFF000000, UI_ALIGN_LEFT, nullptr);
UIDrawRectangle(painter, bounds, 0xFFE0E0E0, 0xFF404040, UI_RECT_1(1));
UIDrawBlock(painter, UI_RECT_4(bounds.l + 1, bounds.r + 1, bounds.b, bounds.b + 1), 0xFF404040);
UIDrawBlock(painter, UI_RECT_4(bounds.r, bounds.r + 1, bounds.t + 1, bounds.b + 1), 0xFF404040);
bounds = UIRectangleAdd(bounds, UI_RECT_1I(3));
if (object->type == OBJ_VAR_COLOR || object->type == OBJ_MOD_COLOR) {
CanvasDrawColorSwatch(painter, bounds, CanvasGetColorFromPaint(object));
} else if (object->type == OBJ_VAR_INT || object->type == OBJ_MOD_MULTIPLY) {
int32_t value = CanvasGetInteger(object);
char buffer[32];
snprintf(buffer, sizeof(buffer), "%d", value);
UIDrawString(painter, bounds, buffer, -1, 0xFF000000, UI_ALIGN_CENTER, nullptr);
} else if (object->type == OBJ_VAR_TEXT_STYLE) {
// TODO Proper rendering.
UIDrawString(painter, bounds, "Text", -1, 0xFF000000, UI_ALIGN_CENTER, nullptr);
} else if (object->type == OBJ_LAYER_BOX) {
// TODO Proper rendering.
UIRectangle borders;
borders.l = CanvasGetIntegerFromProperty(PropertyFindOrInherit(object, "borders0"));
borders.r = CanvasGetIntegerFromProperty(PropertyFindOrInherit(object, "borders1"));
borders.t = CanvasGetIntegerFromProperty(PropertyFindOrInherit(object, "borders2"));
borders.b = CanvasGetIntegerFromProperty(PropertyFindOrInherit(object, "borders3"));
Property *mainPaintProperty = PropertyFindOrInherit(object, "mainPaint", PROP_OBJECT);
Object *mainPaint = ObjectFind(mainPaintProperty ? mainPaintProperty->object : 0);
uint32_t mainPaintColor = CanvasGetColorFromPaint(mainPaint);
Property *borderPaintProperty = PropertyFindOrInherit(object, "borderPaint", PROP_OBJECT);
Object *borderPaint = ObjectFind(borderPaintProperty ? borderPaintProperty->object : 0);
uint32_t borderPaintColor = CanvasGetColorFromPaint(borderPaint);
UIDrawRectangle(painter, bounds, mainPaintColor, borderPaintColor, borders);
} else {
// TODO.
}
}
if (canvasShowArrows) {
// Draw object connections.
// TODO This will be awfully slow when there's many objects...
for (uintptr_t i = 0; i < objects.Length(); i++) {
Object *object = &objects[i];
UIRectangle b1 = CanvasGetObjectBounds(object);
for (uintptr_t j = 0; j < object->properties.Length(); j++) {
if (object->properties[j].type == PROP_OBJECT) {
Object *target = ObjectFind(object->properties[j].object);
if (!target) continue;
UIRectangle b2 = CanvasGetObjectBounds(target);
CanvasDrawArrow(painter, (b2.l + b2.r) / 2, (b2.t + b2.b) / 2, (b1.l + b1.r) / 2, (b1.t + b1.b) / 2, 0xFF000000);
}
}
}
}
} else if (message == UI_MSG_LEFT_DOWN) {
canvasCanDrag = true;
selectedObjectID = 0;
for (uintptr_t i = objects.Length(); i > 0; i--) {
Object *object = &objects[i - 1];
UIRectangle bounds = CanvasGetObjectBounds(object);
if (UIRectangleContains(bounds, element->window->cursorX, element->window->cursorY)) {
if (inspectorPickData) {
selectedObjectID = inspectorPickData->objectID;
canvasCanDrag = false;
Step step = {};
step.type = STEP_MODIFY_PROPERTY;
step.objectID = inspectorPickData->objectID;
strcpy(step.property.cName, inspectorPickData->cPropertyName);
step.property.type = PROP_OBJECT;
step.property.object = object->id;
InspectorBroadcastStep(step, inspectorPickData);
DocumentApplyStep(step);
} else {
selectedObjectID = object->id;
canvasDragOffsetX = bounds.l - element->window->cursorX;
canvasDragOffsetY = bounds.t - element->window->cursorY;
}
break;
}
}
canvasLeftDownX = element->window->cursorX;
canvasLeftDownY = element->window->cursorY;
UIElementRepaint(element, nullptr);
InspectorPopulate();
InspectorPickTargetEnd();
} else if (message == UI_MSG_LEFT_UP && canvasDragging) {
Object *object = ObjectFind(selectedObjectID);
int32_t oldX = PropertyReadInt32(object, "_graphX");
int32_t oldY = PropertyReadInt32(object, "_graphY");
if ((oldX != canvasDragNewX || oldY != canvasDragNewY) && selectedObjectID) {
Step step = {};
step.type = STEP_MODIFY_PROPERTY;
step.objectID = selectedObjectID;
step.property.type = PROP_INT;
strcpy(step.property.cName, "_graphX");
step.property.integer = canvasDragNewX;
DocumentApplyStep(step, STEP_APPLY_GROUPED);
strcpy(step.property.cName, "_graphY");
step.property.integer = canvasDragNewY;
DocumentApplyStep(step);
}
canvasDragging = false;
UIElementRepaint(element, nullptr);
} else if (message == UI_MSG_MOUSE_DRAG && element->window->pressedButton == 1 && selectedObjectID && canvasCanDrag) {
int32_t dx = canvasLeftDownX - element->window->cursorX;
int32_t dy = canvasLeftDownY - element->window->cursorY;
if (canvasDragging || dx * dx + dy * dy > 200) {
canvasDragNewX = element->window->cursorX + canvasPanX + canvasDragOffsetX - element->bounds.l;
canvasDragNewY = element->window->cursorY + canvasPanY + canvasDragOffsetY - element->bounds.t;
canvasDragNewX -= canvasDragNewX % CANVAS_ALIGN, canvasDragNewY -= canvasDragNewY % CANVAS_ALIGN;
canvasDragging = true;
UIElementRepaint(element, nullptr);
}
} else if (message == UI_MSG_MIDDLE_DOWN) {
canvasLastPanPointX = element->window->cursorX;
canvasLastPanPointY = element->window->cursorY;
_UIWindowSetCursor(element->window, UI_CURSOR_HAND);
} else if (message == UI_MSG_MIDDLE_UP) {
_UIWindowSetCursor(element->window, UI_CURSOR_ARROW);
} else if (message == UI_MSG_MOUSE_DRAG && element->window->pressedButton == 2) {
canvasPanX -= element->window->cursorX - canvasLastPanPointX;
canvasPanY -= element->window->cursorY - canvasLastPanPointY;
canvasLastPanPointX = element->window->cursorX;
canvasLastPanPointY = element->window->cursorY;
UIElementRepaint(element, nullptr);
} else if (message == UI_MSG_LAYOUT) {
int width = UIElementMessage(canvasControls, UI_MSG_GET_WIDTH, 0, 0);
int height = UIElementMessage(canvasControls, UI_MSG_GET_HEIGHT, 0, 0);
UIRectangle bounds = UI_RECT_4(element->bounds.l + 10, element->bounds.l + 10 + width, element->bounds.b - 10 - height, element->bounds.b - 10);
UIElementMove(canvasControls, bounds, false);
}
return 0;
}
void CanvasToggleArrows(void *) {
canvasShowArrows = !canvasShowArrows;
UIElementRepaint(canvas, nullptr);
}
//////////////////////////////////////////////////////////////
void ObjectAddCommandInternal(void *cp) {
Object object = {};
object.type = (ObjectType) (uintptr_t) cp;
strcpy(object.cName, "untitled");
object.id = ++objectIDAllocator;
Property p;
int32_t x = canvasPanX + UI_RECT_WIDTH(canvas->bounds) / 2;
int32_t y = canvasPanY + UI_RECT_HEIGHT(canvas->bounds) / 2;
x -= x % CANVAS_ALIGN, y -= y % CANVAS_ALIGN;
p = { .type = PROP_INT, .integer = x }; strcpy(p.cName, "_graphX"); object.properties.Add(p);
p = { .type = PROP_INT, .integer = y }; strcpy(p.cName, "_graphY"); object.properties.Add(p);
p = { .type = PROP_INT, .integer = 60 }; strcpy(p.cName, "_graphW"); object.properties.Add(p);
p = { .type = PROP_INT, .integer = 60 }; strcpy(p.cName, "_graphH"); object.properties.Add(p);
Step step = {};
step.type = STEP_ADD_OBJECT;
step.object = object;
DocumentApplyStep(step);
}
void ObjectAddCommand(void *) {
UIMenu *menu = UIMenuCreate(window->pressed, UI_MENU_NO_SCROLL);
UIMenuAddItem(menu, 0, "Style", -1, ObjectAddCommandInternal, (void *) (uintptr_t) OBJ_STYLE);
UIMenuAddItem(menu, 0, "Color variable", -1, ObjectAddCommandInternal, (void *) (uintptr_t) OBJ_VAR_COLOR);
UIMenuAddItem(menu, 0, "Integer variable", -1, ObjectAddCommandInternal, (void *) (uintptr_t) OBJ_VAR_INT);
UIMenuAddItem(menu, 0, "Text style", -1, ObjectAddCommandInternal, (void *) (uintptr_t) OBJ_VAR_TEXT_STYLE);
UIMenuAddItem(menu, 0, "Icon style", -1, ObjectAddCommandInternal, (void *) (uintptr_t) OBJ_VAR_ICON_STYLE);
UIMenuAddItem(menu, 0, "Metrics", -1, ObjectAddCommandInternal, (void *) (uintptr_t) OBJ_LAYER_METRICS);
UIMenuAddItem(menu, 0, "Overwrite paint", -1, ObjectAddCommandInternal, (void *) (uintptr_t) OBJ_PAINT_OVERWRITE);
UIMenuAddItem(menu, 0, "Box layer", -1, ObjectAddCommandInternal, (void *) (uintptr_t) OBJ_LAYER_BOX);
UIMenuAddItem(menu, 0, "Text layer", -1, ObjectAddCommandInternal, (void *) (uintptr_t) OBJ_LAYER_TEXT);
UIMenuAddItem(menu, 0, "Modify color", -1, ObjectAddCommandInternal, (void *) (uintptr_t) OBJ_MOD_COLOR);
UIMenuAddItem(menu, 0, "Modify integer", -1, ObjectAddCommandInternal, (void *) (uintptr_t) OBJ_MOD_MULTIPLY);
UIMenuShow(menu);
}
void ObjectDeleteCommand(void *) {
if (!selectedObjectID) return;
Step step = {};
step.type = STEP_DELETE_OBJECT;
step.objectID = selectedObjectID;
DocumentApplyStep(step);
}
void ObjectDuplicateCommand(void *) {
if (!selectedObjectID) return;
Object *source = ObjectFind(selectedObjectID);
UI_ASSERT(source);
Object object = {};
object.type = source->type;
strcpy(object.cName, "duplicate");
object.id = ++objectIDAllocator;
object.properties.InsertMany(0, source->properties.Length());
memcpy(object.properties.array, source->properties.array, source->properties.Length() * sizeof(Property));
Property *graphX = PropertyFind(&object, "_graphX", PROP_INT);
if (graphX) graphX->integer += 60;
Step step = {};
step.type = STEP_ADD_OBJECT;
step.object = object;
DocumentApplyStep(step);
}
//////////////////////////////////////////////////////////////
int WindowMessage(UIElement *element, UIMessage message, int di, void *dp) {
if (message == UI_MSG_WINDOW_CLOSE) {
#ifndef OS_ESSENCE
if (documentModified) {
const char *dialog = "Document modified. Save changes?\n%f%b%b";
const char *result = UIDialogShow(window, 0, dialog, "Save", "Discard");
if (0 == strcmp(result, "Save")) {
DocumentSave(nullptr);
}
}
#endif
}
return 0;
}
#ifdef OS_ESSENCE
void DocumentFileMenu(void *) {
EsFileMenuCreate(ui.instance, ui.instance->window, ES_MENU_AT_CURSOR);
}
#endif
int main() {
UIInitialise();
ui.theme = _uiThemeClassic;
window = UIWindowCreate(0, UI_ELEMENT_PARENT_PUSH | UI_WINDOW_MAXIMIZE, "Designer", 0, 0);
window->e.messageUser = WindowMessage;
UIPanelCreate(0, UI_ELEMENT_PARENT_PUSH | UI_PANEL_EXPAND);
UIPanelCreate(0, UI_ELEMENT_PARENT_PUSH | UI_PANEL_HORIZONTAL | UI_PANEL_GRAY | UI_PANEL_SMALL_SPACING);
#ifdef OS_ESSENCE
UIButtonCreate(0, UI_BUTTON_DROP_DOWN, "File", -1)->invoke = DocumentFileMenu;
#else
UIButtonCreate(0, 0, "Save", -1)->invoke = DocumentSave;
#endif
UIButtonCreate(0, 0, "Add object...", -1)->invoke = ObjectAddCommand;
UISpacerCreate(0, 0, 15, 0);
labelMessage = UILabelCreate(0, UI_ELEMENT_H_FILL, 0, 0);
UIParentPop();
UISpacerCreate(0, UI_SPACER_LINE, 0, 1);
UISplitPaneCreate(0, UI_ELEMENT_PARENT_PUSH | UI_ELEMENT_V_FILL, 0.75f);
canvas = UIElementCreate(sizeof(UIElement), 0, 0, CanvasMessage, "Canvas");
inspector = &UIPanelCreate(0, UI_PANEL_GRAY | UI_PANEL_MEDIUM_SPACING | UI_PANEL_SCROLL | UI_PANEL_EXPAND)->e;
InspectorPopulate();
canvasControls = &UIPanelCreate(canvas, UI_PANEL_HORIZONTAL | UI_ELEMENT_PARENT_PUSH)->e;
UIButtonCreate(0, 0, "Toggle arrows", -1)->invoke = CanvasToggleArrows;
UIParentPop();
UIWindowRegisterShortcut(window, UI_SHORTCUT(UI_KEYCODE_LETTER('Z'), 1 /* ctrl */, 0, 0, DocumentUndoStep, 0));
UIWindowRegisterShortcut(window, UI_SHORTCUT(UI_KEYCODE_LETTER('Y'), 1 /* ctrl */, 0, 0, DocumentRedoStep, 0));
UIWindowRegisterShortcut(window, UI_SHORTCUT(UI_KEYCODE_LETTER('D'), 1 /* ctrl */, 0, 0, ObjectDuplicateCommand, 0));
UIWindowRegisterShortcut(window, UI_SHORTCUT(UI_KEYCODE_DELETE, 0, 0, 0, ObjectDeleteCommand, 0));
#ifdef OS_ESSENCE
EsWindowSetIcon(ui.instance->window, ES_ICON_APPLICATIONS_INTERFACEDESIGN);
EsInstanceSetClassEditor(ui.instance, &instanceClassEditorSettings);
#else
UIWindowRegisterShortcut(window, UI_SHORTCUT(UI_KEYCODE_LETTER('S'), 1 /* ctrl */, 0, 0, DocumentSave, 0));
DocumentLoad();
#endif
int result = UIMessageLoop();
DocumentFree();
return result;
}
#ifdef OS_ESSENCE
void _UIMessageProcess(EsMessage *message) {
if (message->type == ES_MSG_INSTANCE_OPEN) {
DocumentFree();
fileStore = message->instanceOpen.file;
DocumentLoad();
fileStore = nullptr;
EsInstanceOpenComplete(message, true);
} else if (message->type == ES_MSG_INSTANCE_SAVE) {
fileStore = message->instanceSave.file;
DocumentSave(nullptr);
fileStore = nullptr;
EsInstanceSaveComplete(message, true);
}
}
void _start() {
_init();
main();
}
#endif