#define UI_IMPLEMENTATION #define ES_CRT_WITHOUT_PREFIX #include "luigi.h" #ifndef OS_ESSENCE #include #include #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 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 undoStack; Array redoStack; bool documentModified; // Document state: Array 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(©, 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 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