From 1f92f55e46c5f3998b4aedb90985929167927c28 Mon Sep 17 00:00:00 2001 From: nakst <> Date: Sat, 25 Dec 2021 20:31:31 +0000 Subject: [PATCH] remove old text code; organize UI code --- desktop/api.cpp | 10 +- desktop/gui.cpp | 891 +++----- desktop/inspector.cpp | 602 ++++++ desktop/text.cpp | 2822 +------------------------- desktop/textbox.cpp | 2132 +++++++++++++++++++ desktop/theme.cpp | 23 - res/Fonts/Bitmap Sans Regular 9.font | Bin 2770 -> 2872 bytes shared/strings.cpp | 5 +- util/build_common.h | 3 +- util/build_core.c | 40 +- util/font_editor.c | 104 +- 11 files changed, 3156 insertions(+), 3476 deletions(-) create mode 100644 desktop/inspector.cpp create mode 100644 desktop/textbox.cpp diff --git a/desktop/api.cpp b/desktop/api.cpp index b57eac2..1b4dc7e 100644 --- a/desktop/api.cpp +++ b/desktop/api.cpp @@ -241,19 +241,13 @@ struct APIInstance { EsTextbox *fileMenuNameTextbox; // Also used by the file save dialog. }; -#define CHARACTER_MONO (1) // 1 bit per pixel. -#define CHARACTER_SUBPIXEL (2) // 24 bits per pixel; each byte specifies the alpha of each RGB channel. -#define CHARACTER_IMAGE (3) // 32 bits per pixel, ARGB. -#define CHARACTER_RECOLOR (4) // 32 bits per pixel, AXXX. - #include "syscall.cpp" +#include "profiling.cpp" #include "renderer.cpp" #include "theme.cpp" -#define TEXT_RENDERER #include "text.cpp" -#undef TEXT_RENDERER -#include "profiling.cpp" #include "gui.cpp" +#include "inspector.cpp" #ifndef NO_API_TABLE const void *const apiTable[] = { diff --git a/desktop/gui.cpp b/desktop/gui.cpp index dffcd33..59f54c8 100644 --- a/desktop/gui.cpp +++ b/desktop/gui.cpp @@ -460,8 +460,6 @@ void HeapDuplicate(void **pointer, size_t *outBytes, const void *data, size_t by } } -// --------------------------------- Windows. - struct EsWindow : EsElement { EsHandle handle; EsObjectID id; @@ -3043,6 +3041,14 @@ EsScrollView *EsCustomScrollViewCreate(EsElement *parent, uint64_t flags, const return element; } +// --------------------------------- Textboxes. + +#include "textbox.cpp" + +// --------------------------------- List views. + +#include "list_view.cpp" + // --------------------------------- Panels. void PanelSwitcherTransitionComplete(EsPanel *panel) { @@ -3889,11 +3895,275 @@ EsCanvasPane *EsCanvasPaneCreate(EsElement *parent, uint64_t flags, const EsStyl return pane; } -// --------------------------------- Text displays and textboxes. +// --------------------------------- Text displays. -#define TEXT_ELEMENTS -#include "text.cpp" -#undef TEXT_ELEMENTS +// TODO Inline images and icons. +// TODO Links. +// TODO Inline backgrounds. + +void TextDisplayFreeRuns(EsTextDisplay *display) { + if (display->usingSyntaxHighlighting) { + Array textRuns = { display->textRuns }; + textRuns.Free(); + } else { + EsHeapFree(display->textRuns); + } +} + +int ProcessTextDisplayMessage(EsElement *element, EsMessage *message) { + EsTextDisplay *display = (EsTextDisplay *) element; + + if (message->type == ES_MSG_PAINT) { + EsRectangle textBounds = EsPainterBoundsInset(message->painter); + + if (!display->plan || display->planWidth != textBounds.r - textBounds.l || display->planHeight != textBounds.b - textBounds.t) { + if (display->plan) EsTextPlanDestroy(display->plan); + display->properties.flags = display->style->textAlign; + if (~display->flags & ES_TEXT_DISPLAY_PREFORMATTED) display->properties.flags |= ES_TEXT_PLAN_TRIM_SPACES; + if (display->flags & ES_TEXT_DISPLAY_NO_FONT_SUBSTITUTION) display->properties.flags |= ES_TEXT_PLAN_NO_FONT_SUBSTITUTION; + display->plan = EsTextPlanCreate(element, &display->properties, textBounds, display->contents, display->textRuns, display->textRunCount); + display->planWidth = textBounds.r - textBounds.l; + display->planHeight = textBounds.b - textBounds.t; + } + + if (display->plan) { + EsDrawTextLayers(message->painter, display->plan, EsPainterBoundsInset(message->painter)); + } + } else if (message->type == ES_MSG_GET_WIDTH || message->type == ES_MSG_GET_HEIGHT) { + if (!display->measurementCache.Get(message, &display->state)) { + if (display->plan) EsTextPlanDestroy(display->plan); + display->properties.flags = display->style->textAlign | ((display->flags & ES_TEXT_DISPLAY_PREFORMATTED) ? 0 : ES_TEXT_PLAN_TRIM_SPACES); + EsRectangle insets = EsElementGetInsets(element); + display->planWidth = message->type == ES_MSG_GET_HEIGHT && message->measure.width + ? (message->measure.width - insets.l - insets.r) : 0; + display->planHeight = 0; + display->plan = EsTextPlanCreate(element, &display->properties, + ES_RECT_4(0, display->planWidth, 0, 0), + display->contents, display->textRuns, display->textRunCount); + + if (!display->plan) { + message->measure.width = message->measure.height = 0; + } else { + if (message->type == ES_MSG_GET_WIDTH) { + message->measure.width = EsTextPlanGetWidth(display->plan) + insets.l + insets.r; + } else { + message->measure.height = EsTextPlanGetHeight(display->plan) + insets.t + insets.b; + } + } + + display->measurementCache.Store(message); + } + } else if (message->type == ES_MSG_DESTROY) { + if (display->plan) { + EsTextPlanDestroy(display->plan); + } + + TextDisplayFreeRuns(display); + EsHeapFree(display->contents); + } else if (message->type == ES_MSG_GET_INSPECTOR_INFORMATION) { + EsBufferFormat(message->getContent.buffer, "'%s'", display->textRuns[display->textRunCount].offset, display->contents); + } else if (message->type == ES_MSG_UI_SCALE_CHANGED) { + if (display->plan) { + EsTextPlanDestroy(display->plan); + display->plan = nullptr; + } + } else { + return 0; + } + + return ES_HANDLED; +} + +void EsTextDisplaySetStyledContents(EsTextDisplay *display, const char *string, EsTextRun *runs, size_t runCount) { + TextDisplayFreeRuns(display); + + display->textRuns = (EsTextRun *) EsHeapAllocate(sizeof(EsTextRun) * (runCount + 1), true); + display->textRunCount = runCount; + + size_t outBytes; + HeapDuplicate((void **) &display->contents, &outBytes, string, runs[runCount].offset); + + if (outBytes != runs[runCount].offset) { + // TODO Handle allocation failure. + } + + EsMemoryCopy(display->textRuns, runs, sizeof(EsTextRun) * (runCount + 1)); + + display->usingSyntaxHighlighting = false; + EsElementUpdateContentSize(display); + InspectorNotifyElementContentChanged(display); +} + +void EsTextDisplaySetContents(EsTextDisplay *display, const char *string, ptrdiff_t stringBytes) { + if (stringBytes == -1) stringBytes = EsCStringLength(string); + + TextDisplayFreeRuns(display); + + if (display->flags & ES_TEXT_DISPLAY_RICH_TEXT) { + EsHeapFree(display->contents); + EsTextStyle baseStyle = {}; + display->style->GetTextStyle(&baseStyle); + EsRichTextParse(string, stringBytes, &display->contents, &display->textRuns, &display->textRunCount, &baseStyle); + } else { + HeapDuplicate((void **) &display->contents, (size_t *) &stringBytes, string, stringBytes); + display->textRuns = (EsTextRun *) EsHeapAllocate(sizeof(EsTextRun) * 2, true); + display->style->GetTextStyle(&display->textRuns[0].style); + display->textRuns[1].offset = stringBytes; + display->textRunCount = 1; + } + + display->usingSyntaxHighlighting = false; + EsElementUpdateContentSize(display); + InspectorNotifyElementContentChanged(display); +} + +EsTextDisplay *EsTextDisplayCreate(EsElement *parent, uint64_t flags, const EsStyle *style, const char *label, ptrdiff_t labelBytes) { + EsTextDisplay *display = (EsTextDisplay *) EsHeapAllocate(sizeof(EsTextDisplay), true); + if (!display) return nullptr; + display->Initialise(parent, flags, ProcessTextDisplayMessage, style ?: UIGetDefaultStyleVariant(ES_STYLE_TEXT_LABEL, parent)); + display->cName = "text display"; + if (labelBytes == -1) labelBytes = EsCStringLength(label); + EsTextDisplaySetContents(display, label, labelBytes); + return display; +} + +void EsTextDisplaySetupSyntaxHighlighting(EsTextDisplay *display, uint32_t language, uint32_t *customColors, size_t customColorCount) { + // Copied from EsTextboxSetupSyntaxHighlighting. + uint32_t colors[8]; + colors[0] = 0x04000000; // Highlighted line. + colors[1] = 0xFF000000; // Default. + colors[2] = 0xFFA11F20; // Comment. + colors[3] = 0xFF037E01; // String. + colors[4] = 0xFF213EF1; // Number. + colors[5] = 0xFF7F0480; // Operator. + colors[6] = 0xFF545D70; // Preprocessor. + colors[7] = 0xFF17546D; // Keyword. + + if (customColorCount > sizeof(colors) / sizeof(uint32_t)) customColorCount = sizeof(colors) / sizeof(uint32_t); + EsMemoryCopy(colors, customColors, customColorCount * sizeof(uint32_t)); + + EsTextStyle textStyle = {}; + display->style->GetTextStyle(&textStyle); + + EsTextRun *newRuns = TextApplySyntaxHighlighting(&textStyle, language, colors, {}, + display->contents, display->textRuns[display->textRunCount].offset).array; + TextDisplayFreeRuns(display); + display->textRuns = newRuns; + display->textRunCount = ArrayLength(display->textRuns) - 1; + display->usingSyntaxHighlighting = true; + display->Repaint(true); +} + +// --------------------------------- List displays. + +struct EsListDisplay : EsElement { + uintptr_t itemCount, startIndex; + EsListDisplay *previous; +}; + +int ProcessListDisplayMessage(EsElement *element, EsMessage *message) { + EsListDisplay *display = (EsListDisplay *) element; + + if (message->type == ES_MSG_GET_HEIGHT) { + int32_t height = 0; + int32_t margin = element->style->insets.l + element->style->insets.r + element->style->gapMinor; + uintptr_t itemCount = 0; + + for (uintptr_t i = 0; i < element->GetChildCount(); i++) { + EsElement *child = element->GetChild(i); + if (child->flags & ES_ELEMENT_NON_CLIENT) continue; + height += child->GetHeight(message->measure.width - margin); + itemCount++; + } + + if (itemCount) { + height += (itemCount - 1) * element->style->gapMajor; + } + + message->measure.height = height + element->style->insets.t + element->style->insets.b; + } else if (message->type == ES_MSG_LAYOUT) { + int32_t position = element->style->insets.t; + int32_t margin = element->style->insets.l + element->style->gapMinor; + int32_t width = element->width - margin - element->style->insets.r; + + for (uintptr_t i = 0; i < element->GetChildCount(); i++) { + EsElement *child = element->GetChild(i); + if (child->flags & ES_ELEMENT_NON_CLIENT) continue; + int height = child->GetHeight(width); + EsElementMove(child, margin, position, width, height); + position += height + element->style->gapMajor; + } + } else if (message->type == ES_MSG_PAINT) { + char buffer[64]; + EsTextPlanProperties properties = {}; + properties.flags = ES_TEXT_H_RIGHT | ES_TEXT_V_TOP | ES_TEXT_PLAN_SINGLE_USE; + EsTextRun textRun[2] = {}; + + EsRectangle bounds = EsPainterBoundsClient(message->painter); + bounds.r = bounds.l + element->style->insets.l; + + uintptr_t counter = display->previous ? display->previous->itemCount : display->startIndex; + uint8_t markerType = element->flags & ES_LIST_DISPLAY_MARKER_TYPE_MASK; + + EsMessage m = {}; + m.type = ES_MSG_LIST_DISPLAY_GET_MARKER; + EsBuffer buffer2 = { .out = (uint8_t *) buffer, .bytes = sizeof(buffer) }; + m.getContent.buffer = &buffer2; + + for (uintptr_t i = 0; i < element->GetChildCount(); i++) { + EsElement *child = element->GetChild(i); + if (child->flags & ES_ELEMENT_NON_CLIENT) continue; + + if (markerType == ES_LIST_DISPLAY_BULLETED) { + EsMemoryCopy(buffer, "\xE2\x80\xA2", (textRun[1].offset = 3)); + } else if (markerType == ES_LIST_DISPLAY_NUMBERED) { + textRun[1].offset = EsStringFormat(buffer, sizeof(buffer), "%d.", counter + 1); + } else if (markerType == ES_LIST_DISPLAY_LOWER_ALPHA) { + textRun[1].offset = EsStringFormat(buffer, sizeof(buffer), "(%c)", counter + 'a'); + } else if (markerType == ES_LIST_DISPLAY_CUSTOM_MARKER) { + m.getContent.index = counter; + EsMessageSend(element, &m); + textRun[1].offset = buffer2.position; + } else { + EsAssert(false); + } + + child->style->GetTextStyle(&textRun[0].style); + textRun[0].style.figures = ES_TEXT_FIGURE_TABULAR; + bounds.t += child->offsetY; + bounds.b = bounds.t + child->height; + EsTextPlan *plan = EsTextPlanCreate(element, &properties, bounds, buffer, textRun, 1); + if (plan) EsDrawText(message->painter, plan, bounds); + bounds.t -= child->offsetY; + counter++; + } + } else if (message->type == ES_MSG_ADD_CHILD) { + display->itemCount++; + } else if (message->type == ES_MSG_REMOVE_CHILD) { + display->itemCount--; + } + + return 0; +} + +EsListDisplay *EsListDisplayCreate(EsElement *parent, uint64_t flags, const EsStyle *style) { + EsListDisplay *display = (EsListDisplay *) EsHeapAllocate(sizeof(EsListDisplay), true); + if (!display) return nullptr; + display->Initialise(parent, flags, ProcessListDisplayMessage, style ?: ES_STYLE_LIST_DISPLAY_DEFAULT); + display->cName = "list display"; + return display; +} + +void EsListDisplaySetCounterContinuation(EsListDisplay *display, EsListDisplay *previous) { + display->previous = previous; + EsElementRepaint(display); +} + +void EsListDisplaySetCounterStart(EsListDisplay *display, uintptr_t index) { + display->startIndex = index; + display->previous = nullptr; + EsElementRepaint(display); +} // --------------------------------- Announcements. @@ -7596,612 +7866,3 @@ void UIProcessWindowManagerMessage(EsWindow *window, EsMessage *message, Process window->willUpdate = false; } - -// --------------------------------- List view. - -#include "list_view.cpp" - -// --------------------------------- Inspector. - -struct InspectorElementEntry { - EsElement *element; - EsRectangle takenBounds, givenBounds; - int depth; -}; - -struct InspectorWindow : EsInstance { - EsInstance *instance; // The instance being inspected. - - EsListView *elementList; - Array elements; // TODO This is being leaked. - InspectorElementEntry hoveredElement; - char *cCategoryFilter; - - intptr_t selectedElement; - EsButton *alignH[6]; - EsButton *alignV[6]; - EsButton *direction[4]; - EsTextbox *contentTextbox; - EsButton *addChildButton; - EsButton *addSiblingButton; - EsButton *visualizeRepaints; - EsButton *visualizeLayoutBounds; - EsButton *visualizePaintSteps; - EsListView *listEvents; - EsTextbox *textboxCategoryFilter; -}; - -int InspectorElementItemCallback(EsElement *element, EsMessage *message) { - InspectorWindow *inspector = (InspectorWindow *) element->instance; - - if (message->type == ES_MSG_HOVERED_START) { - InspectorElementEntry *entry = &inspector->elements[EsListViewGetIndexFromItem(element)]; - if (entry->element->parent) entry->element->parent->Repaint(true); - else entry->element->Repaint(true); - inspector->hoveredElement = *entry; - } else if (message->type == ES_MSG_HOVERED_END || message->type == ES_MSG_DESTROY) { - EsListViewIndex index = EsListViewGetIndexFromItem(element); - InspectorElementEntry *entry = &inspector->elements[index]; - if (entry->element->parent) entry->element->parent->Repaint(true); - else entry->element->Repaint(true); - inspector->hoveredElement = {}; - } - - return 0; -} - -void InspectorUpdateEditor(InspectorWindow *inspector) { - EsElement *e = inspector->selectedElement == -1 ? nullptr : inspector->elements[inspector->selectedElement].element; - - bool isStack = e && e->messageClass == ProcessPanelMessage && !(e->flags & (ES_PANEL_Z_STACK | ES_PANEL_TABLE | ES_PANEL_SWITCHER)); - bool alignHLeft = e ? (e->flags & ES_CELL_H_LEFT) : false, alignHRight = e ? (e->flags & ES_CELL_H_RIGHT) : false; - bool alignHExpand = e ? (e->flags & ES_CELL_H_EXPAND) : false, alignHShrink = e ? (e->flags & ES_CELL_H_SHRINK) : false; - bool alignHPush = e ? (e->flags & ES_CELL_H_PUSH) : false; - bool alignVTop = e ? (e->flags & ES_CELL_V_TOP) : false, alignVBottom = e ? (e->flags & ES_CELL_V_BOTTOM) : false; - bool alignVExpand = e ? (e->flags & ES_CELL_V_EXPAND) : false, alignVShrink = e ? (e->flags & ES_CELL_V_SHRINK) : false; - bool alignVPush = e ? (e->flags & ES_CELL_V_PUSH) : false; - bool stackHorizontal = isStack && (e->flags & ES_PANEL_HORIZONTAL); - bool stackReverse = isStack && (e->flags & ES_PANEL_REVERSE); - - EsButtonSetCheck(inspector->alignH[0], (EsCheckState) (e && alignHLeft && !alignHRight), false); - EsButtonSetCheck(inspector->alignH[1], (EsCheckState) (e && alignHLeft == alignHRight), false); - EsButtonSetCheck(inspector->alignH[2], (EsCheckState) (e && !alignHLeft && alignHRight), false); - EsButtonSetCheck(inspector->alignH[3], (EsCheckState) (e && alignHExpand), false); - EsButtonSetCheck(inspector->alignH[4], (EsCheckState) (e && alignHShrink), false); - EsButtonSetCheck(inspector->alignH[5], (EsCheckState) (e && alignHPush), false); - - EsButtonSetCheck(inspector->alignV[0], (EsCheckState) (e && alignVTop && !alignVBottom), false); - EsButtonSetCheck(inspector->alignV[1], (EsCheckState) (e && alignVTop == alignVBottom), false); - EsButtonSetCheck(inspector->alignV[2], (EsCheckState) (e && !alignVTop && alignVBottom), false); - EsButtonSetCheck(inspector->alignV[3], (EsCheckState) (e && alignVExpand), false); - EsButtonSetCheck(inspector->alignV[4], (EsCheckState) (e && alignVShrink), false); - EsButtonSetCheck(inspector->alignV[5], (EsCheckState) (e && alignVPush), false); - - EsButtonSetCheck(inspector->direction[0], (EsCheckState) (isStack && stackHorizontal && stackReverse), false); - EsButtonSetCheck(inspector->direction[1], (EsCheckState) (isStack && stackHorizontal && !stackReverse), false); - EsButtonSetCheck(inspector->direction[2], (EsCheckState) (isStack && !stackHorizontal && stackReverse), false); - EsButtonSetCheck(inspector->direction[3], (EsCheckState) (isStack && !stackHorizontal && !stackReverse), false); - - EsElementSetDisabled(inspector->alignH[0], !e); - EsElementSetDisabled(inspector->alignH[1], !e); - EsElementSetDisabled(inspector->alignH[2], !e); - EsElementSetDisabled(inspector->alignH[3], !e); - EsElementSetDisabled(inspector->alignH[4], !e); - EsElementSetDisabled(inspector->alignH[5], !e); - EsElementSetDisabled(inspector->alignV[0], !e); - EsElementSetDisabled(inspector->alignV[1], !e); - EsElementSetDisabled(inspector->alignV[2], !e); - EsElementSetDisabled(inspector->alignV[3], !e); - EsElementSetDisabled(inspector->alignV[4], !e); - EsElementSetDisabled(inspector->alignV[5], !e); - EsElementSetDisabled(inspector->direction[0], !isStack); - EsElementSetDisabled(inspector->direction[1], !isStack); - EsElementSetDisabled(inspector->direction[2], !isStack); - EsElementSetDisabled(inspector->direction[3], !isStack); - EsElementSetDisabled(inspector->addChildButton, !isStack); - EsElementSetDisabled(inspector->addSiblingButton, !e || !e->parent); - - EsElementSetDisabled(inspector->textboxCategoryFilter, !e); - - EsTextboxSelectAll(inspector->contentTextbox); - EsTextboxInsert(inspector->contentTextbox, "", 0, false); - - if (e) { -#if 0 - for (uintptr_t i = 0; i < sizeof(builtinStyles) / sizeof(builtinStyles[0]); i++) { - if (e->currentStyleKey.partHash == CalculateCRC64(EsLiteral(builtinStyles[i]))) { - EsTextboxInsert(inspector->styleTextbox, builtinStyles[i], -1, false); - break; - } - } -#endif - - if (e->messageClass == ProcessButtonMessage) { - EsButton *button = (EsButton *) e; - EsElementSetDisabled(inspector->contentTextbox, false); - EsTextboxInsert(inspector->contentTextbox, button->label, button->labelBytes, false); - } else if (e->messageClass == ProcessTextDisplayMessage) { - EsTextDisplay *display = (EsTextDisplay *) e; - EsElementSetDisabled(inspector->contentTextbox, false); - EsTextboxInsert(inspector->contentTextbox, display->contents, display->textRuns[display->textRunCount].offset, false); - } else { - EsElementSetDisabled(inspector->contentTextbox, true); - } - } else { - EsElementSetDisabled(inspector->contentTextbox, true); - } -} - -int InspectorElementListCallback(EsElement *element, EsMessage *message) { - InspectorWindow *inspector = (InspectorWindow *) element->instance; - - if (message->type == ES_MSG_LIST_VIEW_GET_CONTENT) { - int column = message->getContent.columnID, index = message->getContent.index; - EsAssert(index >= 0 && index < (int) inspector->elements.Length()); - InspectorElementEntry *entry = &inspector->elements[index]; - - if (column == 0) { - EsBufferFormat(message->getContent.buffer, "%z", entry->element->cName); - } else if (column == 1) { - EsBufferFormat(message->getContent.buffer, "%R", entry->element->GetWindowBounds(false)); - } else if (column == 2) { - EsMessage m = *message; - m.type = ES_MSG_GET_INSPECTOR_INFORMATION; - EsMessageSend(entry->element, &m); - } - - return ES_HANDLED; - } else if (message->type == ES_MSG_LIST_VIEW_GET_INDENT) { - message->getIndent.indent = inspector->elements[message->getIndent.index].depth; - return ES_HANDLED; - } else if (message->type == ES_MSG_LIST_VIEW_CREATE_ITEM) { - message->createItem.item->messageUser = InspectorElementItemCallback; - return ES_HANDLED; - } else if (message->type == ES_MSG_LIST_VIEW_SELECT) { - if (inspector->selectedElement != -1) { - inspector->elements[inspector->selectedElement].element->state &= ~UI_STATE_INSPECTING; - } - - inspector->selectedElement = message->selectItem.isSelected ? message->selectItem.index : -1; - - if (inspector->selectedElement != -1) { - EsElement *e = inspector->elements[inspector->selectedElement].element; - e->state |= UI_STATE_INSPECTING; - InspectorNotifyElementEvent(e, nullptr, "Viewing events from '%z'.\n", e->cName); - } - - InspectorUpdateEditor(inspector); - return ES_HANDLED; - } else if (message->type == ES_MSG_LIST_VIEW_IS_SELECTED) { - message->selectItem.isSelected = message->selectItem.index == inspector->selectedElement; - return ES_HANDLED; - } - - return 0; -} - -int InspectorContentTextboxCallback(EsElement *element, EsMessage *message) { - InspectorWindow *inspector = (InspectorWindow *) element->instance; - - if (message->type == ES_MSG_TEXTBOX_EDIT_END) { - size_t newContentBytes; - char *newContent = EsTextboxGetContents(inspector->contentTextbox, &newContentBytes); - EsElement *e = inspector->elements[inspector->selectedElement].element; - - if (e->messageClass == ProcessButtonMessage) { - EsButton *button = (EsButton *) e; - HeapDuplicate((void **) &button->label, &button->labelBytes, newContent, newContentBytes); - } else if (e->messageClass == ProcessTextDisplayMessage) { - EsTextDisplay *display = (EsTextDisplay *) e; - EsTextDisplaySetContents(display, newContent, newContentBytes); - } else { - EsAssert(false); - } - - EsElementUpdateContentSize(e); - if (e->parent) EsElementUpdateContentSize(e->parent); - EsHeapFree(newContent); - return ES_HANDLED; - } - - return 0; -} - -int InspectorTextboxCategoryFilterCallback(EsElement *element, EsMessage *message) { - InspectorWindow *inspector = (InspectorWindow *) element->instance; - - if (message->type == ES_MSG_TEXTBOX_UPDATED) { - EsHeapFree(inspector->cCategoryFilter); - inspector->cCategoryFilter = EsTextboxGetContents((EsTextbox *) element); - } - - return 0; -} - -InspectorWindow *InspectorGet(EsElement *element) { - if (!element->window || !element->instance) return nullptr; - APIInstance *instance = (APIInstance *) element->instance->_private; - InspectorWindow *inspector = instance->attachedInspector; - if (!inspector || inspector->instance->window != element->window) return nullptr; - return inspector; -} - -void InspectorNotifyElementEvent(EsElement *element, const char *cCategory, const char *cFormat, ...) { - if (~element->state & UI_STATE_INSPECTING) return; - InspectorWindow *inspector = InspectorGet(element); - if (!inspector) return; - if (inspector->cCategoryFilter && inspector->cCategoryFilter[0] && cCategory && EsCRTstrcmp(cCategory, inspector->cCategoryFilter)) return; - va_list arguments; - va_start(arguments, cFormat); - char _buffer[256]; - EsBuffer buffer = { .out = (uint8_t *) _buffer, .bytes = sizeof(_buffer) }; - if (cCategory) EsBufferFormat(&buffer, "%z: ", cCategory); - EsBufferFormatV(&buffer, cFormat, arguments); - va_end(arguments); - EsListViewFixedItemInsert(inspector->listEvents, _buffer, buffer.position); - EsListViewScrollToEnd(inspector->listEvents); -} - -void InspectorNotifyElementContentChanged(EsElement *element) { - InspectorWindow *inspector = InspectorGet(element); - if (!inspector) return; - - for (uintptr_t i = 0; i < inspector->elements.Length(); i++) { - if (inspector->elements[i].element == element) { - EsListViewInvalidateContent(inspector->elementList, 0, i); - return; - } - } - - EsAssert(false); -} - -void InspectorNotifyElementMoved(EsElement *element, EsRectangle takenBounds) { - InspectorWindow *inspector = InspectorGet(element); - if (!inspector) return; - - for (uintptr_t i = 0; i < inspector->elements.Length(); i++) { - if (inspector->elements[i].element == element) { - inspector->elements[i].takenBounds = takenBounds; - inspector->elements[i].givenBounds = takenBounds; // TODO. - EsListViewInvalidateContent(inspector->elementList, 0, i); - return; - } - } - - EsAssert(false); -} - -void InspectorNotifyElementDestroyed(EsElement *element) { - InspectorWindow *inspector = InspectorGet(element); - if (!inspector) return; - - for (uintptr_t i = 0; i < inspector->elements.Length(); i++) { - if (inspector->elements[i].element == element) { - if (inspector->selectedElement == (intptr_t) i) { - inspector->selectedElement = -1; - InspectorUpdateEditor(inspector); - } else if (inspector->selectedElement > (intptr_t) i) { - inspector->selectedElement--; - } - - EsListViewRemove(inspector->elementList, 0, i, 1); - inspector->elements.Delete(i); - return; - } - } - - EsAssert(false); -} - -void InspectorNotifyElementCreated(EsElement *element) { - InspectorWindow *inspector = InspectorGet(element); - if (!inspector) return; - - ptrdiff_t indexInParent = -1; - - for (uintptr_t i = 0; i < element->parent->children.Length(); i++) { - if (element->parent->children[i] == element) { - indexInParent = i; - break; - } - } - - EsAssert(indexInParent != -1); - - ptrdiff_t insertAfterIndex = -1; - - for (uintptr_t i = 0; i < inspector->elements.Length(); i++) { - if (indexInParent == 0) { - if (inspector->elements[i].element == element->parent) { - insertAfterIndex = i; - break; - } - } else { - if (inspector->elements[i].element == element->parent->children[indexInParent - 1]) { - insertAfterIndex = i; - int baseDepth = inspector->elements[i++].depth; - - for (; i < inspector->elements.Length(); i++) { - if (inspector->elements[i].depth > baseDepth) { - insertAfterIndex++; - } else { - break; - } - } - - break; - } - } - } - - EsAssert(insertAfterIndex != -1); - - int depth = 0; - EsElement *ancestor = element->parent; - - while (ancestor) { - depth++; - ancestor = ancestor->parent; - } - - if (inspector->selectedElement > insertAfterIndex) { - inspector->selectedElement++; - } - - InspectorElementEntry entry; - entry.element = element; - entry.depth = depth; - inspector->elements.Insert(entry, insertAfterIndex + 1); - EsListViewInsert(inspector->elementList, 0, insertAfterIndex + 1, 1); -} - -void InspectorFindElementsRecursively(InspectorWindow *inspector, EsElement *element, int depth) { - InspectorElementEntry entry = {}; - entry.element = element; - entry.depth = depth; - inspector->elements.Add(entry); - - for (uintptr_t i = 0; i < element->children.Length(); i++) { - InspectorFindElementsRecursively(inspector, element->children[i], depth + 1); - } -} - -void InspectorRefreshElementList(InspectorWindow *inspector) { - EsListViewRemoveAll(inspector->elementList, 0); - inspector->elements.Free(); - InspectorFindElementsRecursively(inspector, inspector->instance->window, 0); - EsListViewInsert(inspector->elementList, 0, 0, inspector->elements.Length()); -} - -void InspectorNotifyElementPainted(EsElement *element, EsPainter *painter) { - InspectorWindow *inspector = InspectorGet(element); - if (!inspector) return; - - InspectorElementEntry *entry = inspector->hoveredElement.element ? &inspector->hoveredElement : nullptr; - if (!entry) return; - - EsRectangle bounds = ES_RECT_4(painter->offsetX, painter->offsetX + painter->width, - painter->offsetY, painter->offsetY + painter->height); - - if (entry->element == element) { - EsDrawRectangle(painter, bounds, 0x607F7FFF, 0x60FFFF7F, element->style->insets); - } else if (entry->element->parent == element) { - if ((element->flags & ES_CELL_FILL) != ES_CELL_FILL) { - EsRectangle rectangle = entry->givenBounds; - rectangle.l += bounds.l, rectangle.r += bounds.l; - rectangle.t += bounds.t, rectangle.b += bounds.t; - // EsDrawBlock(painter, rectangle, 0x20FF7FFF); - } - } -} - -#define INSPECTOR_ALIGN_COMMAND(name, clear, set, toggle) \ -void name (EsInstance *instance, EsElement *, EsCommand *) { \ - InspectorWindow *inspector = (InspectorWindow *) instance; \ - EsElement *e = inspector->elements[inspector->selectedElement].element; \ - if (toggle) e->flags ^= set; \ - else { e->flags &= ~(clear); e->flags |= set; } \ - EsElementUpdateContentSize(e); \ - if (e->parent) EsElementUpdateContentSize(e->parent); \ - inspector->elementList->Repaint(true); \ - InspectorUpdateEditor(inspector); \ -} - -INSPECTOR_ALIGN_COMMAND(InspectorHAlignLeft, ES_CELL_H_LEFT | ES_CELL_H_RIGHT, ES_CELL_H_LEFT, false); -INSPECTOR_ALIGN_COMMAND(InspectorHAlignCenter, ES_CELL_H_LEFT | ES_CELL_H_RIGHT, ES_CELL_H_LEFT | ES_CELL_H_RIGHT, false); -INSPECTOR_ALIGN_COMMAND(InspectorHAlignRight, ES_CELL_H_LEFT | ES_CELL_H_RIGHT, ES_CELL_H_RIGHT, false); -INSPECTOR_ALIGN_COMMAND(InspectorHAlignExpand, 0, ES_CELL_H_EXPAND, true); -INSPECTOR_ALIGN_COMMAND(InspectorHAlignShrink, 0, ES_CELL_H_SHRINK, true); -INSPECTOR_ALIGN_COMMAND(InspectorHAlignPush, 0, ES_CELL_H_PUSH, true); -INSPECTOR_ALIGN_COMMAND(InspectorVAlignTop, ES_CELL_V_TOP | ES_CELL_V_BOTTOM, ES_CELL_V_TOP, false); -INSPECTOR_ALIGN_COMMAND(InspectorVAlignCenter, ES_CELL_V_TOP | ES_CELL_V_BOTTOM, ES_CELL_V_TOP | ES_CELL_V_BOTTOM, false); -INSPECTOR_ALIGN_COMMAND(InspectorVAlignBottom, ES_CELL_V_TOP | ES_CELL_V_BOTTOM, ES_CELL_V_BOTTOM, false); -INSPECTOR_ALIGN_COMMAND(InspectorVAlignExpand, 0, ES_CELL_V_EXPAND, true); -INSPECTOR_ALIGN_COMMAND(InspectorVAlignShrink, 0, ES_CELL_V_SHRINK, true); -INSPECTOR_ALIGN_COMMAND(InspectorVAlignPush, 0, ES_CELL_V_PUSH, true); -INSPECTOR_ALIGN_COMMAND(InspectorDirectionLeft, ES_PANEL_HORIZONTAL | ES_PANEL_REVERSE | ES_ELEMENT_LAYOUT_HINT_HORIZONTAL | ES_ELEMENT_LAYOUT_HINT_REVERSE, - ES_PANEL_HORIZONTAL | ES_PANEL_REVERSE | ES_ELEMENT_LAYOUT_HINT_HORIZONTAL | ES_ELEMENT_LAYOUT_HINT_REVERSE, false); -INSPECTOR_ALIGN_COMMAND(InspectorDirectionRight, ES_PANEL_HORIZONTAL | ES_PANEL_REVERSE | ES_ELEMENT_LAYOUT_HINT_HORIZONTAL | ES_ELEMENT_LAYOUT_HINT_REVERSE, - ES_PANEL_HORIZONTAL | ES_ELEMENT_LAYOUT_HINT_HORIZONTAL, false); -INSPECTOR_ALIGN_COMMAND(InspectorDirectionUp, ES_PANEL_HORIZONTAL | ES_PANEL_REVERSE | ES_ELEMENT_LAYOUT_HINT_HORIZONTAL | ES_ELEMENT_LAYOUT_HINT_REVERSE, - ES_PANEL_REVERSE | ES_ELEMENT_LAYOUT_HINT_REVERSE, false); -INSPECTOR_ALIGN_COMMAND(InspectorDirectionDown, ES_PANEL_HORIZONTAL | ES_PANEL_REVERSE | ES_ELEMENT_LAYOUT_HINT_HORIZONTAL | ES_ELEMENT_LAYOUT_HINT_REVERSE, 0, false); - -void InspectorVisualizeRepaints(EsInstance *instance, EsElement *, EsCommand *) { - InspectorWindow *inspector = (InspectorWindow *) instance; - EsWindow *window = inspector->instance->window; - window->visualizeRepaints = !window->visualizeRepaints; - EsButtonSetCheck(inspector->visualizeRepaints, window->visualizeRepaints ? ES_CHECK_CHECKED : ES_CHECK_UNCHECKED, false); -} - -void InspectorVisualizePaintSteps(EsInstance *instance, EsElement *, EsCommand *) { - InspectorWindow *inspector = (InspectorWindow *) instance; - EsWindow *window = inspector->instance->window; - window->visualizePaintSteps = !window->visualizePaintSteps; - EsButtonSetCheck(inspector->visualizePaintSteps, window->visualizePaintSteps ? ES_CHECK_CHECKED : ES_CHECK_UNCHECKED, false); -} - -void InspectorVisualizeLayoutBounds(EsInstance *instance, EsElement *, EsCommand *) { - InspectorWindow *inspector = (InspectorWindow *) instance; - EsWindow *window = inspector->instance->window; - window->visualizeLayoutBounds = !window->visualizeLayoutBounds; - EsButtonSetCheck(inspector->visualizeLayoutBounds, window->visualizeLayoutBounds ? ES_CHECK_CHECKED : ES_CHECK_UNCHECKED, false); - EsElementRepaint(window); -} - -void InspectorAddElement2(EsMenu *menu, EsGeneric context) { - InspectorWindow *inspector = (InspectorWindow *) menu->instance; - if (inspector->selectedElement == -1) return; - EsElement *e = inspector->elements[inspector->selectedElement].element; - int asSibling = context.u & 0x80; - context.u &= ~0x80; - - if (asSibling) { - EsElementInsertAfter(e); - e = e->parent; - } - - if (context.u == 1) { - EsButtonCreate(e); - } else if (context.u == 2) { - EsPanelCreate(e); - } else if (context.u == 3) { - EsSpacerCreate(e); - } else if (context.u == 4) { - EsTextboxCreate(e); - } else if (context.u == 5) { - EsTextDisplayCreate(e); - } -} - -void InspectorAddElement(EsInstance *, EsElement *element, EsCommand *) { - EsMenu *menu = EsMenuCreate(element, ES_FLAGS_DEFAULT); - EsMenuAddItem(menu, 0, "Add button", -1, InspectorAddElement2, element->userData.u | 1); - EsMenuAddItem(menu, 0, "Add panel", -1, InspectorAddElement2, element->userData.u | 2); - EsMenuAddItem(menu, 0, "Add spacer", -1, InspectorAddElement2, element->userData.u | 3); - EsMenuAddItem(menu, 0, "Add textbox", -1, InspectorAddElement2, element->userData.u | 4); - EsMenuAddItem(menu, 0, "Add text display", -1, InspectorAddElement2, element->userData.u | 5); - EsMenuShow(menu); -} - -void InspectorSetup(EsWindow *window) { - InspectorWindow *inspector = (InspectorWindow *) EsHeapAllocate(sizeof(InspectorWindow), true); // TODO Freeing this. - inspector->window = window; - InstanceSetup(inspector); - EsInstanceOpenReference(inspector); - - inspector->instance = window->instance; - window->instance = inspector; - - inspector->selectedElement = -1; - - EsSplitter *splitter = EsSplitterCreate(window, ES_CELL_FILL | ES_SPLITTER_VERTICAL); - EsPanel *panel1 = EsPanelCreate(splitter, ES_CELL_FILL, ES_STYLE_PANEL_FILLED); - EsPanel *panel2 = EsPanelCreate(splitter, ES_CELL_FILL, ES_STYLE_PANEL_FILLED); - - { - EsPanel *toolbar = EsPanelCreate(panel1, ES_CELL_H_FILL | ES_PANEL_HORIZONTAL, ES_STYLE_PANEL_TOOLBAR); - inspector->visualizeRepaints = EsButtonCreate(toolbar, ES_BUTTON_TOOLBAR, 0, "Visualize repaints"); - EsButtonOnCommand(inspector->visualizeRepaints, InspectorVisualizeRepaints); - inspector->visualizeLayoutBounds = EsButtonCreate(toolbar, ES_BUTTON_TOOLBAR, 0, "Visualize layout bounds"); - EsButtonOnCommand(inspector->visualizeLayoutBounds, InspectorVisualizeLayoutBounds); - inspector->visualizePaintSteps = EsButtonCreate(toolbar, ES_BUTTON_TOOLBAR, 0, "Visualize paint steps"); - EsButtonOnCommand(inspector->visualizePaintSteps, InspectorVisualizePaintSteps); - EsSpacerCreate(toolbar, ES_CELL_H_FILL); - } - - inspector->elementList = EsListViewCreate(panel1, ES_CELL_FILL | ES_LIST_VIEW_COLUMNS | ES_LIST_VIEW_SINGLE_SELECT); - inspector->elementList->messageUser = InspectorElementListCallback; - EsListViewRegisterColumn(inspector->elementList, 0, "Name", -1, 0, 300); - EsListViewRegisterColumn(inspector->elementList, 1, "Bounds", -1, 0, 200); - EsListViewRegisterColumn(inspector->elementList, 2, "Information", -1, 0, 200); - EsListViewAddAllColumns(inspector->elementList); - EsListViewInsertGroup(inspector->elementList, 0); - - { - EsPanel *toolbar = EsPanelCreate(panel1, ES_CELL_H_FILL | ES_PANEL_HORIZONTAL, ES_STYLE_PANEL_TOOLBAR); - EsSpacerCreate(toolbar, ES_FLAGS_DEFAULT, nullptr, 5, 0); - EsTextDisplayCreate(toolbar, ES_FLAGS_DEFAULT, nullptr, "Horizontal:"); - EsSpacerCreate(toolbar, ES_FLAGS_DEFAULT, nullptr, 5, 0); - inspector->alignH[0] = EsButtonCreate(toolbar, ES_BUTTON_TOOLBAR | ES_ELEMENT_DISABLED); - EsButtonSetIcon(inspector->alignH[0], ES_ICON_ALIGN_HORIZONTAL_LEFT); - EsButtonOnCommand(inspector->alignH[0], InspectorHAlignLeft); - inspector->alignH[1] = EsButtonCreate(toolbar, ES_BUTTON_TOOLBAR | ES_ELEMENT_DISABLED); - EsButtonSetIcon(inspector->alignH[1], ES_ICON_ALIGN_HORIZONTAL_CENTER); - EsButtonOnCommand(inspector->alignH[1], InspectorHAlignCenter); - inspector->alignH[2] = EsButtonCreate(toolbar, ES_BUTTON_TOOLBAR | ES_ELEMENT_DISABLED); - EsButtonSetIcon(inspector->alignH[2], ES_ICON_ALIGN_HORIZONTAL_RIGHT); - EsButtonOnCommand(inspector->alignH[2], InspectorHAlignRight); - inspector->alignH[3] = EsButtonCreate(toolbar, ES_BUTTON_TOOLBAR | ES_ELEMENT_DISABLED, 0, "Expand"); - EsButtonOnCommand(inspector->alignH[3], InspectorHAlignExpand); - inspector->alignH[4] = EsButtonCreate(toolbar, ES_BUTTON_TOOLBAR | ES_ELEMENT_DISABLED, 0, "Shrink"); - EsButtonOnCommand(inspector->alignH[4], InspectorHAlignShrink); - inspector->alignH[5] = EsButtonCreate(toolbar, ES_BUTTON_TOOLBAR | ES_ELEMENT_DISABLED, 0, "Push"); - EsButtonOnCommand(inspector->alignH[5], InspectorHAlignPush); - EsSpacerCreate(toolbar, ES_FLAGS_DEFAULT, nullptr, 5, 0); - EsTextDisplayCreate(toolbar, ES_FLAGS_DEFAULT, nullptr, "Vertical:"); - EsSpacerCreate(toolbar, ES_FLAGS_DEFAULT, nullptr, 5, 0); - inspector->alignV[0] = EsButtonCreate(toolbar, ES_BUTTON_TOOLBAR | ES_ELEMENT_DISABLED); - EsButtonSetIcon(inspector->alignV[0], ES_ICON_ALIGN_VERTICAL_TOP); - EsButtonOnCommand(inspector->alignV[0], InspectorVAlignTop); - inspector->alignV[1] = EsButtonCreate(toolbar, ES_BUTTON_TOOLBAR | ES_ELEMENT_DISABLED); - EsButtonSetIcon(inspector->alignV[1], ES_ICON_ALIGN_VERTICAL_CENTER); - EsButtonOnCommand(inspector->alignV[1], InspectorVAlignCenter); - inspector->alignV[2] = EsButtonCreate(toolbar, ES_BUTTON_TOOLBAR | ES_ELEMENT_DISABLED); - EsButtonSetIcon(inspector->alignV[2], ES_ICON_ALIGN_VERTICAL_BOTTOM); - EsButtonOnCommand(inspector->alignV[2], InspectorVAlignBottom); - inspector->alignV[3] = EsButtonCreate(toolbar, ES_BUTTON_TOOLBAR | ES_ELEMENT_DISABLED, 0, "Expand"); - EsButtonOnCommand(inspector->alignV[3], InspectorVAlignExpand); - inspector->alignV[4] = EsButtonCreate(toolbar, ES_BUTTON_TOOLBAR | ES_ELEMENT_DISABLED, 0, "Shrink"); - EsButtonOnCommand(inspector->alignV[4], InspectorVAlignShrink); - inspector->alignV[5] = EsButtonCreate(toolbar, ES_BUTTON_TOOLBAR | ES_ELEMENT_DISABLED, 0, "Push"); - EsButtonOnCommand(inspector->alignV[5], InspectorVAlignPush); - } - - { - EsPanel *toolbar = EsPanelCreate(panel1, ES_CELL_H_FILL | ES_PANEL_HORIZONTAL, ES_STYLE_PANEL_TOOLBAR); - EsSpacerCreate(toolbar, ES_FLAGS_DEFAULT, nullptr, 5, 0); - EsTextDisplayCreate(toolbar, ES_FLAGS_DEFAULT, nullptr, "Stack:"); - EsSpacerCreate(toolbar, ES_FLAGS_DEFAULT, nullptr, 5, 0); - inspector->direction[0] = EsButtonCreate(toolbar, ES_BUTTON_TOOLBAR | ES_ELEMENT_DISABLED); - EsButtonSetIcon(inspector->direction[0], ES_ICON_GO_PREVIOUS); - EsButtonOnCommand(inspector->direction[0], InspectorDirectionLeft); - inspector->direction[1] = EsButtonCreate(toolbar, ES_BUTTON_TOOLBAR | ES_ELEMENT_DISABLED); - EsButtonSetIcon(inspector->direction[1], ES_ICON_GO_NEXT); - EsButtonOnCommand(inspector->direction[1], InspectorDirectionRight); - inspector->direction[2] = EsButtonCreate(toolbar, ES_BUTTON_TOOLBAR | ES_ELEMENT_DISABLED); - EsButtonSetIcon(inspector->direction[2], ES_ICON_GO_UP); - EsButtonOnCommand(inspector->direction[2], InspectorDirectionUp); - inspector->direction[3] = EsButtonCreate(toolbar, ES_BUTTON_TOOLBAR | ES_ELEMENT_DISABLED); - EsButtonSetIcon(inspector->direction[3], ES_ICON_GO_DOWN); - EsButtonOnCommand(inspector->direction[3], InspectorDirectionDown); - EsSpacerCreate(toolbar, ES_FLAGS_DEFAULT, nullptr, 25, 0); - inspector->addChildButton = EsButtonCreate(toolbar, ES_BUTTON_TOOLBAR | ES_BUTTON_DROPDOWN | ES_ELEMENT_DISABLED | ES_BUTTON_COMPACT, nullptr, "Add child... "); - EsButtonOnCommand(inspector->addChildButton, InspectorAddElement); - inspector->addSiblingButton = EsButtonCreate(toolbar, ES_BUTTON_TOOLBAR | ES_BUTTON_DROPDOWN | ES_ELEMENT_DISABLED | ES_BUTTON_COMPACT, nullptr, "Add sibling... "); - inspector->addSiblingButton->userData.i = 0x80; - EsButtonOnCommand(inspector->addSiblingButton, InspectorAddElement); - } - - { - EsPanel *toolbar = EsPanelCreate(panel1, ES_CELL_H_FILL | ES_PANEL_HORIZONTAL, ES_STYLE_PANEL_TOOLBAR); - EsSpacerCreate(toolbar, ES_FLAGS_DEFAULT, nullptr, 5, 0); - EsTextDisplayCreate(toolbar, ES_FLAGS_DEFAULT, nullptr, "Content:"); - inspector->contentTextbox = EsTextboxCreate(toolbar, ES_ELEMENT_DISABLED | ES_TEXTBOX_EDIT_BASED); - inspector->contentTextbox->messageUser = InspectorContentTextboxCallback; - EsSpacerCreate(toolbar, ES_FLAGS_DEFAULT, nullptr, 25, 0); - EsTextDisplayCreate(toolbar, ES_FLAGS_DEFAULT, nullptr, "Event category filter:"); - inspector->textboxCategoryFilter = EsTextboxCreate(toolbar, ES_ELEMENT_DISABLED); - inspector->textboxCategoryFilter->messageUser = InspectorTextboxCategoryFilterCallback; - } - - { - inspector->listEvents = EsListViewCreate(panel2, ES_CELL_FILL | ES_LIST_VIEW_CHOICE_SELECT | ES_LIST_VIEW_FIXED_ITEMS, ES_STYLE_LIST_CHOICE_BORDERED); - } - - InspectorRefreshElementList(inspector); - - APIInstance *instance = (APIInstance *) inspector->instance->_private; - instance->attachedInspector = inspector; -} diff --git a/desktop/inspector.cpp b/desktop/inspector.cpp new file mode 100644 index 0000000..3750c62 --- /dev/null +++ b/desktop/inspector.cpp @@ -0,0 +1,602 @@ +struct InspectorElementEntry { + EsElement *element; + EsRectangle takenBounds, givenBounds; + int depth; +}; + +struct InspectorWindow : EsInstance { + EsInstance *instance; // The instance being inspected. + + EsListView *elementList; + Array elements; // TODO This is being leaked. + InspectorElementEntry hoveredElement; + char *cCategoryFilter; + + intptr_t selectedElement; + EsButton *alignH[6]; + EsButton *alignV[6]; + EsButton *direction[4]; + EsTextbox *contentTextbox; + EsButton *addChildButton; + EsButton *addSiblingButton; + EsButton *visualizeRepaints; + EsButton *visualizeLayoutBounds; + EsButton *visualizePaintSteps; + EsListView *listEvents; + EsTextbox *textboxCategoryFilter; +}; + +int InspectorElementItemCallback(EsElement *element, EsMessage *message) { + InspectorWindow *inspector = (InspectorWindow *) element->instance; + + if (message->type == ES_MSG_HOVERED_START) { + InspectorElementEntry *entry = &inspector->elements[EsListViewGetIndexFromItem(element)]; + if (entry->element->parent) entry->element->parent->Repaint(true); + else entry->element->Repaint(true); + inspector->hoveredElement = *entry; + } else if (message->type == ES_MSG_HOVERED_END || message->type == ES_MSG_DESTROY) { + EsListViewIndex index = EsListViewGetIndexFromItem(element); + InspectorElementEntry *entry = &inspector->elements[index]; + if (entry->element->parent) entry->element->parent->Repaint(true); + else entry->element->Repaint(true); + inspector->hoveredElement = {}; + } + + return 0; +} + +void InspectorUpdateEditor(InspectorWindow *inspector) { + EsElement *e = inspector->selectedElement == -1 ? nullptr : inspector->elements[inspector->selectedElement].element; + + bool isStack = e && e->messageClass == ProcessPanelMessage && !(e->flags & (ES_PANEL_Z_STACK | ES_PANEL_TABLE | ES_PANEL_SWITCHER)); + bool alignHLeft = e ? (e->flags & ES_CELL_H_LEFT) : false, alignHRight = e ? (e->flags & ES_CELL_H_RIGHT) : false; + bool alignHExpand = e ? (e->flags & ES_CELL_H_EXPAND) : false, alignHShrink = e ? (e->flags & ES_CELL_H_SHRINK) : false; + bool alignHPush = e ? (e->flags & ES_CELL_H_PUSH) : false; + bool alignVTop = e ? (e->flags & ES_CELL_V_TOP) : false, alignVBottom = e ? (e->flags & ES_CELL_V_BOTTOM) : false; + bool alignVExpand = e ? (e->flags & ES_CELL_V_EXPAND) : false, alignVShrink = e ? (e->flags & ES_CELL_V_SHRINK) : false; + bool alignVPush = e ? (e->flags & ES_CELL_V_PUSH) : false; + bool stackHorizontal = isStack && (e->flags & ES_PANEL_HORIZONTAL); + bool stackReverse = isStack && (e->flags & ES_PANEL_REVERSE); + + EsButtonSetCheck(inspector->alignH[0], (EsCheckState) (e && alignHLeft && !alignHRight), false); + EsButtonSetCheck(inspector->alignH[1], (EsCheckState) (e && alignHLeft == alignHRight), false); + EsButtonSetCheck(inspector->alignH[2], (EsCheckState) (e && !alignHLeft && alignHRight), false); + EsButtonSetCheck(inspector->alignH[3], (EsCheckState) (e && alignHExpand), false); + EsButtonSetCheck(inspector->alignH[4], (EsCheckState) (e && alignHShrink), false); + EsButtonSetCheck(inspector->alignH[5], (EsCheckState) (e && alignHPush), false); + + EsButtonSetCheck(inspector->alignV[0], (EsCheckState) (e && alignVTop && !alignVBottom), false); + EsButtonSetCheck(inspector->alignV[1], (EsCheckState) (e && alignVTop == alignVBottom), false); + EsButtonSetCheck(inspector->alignV[2], (EsCheckState) (e && !alignVTop && alignVBottom), false); + EsButtonSetCheck(inspector->alignV[3], (EsCheckState) (e && alignVExpand), false); + EsButtonSetCheck(inspector->alignV[4], (EsCheckState) (e && alignVShrink), false); + EsButtonSetCheck(inspector->alignV[5], (EsCheckState) (e && alignVPush), false); + + EsButtonSetCheck(inspector->direction[0], (EsCheckState) (isStack && stackHorizontal && stackReverse), false); + EsButtonSetCheck(inspector->direction[1], (EsCheckState) (isStack && stackHorizontal && !stackReverse), false); + EsButtonSetCheck(inspector->direction[2], (EsCheckState) (isStack && !stackHorizontal && stackReverse), false); + EsButtonSetCheck(inspector->direction[3], (EsCheckState) (isStack && !stackHorizontal && !stackReverse), false); + + EsElementSetDisabled(inspector->alignH[0], !e); + EsElementSetDisabled(inspector->alignH[1], !e); + EsElementSetDisabled(inspector->alignH[2], !e); + EsElementSetDisabled(inspector->alignH[3], !e); + EsElementSetDisabled(inspector->alignH[4], !e); + EsElementSetDisabled(inspector->alignH[5], !e); + EsElementSetDisabled(inspector->alignV[0], !e); + EsElementSetDisabled(inspector->alignV[1], !e); + EsElementSetDisabled(inspector->alignV[2], !e); + EsElementSetDisabled(inspector->alignV[3], !e); + EsElementSetDisabled(inspector->alignV[4], !e); + EsElementSetDisabled(inspector->alignV[5], !e); + EsElementSetDisabled(inspector->direction[0], !isStack); + EsElementSetDisabled(inspector->direction[1], !isStack); + EsElementSetDisabled(inspector->direction[2], !isStack); + EsElementSetDisabled(inspector->direction[3], !isStack); + EsElementSetDisabled(inspector->addChildButton, !isStack); + EsElementSetDisabled(inspector->addSiblingButton, !e || !e->parent); + + EsElementSetDisabled(inspector->textboxCategoryFilter, !e); + + EsTextboxSelectAll(inspector->contentTextbox); + EsTextboxInsert(inspector->contentTextbox, "", 0, false); + + if (e) { +#if 0 + for (uintptr_t i = 0; i < sizeof(builtinStyles) / sizeof(builtinStyles[0]); i++) { + if (e->currentStyleKey.partHash == CalculateCRC64(EsLiteral(builtinStyles[i]))) { + EsTextboxInsert(inspector->styleTextbox, builtinStyles[i], -1, false); + break; + } + } +#endif + + if (e->messageClass == ProcessButtonMessage) { + EsButton *button = (EsButton *) e; + EsElementSetDisabled(inspector->contentTextbox, false); + EsTextboxInsert(inspector->contentTextbox, button->label, button->labelBytes, false); + } else if (e->messageClass == ProcessTextDisplayMessage) { + EsTextDisplay *display = (EsTextDisplay *) e; + EsElementSetDisabled(inspector->contentTextbox, false); + EsTextboxInsert(inspector->contentTextbox, display->contents, display->textRuns[display->textRunCount].offset, false); + } else { + EsElementSetDisabled(inspector->contentTextbox, true); + } + } else { + EsElementSetDisabled(inspector->contentTextbox, true); + } +} + +int InspectorElementListCallback(EsElement *element, EsMessage *message) { + InspectorWindow *inspector = (InspectorWindow *) element->instance; + + if (message->type == ES_MSG_LIST_VIEW_GET_CONTENT) { + int column = message->getContent.columnID, index = message->getContent.index; + EsAssert(index >= 0 && index < (int) inspector->elements.Length()); + InspectorElementEntry *entry = &inspector->elements[index]; + + if (column == 0) { + EsBufferFormat(message->getContent.buffer, "%z", entry->element->cName); + } else if (column == 1) { + EsBufferFormat(message->getContent.buffer, "%R", entry->element->GetWindowBounds(false)); + } else if (column == 2) { + EsMessage m = *message; + m.type = ES_MSG_GET_INSPECTOR_INFORMATION; + EsMessageSend(entry->element, &m); + } + + return ES_HANDLED; + } else if (message->type == ES_MSG_LIST_VIEW_GET_INDENT) { + message->getIndent.indent = inspector->elements[message->getIndent.index].depth; + return ES_HANDLED; + } else if (message->type == ES_MSG_LIST_VIEW_CREATE_ITEM) { + message->createItem.item->messageUser = InspectorElementItemCallback; + return ES_HANDLED; + } else if (message->type == ES_MSG_LIST_VIEW_SELECT) { + if (inspector->selectedElement != -1) { + inspector->elements[inspector->selectedElement].element->state &= ~UI_STATE_INSPECTING; + } + + inspector->selectedElement = message->selectItem.isSelected ? message->selectItem.index : -1; + + if (inspector->selectedElement != -1) { + EsElement *e = inspector->elements[inspector->selectedElement].element; + e->state |= UI_STATE_INSPECTING; + InspectorNotifyElementEvent(e, nullptr, "Viewing events from '%z'.\n", e->cName); + } + + InspectorUpdateEditor(inspector); + return ES_HANDLED; + } else if (message->type == ES_MSG_LIST_VIEW_IS_SELECTED) { + message->selectItem.isSelected = message->selectItem.index == inspector->selectedElement; + return ES_HANDLED; + } + + return 0; +} + +int InspectorContentTextboxCallback(EsElement *element, EsMessage *message) { + InspectorWindow *inspector = (InspectorWindow *) element->instance; + + if (message->type == ES_MSG_TEXTBOX_EDIT_END) { + size_t newContentBytes; + char *newContent = EsTextboxGetContents(inspector->contentTextbox, &newContentBytes); + EsElement *e = inspector->elements[inspector->selectedElement].element; + + if (e->messageClass == ProcessButtonMessage) { + EsButton *button = (EsButton *) e; + HeapDuplicate((void **) &button->label, &button->labelBytes, newContent, newContentBytes); + } else if (e->messageClass == ProcessTextDisplayMessage) { + EsTextDisplay *display = (EsTextDisplay *) e; + EsTextDisplaySetContents(display, newContent, newContentBytes); + } else { + EsAssert(false); + } + + EsElementUpdateContentSize(e); + if (e->parent) EsElementUpdateContentSize(e->parent); + EsHeapFree(newContent); + return ES_HANDLED; + } + + return 0; +} + +int InspectorTextboxCategoryFilterCallback(EsElement *element, EsMessage *message) { + InspectorWindow *inspector = (InspectorWindow *) element->instance; + + if (message->type == ES_MSG_TEXTBOX_UPDATED) { + EsHeapFree(inspector->cCategoryFilter); + inspector->cCategoryFilter = EsTextboxGetContents((EsTextbox *) element); + } + + return 0; +} + +InspectorWindow *InspectorGet(EsElement *element) { + if (!element->window || !element->instance) return nullptr; + APIInstance *instance = (APIInstance *) element->instance->_private; + InspectorWindow *inspector = instance->attachedInspector; + if (!inspector || inspector->instance->window != element->window) return nullptr; + return inspector; +} + +void InspectorNotifyElementEvent(EsElement *element, const char *cCategory, const char *cFormat, ...) { + if (~element->state & UI_STATE_INSPECTING) return; + InspectorWindow *inspector = InspectorGet(element); + if (!inspector) return; + if (inspector->cCategoryFilter && inspector->cCategoryFilter[0] && cCategory && EsCRTstrcmp(cCategory, inspector->cCategoryFilter)) return; + va_list arguments; + va_start(arguments, cFormat); + char _buffer[256]; + EsBuffer buffer = { .out = (uint8_t *) _buffer, .bytes = sizeof(_buffer) }; + if (cCategory) EsBufferFormat(&buffer, "%z: ", cCategory); + EsBufferFormatV(&buffer, cFormat, arguments); + va_end(arguments); + EsListViewFixedItemInsert(inspector->listEvents, _buffer, buffer.position); + EsListViewScrollToEnd(inspector->listEvents); +} + +void InspectorNotifyElementContentChanged(EsElement *element) { + InspectorWindow *inspector = InspectorGet(element); + if (!inspector) return; + + for (uintptr_t i = 0; i < inspector->elements.Length(); i++) { + if (inspector->elements[i].element == element) { + EsListViewInvalidateContent(inspector->elementList, 0, i); + return; + } + } + + EsAssert(false); +} + +void InspectorNotifyElementMoved(EsElement *element, EsRectangle takenBounds) { + InspectorWindow *inspector = InspectorGet(element); + if (!inspector) return; + + for (uintptr_t i = 0; i < inspector->elements.Length(); i++) { + if (inspector->elements[i].element == element) { + inspector->elements[i].takenBounds = takenBounds; + inspector->elements[i].givenBounds = takenBounds; // TODO. + EsListViewInvalidateContent(inspector->elementList, 0, i); + return; + } + } + + EsAssert(false); +} + +void InspectorNotifyElementDestroyed(EsElement *element) { + InspectorWindow *inspector = InspectorGet(element); + if (!inspector) return; + + for (uintptr_t i = 0; i < inspector->elements.Length(); i++) { + if (inspector->elements[i].element == element) { + if (inspector->selectedElement == (intptr_t) i) { + inspector->selectedElement = -1; + InspectorUpdateEditor(inspector); + } else if (inspector->selectedElement > (intptr_t) i) { + inspector->selectedElement--; + } + + EsListViewRemove(inspector->elementList, 0, i, 1); + inspector->elements.Delete(i); + return; + } + } + + EsAssert(false); +} + +void InspectorNotifyElementCreated(EsElement *element) { + InspectorWindow *inspector = InspectorGet(element); + if (!inspector) return; + + ptrdiff_t indexInParent = -1; + + for (uintptr_t i = 0; i < element->parent->children.Length(); i++) { + if (element->parent->children[i] == element) { + indexInParent = i; + break; + } + } + + EsAssert(indexInParent != -1); + + ptrdiff_t insertAfterIndex = -1; + + for (uintptr_t i = 0; i < inspector->elements.Length(); i++) { + if (indexInParent == 0) { + if (inspector->elements[i].element == element->parent) { + insertAfterIndex = i; + break; + } + } else { + if (inspector->elements[i].element == element->parent->children[indexInParent - 1]) { + insertAfterIndex = i; + int baseDepth = inspector->elements[i++].depth; + + for (; i < inspector->elements.Length(); i++) { + if (inspector->elements[i].depth > baseDepth) { + insertAfterIndex++; + } else { + break; + } + } + + break; + } + } + } + + EsAssert(insertAfterIndex != -1); + + int depth = 0; + EsElement *ancestor = element->parent; + + while (ancestor) { + depth++; + ancestor = ancestor->parent; + } + + if (inspector->selectedElement > insertAfterIndex) { + inspector->selectedElement++; + } + + InspectorElementEntry entry; + entry.element = element; + entry.depth = depth; + inspector->elements.Insert(entry, insertAfterIndex + 1); + EsListViewInsert(inspector->elementList, 0, insertAfterIndex + 1, 1); +} + +void InspectorFindElementsRecursively(InspectorWindow *inspector, EsElement *element, int depth) { + InspectorElementEntry entry = {}; + entry.element = element; + entry.depth = depth; + inspector->elements.Add(entry); + + for (uintptr_t i = 0; i < element->children.Length(); i++) { + InspectorFindElementsRecursively(inspector, element->children[i], depth + 1); + } +} + +void InspectorRefreshElementList(InspectorWindow *inspector) { + EsListViewRemoveAll(inspector->elementList, 0); + inspector->elements.Free(); + InspectorFindElementsRecursively(inspector, inspector->instance->window, 0); + EsListViewInsert(inspector->elementList, 0, 0, inspector->elements.Length()); +} + +void InspectorNotifyElementPainted(EsElement *element, EsPainter *painter) { + InspectorWindow *inspector = InspectorGet(element); + if (!inspector) return; + + InspectorElementEntry *entry = inspector->hoveredElement.element ? &inspector->hoveredElement : nullptr; + if (!entry) return; + + EsRectangle bounds = ES_RECT_4(painter->offsetX, painter->offsetX + painter->width, + painter->offsetY, painter->offsetY + painter->height); + + if (entry->element == element) { + EsDrawRectangle(painter, bounds, 0x607F7FFF, 0x60FFFF7F, element->style->insets); + } else if (entry->element->parent == element) { + if ((element->flags & ES_CELL_FILL) != ES_CELL_FILL) { + EsRectangle rectangle = entry->givenBounds; + rectangle.l += bounds.l, rectangle.r += bounds.l; + rectangle.t += bounds.t, rectangle.b += bounds.t; + // EsDrawBlock(painter, rectangle, 0x20FF7FFF); + } + } +} + +#define INSPECTOR_ALIGN_COMMAND(name, clear, set, toggle) \ +void name (EsInstance *instance, EsElement *, EsCommand *) { \ + InspectorWindow *inspector = (InspectorWindow *) instance; \ + EsElement *e = inspector->elements[inspector->selectedElement].element; \ + if (toggle) e->flags ^= set; \ + else { e->flags &= ~(clear); e->flags |= set; } \ + EsElementUpdateContentSize(e); \ + if (e->parent) EsElementUpdateContentSize(e->parent); \ + inspector->elementList->Repaint(true); \ + InspectorUpdateEditor(inspector); \ +} + +INSPECTOR_ALIGN_COMMAND(InspectorHAlignLeft, ES_CELL_H_LEFT | ES_CELL_H_RIGHT, ES_CELL_H_LEFT, false); +INSPECTOR_ALIGN_COMMAND(InspectorHAlignCenter, ES_CELL_H_LEFT | ES_CELL_H_RIGHT, ES_CELL_H_LEFT | ES_CELL_H_RIGHT, false); +INSPECTOR_ALIGN_COMMAND(InspectorHAlignRight, ES_CELL_H_LEFT | ES_CELL_H_RIGHT, ES_CELL_H_RIGHT, false); +INSPECTOR_ALIGN_COMMAND(InspectorHAlignExpand, 0, ES_CELL_H_EXPAND, true); +INSPECTOR_ALIGN_COMMAND(InspectorHAlignShrink, 0, ES_CELL_H_SHRINK, true); +INSPECTOR_ALIGN_COMMAND(InspectorHAlignPush, 0, ES_CELL_H_PUSH, true); +INSPECTOR_ALIGN_COMMAND(InspectorVAlignTop, ES_CELL_V_TOP | ES_CELL_V_BOTTOM, ES_CELL_V_TOP, false); +INSPECTOR_ALIGN_COMMAND(InspectorVAlignCenter, ES_CELL_V_TOP | ES_CELL_V_BOTTOM, ES_CELL_V_TOP | ES_CELL_V_BOTTOM, false); +INSPECTOR_ALIGN_COMMAND(InspectorVAlignBottom, ES_CELL_V_TOP | ES_CELL_V_BOTTOM, ES_CELL_V_BOTTOM, false); +INSPECTOR_ALIGN_COMMAND(InspectorVAlignExpand, 0, ES_CELL_V_EXPAND, true); +INSPECTOR_ALIGN_COMMAND(InspectorVAlignShrink, 0, ES_CELL_V_SHRINK, true); +INSPECTOR_ALIGN_COMMAND(InspectorVAlignPush, 0, ES_CELL_V_PUSH, true); +INSPECTOR_ALIGN_COMMAND(InspectorDirectionLeft, ES_PANEL_HORIZONTAL | ES_PANEL_REVERSE | ES_ELEMENT_LAYOUT_HINT_HORIZONTAL | ES_ELEMENT_LAYOUT_HINT_REVERSE, + ES_PANEL_HORIZONTAL | ES_PANEL_REVERSE | ES_ELEMENT_LAYOUT_HINT_HORIZONTAL | ES_ELEMENT_LAYOUT_HINT_REVERSE, false); +INSPECTOR_ALIGN_COMMAND(InspectorDirectionRight, ES_PANEL_HORIZONTAL | ES_PANEL_REVERSE | ES_ELEMENT_LAYOUT_HINT_HORIZONTAL | ES_ELEMENT_LAYOUT_HINT_REVERSE, + ES_PANEL_HORIZONTAL | ES_ELEMENT_LAYOUT_HINT_HORIZONTAL, false); +INSPECTOR_ALIGN_COMMAND(InspectorDirectionUp, ES_PANEL_HORIZONTAL | ES_PANEL_REVERSE | ES_ELEMENT_LAYOUT_HINT_HORIZONTAL | ES_ELEMENT_LAYOUT_HINT_REVERSE, + ES_PANEL_REVERSE | ES_ELEMENT_LAYOUT_HINT_REVERSE, false); +INSPECTOR_ALIGN_COMMAND(InspectorDirectionDown, ES_PANEL_HORIZONTAL | ES_PANEL_REVERSE | ES_ELEMENT_LAYOUT_HINT_HORIZONTAL | ES_ELEMENT_LAYOUT_HINT_REVERSE, 0, false); + +void InspectorVisualizeRepaints(EsInstance *instance, EsElement *, EsCommand *) { + InspectorWindow *inspector = (InspectorWindow *) instance; + EsWindow *window = inspector->instance->window; + window->visualizeRepaints = !window->visualizeRepaints; + EsButtonSetCheck(inspector->visualizeRepaints, window->visualizeRepaints ? ES_CHECK_CHECKED : ES_CHECK_UNCHECKED, false); +} + +void InspectorVisualizePaintSteps(EsInstance *instance, EsElement *, EsCommand *) { + InspectorWindow *inspector = (InspectorWindow *) instance; + EsWindow *window = inspector->instance->window; + window->visualizePaintSteps = !window->visualizePaintSteps; + EsButtonSetCheck(inspector->visualizePaintSteps, window->visualizePaintSteps ? ES_CHECK_CHECKED : ES_CHECK_UNCHECKED, false); +} + +void InspectorVisualizeLayoutBounds(EsInstance *instance, EsElement *, EsCommand *) { + InspectorWindow *inspector = (InspectorWindow *) instance; + EsWindow *window = inspector->instance->window; + window->visualizeLayoutBounds = !window->visualizeLayoutBounds; + EsButtonSetCheck(inspector->visualizeLayoutBounds, window->visualizeLayoutBounds ? ES_CHECK_CHECKED : ES_CHECK_UNCHECKED, false); + EsElementRepaint(window); +} + +void InspectorAddElement2(EsMenu *menu, EsGeneric context) { + InspectorWindow *inspector = (InspectorWindow *) menu->instance; + if (inspector->selectedElement == -1) return; + EsElement *e = inspector->elements[inspector->selectedElement].element; + int asSibling = context.u & 0x80; + context.u &= ~0x80; + + if (asSibling) { + EsElementInsertAfter(e); + e = e->parent; + } + + if (context.u == 1) { + EsButtonCreate(e); + } else if (context.u == 2) { + EsPanelCreate(e); + } else if (context.u == 3) { + EsSpacerCreate(e); + } else if (context.u == 4) { + EsTextboxCreate(e); + } else if (context.u == 5) { + EsTextDisplayCreate(e); + } +} + +void InspectorAddElement(EsInstance *, EsElement *element, EsCommand *) { + EsMenu *menu = EsMenuCreate(element, ES_FLAGS_DEFAULT); + EsMenuAddItem(menu, 0, "Add button", -1, InspectorAddElement2, element->userData.u | 1); + EsMenuAddItem(menu, 0, "Add panel", -1, InspectorAddElement2, element->userData.u | 2); + EsMenuAddItem(menu, 0, "Add spacer", -1, InspectorAddElement2, element->userData.u | 3); + EsMenuAddItem(menu, 0, "Add textbox", -1, InspectorAddElement2, element->userData.u | 4); + EsMenuAddItem(menu, 0, "Add text display", -1, InspectorAddElement2, element->userData.u | 5); + EsMenuShow(menu); +} + +void InspectorSetup(EsWindow *window) { + InspectorWindow *inspector = (InspectorWindow *) EsHeapAllocate(sizeof(InspectorWindow), true); // TODO Freeing this. + inspector->window = window; + InstanceSetup(inspector); + EsInstanceOpenReference(inspector); + + inspector->instance = window->instance; + window->instance = inspector; + + inspector->selectedElement = -1; + + EsSplitter *splitter = EsSplitterCreate(window, ES_CELL_FILL | ES_SPLITTER_VERTICAL); + EsPanel *panel1 = EsPanelCreate(splitter, ES_CELL_FILL, ES_STYLE_PANEL_FILLED); + EsPanel *panel2 = EsPanelCreate(splitter, ES_CELL_FILL, ES_STYLE_PANEL_FILLED); + + { + EsPanel *toolbar = EsPanelCreate(panel1, ES_CELL_H_FILL | ES_PANEL_HORIZONTAL, ES_STYLE_PANEL_TOOLBAR); + inspector->visualizeRepaints = EsButtonCreate(toolbar, ES_BUTTON_TOOLBAR, 0, "Visualize repaints"); + EsButtonOnCommand(inspector->visualizeRepaints, InspectorVisualizeRepaints); + inspector->visualizeLayoutBounds = EsButtonCreate(toolbar, ES_BUTTON_TOOLBAR, 0, "Visualize layout bounds"); + EsButtonOnCommand(inspector->visualizeLayoutBounds, InspectorVisualizeLayoutBounds); + inspector->visualizePaintSteps = EsButtonCreate(toolbar, ES_BUTTON_TOOLBAR, 0, "Visualize paint steps"); + EsButtonOnCommand(inspector->visualizePaintSteps, InspectorVisualizePaintSteps); + EsSpacerCreate(toolbar, ES_CELL_H_FILL); + } + + inspector->elementList = EsListViewCreate(panel1, ES_CELL_FILL | ES_LIST_VIEW_COLUMNS | ES_LIST_VIEW_SINGLE_SELECT); + inspector->elementList->messageUser = InspectorElementListCallback; + EsListViewRegisterColumn(inspector->elementList, 0, "Name", -1, 0, 300); + EsListViewRegisterColumn(inspector->elementList, 1, "Bounds", -1, 0, 200); + EsListViewRegisterColumn(inspector->elementList, 2, "Information", -1, 0, 200); + EsListViewAddAllColumns(inspector->elementList); + EsListViewInsertGroup(inspector->elementList, 0); + + { + EsPanel *toolbar = EsPanelCreate(panel1, ES_CELL_H_FILL | ES_PANEL_HORIZONTAL, ES_STYLE_PANEL_TOOLBAR); + EsSpacerCreate(toolbar, ES_FLAGS_DEFAULT, nullptr, 5, 0); + EsTextDisplayCreate(toolbar, ES_FLAGS_DEFAULT, nullptr, "Horizontal:"); + EsSpacerCreate(toolbar, ES_FLAGS_DEFAULT, nullptr, 5, 0); + inspector->alignH[0] = EsButtonCreate(toolbar, ES_BUTTON_TOOLBAR | ES_ELEMENT_DISABLED); + EsButtonSetIcon(inspector->alignH[0], ES_ICON_ALIGN_HORIZONTAL_LEFT); + EsButtonOnCommand(inspector->alignH[0], InspectorHAlignLeft); + inspector->alignH[1] = EsButtonCreate(toolbar, ES_BUTTON_TOOLBAR | ES_ELEMENT_DISABLED); + EsButtonSetIcon(inspector->alignH[1], ES_ICON_ALIGN_HORIZONTAL_CENTER); + EsButtonOnCommand(inspector->alignH[1], InspectorHAlignCenter); + inspector->alignH[2] = EsButtonCreate(toolbar, ES_BUTTON_TOOLBAR | ES_ELEMENT_DISABLED); + EsButtonSetIcon(inspector->alignH[2], ES_ICON_ALIGN_HORIZONTAL_RIGHT); + EsButtonOnCommand(inspector->alignH[2], InspectorHAlignRight); + inspector->alignH[3] = EsButtonCreate(toolbar, ES_BUTTON_TOOLBAR | ES_ELEMENT_DISABLED, 0, "Expand"); + EsButtonOnCommand(inspector->alignH[3], InspectorHAlignExpand); + inspector->alignH[4] = EsButtonCreate(toolbar, ES_BUTTON_TOOLBAR | ES_ELEMENT_DISABLED, 0, "Shrink"); + EsButtonOnCommand(inspector->alignH[4], InspectorHAlignShrink); + inspector->alignH[5] = EsButtonCreate(toolbar, ES_BUTTON_TOOLBAR | ES_ELEMENT_DISABLED, 0, "Push"); + EsButtonOnCommand(inspector->alignH[5], InspectorHAlignPush); + EsSpacerCreate(toolbar, ES_FLAGS_DEFAULT, nullptr, 5, 0); + EsTextDisplayCreate(toolbar, ES_FLAGS_DEFAULT, nullptr, "Vertical:"); + EsSpacerCreate(toolbar, ES_FLAGS_DEFAULT, nullptr, 5, 0); + inspector->alignV[0] = EsButtonCreate(toolbar, ES_BUTTON_TOOLBAR | ES_ELEMENT_DISABLED); + EsButtonSetIcon(inspector->alignV[0], ES_ICON_ALIGN_VERTICAL_TOP); + EsButtonOnCommand(inspector->alignV[0], InspectorVAlignTop); + inspector->alignV[1] = EsButtonCreate(toolbar, ES_BUTTON_TOOLBAR | ES_ELEMENT_DISABLED); + EsButtonSetIcon(inspector->alignV[1], ES_ICON_ALIGN_VERTICAL_CENTER); + EsButtonOnCommand(inspector->alignV[1], InspectorVAlignCenter); + inspector->alignV[2] = EsButtonCreate(toolbar, ES_BUTTON_TOOLBAR | ES_ELEMENT_DISABLED); + EsButtonSetIcon(inspector->alignV[2], ES_ICON_ALIGN_VERTICAL_BOTTOM); + EsButtonOnCommand(inspector->alignV[2], InspectorVAlignBottom); + inspector->alignV[3] = EsButtonCreate(toolbar, ES_BUTTON_TOOLBAR | ES_ELEMENT_DISABLED, 0, "Expand"); + EsButtonOnCommand(inspector->alignV[3], InspectorVAlignExpand); + inspector->alignV[4] = EsButtonCreate(toolbar, ES_BUTTON_TOOLBAR | ES_ELEMENT_DISABLED, 0, "Shrink"); + EsButtonOnCommand(inspector->alignV[4], InspectorVAlignShrink); + inspector->alignV[5] = EsButtonCreate(toolbar, ES_BUTTON_TOOLBAR | ES_ELEMENT_DISABLED, 0, "Push"); + EsButtonOnCommand(inspector->alignV[5], InspectorVAlignPush); + } + + { + EsPanel *toolbar = EsPanelCreate(panel1, ES_CELL_H_FILL | ES_PANEL_HORIZONTAL, ES_STYLE_PANEL_TOOLBAR); + EsSpacerCreate(toolbar, ES_FLAGS_DEFAULT, nullptr, 5, 0); + EsTextDisplayCreate(toolbar, ES_FLAGS_DEFAULT, nullptr, "Stack:"); + EsSpacerCreate(toolbar, ES_FLAGS_DEFAULT, nullptr, 5, 0); + inspector->direction[0] = EsButtonCreate(toolbar, ES_BUTTON_TOOLBAR | ES_ELEMENT_DISABLED); + EsButtonSetIcon(inspector->direction[0], ES_ICON_GO_PREVIOUS); + EsButtonOnCommand(inspector->direction[0], InspectorDirectionLeft); + inspector->direction[1] = EsButtonCreate(toolbar, ES_BUTTON_TOOLBAR | ES_ELEMENT_DISABLED); + EsButtonSetIcon(inspector->direction[1], ES_ICON_GO_NEXT); + EsButtonOnCommand(inspector->direction[1], InspectorDirectionRight); + inspector->direction[2] = EsButtonCreate(toolbar, ES_BUTTON_TOOLBAR | ES_ELEMENT_DISABLED); + EsButtonSetIcon(inspector->direction[2], ES_ICON_GO_UP); + EsButtonOnCommand(inspector->direction[2], InspectorDirectionUp); + inspector->direction[3] = EsButtonCreate(toolbar, ES_BUTTON_TOOLBAR | ES_ELEMENT_DISABLED); + EsButtonSetIcon(inspector->direction[3], ES_ICON_GO_DOWN); + EsButtonOnCommand(inspector->direction[3], InspectorDirectionDown); + EsSpacerCreate(toolbar, ES_FLAGS_DEFAULT, nullptr, 25, 0); + inspector->addChildButton = EsButtonCreate(toolbar, ES_BUTTON_TOOLBAR | ES_BUTTON_DROPDOWN | ES_ELEMENT_DISABLED | ES_BUTTON_COMPACT, nullptr, "Add child... "); + EsButtonOnCommand(inspector->addChildButton, InspectorAddElement); + inspector->addSiblingButton = EsButtonCreate(toolbar, ES_BUTTON_TOOLBAR | ES_BUTTON_DROPDOWN | ES_ELEMENT_DISABLED | ES_BUTTON_COMPACT, nullptr, "Add sibling... "); + inspector->addSiblingButton->userData.i = 0x80; + EsButtonOnCommand(inspector->addSiblingButton, InspectorAddElement); + } + + { + EsPanel *toolbar = EsPanelCreate(panel1, ES_CELL_H_FILL | ES_PANEL_HORIZONTAL, ES_STYLE_PANEL_TOOLBAR); + EsSpacerCreate(toolbar, ES_FLAGS_DEFAULT, nullptr, 5, 0); + EsTextDisplayCreate(toolbar, ES_FLAGS_DEFAULT, nullptr, "Content:"); + inspector->contentTextbox = EsTextboxCreate(toolbar, ES_ELEMENT_DISABLED | ES_TEXTBOX_EDIT_BASED); + inspector->contentTextbox->messageUser = InspectorContentTextboxCallback; + EsSpacerCreate(toolbar, ES_FLAGS_DEFAULT, nullptr, 25, 0); + EsTextDisplayCreate(toolbar, ES_FLAGS_DEFAULT, nullptr, "Event category filter:"); + inspector->textboxCategoryFilter = EsTextboxCreate(toolbar, ES_ELEMENT_DISABLED); + inspector->textboxCategoryFilter->messageUser = InspectorTextboxCategoryFilterCallback; + } + + { + inspector->listEvents = EsListViewCreate(panel2, ES_CELL_FILL | ES_LIST_VIEW_CHOICE_SELECT | ES_LIST_VIEW_FIXED_ITEMS, ES_STYLE_LIST_CHOICE_BORDERED); + } + + InspectorRefreshElementList(inspector); + + APIInstance *instance = (APIInstance *) inspector->instance->_private; + instance->attachedInspector = inspector; +} diff --git a/desktop/text.cpp b/desktop/text.cpp index fe92560..345a14c 100644 --- a/desktop/text.cpp +++ b/desktop/text.cpp @@ -2,39 +2,31 @@ // It is released under the terms of the MIT license -- see LICENSE.md. // Written by: nakst. -#if defined(TEXT_RENDERER) - -// TODO Fallback VGA font. // TODO If the font size is sufficiently large disable subpixel anti-aliasing. // TODO Variable font support. -#ifdef USE_HARFBUZZ +#ifdef USE_FREETYPE_AND_HARFBUZZ #include #include #define HB_SHAPE(plan, features, featureCount) hb_shape(plan->font.hb, plan->buffer, features, featureCount) -#endif - -#ifdef USE_FREETYPE #define FT_EXPORT(x) extern "C" x #include #include FT_FREETYPE_H #include #endif +#define CHARACTER_SUBPIXEL (2) // 24 bits per pixel; each byte specifies the alpha of each RGB channel. +#define CHARACTER_IMAGE (3) // 32 bits per pixel, ARGB. +#define CHARACTER_RECOLOR (4) // 32 bits per pixel, AXXX. + #define FREETYPE_UNIT_SCALE (64) #define FALLBACK_SCRIPT_LANGUAGE ("en") #define FALLBACK_SCRIPT (0x4C61746E) // "Latn" struct Font { -#ifdef USE_FREETYPE +#ifdef USE_FREETYPE_AND_HARFBUZZ FT_Face ft; -#else - float scale; - const BasicFontHeader *header; -#endif - -#ifdef USE_HARFBUZZ hb_font_t *hb; #endif }; @@ -76,7 +68,7 @@ struct { char *sansName, *serifName, *monospacedName, *fallbackName; // Rendering. -#ifdef USE_FREETYPE +#ifdef USE_FREETYPE_AND_HARFBUZZ FT_Library freetypeLibrary; #endif @@ -140,7 +132,7 @@ GlyphCacheEntry *LookupGlyphCacheEntry(GlyphCacheKey key) { // --------------------------------- Font renderer. bool FontLoad(Font *font, const void *data, size_t dataBytes) { -#ifdef USE_FREETYPE +#ifdef USE_FREETYPE_AND_HARFBUZZ if (!fontManagement.freetypeLibrary) { FT_Init_FreeType(&fontManagement.freetypeLibrary); } @@ -148,33 +140,7 @@ bool FontLoad(Font *font, const void *data, size_t dataBytes) { if (FT_New_Memory_Face(fontManagement.freetypeLibrary, (uint8_t *) data, dataBytes, 0, &font->ft)) { return false; } -#else - if (dataBytes < sizeof(BasicFontHeader)) { - return false; - } - const BasicFontHeader *header = (const BasicFontHeader *) data; - - if (header->signature != BASIC_FONT_SIGNATURE - || (dataBytes < sizeof(BasicFontHeader) - + header->glyphCount * sizeof(BasicFontGlyph) - + header->kerningEntries * sizeof(BasicFontKerningEntry))) { - return false; - } - - const BasicFontGlyph *glyphs = (const BasicFontGlyph *) (header + 1); - - for (uintptr_t i = 0; i < header->glyphCount; i++) { - if (dataBytes <= glyphs[i].offsetToPoints - || dataBytes < glyphs[i].offsetToPoints + glyphs[i].pointCount * 24) { - return false; - } - } - - font->header = header; -#endif - -#ifdef USE_HARFBUZZ font->hb = hb_ft_font_create(font->ft, nullptr); #endif @@ -182,112 +148,50 @@ bool FontLoad(Font *font, const void *data, size_t dataBytes) { } void FontSetSize(Font *font, uint32_t size) { -#ifdef USE_FREETYPE +#ifdef USE_FREETYPE_AND_HARFBUZZ FT_Set_Char_Size(font->ft, 0, size * FREETYPE_UNIT_SCALE, 100, 100); -#else - font->scale = 1.75f * (float) size / (font->header->ascender - font->header->descender); -#endif - -#ifdef USE_HARFBUZZ hb_ft_font_changed(font->hb); #endif } uint32_t FontCodepointToGlyphIndex(Font *font, uint32_t codepoint) { -#ifdef USE_FREETYPE +#ifdef USE_FREETYPE_AND_HARFBUZZ return FT_Get_Char_Index(font->ft, codepoint); -#else - const BasicFontGlyph *glyphs = (const BasicFontGlyph *) (font->header + 1); - - for (uintptr_t i = 0; i < font->header->glyphCount; i++) { - if (glyphs[i].codepoint == codepoint) { - return i; - } - } - - return 0; #endif } void FontGetGlyphMetrics(Font *font, uint32_t glyphIndex, uint32_t *xAdvance, uint32_t *yAdvance, uint32_t *xOffset, uint32_t *yOffset) { -#ifdef USE_FREETYPE +#ifdef USE_FREETYPE_AND_HARFBUZZ FT_Load_Glyph(font->ft, glyphIndex, 0); *xAdvance = font->ft->glyph->advance.x; *yAdvance = font->ft->glyph->advance.y; - // *xOffset = font->ft->glyph->bitmap_left; - // *yOffset = font->ft->glyph->bitmap_top; *xOffset = *yOffset = 0; -#else - const BasicFontGlyph *glyph = ((const BasicFontGlyph *) (font->header + 1)) + glyphIndex; - *xOffset = *yOffset = *yAdvance = 0; - *xAdvance = glyph->xAdvance * font->scale * FREETYPE_UNIT_SCALE; #endif } int32_t FontGetKerning(Font *font, uint32_t previous, uint32_t next) { -#ifdef USE_FREETYPE +#ifdef USE_FREETYPE_AND_HARFBUZZ FT_Vector kerning = {}; if (previous) FT_Get_Kerning(font->ft, previous, next, 0, &kerning); return kerning.x; -#else - const BasicFontKerningEntry *entries = (const BasicFontKerningEntry *) (((const BasicFontGlyph *) (font->header + 1)) + font->header->glyphCount); - - uintptr_t currentIndex = 0; - bool startFound = false; - ES_MACRO_SEARCH(font->header->kerningEntries, result = previous - entries[index].leftGlyphIndex;, currentIndex, startFound); - int32_t xAdvance = 0; - - if (startFound) { - if (entries[currentIndex].rightGlyphIndex == next) { - xAdvance = entries[currentIndex].xAdvance; - } else if (entries[currentIndex].rightGlyphIndex < next) { - while (currentIndex != font->header->kerningEntries && entries[currentIndex].leftGlyphIndex == previous) { - if (entries[currentIndex].rightGlyphIndex == next) { - xAdvance = entries[currentIndex].xAdvance; - break; - } else { - currentIndex++; - } - } - } else { - while (entries[currentIndex].leftGlyphIndex == previous) { - if (entries[currentIndex].rightGlyphIndex == next) { - xAdvance = entries[currentIndex].xAdvance; - break; - } else if (!currentIndex) { - break; - } else { - currentIndex--; - } - } - } - } - - return xAdvance * FREETYPE_UNIT_SCALE * font->scale; #endif } int32_t FontGetAscent(Font *font) { -#ifdef USE_FREETYPE +#ifdef USE_FREETYPE_AND_HARFBUZZ return font->ft->size->metrics.ascender; -#else - return font->header->ascender * font->scale * FREETYPE_UNIT_SCALE; #endif } int32_t FontGetDescent(Font *font) { -#ifdef USE_FREETYPE +#ifdef USE_FREETYPE_AND_HARFBUZZ return font->ft->size->metrics.descender; -#else - return font->header->descender * font->scale * FREETYPE_UNIT_SCALE; #endif } int32_t FontGetEmWidth(Font *font) { -#ifdef USE_FREETYPE +#ifdef USE_FREETYPE_AND_HARFBUZZ return font->ft->size->metrics.x_ppem; -#else - return font->header->ascender * font->scale; // TODO. #endif } @@ -299,8 +203,8 @@ int TextGetLineHeight(EsElement *element, const EsTextStyle *textStyle) { return (FontGetAscent(&font) - FontGetDescent(&font) + FREETYPE_UNIT_SCALE / 2) / FREETYPE_UNIT_SCALE; } -bool FontRenderGlyph(bool mono, GlyphCacheKey key, GlyphCacheEntry *entry) { -#ifdef USE_FREETYPE +bool FontRenderGlyph(GlyphCacheKey key, GlyphCacheEntry *entry) { +#ifdef USE_FREETYPE_AND_HARFBUZZ FT_Load_Glyph(key.font.ft, key.glyphIndex, FT_LOAD_DEFAULT); FT_Outline_Translate(&key.font.ft->glyph->outline, key.fractionalPosition, 0); @@ -310,73 +214,40 @@ bool FontRenderGlyph(bool mono, GlyphCacheKey key, GlyphCacheEntry *entry) { int yoff; uint8_t *output; - if (mono) { - FT_Render_Glyph(key.font.ft->glyph, FT_RENDER_MODE_MONO); + FT_Render_Glyph(key.font.ft->glyph, FT_RENDER_MODE_LCD); - FT_Bitmap *bitmap = &key.font.ft->glyph->bitmap; - width = bitmap->width; - height = bitmap->rows; - xoff = key.font.ft->glyph->bitmap_left; - yoff = -key.font.ft->glyph->bitmap_top; + FT_Bitmap *bitmap = &key.font.ft->glyph->bitmap; + width = bitmap->width / 3; + height = bitmap->rows; + xoff = key.font.ft->glyph->bitmap_left; + yoff = -key.font.ft->glyph->bitmap_top; - if ((uint64_t) width * (uint64_t) height * 4 > 100000000) { - // Refuse to output glyphs more than 100MB. - return false; - } + entry->dataBytes = 1 /*stupid hack for whitespace*/ + width * height * 4; + output = (uint8_t *) EsHeapAllocate(entry->dataBytes, false); - entry->dataBytes = 1 + (width * height + 7) / 8; - output = (uint8_t *) EsHeapAllocate(entry->dataBytes, true); + if (!output) { + return false; + } - if (!output) { - return false; - } + for (int y = 0; y < height; y++) { + for (int x = 0; x < width; x++) { + int32_t r = (int32_t) ((uint8_t *) bitmap->buffer)[x * 3 + y * bitmap->pitch + 0]; + int32_t g = (int32_t) ((uint8_t *) bitmap->buffer)[x * 3 + y * bitmap->pitch + 1]; + int32_t b = (int32_t) ((uint8_t *) bitmap->buffer)[x * 3 + y * bitmap->pitch + 2]; - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - uintptr_t s = bitmap->pitch * 8 * y + x; - uintptr_t d = width * y + x; + // Reduce how noticible the colour fringes are. + // TODO Make this adjustable? + int32_t average = (r + g + b) / 3; + r -= (r - average) / 3; + g -= (g - average) / 3; + b -= (b - average) / 3; - if (bitmap->buffer[s / 8] & (1 << (7 - (s & 7)))) { - output[d / 8] |= (1 << (d & 7)); - } - } - } - } else { - FT_Render_Glyph(key.font.ft->glyph, FT_RENDER_MODE_LCD); + output[(x + y * width) * 4 + 0] = (uint8_t) r; + output[(x + y * width) * 4 + 1] = (uint8_t) g; + output[(x + y * width) * 4 + 2] = (uint8_t) b; + output[(x + y * width) * 4 + 3] = 0xFF; - FT_Bitmap *bitmap = &key.font.ft->glyph->bitmap; - width = bitmap->width / 3; - height = bitmap->rows; - xoff = key.font.ft->glyph->bitmap_left; - yoff = -key.font.ft->glyph->bitmap_top; - - entry->dataBytes = 1 /*stupid hack for whitespace*/ + width * height * 4; - output = (uint8_t *) EsHeapAllocate(entry->dataBytes, false); - - if (!output) { - return false; - } - - for (int y = 0; y < height; y++) { - for (int x = 0; x < width; x++) { - int32_t r = (int32_t) ((uint8_t *) bitmap->buffer)[x * 3 + y * bitmap->pitch + 0]; - int32_t g = (int32_t) ((uint8_t *) bitmap->buffer)[x * 3 + y * bitmap->pitch + 1]; - int32_t b = (int32_t) ((uint8_t *) bitmap->buffer)[x * 3 + y * bitmap->pitch + 2]; - - // Reduce how noticible the colour fringes are. - // TODO Make this adjustable? - int32_t average = (r + g + b) / 3; - r -= (r - average) / 3; - g -= (g - average) / 3; - b -= (b - average) / 3; - - output[(x + y * width) * 4 + 0] = (uint8_t) r; - output[(x + y * width) * 4 + 1] = (uint8_t) g; - output[(x + y * width) * 4 + 2] = (uint8_t) b; - output[(x + y * width) * 4 + 3] = 0xFF; - - // EsPrint("\tPixel %d, %d: red %X, green %X, blue %X\n", x, y, r, g, b); - } + // EsPrint("\tPixel %d, %d: red %X, green %X, blue %X\n", x, y, r, g, b); } } @@ -390,72 +261,6 @@ bool FontRenderGlyph(bool mono, GlyphCacheKey key, GlyphCacheEntry *entry) { } return false; -#else - EsAssert(!mono); - - const BasicFontGlyph *glyph = ((const BasicFontGlyph *) (key.font.header + 1)) + key.glyphIndex; - uint32_t width = glyph->width * key.font.scale + 2; - uint32_t height = glyph->height * key.font.scale + 2; - - if (width > 4096 || height > 4096) { - return false; - } - - RastSurface surface = {}; - RastPath path = {}; - RastPaint paint = {}; - paint.type = RAST_PAINT_SOLID; - paint.solid.alpha = 1.0f; - - float vertexScale = key.font.scale * (key.font.header->ascender - key.font.header->descender) * 0.01f; - - entry->data = (uint8_t *) EsHeapAllocate(width * height * 4, true); - - if (!entry->data) { - return false; - } - - if (glyph->pointCount) { - float *points = (float *) ((const uint8_t *) key.font.header + glyph->offsetToPoints); - - for (uintptr_t i = 0, j = 0; i < glyph->pointCount * 3; i += 3) { - if ((int) i == glyph->pointCount * 3 - 3 || points[i * 2 + 2] == -1e6) { - RastPathAppendBezier(&path, (RastVertex *) points + j, i - j + 1, { vertexScale, vertexScale }); - RastPathCloseSegment(&path); - j = i + 3; - } - } - - RastPathTranslate(&path, (float) key.fractionalPosition / FREETYPE_UNIT_SCALE - glyph->xOffset * key.font.scale, -glyph->yOffset * key.font.scale); - RastShape shape = RastShapeCreateSolid(&path); - - if (RastSurfaceInitialise(&surface, width, height, false)) { - RastSurfaceFill(surface, shape, paint, false); - RastPathDestroy(&path); - - uint32_t *in = surface.buffer; - uint8_t *out = entry->data; - - for (uintptr_t i = 0; i < height; i++) { - for (uintptr_t j = 0; j < width; j++) { - int32_t a = in[(height - i - 1) * width + j] >> 24; - *out++ = (uint8_t) a; - *out++ = (uint8_t) a; - *out++ = (uint8_t) a; - *out++ = 0xFF; - } - } - } - - RastSurfaceDestroy(&surface); - } - - entry->width = width; - entry->height = height; - entry->xoff = glyph->xOffset * key.font.scale + 0.25f; - entry->yoff = -glyph->yOffset * key.font.scale - height + 0.25f; - - return true; #endif } @@ -784,10 +589,8 @@ void FontDatabaseFree() { for (uintptr_t i = 0; i < fontManagement.loaded.Count(); i++) { // TODO Unmap file store data. Font font = fontManagement.loaded[i]; -#ifdef USE_HARFBUZZ +#ifdef USE_FREETYPE_AND_HARFBUZZ hb_font_destroy(font.hb); -#endif -#ifdef USE_FREETYPE FT_Done_Face(font.ft); #endif } @@ -815,7 +618,7 @@ void FontDatabaseFree() { fontManagement.database.Free(); fontManagement.loaded.Free(); -#ifdef USE_FREETYPE +#ifdef USE_FREETYPE_AND_HARFBUZZ FT_Done_FreeType(fontManagement.freetypeLibrary); #endif } @@ -955,13 +758,6 @@ void DrawSingleCharacter(int width, int height, int xoff, int yoff, uint32_t a = c / (d * 256.0f); DrawStringPixel(oX, oY, target->bits, target->stride, color, selectionColor, backgroundColor, a | (a << 8) | (a << 16), selected, fullAlpha); - } else if (type == CHARACTER_MONO) { - uintptr_t n = y * width + x; - - if (output[n / 8] & (1 << (n & 7))) { - uint32_t *destination = (uint32_t *) ((uint8_t *) target->bits + oX * 4 + oY * target->stride); - *destination = 0xFF000000 | color; - } } else if (type == CHARACTER_IMAGE || type == CHARACTER_RECOLOR) { uint32_t pixel = *((uint32_t *) (output + (x * 4 + y * width * 4))); uint32_t *destination = (uint32_t *) ((uint8_t *) target->bits + (oX) * 4 + (oY) * target->stride); @@ -1441,110 +1237,6 @@ void EsDrawVectorFile(EsPainter *painter, EsRectangle bounds, const void *data, EsHeapFree(iconManagement.buffer); } -// --------------------------------- Basic shaping engine. - -#ifndef USE_HARFBUZZ - -#define HB_SCRIPT_COMMON (1) -#define HB_SCRIPT_INHERITED (1) - -#define HB_SHAPE(plan, features, featureCount) hb_shape(plan->font, plan->buffer, features, featureCount) - -struct hb_segment_properties_t { - uint32_t script; -}; - -struct hb_glyph_info_t { - uint32_t cluster; - uint32_t codepoint; -}; - -struct hb_glyph_position_t { - uint32_t x_advance; - uint32_t y_advance; - uint32_t x_offset; - uint32_t y_offset; -}; - -struct hb_feature_t { -}; - -struct hb_buffer_t { - const char *text; - size_t textBytes; - uintptr_t shapeOffset; - size_t shapeBytes; - - Array glyphInfos; - Array glyphPositions; -}; - -void hb_buffer_clear_contents(hb_buffer_t *buffer) { - buffer->glyphInfos.Free(); - buffer->glyphPositions.Free(); -} - -void hb_buffer_set_segment_properties(hb_buffer_t *, hb_segment_properties_t *) { -} - -void hb_buffer_add_utf8(hb_buffer_t *buffer, const char *text, size_t textBytes, uintptr_t shapeOffset, size_t shapeBytes) { - buffer->text = text; - buffer->textBytes = textBytes; - buffer->shapeOffset = shapeOffset; - buffer->shapeBytes = shapeBytes; -} - -hb_glyph_info_t *hb_buffer_get_glyph_infos(hb_buffer_t *buffer, uint32_t *glyphCount) { - *glyphCount = buffer->glyphInfos.Length(); - return buffer->glyphInfos.array; -} - -hb_glyph_position_t *hb_buffer_get_glyph_positions(hb_buffer_t *buffer, uint32_t *glyphCount) { - *glyphCount = buffer->glyphPositions.Length(); - return buffer->glyphPositions.array; -} - -uint32_t hb_unicode_script(struct hb_unicode_funcs_t *, uint32_t) { - return FALLBACK_SCRIPT; -} - -struct hb_unicode_funcs_t *hb_unicode_funcs_get_default() { - return nullptr; -} - -hb_buffer_t *hb_buffer_create() { - return (hb_buffer_t *) EsHeapAllocate(sizeof(hb_buffer_t), true); -} - -void hb_buffer_destroy(hb_buffer_t *buffer) { - hb_buffer_clear_contents(buffer); - EsHeapFree(buffer); -} - -void hb_shape(Font font, hb_buffer_t *buffer, const hb_feature_t *, uint32_t) { - // TODO Cache glyph metrics. - - const char *text = buffer->text + buffer->shapeOffset; - uint32_t previous = 0; - - while (true) { - hb_glyph_info_t info = {}; - hb_glyph_position_t position = {}; - info.cluster = text - buffer->text; - uint32_t codepoint = utf8_value(text, buffer->text + buffer->shapeOffset + buffer->shapeBytes - text, nullptr); - if (!codepoint) break; - text = utf8_advance(text); - info.codepoint = FontCodepointToGlyphIndex(&font, codepoint); - FontGetGlyphMetrics(&font, info.codepoint, &position.x_advance, &position.y_advance, &position.x_offset, &position.y_offset); - position.x_advance += FontGetKerning(&font, previous, info.codepoint); - previous = info.codepoint; - buffer->glyphInfos.Add(info); - buffer->glyphPositions.Add(position); - } -} - -#endif - // --------------------------------- Text shaping. enum TextStyleDifference { @@ -2056,11 +1748,9 @@ int32_t TextBuildTextPieces(EsTextPlan *plan, uintptr_t sectionStart, uintptr_t hb_feature_t features[4] = {}; size_t featureCount = 0; -#ifdef USE_HARFBUZZ if (plan->currentTextStyle->figures == ES_TEXT_FIGURE_OLD) hb_feature_from_string("onum", -1, features + (featureCount++)); if (plan->currentTextStyle->figures == ES_TEXT_FIGURE_TABULAR) hb_feature_from_string("tnum", -1, features + (featureCount++)); plan->segmentProperties.script = (hb_script_t) run->script; -#endif hb_buffer_clear_contents(plan->buffer); hb_buffer_set_segment_properties(plan->buffer, &plan->segmentProperties); @@ -2161,13 +1851,11 @@ EsTextPlan *EsTextPlanCreate(EsElement *element, EsTextPlanProperties *propertie // Setup the HarfBuzz buffer. plan.buffer = hb_buffer_create(); -#ifdef USE_HARFBUZZ hb_buffer_set_cluster_level(plan.buffer, HB_BUFFER_CLUSTER_LEVEL_MONOTONE_CHARACTERS); plan.segmentProperties.direction = (properties->flags & ES_TEXT_PLAN_RTL) ? HB_DIRECTION_RTL : HB_DIRECTION_LTR; plan.segmentProperties.script = (hb_script_t) FALLBACK_SCRIPT; plan.segmentProperties.language = hb_language_from_string(properties->cLanguage ?: FALLBACK_SCRIPT_LANGUAGE, -1); -#endif // Subdivide the runs by character script. // This is also responsible for scaling the text sizes. @@ -2400,9 +2088,6 @@ void DrawTextPiece(EsPainter *painter, EsTextPlan *plan, TextPiece *piece, TextL positionY = ((glyphPositions[i].y_offset + FREETYPE_UNIT_SCALE / 2) / FREETYPE_UNIT_SCALE + cursorY); uint32_t color = plan->currentTextStyle->color; - // bool mono = api.systemConstants[ES_SYSTEM_CONSTANT_NO_FANCY_GRAPHICS]; - bool mono = false; - GlyphCacheKey key = {}; key.glyphIndex = codepoint; key.size = plan->currentTextStyle->size; @@ -2428,7 +2113,7 @@ void DrawTextPiece(EsPainter *painter, EsTextPlan *plan, TextPiece *piece, TextL } if (!entry->data) { - if (!FontRenderGlyph(mono, key, entry)) { + if (!FontRenderGlyph(key, entry)) { EsHeapFree(entry); goto nextCharacter; } else { @@ -2448,7 +2133,7 @@ void DrawTextPiece(EsPainter *painter, EsTextPlan *plan, TextPiece *piece, TextL ES_POINT(positionX, positionY + line->ascent / FREETYPE_UNIT_SCALE), painter->clip, painter->target, plan->currentTextStyle->blur, - mono ? CHARACTER_MONO : CHARACTER_SUBPIXEL, + CHARACTER_SUBPIXEL, false, entry->data, color, 0, -1, painter->target->fullAlpha); @@ -2610,8 +2295,6 @@ void EsDrawTextThemed(EsPainter *painter, EsElement *element, EsRectangle bounds EsDrawTextSimple(painter, element, bounds, string, stringBytes, textStyle, flags); } -#elif defined(TEXT_ELEMENTS) - // --------------------------------- Markup parsing. void EsRichTextParse(const char *inString, ptrdiff_t inStringBytes, @@ -2717,6 +2400,8 @@ void EsRichTextParse(const char *inString, ptrdiff_t inStringBytes, *outTextRunCount = textRunCount; } +// --------------------------------- Syntax highlighting. + const char *const keywords_a[] = { "auto", nullptr }; const char *const keywords_b[] = { "bool", "break", nullptr }; const char *const keywords_c[] = { "case", "char", "const", "continue", nullptr }; @@ -2881,2410 +2566,3 @@ Array TextApplySyntaxHighlighting(const EsTextStyle *baseStyle, int l return runs; } - -// --------------------------------- Textboxes. - -// TODO Caret blinking. -// TODO Wrapped lines. -// TODO Unicode grapheme/word boundaries. -// TODO Selecting lines with the margin. - -#define GET_BUFFER(line) TextboxGetDocumentLineBuffer(textbox, line) - -struct DocumentLine { - int32_t lengthBytes, - lengthWidth, - height, - yPosition, - offset; -}; - -struct TextboxVisibleLine { - int32_t yPosition; -}; - -struct TextboxCaret { - int32_t byte, // Relative to the start of the line. - line; -}; - -struct EsTextbox : EsElement { - ScrollPane scroll; - - char *data; // Call TextboxSetActiveLine(textbox, -1) to access this. - uintptr_t dataAllocated; - int32_t dataBytes; - - bool editing; - char *editStartContent; - int32_t editStartContentBytes; - - bool ensureCaretVisibleQueued; - - EsElementCallback overlayCallback; - EsGeneric overlayData; - - char *activeLine; - uintptr_t activeLineAllocated; - int32_t activeLineIndex, activeLineStart, activeLineOldBytes, activeLineBytes; - - int32_t longestLine, longestLineWidth; // To set the horizontal scroll bar's size. - - TextboxCaret carets[2]; // carets[1] is the actual caret; carets[0] is the selection anchor. - TextboxCaret wordSelectionAnchor, wordSelectionAnchor2; - - Array lines; - Array visibleLines; - int32_t firstVisibleLine; - - int verticalMotionHorizontalDepth; - int oldHorizontalScroll; - - EsUndoManager *undo; - EsUndoManager localUndo; - - EsElement *margin; - - EsRectangle borders, insets; - EsTextStyle textStyle; - EsFont overrideFont; - uint16_t overrideTextSize; - - uint32_t syntaxHighlightingLanguage; - uint32_t syntaxHighlightingColors[8]; - - bool smartQuotes; - - bool inRightClickDrag; - - // For smart context menus: - bool colorUppercase; -}; - -#define MOVE_CARET_SINGLE (2) -#define MOVE_CARET_WORD (3) -#define MOVE_CARET_LINE (4) -#define MOVE_CARET_VERTICAL (5) -#define MOVE_CARET_ALL (6) - -#define MOVE_CARET_BACKWARDS (false) -#define MOVE_CARET_FORWARDS (true) - -void TextboxBufferResize(void **array, uintptr_t *allocated, uintptr_t needed, uintptr_t itemSize) { - if (*allocated >= needed) { - return; - } - - uintptr_t oldAllocated = *allocated; - void *oldArray = *array; - - uintptr_t newAllocated = oldAllocated * 2; - if (newAllocated < needed) newAllocated = needed + 16; - void *newArray = EsHeapAllocate(newAllocated * itemSize, false); - - EsMemoryCopy(newArray, oldArray, oldAllocated * itemSize); - EsHeapFree(oldArray); - - *allocated = newAllocated; - *array = newArray; -} - -void KeyboardLayoutLoad() { - if (api.keyboardLayoutIdentifier != api.global->keyboardLayout) { - char buffer[64]; - api.keyboardLayoutIdentifier = api.global->keyboardLayout; - api.keyboardLayout = (const uint16_t *) EsBundleFind(&bundleDesktop, buffer, EsStringFormat(buffer, sizeof(buffer), "Keyboard Layouts/%c%c.dat", - (uint8_t) api.keyboardLayoutIdentifier, (uint8_t) (api.keyboardLayoutIdentifier >> 8))); - - if (!api.keyboardLayout) { - // Fallback to the US layout if the specifier layout was not found. - api.keyboardLayout = (const uint16_t *) EsBundleFind(&bundleDesktop, buffer, EsStringFormat(buffer, sizeof(buffer), "Keyboard Layouts/us.dat")); - } - } -} - -const char *KeyboardLayoutLookup(uint32_t scancode, bool isShiftHeld, bool isAltGrHeld, bool enableTabs, bool enableNewline) { - KeyboardLayoutLoad(); - if (scancode >= 0x200) return nullptr; - if (scancode == ES_SCANCODE_ENTER || scancode == ES_SCANCODE_NUM_ENTER) return enableNewline ? "\n" : nullptr; - if (scancode == ES_SCANCODE_TAB) return enableTabs ? "\t" : nullptr; - if (scancode == ES_SCANCODE_BACKSPACE || scancode == ES_SCANCODE_DELETE) return nullptr; - uint16_t offset = api.keyboardLayout[scancode + (isShiftHeld ? 0x200 : 0) + (isAltGrHeld ? 0x400 : 0)]; - return offset ? ((char *) api.keyboardLayout + 0x1000 + offset) : nullptr; -} - -uint32_t ScancodeMapToLabel(uint32_t scancode) { - KeyboardLayoutLoad(); - const char *string = KeyboardLayoutLookup(scancode, false, false, false, false); - - if (string && string[0] && !string[1]) { - char c = string[0]; - if (c >= 'a' && c <= 'z') return ES_SCANCODE_A + c - 'a'; - if (c >= 'A' && c <= 'Z') return ES_SCANCODE_A + c - 'A'; - if (c >= '0' && c <= '9') return ES_SCANCODE_0 + c - '0'; - if (c == '/') return ES_SCANCODE_SLASH; - if (c == '[') return ES_SCANCODE_LEFT_BRACE; - if (c == ']') return ES_SCANCODE_RIGHT_BRACE; - if (c == '=') return ES_SCANCODE_EQUALS; - if (c == '-') return ES_SCANCODE_HYPHEN; - if (c == ',') return ES_SCANCODE_COMMA; - if (c == '.') return ES_SCANCODE_PERIOD; - if (c == '\\') return ES_SCANCODE_PUNCTUATION_1; - if (c == ';') return ES_SCANCODE_PUNCTUATION_3; - if (c == '\'') return ES_SCANCODE_PUNCTUATION_4; - if (c == '`') return ES_SCANCODE_PUNCTUATION_5; - } - - return scancode; -} - -bool ScancodeIsNonTypeable(uint32_t scancode) { - switch (scancode) { - case ES_SCANCODE_CAPS_LOCK: - case ES_SCANCODE_SCROLL_LOCK: - case ES_SCANCODE_NUM_LOCK: - case ES_SCANCODE_LEFT_SHIFT: - case ES_SCANCODE_LEFT_CTRL: - case ES_SCANCODE_LEFT_ALT: - case ES_SCANCODE_LEFT_FLAG: - case ES_SCANCODE_RIGHT_SHIFT: - case ES_SCANCODE_RIGHT_CTRL: - case ES_SCANCODE_RIGHT_ALT: - case ES_SCANCODE_PAUSE: - case ES_SCANCODE_CONTEXT_MENU: - case ES_SCANCODE_PRINT_SCREEN: - case ES_SCANCODE_F1: - case ES_SCANCODE_F2: - case ES_SCANCODE_F3: - case ES_SCANCODE_F4: - case ES_SCANCODE_F5: - case ES_SCANCODE_F6: - case ES_SCANCODE_F7: - case ES_SCANCODE_F8: - case ES_SCANCODE_F9: - case ES_SCANCODE_F10: - case ES_SCANCODE_F11: - case ES_SCANCODE_F12: - case ES_SCANCODE_ACPI_POWER: - case ES_SCANCODE_ACPI_SLEEP: - case ES_SCANCODE_ACPI_WAKE: - case ES_SCANCODE_MM_NEXT: - case ES_SCANCODE_MM_PREVIOUS: - case ES_SCANCODE_MM_STOP: - case ES_SCANCODE_MM_PAUSE: - case ES_SCANCODE_MM_MUTE: - case ES_SCANCODE_MM_QUIETER: - case ES_SCANCODE_MM_LOUDER: - case ES_SCANCODE_MM_SELECT: - case ES_SCANCODE_MM_EMAIL: - case ES_SCANCODE_MM_CALC: - case ES_SCANCODE_MM_FILES: - case ES_SCANCODE_WWW_SEARCH: - case ES_SCANCODE_WWW_HOME: - case ES_SCANCODE_WWW_BACK: - case ES_SCANCODE_WWW_FORWARD: - case ES_SCANCODE_WWW_STOP: - case ES_SCANCODE_WWW_REFRESH: - case ES_SCANCODE_WWW_STARRED: - return true; - - default: - return false; - } -} - -size_t EsMessageGetInputText(EsMessage *message, char *buffer) { - const char *string = KeyboardLayoutLookup(message->keyboard.scancode, - message->keyboard.modifiers & ES_MODIFIER_SHIFT, message->keyboard.modifiers & ES_MODIFIER_ALT_GR, - true, true); - size_t bytes = string ? EsCStringLength(string) : 0; - EsAssert(bytes < 64); - EsMemoryCopy(buffer, string, bytes); - return bytes; -} - -enum CharacterType { - CHARACTER_INVALID, - CHARACTER_IDENTIFIER, // A-Z, a-z, 0-9, _, >= 0x7F - CHARACTER_WHITESPACE, // space, tab, newline - CHARACTER_OTHER, -}; - -static CharacterType GetCharacterType(int character) { - if ((character >= '0' && character <= '9') - || (character >= 'a' && character <= 'z') - || (character >= 'A' && character <= 'Z') - || (character == '_') - || (character >= 0x80)) { - return CHARACTER_IDENTIFIER; - } - - if (character == '\n' || character == '\t' || character == ' ') { - return CHARACTER_WHITESPACE; - } - - return CHARACTER_OTHER; -} - -int TextboxCompareCarets(const TextboxCaret *left, const TextboxCaret *right) { - if (left->line < right->line) return -1; - if (left->line > right->line) return 1; - if (left->byte < right->byte) return -1; - if (left->byte > right->byte) return 1; - return 0; -} - -void TextboxSetActiveLine(EsTextbox *textbox, int lineIndex) { - if (textbox->activeLineIndex == lineIndex) { - return; - } - - if (lineIndex == -1) { - int32_t lineBytesDelta = textbox->activeLineBytes - textbox->activeLineOldBytes; - - // Step 1: Resize the data buffer to fit the new contents of the line. - - TextboxBufferResize((void **) &textbox->data, &textbox->dataAllocated, textbox->dataBytes + lineBytesDelta, 1); - - // Step 2: Move everything after the old end of the active line to its new position. - - EsMemoryMove(textbox->data + textbox->activeLineStart + textbox->activeLineOldBytes, - textbox->data + textbox->dataBytes, - lineBytesDelta, - false); - textbox->dataBytes += lineBytesDelta; - - // Step 3: Copy the active line back into the data buffer. - - EsMemoryCopy(textbox->data + textbox->activeLineStart, - textbox->activeLine, - textbox->activeLineBytes); - - // Step 4: Update the line byte offsets. - - for (uintptr_t i = textbox->activeLineIndex + 1; i < textbox->lines.Length(); i++) { - textbox->lines[i].offset += lineBytesDelta; - } - } else { - TextboxSetActiveLine(textbox, -1); - - DocumentLine *line = &textbox->lines[lineIndex]; - - TextboxBufferResize((void **) &textbox->activeLine, &textbox->activeLineAllocated, (textbox->activeLineBytes = line->lengthBytes), 1); - EsMemoryCopy(textbox->activeLine, textbox->data + line->offset, textbox->activeLineBytes); - - textbox->activeLineStart = line->offset; - textbox->activeLineOldBytes = textbox->activeLineBytes; - } - - textbox->activeLineIndex = lineIndex; -} - -void EsTextboxStartEdit(EsTextbox *textbox) { - textbox->state &= ~UI_STATE_LOST_STRONG_FOCUS; - - if ((textbox->flags & ES_TEXTBOX_EDIT_BASED) && !textbox->editing) { - EsMessage m = { ES_MSG_TEXTBOX_EDIT_START }; - - if (0 == EsMessageSend(textbox, &m)) { - EsTextboxSelectAll(textbox); - } - - if (textbox->state & UI_STATE_DESTROYING) { - return; - } - - textbox->editing = true; // Update this after sending the message so overlays can receive it. - TextboxSetActiveLine(textbox, -1); - textbox->editStartContent = (char *) EsHeapAllocate(textbox->dataBytes, false); - textbox->editStartContentBytes = textbox->dataBytes; - EsMemoryCopy(textbox->editStartContent, textbox->data, textbox->editStartContentBytes); - textbox->Repaint(true); - } -} - -void TextboxEndEdit(EsTextbox *textbox, bool reject) { - if ((textbox->flags & ES_TEXTBOX_EDIT_BASED) && textbox->editing) { - TextboxSetActiveLine(textbox, -1); - textbox->editing = false; - EsMessage m = { ES_MSG_TEXTBOX_EDIT_END }; - m.endEdit.rejected = reject; - m.endEdit.unchanged = textbox->dataBytes == textbox->editStartContentBytes - && 0 == EsMemoryCompare(textbox->data, textbox->editStartContent, textbox->dataBytes); - - if (reject || ES_REJECTED == EsMessageSend(textbox, &m)) { - EsTextboxSelectAll(textbox); - EsTextboxInsert(textbox, textbox->editStartContent, textbox->editStartContentBytes); - TextboxSetActiveLine(textbox, -1); - if (reject) EsMessageSend(textbox, &m); - } - - if (textbox->state & UI_STATE_DESTROYING) { - return; - } - - EsTextboxSetSelection(textbox, 0, 0, 0, 0); - EsHeapFree(textbox->editStartContent); - textbox->editStartContent = nullptr; - textbox->scroll.SetX(0); - textbox->Repaint(true); - } -} - -void TextboxUpdateCommands(EsTextbox *textbox, bool noClipboard) { - if (~textbox->state & UI_STATE_FOCUSED) { - return; - } - - EsCommand *command; - - bool selectionEmpty = !TextboxCompareCarets(textbox->carets + 0, textbox->carets + 1) && textbox->editing; - - command = EsCommandByID(textbox->instance, ES_COMMAND_DELETE); - command->data = textbox; - EsCommandSetDisabled(command, selectionEmpty); - - EsCommandSetCallback(command, [] (EsInstance *, EsElement *, EsCommand *command) { - EsTextbox *textbox = (EsTextbox *) command->data.p; - EsTextboxInsert(textbox, "", 0, true); - }); - - command = EsCommandByID(textbox->instance, ES_COMMAND_COPY); - command->data = textbox; - EsCommandSetDisabled(command, selectionEmpty); - - EsCommandSetCallback(command, [] (EsInstance *, EsElement *, EsCommand *command) { - EsTextbox *textbox = (EsTextbox *) command->data.p; - size_t textBytes; - char *text = EsTextboxGetContents(textbox, &textBytes, textbox->editing ? ES_TEXTBOX_GET_CONTENTS_SELECTED_ONLY : ES_FLAGS_DEFAULT); - EsError error = EsClipboardAddText(ES_CLIPBOARD_PRIMARY, text, textBytes); - EsHeapFree(text); - - EsRectangle bounds = EsElementGetWindowBounds(textbox); - int32_t x = (bounds.l + bounds.r) / 2; - int32_t y = (bounds.t + bounds.b) / 2; // TODO Position this in the middle of the selection. - - if (error == ES_SUCCESS) { - EsAnnouncementShow(textbox->window, ES_FLAGS_DEFAULT, x, y, INTERFACE_STRING(CommonAnnouncementTextCopied)); - } else if (error == ES_ERROR_INSUFFICIENT_RESOURCES || error == ES_ERROR_DRIVE_FULL) { - EsAnnouncementShow(textbox->window, ES_FLAGS_DEFAULT, x, y, INTERFACE_STRING(CommonAnnouncementCopyErrorResources)); - } else { - EsAnnouncementShow(textbox->window, ES_FLAGS_DEFAULT, x, y, INTERFACE_STRING(CommonAnnouncementCopyErrorOther)); - } - }); - - command = EsCommandByID(textbox->instance, ES_COMMAND_CUT); - command->data = textbox; - EsCommandSetDisabled(command, selectionEmpty); - - EsCommandSetCallback(command, [] (EsInstance *, EsElement *, EsCommand *command) { - EsTextbox *textbox = (EsTextbox *) command->data.p; - size_t textBytes; - char *text = EsTextboxGetContents(textbox, &textBytes, textbox->editing ? ES_TEXTBOX_GET_CONTENTS_SELECTED_ONLY : ES_FLAGS_DEFAULT); - EsClipboardAddText(ES_CLIPBOARD_PRIMARY, text, textBytes); - EsHeapFree(text); - EsTextboxStartEdit(textbox); - EsTextboxInsert(textbox, "", 0, true); - }); - - EsInstanceSetActiveUndoManager(textbox->instance, textbox->undo); - - command = EsCommandByID(textbox->instance, ES_COMMAND_SELECT_ALL); - command->data = textbox; - EsCommandSetDisabled(command, !(textbox->lines.Length() > 1 || textbox->lines[0].lengthBytes)); - - EsCommandSetCallback(command, [] (EsInstance *, EsElement *, EsCommand *command) { - EsTextboxSelectAll((EsTextbox *) command->data.p); - }); - - if (!noClipboard) { - command = EsCommandByID(textbox->instance, ES_COMMAND_PASTE); - command->data = textbox; - EsCommandSetDisabled(command, !EsClipboardHasText(ES_CLIPBOARD_PRIMARY)); - - EsCommandSetCallback(command, [] (EsInstance *, EsElement *, EsCommand *command) { - EsTextbox *textbox = (EsTextbox *) command->data.p; - - size_t textBytes = 0; - char *text = EsClipboardReadText(ES_CLIPBOARD_PRIMARY, &textBytes); - EsTextboxInsert(textbox, text, textBytes, true); - EsTextboxEnsureCaretVisible(textbox); - EsHeapFree(text); - }); - } -} - -char *TextboxGetDocumentLineBuffer(EsTextbox *textbox, DocumentLine *line) { - if (textbox->activeLineIndex == line - textbox->lines.array) { - return textbox->activeLine; - } else { - return textbox->data + line->offset; - } -} - -void TextboxFindLongestLine(EsTextbox *textbox) { - if (textbox->longestLine == -1) { - textbox->longestLine = 0; - textbox->longestLineWidth = textbox->lines[0].lengthWidth; - - for (uintptr_t i = 1; i < textbox->lines.Length(); i++) { - int32_t width = textbox->lines[i].lengthWidth; - - if (width > textbox->longestLineWidth) { - textbox->longestLine = i, textbox->longestLineWidth = width; - } - } - } -} - -TextboxVisibleLine *TextboxGetVisibleLine(EsTextbox *textbox, int32_t documentLineIndex) { - return textbox->firstVisibleLine > documentLineIndex - || textbox->firstVisibleLine + (int32_t) textbox->visibleLines.Length() <= documentLineIndex - ? nullptr : &textbox->visibleLines[documentLineIndex - textbox->firstVisibleLine]; -} - -void TextboxEnsureCaretVisibleActionCallback(EsElement *element, EsGeneric context) { - EsTextbox *textbox = (EsTextbox *) element; - bool verticallyCenter = context.u; - TextboxCaret caret = textbox->carets[1]; - - for (uintptr_t i = 0; i < 3; i++) { - // ScrollPane::SetY causes ES_MSG_SCROLL_Y to get sent to the textbox. - // This causes a TextboxRefreshVisibleLines, which may cause new lines to added. - // If these lines had not been previously horizontally measured, this will then occur. - // This then causes a ScrollPane::Refresh for the new horizontal width. - // If this causes the horizontal scroll bar to appear, then the caret may no longer be fully visible. - // Therefore, we repeat up to 3 times to ensure that the caret is definitely fully visible. - - EsRectangle bounds = textbox->GetBounds(); - DocumentLine *line = &textbox->lines[caret.line]; - int caretY = line->yPosition + textbox->insets.t; - - int scrollY = textbox->scroll.position[1]; - int viewportHeight = bounds.b; - caretY -= scrollY; - - if (viewportHeight > 0) { - if (verticallyCenter) { - scrollY += caretY - viewportHeight / 2; - } else { - if (caretY < textbox->insets.t) { - scrollY += caretY - textbox->insets.t; - } else if (caretY + line->height > viewportHeight - textbox->insets.b) { - scrollY += caretY + line->height - viewportHeight + textbox->insets.b; - } - } - - if (textbox->scroll.position[1] != scrollY) { - textbox->scroll.SetY(scrollY); - } else { - break; - } - } else { - break; - } - } - - TextboxVisibleLine *visibleLine = TextboxGetVisibleLine(textbox, caret.line); - - if (visibleLine) { - EsRectangle bounds = textbox->GetBounds(); - DocumentLine *line = &textbox->lines[caret.line]; - int scrollX = textbox->scroll.position[0]; - int viewportWidth = bounds.r; - int caretX = TextGetPartialStringWidth(textbox, &textbox->textStyle, - GET_BUFFER(line), line->lengthBytes, caret.byte) - scrollX + textbox->insets.l; - - if (caretX < textbox->insets.l) { - scrollX += caretX - textbox->insets.l; - } else if (caretX + 1 > viewportWidth - textbox->insets.r) { - scrollX += caretX + 1 - viewportWidth + textbox->insets.r; - } - - textbox->scroll.SetX(scrollX); - } - - UIQueueEnsureVisibleMessage(textbox, false); - textbox->ensureCaretVisibleQueued = false; -} - -void EsTextboxEnsureCaretVisible(EsTextbox *textbox, bool verticallyCenter) { - if (!textbox->ensureCaretVisibleQueued) { - UpdateAction action = {}; - action.element = textbox; - action.callback = TextboxEnsureCaretVisibleActionCallback; - action.context.u = verticallyCenter; - textbox->window->updateActions.Add(action); - textbox->ensureCaretVisibleQueued = true; - } -} - -bool TextboxMoveCaret(EsTextbox *textbox, TextboxCaret *caret, bool right, int moveType, bool strongWhitespace = false) { - TextboxCaret old = *caret; - EsDefer(TextboxUpdateCommands(textbox, true)); - - if (moveType == MOVE_CARET_LINE) { - caret->byte = right ? textbox->lines[caret->line].lengthBytes : 0; - } else if (moveType == MOVE_CARET_ALL) { - caret->line = right ? textbox->lines.Length() - 1 : 0; - caret->byte = right ? textbox->lines[caret->line].lengthBytes : 0; - } else if (moveType == MOVE_CARET_VERTICAL) { - if ((right && caret->line + 1 == (int32_t) textbox->lines.Length()) || (!right && !caret->line)) { - return false; - } - - if (textbox->verticalMotionHorizontalDepth == -1) { - textbox->verticalMotionHorizontalDepth = TextGetPartialStringWidth(textbox, &textbox->textStyle, - GET_BUFFER(&textbox->lines[caret->line]), textbox->lines[caret->line].lengthBytes, caret->byte); - } - - if (right) caret->line++; else caret->line--; - caret->byte = 0; - - DocumentLine *line = &textbox->lines[caret->line]; - int pointX = textbox->verticalMotionHorizontalDepth ? textbox->verticalMotionHorizontalDepth - 1 : 0; - ptrdiff_t result = TextGetCharacterAtPoint(textbox, &textbox->textStyle, - GET_BUFFER(line), line->lengthBytes, &pointX, ES_TEXT_GET_CHARACTER_AT_POINT_MIDDLE); - caret->byte = result == -1 ? line->lengthBytes : result; - } else { - CharacterType type = CHARACTER_INVALID; - char *currentLineBuffer = GET_BUFFER(&textbox->lines[caret->line]); - if (moveType == MOVE_CARET_WORD && right) goto checkCharacterType; - - while (true) { - if (!right) { - if (caret->byte || caret->line) { - if (caret->byte) { - caret->byte = utf8_retreat(currentLineBuffer + caret->byte) - currentLineBuffer; - } else { - caret->byte = textbox->lines[--caret->line].lengthBytes; - currentLineBuffer = GET_BUFFER(&textbox->lines[caret->line]); - } - } else { - break; // We cannot move any further left. - } - } else { - if (caret->line < (int32_t) textbox->lines.Length() - 1 || caret->byte < textbox->lines[caret->line].lengthBytes) { - if (caret->byte < textbox->lines[caret->line].lengthBytes) { - caret->byte = utf8_advance(currentLineBuffer + caret->byte) - currentLineBuffer; - } else { - caret->line++; - caret->byte = 0; - currentLineBuffer = GET_BUFFER(&textbox->lines[caret->line]); - } - } else { - break; // We cannot move any further right. - } - } - - if (moveType == MOVE_CARET_SINGLE) { - break; - } - - checkCharacterType:; - - int character; - - if (caret->byte == textbox->lines[caret->line].lengthBytes) { - character = '\n'; - } else { - character = utf8_value(currentLineBuffer + caret->byte); - } - - CharacterType newType = GetCharacterType(character); - - if (type == CHARACTER_INVALID) { - if (newType != CHARACTER_WHITESPACE || strongWhitespace) { - type = newType; - } - } else { - if (newType != type) { - if (!right) { - // We've gone too far. - TextboxMoveCaret(textbox, caret, true, MOVE_CARET_SINGLE); - } - - break; - } - } - } - } - - return caret->line != old.line; -} - -void EsTextboxMoveCaretRelative(EsTextbox *textbox, uint32_t flags) { - if (~flags & ES_TEXTBOX_MOVE_CARET_SECOND_ONLY) { - TextboxMoveCaret(textbox, &textbox->carets[0], ~flags & ES_TEXTBOX_MOVE_CARET_BACKWARDS, - flags & 0xFF, flags & ES_TEXTBOX_MOVE_CARET_STRONG_WHITESPACE); - } - - if (~flags & ES_TEXTBOX_MOVE_CARET_FIRST_ONLY) { - TextboxMoveCaret(textbox, &textbox->carets[1], ~flags & ES_TEXTBOX_MOVE_CARET_BACKWARDS, - flags & 0xFF, flags & ES_TEXTBOX_MOVE_CARET_STRONG_WHITESPACE); - } -} - -void TextboxRepaintLine(EsTextbox *textbox, int line) { - if (line == -1 || (~textbox->flags & ES_TEXTBOX_MULTILINE)) { - textbox->Repaint(true); - } else { - EsRectangle borders = textbox->borders; - int topInset = textbox->insets.t; - - TextboxVisibleLine *visibleLine = TextboxGetVisibleLine(textbox, line); - - if (visibleLine) { - EsRectangle bounds = textbox->GetBounds(); - EsRectangle lineBounds = ES_RECT_4(bounds.l + borders.l, bounds.r - borders.r, - visibleLine->yPosition + topInset - 1 - textbox->scroll.position[1], - visibleLine->yPosition + topInset + textbox->lines[line].height - textbox->scroll.position[1]); - // EsPrint("textbox bounds %R; line bounds %R\n", bounds); - textbox->Repaint(false, lineBounds); - } - } -} - -void TextboxSetHorizontalScroll(EsTextbox *textbox, int scroll) { - textbox->Repaint(true); - textbox->oldHorizontalScroll = scroll; -} - -void TextboxRefreshVisibleLines(EsTextbox *textbox, bool repaint = true) { - if (textbox->visibleLines.Length()) { - textbox->visibleLines.SetLength(0); - } - - int scrollX = textbox->scroll.position[0], scrollY = textbox->scroll.position[1]; - EsRectangle bounds = textbox->GetBounds(); - - int32_t low = 0, high = textbox->lines.Length() - 1, target = scrollY - textbox->insets.t; - - while (low != high) { - int32_t middle = (low + high) / 2; - int32_t position = textbox->lines[middle].yPosition; - - if (position < target && low != middle) low = middle; - else if (position > target && high != middle) high = middle; - else break; - } - - textbox->firstVisibleLine = (low + high) / 2; - if (textbox->firstVisibleLine) textbox->firstVisibleLine--; - - for (int32_t i = textbox->firstVisibleLine; i < (int32_t) textbox->lines.Length(); i++) { - TextboxVisibleLine line = {}; - line.yPosition = textbox->lines[i].yPosition; - textbox->visibleLines.Add(line); - - if (line.yPosition - scrollY > bounds.b) { - break; - } - } - - bool refreshXLimit = false; - - for (uintptr_t i = 0; i < textbox->visibleLines.Length(); i++) { - DocumentLine *line = &textbox->lines[textbox->firstVisibleLine + i]; - - if (line->lengthWidth != -1) { - continue; - } - - line->lengthWidth = TextGetStringWidth(textbox, &textbox->textStyle, - GET_BUFFER(line), line->lengthBytes); - - if (textbox->longestLine != -1 && line->lengthWidth > textbox->longestLineWidth) { - textbox->longestLine = textbox->firstVisibleLine + i; - textbox->longestLineWidth = line->lengthWidth; - refreshXLimit = true; - } - } - - if (refreshXLimit) { - textbox->scroll.Refresh(); - } - - textbox->scroll.SetX(scrollX); - - if (repaint) { - textbox->Repaint(true); - } -} - -void TextboxLineCountChangeCleanup(EsTextbox *textbox, int32_t offsetDelta, int32_t startLine) { - for (int32_t i = startLine; i < (int32_t) textbox->lines.Length(); i++) { - DocumentLine *line = &textbox->lines[i], *previous = &textbox->lines[i - 1]; - line->yPosition = previous->yPosition + previous->height; - line->offset += offsetDelta; - } - - TextboxRefreshVisibleLines(textbox); -} - -void EsTextboxMoveCaret(EsTextbox *textbox, int32_t line, int32_t byte) { - EsMessageMutexCheck(); - - textbox->carets[0].line = line; - textbox->carets[0].byte = byte; - textbox->carets[1].line = line; - textbox->carets[1].byte = byte; - textbox->Repaint(true); - TextboxUpdateCommands(textbox, true); -} - -void EsTextboxGetSelection(EsTextbox *textbox, int32_t *fromLine, int32_t *fromByte, int32_t *toLine, int32_t *toByte) { - EsMessageMutexCheck(); - - *fromLine = textbox->carets[0].line; - *fromByte = textbox->carets[0].byte; - *toLine = textbox->carets[1].line; - *toByte = textbox->carets[1].byte; -} - -void EsTextboxSetSelection(EsTextbox *textbox, int32_t fromLine, int32_t fromByte, int32_t toLine, int32_t toByte) { - EsMessageMutexCheck(); - - if (fromByte == -1) fromByte = textbox->lines[fromLine].lengthBytes; - if (toByte == -1) toByte = textbox->lines[toLine].lengthBytes; - if (fromByte < 0 || toByte < 0 || fromByte > textbox->lines[fromLine].lengthBytes || toByte > textbox->lines[toLine].lengthBytes) return; - textbox->carets[0].line = fromLine; - textbox->carets[0].byte = fromByte; - textbox->carets[1].line = toLine; - textbox->carets[1].byte = toByte; - textbox->Repaint(true); - TextboxUpdateCommands(textbox, true); - EsTextboxEnsureCaretVisible(textbox); -} - -void EsTextboxSelectAll(EsTextbox *textbox) { - EsMessageMutexCheck(); - - TextboxMoveCaret(textbox, &textbox->carets[0], false, MOVE_CARET_ALL); - TextboxMoveCaret(textbox, &textbox->carets[1], true, MOVE_CARET_ALL); - EsTextboxEnsureCaretVisible(textbox); - textbox->Repaint(true); -} - -void EsTextboxClear(EsTextbox *textbox, bool sendUpdatedMessage) { - EsMessageMutexCheck(); - - EsTextboxSelectAll(textbox); - EsTextboxInsert(textbox, "", 0, sendUpdatedMessage); -} - -size_t EsTextboxGetLineLength(EsTextbox *textbox, uintptr_t line) { - EsMessageMutexCheck(); - - return textbox->lines[line].lengthBytes; -} - -struct TextboxUndoItemHeader { - EsTextbox *textbox; - TextboxCaret caretsBefore[2]; - size_t insertBytes; - double timeStampMs; - // Followed by insert string. -}; - -void TextboxUndoItemCallback(const void *item, EsUndoManager *manager, EsMessage *message) { - if (message->type == ES_MSG_UNDO_INVOKE) { - TextboxUndoItemHeader *header = (TextboxUndoItemHeader *) item; - EsTextbox *textbox = header->textbox; - EsAssert(textbox->undo == manager); - TextboxRepaintLine(textbox, textbox->carets[0].line); - TextboxRepaintLine(textbox, textbox->carets[0].line); - textbox->carets[0] = header->caretsBefore[0]; - textbox->carets[1] = header->caretsBefore[1]; - EsTextboxInsert(textbox, (const char *) (header + 1), header->insertBytes, true); - EsTextboxEnsureCaretVisible(textbox); - } else if (message->type == ES_MSG_UNDO_CANCEL) { - // Nothing to do. - } -} - -void EsTextboxInsert(EsTextbox *textbox, const char *string, ptrdiff_t stringBytes, bool sendUpdatedMessage) { - EsMessageMutexCheck(); - - // EsPerformanceTimerPush(); - // double measureLineTime = 0; - - if (stringBytes == -1) { - stringBytes = EsCStringLength(string); - } - - TextboxUndoItemHeader *undoItem = nullptr; - size_t undoItemBytes = 0; - - textbox->wordSelectionAnchor = textbox->carets[0]; - textbox->wordSelectionAnchor2 = textbox->carets[1]; - - textbox->verticalMotionHorizontalDepth = -1; - - // ::: Delete the selected text. - - // Step 1: Get the range of text we're deleting. - - TextboxCaret deleteFrom, deleteTo; - int comparison = TextboxCompareCarets(textbox->carets + 0, textbox->carets + 1); - - if (comparison < 0) deleteFrom = textbox->carets[0], deleteTo = textbox->carets[1]; - else if (comparison > 0) deleteFrom = textbox->carets[1], deleteTo = textbox->carets[0]; - - if (comparison) { - textbox->carets[0] = textbox->carets[1] = deleteFrom; - - // Step 2: Calculate the number of bytes we are deleting. - - int32_t deltaBytes; - - if (deleteFrom.line == deleteTo.line) { - deltaBytes = deleteFrom.byte - deleteTo.byte; - } else { - TextboxSetActiveLine(textbox, -1); - - deltaBytes = deleteFrom.byte - deleteTo.byte; - - for (int32_t i = deleteFrom.line; i < deleteTo.line; i++) { - deltaBytes -= textbox->lines[i].lengthBytes; - } - } - - if (textbox->undo) { - // Step 3: Allocate space for an undo item. - - undoItemBytes = sizeof(TextboxUndoItemHeader) - deltaBytes + deleteTo.line - deleteFrom.line; - undoItem = (TextboxUndoItemHeader *) EsHeapAllocate(undoItemBytes, false); - EsMemoryZero(undoItem, sizeof(TextboxUndoItemHeader)); - undoItem->insertBytes = undoItemBytes - sizeof(TextboxUndoItemHeader); - } - - if (deleteFrom.line == deleteTo.line) { - EsAssert(deltaBytes < 0); // Expected deleteTo > deleteFrom. - DocumentLine *line = &textbox->lines[deleteFrom.line]; - TextboxSetActiveLine(textbox, deleteFrom.line); - - // Step 4: Update the width of the line and repaint it. - - line->lengthWidth = TextGetStringWidth(textbox, &textbox->textStyle, textbox->activeLine, textbox->activeLineBytes); - TextboxRepaintLine(textbox, deleteFrom.line); - - // Step 5: Update the active line buffer. - - if (undoItem) EsMemoryCopy(undoItem + 1, textbox->activeLine + deleteFrom.byte, -deltaBytes); - EsMemoryMove(textbox->activeLine + deleteTo.byte, textbox->activeLine + line->lengthBytes, deltaBytes, false); - textbox->activeLineBytes += deltaBytes; - line->lengthBytes += deltaBytes; - - // Step 6: Update the longest line. - - if (textbox->longestLine == deleteFrom.line && line->lengthWidth < textbox->longestLineWidth) { - textbox->longestLine = -1; - } - } else { - if (undoItem) { - // Step 4: Copy into the undo item. - - char *position = (char *) (undoItem + 1); - - for (int32_t i = deleteFrom.line; i <= deleteTo.line; i++) { - char *from = textbox->data + textbox->lines[i].offset; - char *to = textbox->data + textbox->lines[i].offset + textbox->lines[i].lengthBytes; - if (i == deleteFrom.line) from += deleteFrom.byte; - if (i == deleteTo.line) to += deleteTo.byte - textbox->lines[i].lengthBytes; - EsMemoryCopy(position, from, to - from); - position += to - from; - if (i != deleteTo.line) *position++ = '\n'; - } - } - - // Step 5: Remove the text from the buffer. - - EsMemoryMove(textbox->data + deleteTo.byte + textbox->lines[deleteTo.line].offset, textbox->data + textbox->dataBytes, deltaBytes, false); - textbox->dataBytes += deltaBytes; - - // Step 6: Merged the joined lines. - - DocumentLine *firstLine = &textbox->lines[deleteFrom.line]; - firstLine->lengthBytes = textbox->lines[deleteTo.line].lengthBytes - deleteTo.byte + deleteFrom.byte; - firstLine->lengthWidth = TextGetStringWidth(textbox, &textbox->textStyle, textbox->data + firstLine->offset, firstLine->lengthBytes); - - // Step 7: Remove the deleted lines and update the textbox. - - textbox->lines.DeleteMany(deleteFrom.line + 1, deleteTo.line - deleteFrom.line); - textbox->longestLine = -1; - TextboxLineCountChangeCleanup(textbox, deltaBytes, deleteFrom.line + 1); - } - } else { - if (textbox->undo) { - undoItemBytes = sizeof(TextboxUndoItemHeader); - undoItem = (TextboxUndoItemHeader *) EsHeapAllocate(undoItemBytes, false); - EsMemoryZero(undoItem, sizeof(TextboxUndoItemHeader)); - } - } - - if (undoItem) { - undoItem->caretsBefore[0] = undoItem->caretsBefore[1] = textbox->carets[0]; - } - - // ::: Insert the new text. - - if (!stringBytes) goto done; - - { - TextboxCaret insertionPoint = textbox->carets[0]; - - DocumentLine *line = &textbox->lines[insertionPoint.line]; - int32_t lineByteOffset = line->offset, - offsetIntoLine = insertionPoint.byte, - byteOffset = offsetIntoLine + lineByteOffset; - - // Step 1: Count the number of newlines in the input string. - - uintptr_t position = 0, - newlines = 0, - carriageReturns = 0; - - while (position < (size_t) stringBytes) { - int length; - UTF8_LENGTH_CHAR(string + position, length); - if (length == 0) length = 1; - - if (position + length > (size_t) stringBytes) { - break; - } else if (string[position] == '\n') { - newlines++; - } else if (string[position] == '\r' && position != (size_t) stringBytes - 1 && string[position + 1] == '\n') { - carriageReturns++; - } - - position += length; - } - - size_t bytesToInsert = stringBytes - newlines - carriageReturns; - - if (!newlines || (~textbox->flags & ES_TEXTBOX_MULTILINE)) { - // Step 2: Update the active line buffer. - - TextboxSetActiveLine(textbox, insertionPoint.line); - TextboxBufferResize((void **) &textbox->activeLine, &textbox->activeLineAllocated, (textbox->activeLineBytes += bytesToInsert), 1); - EsMemoryMove(textbox->activeLine + offsetIntoLine, textbox->activeLine + line->lengthBytes, bytesToInsert, false); - - const char *dataToInsert = string; - size_t added = 0; - - for (uintptr_t i = 0; i < newlines + 1; i++) { - const char *end = (const char *) EsCRTmemchr(dataToInsert, '\n', stringBytes - (dataToInsert - string)) ?: string + stringBytes; - bool carriageReturn = end != string && end[-1] == '\r'; - if (carriageReturn) end--; - EsMemoryCopy(textbox->activeLine + offsetIntoLine + added, dataToInsert, end - dataToInsert); - added += end - dataToInsert; - dataToInsert = end + (carriageReturn ? 2 : 1); - } - - EsAssert(added == bytesToInsert); // Added incorrect number of bytes in EsTextboxInsert. - - line->lengthBytes += bytesToInsert; - - // Step 3: Update the carets, line width, and repaint it. - - textbox->carets[0].byte += bytesToInsert; - textbox->carets[1].byte += bytesToInsert; - line->lengthWidth = TextGetStringWidth(textbox, &textbox->textStyle, textbox->activeLine, line->lengthBytes); - TextboxRepaintLine(textbox, insertionPoint.line); - - // Step 4: Update the longest line. - - if (textbox->longestLine != -1 && line->lengthWidth > textbox->longestLineWidth) { - textbox->longestLine = insertionPoint.line; - textbox->longestLineWidth = line->lengthWidth; - } - } else { - // Step 2: Make room in the buffer for the contents of the string. - - TextboxSetActiveLine(textbox, -1); - TextboxBufferResize((void **) &textbox->data, &textbox->dataAllocated, textbox->dataBytes + bytesToInsert, 1); - EsMemoryMove(textbox->data + byteOffset, textbox->data + textbox->dataBytes, bytesToInsert, false); - textbox->dataBytes += bytesToInsert; - - // Step 3: Truncate the insertion line. - - int32_t truncation = line->lengthBytes - insertionPoint.byte; - line->lengthBytes = insertionPoint.byte; - - // Step 4: Add the new lines. - - textbox->lines.InsertMany(insertionPoint.line + 1, newlines); - const char *dataToInsert = string; - uintptr_t insertedBytes = 0; - - for (uintptr_t i = 0; i < newlines + 1; i++) { - DocumentLine *line = &textbox->lines[insertionPoint.line + i], *previous = line - 1; - - // Step 4a: Initialise the line. - - if (i) { - EsMemoryZero(line, sizeof(*line)); - line->height = TextGetLineHeight(textbox, &textbox->textStyle); - line->yPosition = previous->yPosition + previous->height; - line->offset = lineByteOffset + insertedBytes; - } - - // Step 4b: Copy the string data into the line. - - const char *end = (const char *) EsCRTmemchr(dataToInsert, '\n', stringBytes - (dataToInsert - string)) ?: string + stringBytes; - bool carriageReturn = end != string && end[-1] == '\r'; - if (carriageReturn) end--; - EsMemoryCopy(textbox->data + line->offset + line->lengthBytes, dataToInsert, end - dataToInsert); - line->lengthBytes += end - dataToInsert; - insertedBytes += line->lengthBytes; - dataToInsert = end + (carriageReturn ? 2 : 1); - - if (i == newlines) { - line->lengthBytes += truncation; - } - - // Step 4c: Update the line's width. - - // EsPerformanceTimerPush(); -#if 0 - line->lengthWidth = EsTextGetPartialStringWidth(&textbox->textStyle, textbox->data + line->offset, line->lengthBytes, 0, line->lengthBytes); -#else - line->lengthWidth = -1; -#endif - // double time = EsPerformanceTimerPop(); - // measureLineTime += time; - // EsPrint("Measured the length of line %d in %Fms.\n", insertionPoint.line + i, time * 1000); - } - - // Step 5: Update the carets. - - textbox->carets[0].line = insertionPoint.line + newlines; - textbox->carets[1].line = insertionPoint.line + newlines; - textbox->carets[0].byte = textbox->lines[insertionPoint.line + newlines].lengthBytes - truncation; - textbox->carets[1].byte = textbox->lines[insertionPoint.line + newlines].lengthBytes - truncation; - - // Step 6: Update the textbox. - - textbox->longestLine = -1; - TextboxLineCountChangeCleanup(textbox, bytesToInsert, insertionPoint.line + 1 + newlines); - } - - if (undoItem) undoItem->caretsBefore[1] = textbox->carets[0]; - } - - done:; - - if (sendUpdatedMessage) { - EsMessage m = { ES_MSG_TEXTBOX_UPDATED }; - EsMessageSend(textbox, &m); - } else if (textbox->overlayCallback) { - EsMessage m = { ES_MSG_TEXTBOX_UPDATED }; - textbox->overlayCallback(textbox, &m); - } - - if (textbox->state & UI_STATE_DESTROYING) { - return; - } - - TextboxFindLongestLine(textbox); - InspectorNotifyElementContentChanged(textbox); - - if (undoItem && (undoItem->insertBytes || TextboxCompareCarets(undoItem->caretsBefore + 0, undoItem->caretsBefore + 1))) { - undoItem->timeStampMs = EsTimeStampMs(); - - EsUndoCallback previousCallback; - const void *previousItem; - - if (!EsUndoInUndo(textbox->undo) - && EsUndoPeek(textbox->undo, &previousCallback, &previousItem) - && previousCallback == TextboxUndoItemCallback) { - TextboxUndoItemHeader *header = (TextboxUndoItemHeader *) previousItem; - -#define TEXTBOX_UNDO_TIMEOUT (500) // TODO Make this configurable. - if (undoItem->timeStampMs - header->timeStampMs < TEXTBOX_UNDO_TIMEOUT) { - if (!undoItem->insertBytes && !header->insertBytes - && undoItem->caretsBefore[0].line == header->caretsBefore[1].line - && undoItem->caretsBefore[0].byte == header->caretsBefore[1].byte) { - // Merge the items. - undoItem->caretsBefore[0] = header->caretsBefore[0]; - EsUndoPop(textbox->undo); - } else { - // Add the new item to the same group as the previous. - EsUndoContinueGroup(textbox->undo); - } - } - } - - undoItem->textbox = textbox; - EsUndoPush(textbox->undo, TextboxUndoItemCallback, undoItem, undoItemBytes, false /* do not set instance's undo manager */); - } - - EsHeapFree(undoItem); - - // double time = EsPerformanceTimerPop(); - // EsPrint("EsTextboxInsert in %Fms (%Fms measuring new lines).\n", time * 1000, measureLineTime * 1000); - - textbox->scroll.Refresh(); - TextboxUpdateCommands(textbox, true); -} - -char *EsTextboxGetContents(EsTextbox *textbox, size_t *_bytes, uint32_t flags) { - EsMessageMutexCheck(); - - TextboxSetActiveLine(textbox, -1); - - bool includeNewline = textbox->flags & ES_TEXTBOX_MULTILINE; - size_t bytes = textbox->dataBytes + (includeNewline ? textbox->lines.Length() : 0); - char *buffer = (char *) EsHeapAllocate(bytes + 1, false); - buffer[bytes] = 0; - - uintptr_t position = 0; - uintptr_t lineFrom = 0, lineTo = textbox->lines.Length() - 1; - - if (flags & ES_TEXTBOX_GET_CONTENTS_SELECTED_ONLY) { - lineFrom = textbox->carets[0].line; - lineTo = textbox->carets[1].line; - - if (lineFrom > lineTo) { - uintptr_t swap = lineFrom; - lineFrom = lineTo, lineTo = swap; - } - } - - for (uintptr_t i = lineFrom; i <= lineTo; i++) { - DocumentLine *line = &textbox->lines[i]; - - uintptr_t offsetFrom = 0; - uintptr_t offsetTo = line->lengthBytes; - - if (flags & ES_TEXTBOX_GET_CONTENTS_SELECTED_ONLY) { - if (i == lineFrom) { - offsetFrom = TextboxCompareCarets(textbox->carets + 0, textbox->carets + 1) < 0 ? textbox->carets[0].byte : textbox->carets[1].byte; - } - - if (i == lineTo) { - offsetTo = TextboxCompareCarets(textbox->carets + 0, textbox->carets + 1) > 0 ? textbox->carets[0].byte : textbox->carets[1].byte; - } - } - - EsMemoryCopy(buffer + position, GET_BUFFER(line) + offsetFrom, offsetTo - offsetFrom); - position += offsetTo - offsetFrom; - - if (includeNewline && i != lineTo) { - buffer[position++] = '\n'; - } - } - - buffer[position] = 0; - EsAssert(position <= bytes); - if (_bytes) *_bytes = position; - - char *result = (char *) EsHeapReallocate(buffer, position + 1, false); - - if (!result) { - EsHeapFree(buffer); - } - - return result; -} - -double EsTextboxGetContentsAsDouble(EsTextbox *textbox, uint32_t flags) { - size_t bytes; - char *text = EsTextboxGetContents(textbox, &bytes, flags); - double result = EsDoubleParse(text, bytes, nullptr); - EsHeapFree(text); - return result; -} - -bool EsTextboxFind(EsTextbox *textbox, const char *needle, intptr_t _needleBytes, int32_t *_line, int32_t *_byte, uint32_t flags) { - EsMessageMutexCheck(); - - if (_needleBytes == 0) { - return false; - } - - uintptr_t needleBytes = _needleBytes == -1 ? EsCStringLength(needle) : _needleBytes; - uint32_t lineIndex = *_line, byteIndex = *_byte; - bool firstLoop = true; - - while (true) { - DocumentLine *line = &textbox->lines[lineIndex]; - const char *buffer = GET_BUFFER(line); - size_t bufferBytes = line->lengthBytes; - EsAssert(byteIndex <= bufferBytes); // Invalid find byte offset. - - // TODO Case-insensitive search. - // TODO Ignore quotation mark type. - - if (flags & ES_TEXTBOX_FIND_BACKWARDS) { - if (bufferBytes >= needleBytes) { - for (uintptr_t i = byteIndex; i >= needleBytes; i--) { - for (uintptr_t j = 0; j < needleBytes; j++) { - if (buffer[i - needleBytes + j] != needle[j]) { - goto previousPosition; - } - } - - *_line = lineIndex; - *_byte = i - needleBytes; - return true; - - previousPosition:; - } - } - - if ((int32_t) lineIndex <= *_line && !firstLoop) { - return false; - } - - if (lineIndex == 0) { - firstLoop = false; - lineIndex = textbox->lines.Length() - 1; - } else { - lineIndex--; - } - - byteIndex = textbox->lines[lineIndex].lengthBytes; - } else { - if (bufferBytes >= needleBytes) { - for (uintptr_t i = byteIndex; i <= bufferBytes - needleBytes; i++) { - for (uintptr_t j = 0; j < needleBytes; j++) { - if (buffer[i + j] != needle[j]) { - goto nextPosition; - } - } - - *_line = lineIndex; - *_byte = i; - return true; - - nextPosition:; - } - } - - lineIndex++; - - if ((int32_t) lineIndex > *_line && !firstLoop) { - return false; - } - - if (lineIndex == textbox->lines.Length()) { - firstLoop = false; - lineIndex = 0; - } - - byteIndex = 0; - } - } - - return false; -} - -bool TextboxFindCaret(EsTextbox *textbox, int positionX, int positionY, bool secondCaret, int clickChainCount) { - int startLine0 = textbox->carets[0].line, startLine1 = textbox->carets[1].line; - EsRectangle bounds = textbox->GetBounds(); - - if (positionX < 0) { - positionX = 0; - } else if (positionX >= bounds.r) { - positionX = bounds.r - 1; - } - - if (positionY < 0) { - positionY = 0; - } else if (positionY >= bounds.b) { - positionY = bounds.b - 1; - } - - if (clickChainCount >= 4) { - textbox->carets[0].line = 0; - textbox->carets[0].byte = 0; - textbox->carets[1].line = textbox->lines.Length() - 1; - textbox->carets[1].byte = textbox->lines[textbox->lines.Length() - 1].lengthBytes; - } else { - for (uintptr_t i = 0; i < textbox->visibleLines.Length(); i++) { - TextboxVisibleLine *visibleLine = &textbox->visibleLines[i]; - DocumentLine *line = &textbox->lines[textbox->firstVisibleLine + i]; - - EsRectangle lineBounds = ES_RECT_4(textbox->insets.l, bounds.r, - textbox->insets.t + visibleLine->yPosition, - textbox->insets.t + visibleLine->yPosition + line->height); - lineBounds.l -= textbox->scroll.position[0]; - lineBounds.t -= textbox->scroll.position[1]; - lineBounds.b -= textbox->scroll.position[1]; - - if (!((positionY >= lineBounds.t || i + textbox->firstVisibleLine == 0) && (positionY < lineBounds.b - || i + textbox->firstVisibleLine == textbox->lines.Length() - 1))) { - continue; - } - - if (!line->lengthBytes) { - textbox->carets[1].byte = 0; - } else { - DocumentLine *line = &textbox->lines[i + textbox->firstVisibleLine]; - int pointX = positionX + textbox->scroll.position[0] - textbox->insets.l; - if (pointX < 0) pointX = 0; - ptrdiff_t result = TextGetCharacterAtPoint(textbox, &textbox->textStyle, - GET_BUFFER(line), line->lengthBytes, - &pointX, ES_TEXT_GET_CHARACTER_AT_POINT_MIDDLE); - textbox->carets[1].byte = result == -1 ? line->lengthBytes : result; - } - - textbox->carets[1].line = i + textbox->firstVisibleLine; - - break; - } - - if (!secondCaret) { - textbox->carets[0] = textbox->carets[1]; - - if (clickChainCount == 2) { - TextboxMoveCaret(textbox, textbox->carets + 0, MOVE_CARET_BACKWARDS, MOVE_CARET_WORD, true); - TextboxMoveCaret(textbox, textbox->carets + 1, MOVE_CARET_FORWARDS, MOVE_CARET_WORD, true); - textbox->wordSelectionAnchor = textbox->carets[0]; - textbox->wordSelectionAnchor2 = textbox->carets[1]; - } else if (clickChainCount == 3) { - TextboxMoveCaret(textbox, textbox->carets + 0, MOVE_CARET_BACKWARDS, MOVE_CARET_LINE, true); - TextboxMoveCaret(textbox, textbox->carets + 1, MOVE_CARET_FORWARDS, MOVE_CARET_LINE, true); - textbox->wordSelectionAnchor = textbox->carets[0]; - textbox->wordSelectionAnchor2 = textbox->carets[1]; - } - } else { - if (clickChainCount == 2) { - if (TextboxCompareCarets(textbox->carets + 1, textbox->carets + 0) < 0) { - TextboxMoveCaret(textbox, textbox->carets + 1, MOVE_CARET_BACKWARDS, MOVE_CARET_WORD); - textbox->carets[0] = textbox->wordSelectionAnchor2; - } else { - TextboxMoveCaret(textbox, textbox->carets + 1, MOVE_CARET_FORWARDS, MOVE_CARET_WORD); - textbox->carets[0] = textbox->wordSelectionAnchor; - } - } else if (clickChainCount == 3) { - if (TextboxCompareCarets(textbox->carets + 1, textbox->carets + 0) < 0) { - TextboxMoveCaret(textbox, textbox->carets + 1, MOVE_CARET_BACKWARDS, MOVE_CARET_LINE); - textbox->carets[0] = textbox->wordSelectionAnchor2; - } else { - TextboxMoveCaret(textbox, textbox->carets + 1, MOVE_CARET_FORWARDS, MOVE_CARET_LINE); - textbox->carets[0] = textbox->wordSelectionAnchor; - } - } - } - } - - TextboxUpdateCommands(textbox, true); - return textbox->carets[0].line != startLine0 || textbox->carets[1].line != startLine1; -} - -void TextboxMoveCaretToCursor(EsTextbox *textbox, int x, int y, bool doNotMoveIfNoSelection) { - int oldCompare = TextboxCompareCarets(textbox->carets + 0, textbox->carets + 1); - bool hasSelection = oldCompare != 0; - TextboxCaret old[2] = { textbox->carets[0], textbox->carets[1] }; - bool lineChanged = TextboxFindCaret(textbox, x, y, gui.clickChainCount == 1, gui.clickChainCount); - - if (doNotMoveIfNoSelection && TextboxCompareCarets(&old[0], &old[1]) != 0) { - textbox->carets[0] = old[0]; - textbox->carets[1] = old[1]; - } else if (gui.clickChainCount == 1 && !EsKeyboardIsShiftHeld()) { - textbox->carets[0] = textbox->carets[1]; - } - - TextboxUpdateCommands(textbox, true); - textbox->verticalMotionHorizontalDepth = -1; - TextboxRepaintLine(textbox, lineChanged || hasSelection ? -1 : textbox->carets[0].line); - EsTextboxEnsureCaretVisible(textbox); -} - -int ProcessTextboxMarginMessage(EsElement *element, EsMessage *message) { - EsTextbox *textbox = (EsTextbox *) element->parent; - - if (message->type == ES_MSG_PAINT) { - EsPainter *painter = message->painter; - - for (int32_t i = 0; i < (int32_t) textbox->visibleLines.Length(); i++) { - TextboxVisibleLine *visibleLine = &textbox->visibleLines[i]; - DocumentLine *line = &textbox->lines[i + textbox->firstVisibleLine]; - - EsRectangle bounds; - bounds.l = painter->offsetX + element->style->insets.l; - bounds.r = painter->offsetX + painter->width - element->style->insets.r; - bounds.t = painter->offsetY + textbox->insets.t + visibleLine->yPosition - textbox->scroll.position[1]; - bounds.b = bounds.t + line->height; - - char label[64]; - EsTextRun textRun[2] = {}; - element->style->GetTextStyle(&textRun[0].style); - textRun[0].style.figures = ES_TEXT_FIGURE_TABULAR; - textRun[1].offset = EsStringFormat(label, sizeof(label), "%d", i + textbox->firstVisibleLine + 1); - EsTextPlanProperties properties = {}; - properties.flags = ES_TEXT_V_CENTER | ES_TEXT_H_RIGHT | ES_TEXT_ELLIPSIS | ES_TEXT_PLAN_SINGLE_USE; - EsTextPlan *plan = EsTextPlanCreate(element, &properties, bounds, label, textRun, 1); - if (plan) EsDrawText(painter, plan, bounds, nullptr, nullptr); - } - } else if (message->type == ES_MSG_MOUSE_LEFT_DOWN) { - return ES_HANDLED; - } - - return 0; -} - -void TextboxStyleChanged(EsTextbox *textbox) { - textbox->borders = textbox->style->borders; - textbox->insets = textbox->style->insets; - - if (textbox->flags & ES_TEXTBOX_MARGIN) { - int marginWidth = textbox->margin->style->preferredWidth; - textbox->borders.l += marginWidth; - textbox->insets.l += marginWidth + textbox->margin->style->gapMajor; - } - - int lineHeight = TextGetLineHeight(textbox, &textbox->textStyle); - - for (int32_t i = 0; i < (int32_t) textbox->lines.Length(); i++) { - DocumentLine *line = &textbox->lines[i]; - DocumentLine *previous = i ? (&textbox->lines[i - 1]) : nullptr; - line->height = lineHeight; - line->yPosition = previous ? (previous->yPosition + previous->height) : 0; - line->lengthWidth = -1; - textbox->longestLine = -1; - } - - TextboxRefreshVisibleLines(textbox); - TextboxFindLongestLine(textbox); - textbox->scroll.Refresh(); - EsElementRepaint(textbox); -} - -int ProcessTextboxMessage(EsElement *element, EsMessage *message) { - EsTextbox *textbox = (EsTextbox *) element; - - if (!textbox->editing && textbox->overlayCallback) { - int response = textbox->overlayCallback(element, message); - if (response != 0 && message->type != ES_MSG_DESTROY) return response; - } - - int response = textbox->scroll.ReceivedMessage(message); - if (response) return response; - response = ES_HANDLED; - - if (message->type == ES_MSG_PAINT) { - EsPainter *painter = message->painter; - - EsTextSelection selectionProperties = {}; - selectionProperties.hideCaret = (~textbox->state & UI_STATE_FOCUSED) || (textbox->flags & ES_ELEMENT_DISABLED) || !textbox->editing; - selectionProperties.snapCaretToInsets = true; - selectionProperties.background = textbox->style->metrics->selectedBackground; - selectionProperties.foreground = textbox->style->metrics->selectedText; - - EsRectangle clip; - EsRectangleClip(painter->clip, ES_RECT_4(painter->offsetX + textbox->borders.l, - painter->offsetX + painter->width - textbox->borders.r, - painter->offsetY + textbox->borders.t, - painter->offsetY + painter->height - textbox->borders.b), &clip); - - Array textRuns = {}; - - for (int32_t i = 0; i < (int32_t) textbox->visibleLines.Length(); i++) { - TextboxVisibleLine *visibleLine = &textbox->visibleLines[i]; - DocumentLine *line = &textbox->lines[i + textbox->firstVisibleLine]; - - EsRectangle lineBounds = ES_RECT_4(painter->offsetX + textbox->insets.l, - painter->offsetX + painter->width, - painter->offsetY + textbox->insets.t + visibleLine->yPosition, - painter->offsetY + textbox->insets.t + visibleLine->yPosition + line->height); - lineBounds.l -= textbox->scroll.position[0]; - lineBounds.t -= textbox->scroll.position[1]; - lineBounds.b -= textbox->scroll.position[1]; - - if (~textbox->flags & ES_TEXTBOX_MULTILINE) { - lineBounds.b = painter->offsetY + painter->height - textbox->insets.b; - } - - int32_t caret0 = textbox->carets[0].byte, caret1 = textbox->carets[1].byte; - if (textbox->carets[0].line < i + textbox->firstVisibleLine) caret0 = -2; - if (textbox->carets[0].line > i + textbox->firstVisibleLine) caret0 = line->lengthBytes + 2; - if (textbox->carets[1].line < i + textbox->firstVisibleLine) caret1 = -2; - if (textbox->carets[1].line > i + textbox->firstVisibleLine) caret1 = line->lengthBytes + 2; - - if (textbox->carets[1].line == i + textbox->firstVisibleLine && textbox->syntaxHighlightingLanguage) { - EsRectangle line = ES_RECT_4(painter->offsetX, painter->offsetX + painter->width, lineBounds.t, lineBounds.b); - EsDrawBlock(painter, line, textbox->syntaxHighlightingColors[0]); - } - - if (textbox->syntaxHighlightingLanguage && line->lengthBytes) { - if (textRuns.Length()) textRuns.SetLength(0); - textRuns = TextApplySyntaxHighlighting(&textbox->textStyle, textbox->syntaxHighlightingLanguage, - textbox->syntaxHighlightingColors, textRuns, GET_BUFFER(line), line->lengthBytes); - } else { - textRuns.SetLength(2); - textRuns[0].style = textbox->textStyle; - textRuns[0].offset = 0; - textRuns[1].offset = line->lengthBytes; - } - - EsTextPlanProperties properties = {}; - properties.flags = ES_TEXT_V_CENTER | ES_TEXT_H_LEFT | ES_TEXT_PLAN_SINGLE_USE; - selectionProperties.caret0 = caret0; - selectionProperties.caret1 = caret1; - EsTextPlan *plan; - - if (!textRuns.Length()) { - plan = nullptr; - } else if (textRuns[1].offset) { - plan = EsTextPlanCreate(element, &properties, lineBounds, GET_BUFFER(line), textRuns.array, textRuns.Length() - 1); - } else { - textRuns[1].offset = 1; // Make sure that the caret and selection is draw correctly, even on empty lines. - plan = EsTextPlanCreate(element, &properties, lineBounds, " ", textRuns.array, textRuns.Length() - 1); - } - - if (plan) { - EsDrawText(painter, plan, lineBounds, &clip, &selectionProperties); - } - } - - textRuns.Free(); - } else if (message->type == ES_MSG_LAYOUT) { - EsRectangle bounds = textbox->GetBounds(); - - if (textbox->margin) { - int marginWidth = textbox->margin->style->preferredWidth; - textbox->margin->InternalMove(marginWidth, Height(bounds), bounds.l, bounds.t); - } - - TextboxRefreshVisibleLines(textbox); - } else if (message->type == ES_MSG_DESTROY) { - textbox->visibleLines.Free(); - textbox->lines.Free(); - UndoManagerDestroy(&textbox->localUndo); - EsHeapFree(textbox->activeLine); - EsHeapFree(textbox->data); - EsHeapFree(textbox->editStartContent); - } else if (message->type == ES_MSG_KEY_TYPED && !ScancodeIsNonTypeable(message->keyboard.scancode)) { - bool verticalMotion = false; - bool ctrl = message->keyboard.modifiers & ES_MODIFIER_CTRL; - - if (message->keyboard.modifiers & ~(ES_MODIFIER_CTRL | ES_MODIFIER_ALT | ES_MODIFIER_SHIFT | ES_MODIFIER_ALT_GR)) { - // Unused modifier. - return 0; - } - - if (message->keyboard.scancode == ES_SCANCODE_LEFT_ARROW || message->keyboard.scancode == ES_SCANCODE_RIGHT_ARROW - || message->keyboard.scancode == ES_SCANCODE_HOME || message->keyboard.scancode == ES_SCANCODE_END - || message->keyboard.scancode == ES_SCANCODE_UP_ARROW || message->keyboard.scancode == ES_SCANCODE_DOWN_ARROW) { - bool direction = (message->keyboard.scancode == ES_SCANCODE_LEFT_ARROW || message->keyboard.scancode == ES_SCANCODE_HOME - || message->keyboard.scancode == ES_SCANCODE_UP_ARROW) - ? MOVE_CARET_BACKWARDS : MOVE_CARET_FORWARDS; - int moveType = (message->keyboard.scancode == ES_SCANCODE_HOME || message->keyboard.scancode == ES_SCANCODE_END) - ? (ctrl ? MOVE_CARET_ALL : MOVE_CARET_LINE) - : ((message->keyboard.scancode == ES_SCANCODE_UP_ARROW || message->keyboard.scancode == ES_SCANCODE_DOWN_ARROW) - ? MOVE_CARET_VERTICAL : (ctrl ? MOVE_CARET_WORD : MOVE_CARET_SINGLE)); - if (moveType == MOVE_CARET_VERTICAL) verticalMotion = true; - - int32_t lineFrom = textbox->carets[1].line; - - if (message->keyboard.modifiers & ES_MODIFIER_SHIFT) { - TextboxMoveCaret(textbox, &textbox->carets[1], direction, moveType); - } else { - int caretCompare = TextboxCompareCarets(textbox->carets + 1, textbox->carets + 0); - - if ((caretCompare < 0 && direction == MOVE_CARET_BACKWARDS) || (caretCompare > 0 && direction == MOVE_CARET_FORWARDS)) { - textbox->carets[0] = textbox->carets[1]; - TextboxUpdateCommands(textbox, true); - } else if ((caretCompare > 0 && direction == MOVE_CARET_BACKWARDS) || (caretCompare < 0 && direction == MOVE_CARET_FORWARDS)) { - textbox->carets[1] = textbox->carets[0]; - TextboxUpdateCommands(textbox, true); - } else { - TextboxMoveCaret(textbox, &textbox->carets[1], direction, moveType); - textbox->carets[0] = textbox->carets[1]; - TextboxUpdateCommands(textbox, true); - } - } - - int32_t lineTo = textbox->carets[1].line; - if (lineFrom > lineTo) { int32_t t = lineTo; lineTo = lineFrom; lineFrom = t; } - for (int32_t i = lineFrom; i <= lineTo; i++) TextboxRepaintLine(textbox, i); - } else if (message->keyboard.scancode == ES_SCANCODE_PAGE_UP || message->keyboard.scancode == ES_SCANCODE_PAGE_DOWN) { - for (uintptr_t i = 0; i < 10; i++) { - TextboxMoveCaret(textbox, textbox->carets + 1, - message->keyboard.scancode == ES_SCANCODE_PAGE_UP ? MOVE_CARET_BACKWARDS : MOVE_CARET_FORWARDS, - MOVE_CARET_VERTICAL); - } - - if (~message->keyboard.modifiers & ES_MODIFIER_SHIFT) { - textbox->carets[0] = textbox->carets[1]; - TextboxUpdateCommands(textbox, true); - } - - textbox->Repaint(true); - verticalMotion = true; - } else if (message->keyboard.scancode == ES_SCANCODE_BACKSPACE || message->keyboard.scancode == ES_SCANCODE_DELETE) { - if (!textbox->editing) { - EsTextboxStartEdit(textbox); - } - - if (!TextboxCompareCarets(textbox->carets + 0, textbox->carets + 1)) { - TextboxMoveCaret(textbox, textbox->carets + 1, message->keyboard.scancode == ES_SCANCODE_BACKSPACE ? MOVE_CARET_BACKWARDS : MOVE_CARET_FORWARDS, - ctrl ? MOVE_CARET_WORD : MOVE_CARET_SINGLE); - } - - EsTextboxInsert(textbox, EsLiteral("")); - } else if (message->keyboard.scancode == ES_SCANCODE_ENTER && (textbox->flags & ES_TEXTBOX_EDIT_BASED)) { - if (textbox->editing) { - TextboxEndEdit(textbox, false); - } else { - EsTextboxStartEdit(textbox); - } - } else if (message->keyboard.scancode == ES_SCANCODE_ESCAPE && (textbox->flags & ES_TEXTBOX_EDIT_BASED)) { - TextboxEndEdit(textbox, true); - } else if (message->keyboard.scancode == ES_SCANCODE_TAB && (~textbox->flags & ES_TEXTBOX_ALLOW_TABS)) { - response = 0; - } else { - if (!textbox->editing) { - EsTextboxStartEdit(textbox); - } - - const char *inputString = KeyboardLayoutLookup(message->keyboard.scancode, - message->keyboard.modifiers & ES_MODIFIER_SHIFT, message->keyboard.modifiers & ES_MODIFIER_ALT_GR, - true, textbox->flags & ES_TEXTBOX_MULTILINE); - - if (inputString && (message->keyboard.modifiers & ~(ES_MODIFIER_SHIFT | ES_MODIFIER_ALT_GR)) == 0) { - if (textbox->smartQuotes && api.global->useSmartQuotes) { - DocumentLine *currentLine = &textbox->lines[textbox->carets[0].line]; - const char *buffer = GET_BUFFER(currentLine); - bool left = !textbox->carets[0].byte || buffer[textbox->carets[0].byte - 1] == ' '; - - if (inputString[0] == '"' && inputString[1] == 0) { - inputString = left ? "\u201C" : "\u201D"; - } else if (inputString[0] == '\'' && inputString[1] == 0) { - inputString = left ? "\u2018" : "\u2019"; - } - } - - EsTextboxInsert(textbox, inputString, -1); - - if (inputString[0] == '\n' && inputString[1] == 0 && textbox->carets[0].line) { - // Copy the indentation from the previous line. - - DocumentLine *previousLine = &textbox->lines[textbox->carets[0].line - 1]; - const char *buffer = GET_BUFFER(previousLine); - int32_t i = 0; - - for (; i < previousLine->lengthBytes; i++) { - if (buffer[i] != '\t') { - break; - } - } - - EsTextboxInsert(textbox, buffer, i); - } - } else { - response = 0; - } - } - - if (!verticalMotion) { - textbox->verticalMotionHorizontalDepth = -1; - } - - if (response != 0 && (~textbox->state & UI_STATE_DESTROYING)) { - TextboxFindLongestLine(textbox); - textbox->scroll.Refresh(); - EsTextboxEnsureCaretVisible(textbox); - } - } else if (message->type == ES_MSG_MOUSE_LEFT_DOWN || message->type == ES_MSG_MOUSE_RIGHT_DOWN) { - TextboxMoveCaretToCursor(textbox, message->mouseDown.positionX, message->mouseDown.positionY, message->type == ES_MSG_MOUSE_RIGHT_DOWN); - } else if (message->type == ES_MSG_MOUSE_LEFT_CLICK) { - EsTextboxStartEdit(textbox); - } else if (message->type == ES_MSG_FOCUSED_START || message->type == ES_MSG_PRIMARY_CLIPBOARD_UPDATED) { - TextboxUpdateCommands(textbox, false); - EsInstanceSetActiveUndoManager(textbox->instance, textbox->undo); - textbox->Repaint(true); - } else if (message->type == ES_MSG_FOCUSED_END) { - EsCommandSetCallback(EsCommandByID(textbox->instance, ES_COMMAND_SELECT_ALL), nullptr); - EsCommandSetCallback(EsCommandByID(textbox->instance, ES_COMMAND_DELETE), nullptr); - EsCommandSetCallback(EsCommandByID(textbox->instance, ES_COMMAND_COPY), nullptr); - EsCommandSetCallback(EsCommandByID(textbox->instance, ES_COMMAND_CUT), nullptr); - EsCommandSetCallback(EsCommandByID(textbox->instance, ES_COMMAND_PASTE), nullptr); - EsInstanceSetActiveUndoManager(textbox->instance, textbox->instance->undoManager); - textbox->Repaint(true); - } else if (message->type == ES_MSG_STRONG_FOCUS_END) { - TextboxEndEdit(textbox, textbox->flags & ES_TEXTBOX_REJECT_EDIT_IF_LOST_FOCUS); - } else if (message->type == ES_MSG_MOUSE_LEFT_DRAG || message->type == ES_MSG_MOUSE_RIGHT_DRAG || (message->type == ES_MSG_ANIMATE && textbox->scroll.dragScrolling)) { - int32_t lineFrom = textbox->carets[1].line; - - if (gui.lastClickButton == ES_MSG_MOUSE_RIGHT_DOWN && !textbox->inRightClickDrag) { - TextboxMoveCaretToCursor(textbox, message->mouseDragged.originalPositionX, message->mouseDragged.originalPositionY, false); - textbox->inRightClickDrag = true; - } - - EsPoint position = EsMouseGetPosition(textbox); - TextboxFindCaret(textbox, position.x, position.y, true, gui.clickChainCount); - - int32_t lineTo = textbox->carets[1].line; - if (lineFrom > lineTo) { int32_t t = lineTo; lineTo = lineFrom; lineFrom = t; } - for (int32_t i = lineFrom; i <= lineTo; i++) TextboxRepaintLine(textbox, i); - } else if (message->type == ES_MSG_GET_CURSOR) { - if (!textbox->editing || (textbox->flags & ES_ELEMENT_DISABLED)) { - message->cursorStyle = ES_CURSOR_NORMAL; - } else { - return 0; - } - } else if (message->type == ES_MSG_MOUSE_RIGHT_UP) { - textbox->inRightClickDrag = false; - EsMenu *menu = EsMenuCreate(textbox, ES_MENU_AT_CURSOR); - if (!menu) return ES_HANDLED; - - // TODO User customisation of menus. - - if (textbox->editing) { - EsMenuAddCommand(menu, 0, INTERFACE_STRING(CommonUndo), EsCommandByID(textbox->instance, ES_COMMAND_UNDO)); - EsMenuAddSeparator(menu); - } - - EsMenuAddCommand(menu, 0, INTERFACE_STRING(CommonClipboardCut), EsCommandByID(textbox->instance, ES_COMMAND_CUT)); - EsMenuAddCommand(menu, 0, INTERFACE_STRING(CommonClipboardCopy), EsCommandByID(textbox->instance, ES_COMMAND_COPY)); - EsMenuAddCommand(menu, 0, INTERFACE_STRING(CommonClipboardPaste), EsCommandByID(textbox->instance, ES_COMMAND_PASTE)); - - if (textbox->editing) { - EsMenuAddSeparator(menu); - EsMenuAddCommand(menu, 0, INTERFACE_STRING(CommonSelectionSelectAll), EsCommandByID(textbox->instance, ES_COMMAND_SELECT_ALL)); - EsMenuAddCommand(menu, 0, INTERFACE_STRING(CommonSelectionDelete), EsCommandByID(textbox->instance, ES_COMMAND_DELETE)); - - // Add the smart context menu, if necessary. - - if ((~textbox->flags & ES_TEXTBOX_NO_SMART_CONTEXT_MENUS) && textbox->carets[0].line == textbox->carets[1].line) { - int32_t selectionFrom = textbox->carets[0].byte, selectionTo = textbox->carets[1].byte; - - if (selectionTo < selectionFrom) { - int32_t temporary = selectionFrom; - selectionFrom = selectionTo; - selectionTo = temporary; - } - - if (selectionTo - selectionFrom == 7) { - char buffer[7]; - EsMemoryCopy(buffer, GET_BUFFER(&textbox->lines[textbox->carets[0].line]) + selectionFrom, 7); - - if (buffer[0] == '#' && EsCRTisxdigit(buffer[1]) && EsCRTisxdigit(buffer[2]) && EsCRTisxdigit(buffer[3]) - && EsCRTisxdigit(buffer[4]) && EsCRTisxdigit(buffer[5]) && EsCRTisxdigit(buffer[6])) { - // It's a color hex-code! - // TODO Versions with alpha. - EsMenuNextColumn(menu); - ColorPickerCreate(menu, { textbox }, EsColorParse(buffer, 7), false); - - textbox->colorUppercase = true; - - for (uintptr_t i = 1; i <= 6; i++) { - if (buffer[i] >= 'a' && buffer[i] <= 'f') { - textbox->colorUppercase = false; - break; - } - } - } - } - } - } - - EsMenuShow(menu); - } else if (message->type == ES_MSG_COLOR_CHANGED) { - EsAssert(~textbox->flags & ES_TEXTBOX_NO_SMART_CONTEXT_MENUS); // Textbox sent color changed message, but it cannot have smart context menus? - uint32_t color = message->colorChanged.newColor; - - if (message->colorChanged.pickerClosed) { - int32_t selectionFrom = textbox->carets[0].byte, selectionTo = textbox->carets[1].byte; - - if (textbox->carets[0].line == textbox->carets[1].line && AbsoluteInteger(selectionFrom - selectionTo) == 7) { - char buffer[7]; - const char *hexChars = textbox->colorUppercase ? "0123456789ABCDEF" : "0123456789abcedf"; - size_t length = EsStringFormat(buffer, 7, "#%c%c%c%c%c%c", - hexChars[(color >> 20) & 0xF], hexChars[(color >> 16) & 0xF], hexChars[(color >> 12) & 0xF], - hexChars[(color >> 8) & 0xF], hexChars[(color >> 4) & 0xF], hexChars[(color >> 0) & 0xF]); - EsTextboxInsert(textbox, buffer, length, true); - EsTextboxSetSelection(textbox, textbox->carets[1].line, textbox->carets[1].byte - 7, - textbox->carets[1].line, textbox->carets[1].byte); - } - } - } else if (message->type == ES_MSG_GET_WIDTH) { - message->measure.width = textbox->longestLineWidth + textbox->insets.l + textbox->insets.r; - } else if (message->type == ES_MSG_GET_HEIGHT) { - DocumentLine *lastLine = &textbox->lines.Last(); - message->measure.height = lastLine->yPosition + lastLine->height + textbox->insets.t + textbox->insets.b; - } else if (message->type == ES_MSG_SCROLL_X) { - TextboxSetHorizontalScroll(textbox, message->scroll.scroll); - } else if (message->type == ES_MSG_SCROLL_Y) { - TextboxRefreshVisibleLines(textbox, false); - EsElementRepaintForScroll(textbox, message, EsRectangleAdd(element->GetInternalOffset(), element->style->borders)); - } else if (message->type == ES_MSG_GET_INSPECTOR_INFORMATION) { - DocumentLine *firstLine = &textbox->lines.First(); - EsBufferFormat(message->getContent.buffer, "'%s'", firstLine->lengthBytes, GET_BUFFER(firstLine)); - } else if (message->type == ES_MSG_UI_SCALE_CHANGED) { - if (textbox->margin) { - // Force the margin to update its style now, so that its width can be read correctly by TextboxStyleChanged. - textbox->margin->RefreshStyle(nullptr, false, true); - } - - textbox->style->GetTextStyle(&textbox->textStyle); - - if (textbox->overrideTextSize) { - textbox->textStyle.size = textbox->overrideTextSize; - } - - if (textbox->overrideFont.family) { - textbox->textStyle.font = textbox->overrideFont; - } - - TextboxStyleChanged(textbox); - } else { - response = 0; - } - - return response; -} - -EsTextbox *EsTextboxCreate(EsElement *parent, uint64_t flags, const EsStyle *style) { - EsTextbox *textbox = (EsTextbox *) EsHeapAllocate(sizeof(EsTextbox), true); - if (!textbox) return nullptr; - - if (!style) { - if (flags & ES_TEXTBOX_MULTILINE) { - style = ES_STYLE_TEXTBOX_BORDERED_MULTILINE; - } else { - style = ES_STYLE_TEXTBOX_BORDERED_SINGLE; - } - } - - textbox->Initialise(parent, ES_ELEMENT_FOCUSABLE | flags, ProcessTextboxMessage, style); - textbox->cName = "textbox"; - - textbox->scroll.Setup(textbox, - (flags & ES_TEXTBOX_MULTILINE) ? ES_SCROLL_MODE_AUTO : ES_SCROLL_MODE_HIDDEN, - (flags & ES_TEXTBOX_MULTILINE) ? ES_SCROLL_MODE_AUTO : ES_SCROLL_MODE_NONE, - ES_SCROLL_X_DRAG | ES_SCROLL_Y_DRAG); - - textbox->undo = &textbox->localUndo; - textbox->undo->instance = textbox->instance; - - textbox->borders = textbox->style->borders; - textbox->insets = textbox->style->insets; - - textbox->style->GetTextStyle(&textbox->textStyle); - - textbox->smartQuotes = true; - - DocumentLine firstLine = {}; - firstLine.height = TextGetLineHeight(textbox, &textbox->textStyle); - textbox->lines.Add(firstLine); - - TextboxVisibleLine firstVisibleLine = {}; - textbox->visibleLines.Add(firstVisibleLine); - - textbox->activeLineIndex = textbox->verticalMotionHorizontalDepth = textbox->longestLine = -1; - - if (~flags & ES_TEXTBOX_EDIT_BASED) { - textbox->editing = true; - } - - if (textbox->flags & ES_TEXTBOX_MARGIN) { - textbox->margin = EsCustomElementCreate(textbox, ES_CELL_FILL, ES_STYLE_TEXTBOX_MARGIN); - textbox->margin->cName = "margin"; - textbox->margin->messageUser = ProcessTextboxMarginMessage; - - int marginWidth = textbox->margin->style->preferredWidth; - textbox->borders.l += marginWidth; - textbox->insets.l += marginWidth + textbox->margin->style->gapMajor; - } - - return textbox; -} - -void EsTextboxUseNumberOverlay(EsTextbox *textbox, bool defaultBehaviour) { - EsMessageMutexCheck(); - - EsAssert(textbox->flags & ES_TEXTBOX_EDIT_BASED); // Using textbox overlay without edit based mode. - EsAssert(~textbox->flags & ES_TEXTBOX_MULTILINE); // Using number overlay with multiline mode. - - textbox->overlayData = defaultBehaviour; - - textbox->overlayCallback = [] (EsElement *element, EsMessage *message) { - EsTextbox *textbox = (EsTextbox *) element; - bool defaultBehaviour = textbox->overlayData.u; - - if (message->type == ES_MSG_MOUSE_LEFT_DRAG) { - if (!gui.draggingStarted) { - EsMessage m = { ES_MSG_TEXTBOX_NUMBER_DRAG_START }; - EsMessageSend(textbox, &m); - } - - TextboxFindCaret(textbox, message->mouseDragged.originalPositionX, message->mouseDragged.originalPositionY, false, 1); - - EsMessage m = { ES_MSG_TEXTBOX_NUMBER_DRAG_DELTA }; - m.numberDragDelta.delta = message->mouseDragged.originalPositionY - message->mouseDragged.newPositionY; - m.numberDragDelta.fast = EsKeyboardIsShiftHeld(); - m.numberDragDelta.hoverCharacter = textbox->carets[1].byte; - EsMessageSend(textbox, &m); - - EsMouseSetPosition(textbox->window, gui.lastClickX, gui.lastClickY); - } else if (message->type == ES_MSG_KEY_TYPED && message->keyboard.scancode == ES_SCANCODE_UP_ARROW) { - EsMessage m = { ES_MSG_TEXTBOX_NUMBER_DRAG_DELTA }; - m.numberDragDelta.delta = 1; - m.numberDragDelta.fast = EsKeyboardIsShiftHeld(); - m.numberDragDelta.hoverCharacter = 0; - EsMessageSend(textbox, &m); - } else if (message->type == ES_MSG_KEY_TYPED && message->keyboard.scancode == ES_SCANCODE_DOWN_ARROW) { - EsMessage m = { ES_MSG_TEXTBOX_NUMBER_DRAG_DELTA }; - m.numberDragDelta.delta = -1; - m.numberDragDelta.fast = EsKeyboardIsShiftHeld(); - m.numberDragDelta.hoverCharacter = 0; - EsMessageSend(textbox, &m); - } else if (message->type == ES_MSG_MOUSE_LEFT_UP) { - if (gui.draggingStarted) { - EsMessage m = { ES_MSG_TEXTBOX_NUMBER_DRAG_END }; - EsMessageSend(textbox, &m); - } - } else if (message->type == ES_MSG_GET_CURSOR) { - if (gui.draggingStarted) { - message->cursorStyle = ES_CURSOR_BLANK; - } else if (~textbox->flags & ES_ELEMENT_DISABLED) { - message->cursorStyle = ES_CURSOR_RESIZE_VERTICAL; - } else { - message->cursorStyle = ES_CURSOR_NORMAL; - } - } else if (message->type == ES_MSG_TEXTBOX_EDIT_END && defaultBehaviour) { - double oldValue = EsDoubleParse(textbox->editStartContent, textbox->editStartContentBytes, nullptr); - - char *expression = EsTextboxGetContents(textbox); - EsCalculationValue value = EsCalculateFromUserExpression(expression); - EsHeapFree(expression); - - if (value.error) { - return ES_REJECTED; - } else { - EsMessage m = { ES_MSG_TEXTBOX_NUMBER_UPDATED }; - m.numberUpdated.delta = value.number - oldValue; - m.numberUpdated.newValue = value.number; - EsMessageSend(textbox, &m); - - char result[64]; - size_t resultBytes = EsStringFormat(result, sizeof(result), "%F", (double) m.numberUpdated.newValue); - EsTextboxSelectAll(textbox); - EsTextboxInsert(textbox, result, resultBytes); - } - } else if (message->type == ES_MSG_TEXTBOX_NUMBER_DRAG_DELTA && defaultBehaviour) { - TextboxSetActiveLine(textbox, -1); - double oldValue = EsDoubleParse(textbox->data, textbox->lines[0].lengthBytes, nullptr); - double newValue = oldValue + message->numberDragDelta.delta * (message->numberDragDelta.fast ? 10 : 1); - - EsMessage m = { ES_MSG_TEXTBOX_NUMBER_UPDATED }; - m.numberUpdated.delta = newValue - oldValue; - m.numberUpdated.newValue = newValue; - EsMessageSend(textbox, &m); - - char result[64]; - size_t resultBytes = EsStringFormat(result, sizeof(result), "%F", m.numberUpdated.newValue); - EsTextboxSelectAll(textbox); - EsTextboxInsert(textbox, result, resultBytes); - } else { - return 0; - } - - return ES_HANDLED; - }; -} - -void TextboxBreadcrumbOverlayRecreate(EsTextbox *textbox) { - if (textbox->overlayData.p) { - // Remove the old breadcrumb panel. - ((EsElement *) textbox->overlayData.p)->Destroy(); - } - - EsPanel *panel = EsPanelCreate(textbox, ES_PANEL_HORIZONTAL | ES_CELL_FILL | ES_ELEMENT_NO_HOVER, ES_STYLE_BREADCRUMB_BAR_PANEL); - textbox->overlayData = panel; - - if (!panel) { - return; - } - - uint8_t _buffer[256]; - EsBuffer buffer = { .out = _buffer, .bytes = sizeof(_buffer) }; - EsMessage m = { ES_MSG_TEXTBOX_GET_BREADCRUMB }; - m.getBreadcrumb.buffer = &buffer; - - while (true) { - buffer.position = 0, m.getBreadcrumb.icon = 0; - int response = EsMessageSend(textbox, &m); - EsAssert(response != 0); // Must handle ES_MSG_TEXTBOX_GET_BREADCRUMB message for breadcrumb overlay. - if (response == ES_REJECTED) break; - - EsButton *crumb = EsButtonCreate(panel, ES_BUTTON_NOT_FOCUSABLE | ES_BUTTON_COMPACT | ES_CELL_V_FILL, - ES_STYLE_BREADCRUMB_BAR_CRUMB, (char *) buffer.out, buffer.position); - - if (crumb) { - EsButtonSetIcon(crumb, m.getBreadcrumb.icon); - - crumb->userData = m.getBreadcrumb.index; - - crumb->messageUser = [] (EsElement *element, EsMessage *message) { - if (message->type == ES_MSG_MOUSE_LEFT_CLICK) { - EsMessage m = { ES_MSG_TEXTBOX_ACTIVATE_BREADCRUMB }; - m.activateBreadcrumb = element->userData.u; - EsMessageSend(element->parent->parent, &m); - } else { - return 0; - } - - return ES_HANDLED; - }; - } - - m.getBreadcrumb.index++; - } -} - -void EsTextboxUseBreadcrumbOverlay(EsTextbox *textbox) { - EsMessageMutexCheck(); - - EsAssert(textbox->flags & ES_TEXTBOX_EDIT_BASED); // Using textbox overlay without edit based mode. - - // Use this to store the panel containing the breadcrumb buttons. - textbox->overlayData = nullptr; - - textbox->overlayCallback = [] (EsElement *element, EsMessage *message) { - EsTextbox *textbox = (EsTextbox *) element; - - if (message->type == ES_MSG_TEXTBOX_UPDATED) { - TextboxBreadcrumbOverlayRecreate(textbox); - } else if (message->type == ES_MSG_TEXTBOX_EDIT_START) { - ((EsElement *) textbox->overlayData.p)->Destroy(); - textbox->overlayData.p = nullptr; - } else if (message->type == ES_MSG_TEXTBOX_EDIT_END) { - TextboxBreadcrumbOverlayRecreate(textbox); - } else if (message->type == ES_MSG_LAYOUT) { - EsRectangle bounds = textbox->GetBounds(); - ((EsElement *) textbox->overlayData.p)->InternalMove(bounds.r, bounds.b, 0, 0); - } else if (message->type == ES_MSG_PAINT) { - return ES_HANDLED; - } - - return 0; - }; - - TextboxBreadcrumbOverlayRecreate(textbox); -} - -void EsTextboxSetUndoManager(EsTextbox *textbox, EsUndoManager *undoManager) { - EsMessageMutexCheck(); - EsAssert(~textbox->state & UI_STATE_FOCUSED); // Can't change undo manager if the textbox is focused. - EsAssert(textbox->undo == &textbox->localUndo); // This can only be set once. - textbox->undo = undoManager; -} - -void EsTextboxSetTextSize(EsTextbox *textbox, uint16_t size) { - textbox->overrideTextSize = size; - textbox->textStyle.size = size; - TextboxStyleChanged(textbox); -} - -void EsTextboxSetFont(EsTextbox *textbox, EsFont font) { - textbox->overrideFont = font; - textbox->textStyle.font = font; - TextboxStyleChanged(textbox); -} - -void EsTextboxSetupSyntaxHighlighting(EsTextbox *textbox, uint32_t language, uint32_t *customColors, size_t customColorCount) { - textbox->syntaxHighlightingLanguage = language; - - // TODO Load these from the theme file. - textbox->syntaxHighlightingColors[0] = 0x04000000; // Highlighted line. - textbox->syntaxHighlightingColors[1] = 0xFF000000; // Default. - textbox->syntaxHighlightingColors[2] = 0xFFA11F20; // Comment. - textbox->syntaxHighlightingColors[3] = 0xFF037E01; // String. - textbox->syntaxHighlightingColors[4] = 0xFF213EF1; // Number. - textbox->syntaxHighlightingColors[5] = 0xFF7F0480; // Operator. - textbox->syntaxHighlightingColors[6] = 0xFF545D70; // Preprocessor. - textbox->syntaxHighlightingColors[7] = 0xFF17546D; // Keyword. - - if (customColorCount > sizeof(textbox->syntaxHighlightingColors) / sizeof(uint32_t)) { - customColorCount = sizeof(textbox->syntaxHighlightingColors) / sizeof(uint32_t); - } - - EsMemoryCopy(textbox->syntaxHighlightingColors, customColors, customColorCount * sizeof(uint32_t)); - - textbox->Repaint(true); -} - -void EsTextboxEnableSmartQuotes(EsTextbox *textbox, bool enabled) { - textbox->smartQuotes = enabled; -} - -#undef GET_BUFFER - -// --------------------------------- Text displays. - -// TODO Inline images and icons. -// TODO Links. -// TODO Inline backgrounds. - -void TextDisplayFreeRuns(EsTextDisplay *display) { - if (display->usingSyntaxHighlighting) { - Array textRuns = { display->textRuns }; - textRuns.Free(); - } else { - EsHeapFree(display->textRuns); - } -} - -int ProcessTextDisplayMessage(EsElement *element, EsMessage *message) { - EsTextDisplay *display = (EsTextDisplay *) element; - - if (message->type == ES_MSG_PAINT) { - EsRectangle textBounds = EsPainterBoundsInset(message->painter); - - if (!display->plan || display->planWidth != textBounds.r - textBounds.l || display->planHeight != textBounds.b - textBounds.t) { - if (display->plan) EsTextPlanDestroy(display->plan); - display->properties.flags = display->style->textAlign; - if (~display->flags & ES_TEXT_DISPLAY_PREFORMATTED) display->properties.flags |= ES_TEXT_PLAN_TRIM_SPACES; - if (display->flags & ES_TEXT_DISPLAY_NO_FONT_SUBSTITUTION) display->properties.flags |= ES_TEXT_PLAN_NO_FONT_SUBSTITUTION; - display->plan = EsTextPlanCreate(element, &display->properties, textBounds, display->contents, display->textRuns, display->textRunCount); - display->planWidth = textBounds.r - textBounds.l; - display->planHeight = textBounds.b - textBounds.t; - } - - if (display->plan) { - EsDrawTextLayers(message->painter, display->plan, EsPainterBoundsInset(message->painter)); - } - } else if (message->type == ES_MSG_GET_WIDTH || message->type == ES_MSG_GET_HEIGHT) { - if (!display->measurementCache.Get(message, &display->state)) { - if (display->plan) EsTextPlanDestroy(display->plan); - display->properties.flags = display->style->textAlign | ((display->flags & ES_TEXT_DISPLAY_PREFORMATTED) ? 0 : ES_TEXT_PLAN_TRIM_SPACES); - EsRectangle insets = EsElementGetInsets(element); - display->planWidth = message->type == ES_MSG_GET_HEIGHT && message->measure.width - ? (message->measure.width - insets.l - insets.r) : 0; - display->planHeight = 0; - display->plan = EsTextPlanCreate(element, &display->properties, - ES_RECT_4(0, display->planWidth, 0, 0), - display->contents, display->textRuns, display->textRunCount); - - if (!display->plan) { - message->measure.width = message->measure.height = 0; - } else { - if (message->type == ES_MSG_GET_WIDTH) { - message->measure.width = EsTextPlanGetWidth(display->plan) + insets.l + insets.r; - } else { - message->measure.height = EsTextPlanGetHeight(display->plan) + insets.t + insets.b; - } - } - - display->measurementCache.Store(message); - } - } else if (message->type == ES_MSG_DESTROY) { - if (display->plan) { - EsTextPlanDestroy(display->plan); - } - - TextDisplayFreeRuns(display); - EsHeapFree(display->contents); - } else if (message->type == ES_MSG_GET_INSPECTOR_INFORMATION) { - EsBufferFormat(message->getContent.buffer, "'%s'", display->textRuns[display->textRunCount].offset, display->contents); - } else if (message->type == ES_MSG_UI_SCALE_CHANGED) { - if (display->plan) { - EsTextPlanDestroy(display->plan); - display->plan = nullptr; - } - } else { - return 0; - } - - return ES_HANDLED; -} - -void EsTextDisplaySetStyledContents(EsTextDisplay *display, const char *string, EsTextRun *runs, size_t runCount) { - TextDisplayFreeRuns(display); - - display->textRuns = (EsTextRun *) EsHeapAllocate(sizeof(EsTextRun) * (runCount + 1), true); - display->textRunCount = runCount; - - size_t outBytes; - HeapDuplicate((void **) &display->contents, &outBytes, string, runs[runCount].offset); - - if (outBytes != runs[runCount].offset) { - // TODO Handle allocation failure. - } - - EsMemoryCopy(display->textRuns, runs, sizeof(EsTextRun) * (runCount + 1)); - - display->usingSyntaxHighlighting = false; - EsElementUpdateContentSize(display); - InspectorNotifyElementContentChanged(display); -} - -void EsTextDisplaySetContents(EsTextDisplay *display, const char *string, ptrdiff_t stringBytes) { - if (stringBytes == -1) stringBytes = EsCStringLength(string); - - TextDisplayFreeRuns(display); - - if (display->flags & ES_TEXT_DISPLAY_RICH_TEXT) { - EsHeapFree(display->contents); - EsTextStyle baseStyle = {}; - display->style->GetTextStyle(&baseStyle); - EsRichTextParse(string, stringBytes, &display->contents, &display->textRuns, &display->textRunCount, &baseStyle); - } else { - HeapDuplicate((void **) &display->contents, (size_t *) &stringBytes, string, stringBytes); - display->textRuns = (EsTextRun *) EsHeapAllocate(sizeof(EsTextRun) * 2, true); - display->style->GetTextStyle(&display->textRuns[0].style); - display->textRuns[1].offset = stringBytes; - display->textRunCount = 1; - } - - display->usingSyntaxHighlighting = false; - EsElementUpdateContentSize(display); - InspectorNotifyElementContentChanged(display); -} - -EsTextDisplay *EsTextDisplayCreate(EsElement *parent, uint64_t flags, const EsStyle *style, const char *label, ptrdiff_t labelBytes) { - EsTextDisplay *display = (EsTextDisplay *) EsHeapAllocate(sizeof(EsTextDisplay), true); - if (!display) return nullptr; - display->Initialise(parent, flags, ProcessTextDisplayMessage, style ?: UIGetDefaultStyleVariant(ES_STYLE_TEXT_LABEL, parent)); - display->cName = "text display"; - if (labelBytes == -1) labelBytes = EsCStringLength(label); - EsTextDisplaySetContents(display, label, labelBytes); - return display; -} - -void EsTextDisplaySetupSyntaxHighlighting(EsTextDisplay *display, uint32_t language, uint32_t *customColors, size_t customColorCount) { - // Copied from EsTextboxSetupSyntaxHighlighting. - uint32_t colors[8]; - colors[0] = 0x04000000; // Highlighted line. - colors[1] = 0xFF000000; // Default. - colors[2] = 0xFFA11F20; // Comment. - colors[3] = 0xFF037E01; // String. - colors[4] = 0xFF213EF1; // Number. - colors[5] = 0xFF7F0480; // Operator. - colors[6] = 0xFF545D70; // Preprocessor. - colors[7] = 0xFF17546D; // Keyword. - - if (customColorCount > sizeof(colors) / sizeof(uint32_t)) customColorCount = sizeof(colors) / sizeof(uint32_t); - EsMemoryCopy(colors, customColors, customColorCount * sizeof(uint32_t)); - - EsTextStyle textStyle = {}; - display->style->GetTextStyle(&textStyle); - - EsTextRun *newRuns = TextApplySyntaxHighlighting(&textStyle, language, colors, {}, - display->contents, display->textRuns[display->textRunCount].offset).array; - TextDisplayFreeRuns(display); - display->textRuns = newRuns; - display->textRunCount = ArrayLength(display->textRuns) - 1; - display->usingSyntaxHighlighting = true; - display->Repaint(true); -} - -// --------------------------------- List displays. - -struct EsListDisplay : EsElement { - uintptr_t itemCount, startIndex; - EsListDisplay *previous; -}; - -int ProcessListDisplayMessage(EsElement *element, EsMessage *message) { - EsListDisplay *display = (EsListDisplay *) element; - - if (message->type == ES_MSG_GET_HEIGHT) { - int32_t height = 0; - int32_t margin = element->style->insets.l + element->style->insets.r + element->style->gapMinor; - uintptr_t itemCount = 0; - - for (uintptr_t i = 0; i < element->GetChildCount(); i++) { - EsElement *child = element->GetChild(i); - if (child->flags & ES_ELEMENT_NON_CLIENT) continue; - height += child->GetHeight(message->measure.width - margin); - itemCount++; - } - - if (itemCount) { - height += (itemCount - 1) * element->style->gapMajor; - } - - message->measure.height = height + element->style->insets.t + element->style->insets.b; - } else if (message->type == ES_MSG_LAYOUT) { - int32_t position = element->style->insets.t; - int32_t margin = element->style->insets.l + element->style->gapMinor; - int32_t width = element->width - margin - element->style->insets.r; - - for (uintptr_t i = 0; i < element->GetChildCount(); i++) { - EsElement *child = element->GetChild(i); - if (child->flags & ES_ELEMENT_NON_CLIENT) continue; - int height = child->GetHeight(width); - EsElementMove(child, margin, position, width, height); - position += height + element->style->gapMajor; - } - } else if (message->type == ES_MSG_PAINT) { - char buffer[64]; - EsTextPlanProperties properties = {}; - properties.flags = ES_TEXT_H_RIGHT | ES_TEXT_V_TOP | ES_TEXT_PLAN_SINGLE_USE; - EsTextRun textRun[2] = {}; - - EsRectangle bounds = EsPainterBoundsClient(message->painter); - bounds.r = bounds.l + element->style->insets.l; - - uintptr_t counter = display->previous ? display->previous->itemCount : display->startIndex; - uint8_t markerType = element->flags & ES_LIST_DISPLAY_MARKER_TYPE_MASK; - - EsMessage m = {}; - m.type = ES_MSG_LIST_DISPLAY_GET_MARKER; - EsBuffer buffer2 = { .out = (uint8_t *) buffer, .bytes = sizeof(buffer) }; - m.getContent.buffer = &buffer2; - - for (uintptr_t i = 0; i < element->GetChildCount(); i++) { - EsElement *child = element->GetChild(i); - if (child->flags & ES_ELEMENT_NON_CLIENT) continue; - - if (markerType == ES_LIST_DISPLAY_BULLETED) { - EsMemoryCopy(buffer, "\xE2\x80\xA2", (textRun[1].offset = 3)); - } else if (markerType == ES_LIST_DISPLAY_NUMBERED) { - textRun[1].offset = EsStringFormat(buffer, sizeof(buffer), "%d.", counter + 1); - } else if (markerType == ES_LIST_DISPLAY_LOWER_ALPHA) { - textRun[1].offset = EsStringFormat(buffer, sizeof(buffer), "(%c)", counter + 'a'); - } else if (markerType == ES_LIST_DISPLAY_CUSTOM_MARKER) { - m.getContent.index = counter; - EsMessageSend(element, &m); - textRun[1].offset = buffer2.position; - } else { - EsAssert(false); - } - - child->style->GetTextStyle(&textRun[0].style); - textRun[0].style.figures = ES_TEXT_FIGURE_TABULAR; - bounds.t += child->offsetY; - bounds.b = bounds.t + child->height; - EsTextPlan *plan = EsTextPlanCreate(element, &properties, bounds, buffer, textRun, 1); - if (plan) EsDrawText(message->painter, plan, bounds); - bounds.t -= child->offsetY; - counter++; - } - } else if (message->type == ES_MSG_ADD_CHILD) { - display->itemCount++; - } else if (message->type == ES_MSG_REMOVE_CHILD) { - display->itemCount--; - } - - return 0; -} - -EsListDisplay *EsListDisplayCreate(EsElement *parent, uint64_t flags, const EsStyle *style) { - EsListDisplay *display = (EsListDisplay *) EsHeapAllocate(sizeof(EsListDisplay), true); - if (!display) return nullptr; - display->Initialise(parent, flags, ProcessListDisplayMessage, style ?: ES_STYLE_LIST_DISPLAY_DEFAULT); - display->cName = "list display"; - return display; -} - -void EsListDisplaySetCounterContinuation(EsListDisplay *display, EsListDisplay *previous) { - display->previous = previous; - EsElementRepaint(display); -} - -void EsListDisplaySetCounterStart(EsListDisplay *display, uintptr_t index) { - display->startIndex = index; - display->previous = nullptr; - EsElementRepaint(display); -} - -#endif diff --git a/desktop/textbox.cpp b/desktop/textbox.cpp new file mode 100644 index 0000000..8ca5de5 --- /dev/null +++ b/desktop/textbox.cpp @@ -0,0 +1,2132 @@ +// TODO Caret blinking. +// TODO Wrapped lines. +// TODO Unicode grapheme/word boundaries. +// TODO Selecting lines with the margin. + +#define GET_BUFFER(line) TextboxGetDocumentLineBuffer(textbox, line) + +struct DocumentLine { + int32_t lengthBytes, + lengthWidth, + height, + yPosition, + offset; +}; + +struct TextboxVisibleLine { + int32_t yPosition; +}; + +struct TextboxCaret { + int32_t byte, // Relative to the start of the line. + line; +}; + +struct EsTextbox : EsElement { + ScrollPane scroll; + + char *data; // Call TextboxSetActiveLine(textbox, -1) to access this. + uintptr_t dataAllocated; + int32_t dataBytes; + + bool editing; + char *editStartContent; + int32_t editStartContentBytes; + + bool ensureCaretVisibleQueued; + + EsElementCallback overlayCallback; + EsGeneric overlayData; + + char *activeLine; + uintptr_t activeLineAllocated; + int32_t activeLineIndex, activeLineStart, activeLineOldBytes, activeLineBytes; + + int32_t longestLine, longestLineWidth; // To set the horizontal scroll bar's size. + + TextboxCaret carets[2]; // carets[1] is the actual caret; carets[0] is the selection anchor. + TextboxCaret wordSelectionAnchor, wordSelectionAnchor2; + + Array lines; + Array visibleLines; + int32_t firstVisibleLine; + + int verticalMotionHorizontalDepth; + int oldHorizontalScroll; + + EsUndoManager *undo; + EsUndoManager localUndo; + + EsElement *margin; + + EsRectangle borders, insets; + EsTextStyle textStyle; + EsFont overrideFont; + uint16_t overrideTextSize; + + uint32_t syntaxHighlightingLanguage; + uint32_t syntaxHighlightingColors[8]; + + bool smartQuotes; + + bool inRightClickDrag; + + // For smart context menus: + bool colorUppercase; +}; + +#define MOVE_CARET_SINGLE (2) +#define MOVE_CARET_WORD (3) +#define MOVE_CARET_LINE (4) +#define MOVE_CARET_VERTICAL (5) +#define MOVE_CARET_ALL (6) + +#define MOVE_CARET_BACKWARDS (false) +#define MOVE_CARET_FORWARDS (true) + +void TextboxBufferResize(void **array, uintptr_t *allocated, uintptr_t needed, uintptr_t itemSize) { + if (*allocated >= needed) { + return; + } + + uintptr_t oldAllocated = *allocated; + void *oldArray = *array; + + uintptr_t newAllocated = oldAllocated * 2; + if (newAllocated < needed) newAllocated = needed + 16; + void *newArray = EsHeapAllocate(newAllocated * itemSize, false); + + EsMemoryCopy(newArray, oldArray, oldAllocated * itemSize); + EsHeapFree(oldArray); + + *allocated = newAllocated; + *array = newArray; +} + +void KeyboardLayoutLoad() { + if (api.keyboardLayoutIdentifier != api.global->keyboardLayout) { + char buffer[64]; + api.keyboardLayoutIdentifier = api.global->keyboardLayout; + api.keyboardLayout = (const uint16_t *) EsBundleFind(&bundleDesktop, buffer, EsStringFormat(buffer, sizeof(buffer), "Keyboard Layouts/%c%c.dat", + (uint8_t) api.keyboardLayoutIdentifier, (uint8_t) (api.keyboardLayoutIdentifier >> 8))); + + if (!api.keyboardLayout) { + // Fallback to the US layout if the specifier layout was not found. + api.keyboardLayout = (const uint16_t *) EsBundleFind(&bundleDesktop, buffer, EsStringFormat(buffer, sizeof(buffer), "Keyboard Layouts/us.dat")); + } + } +} + +const char *KeyboardLayoutLookup(uint32_t scancode, bool isShiftHeld, bool isAltGrHeld, bool enableTabs, bool enableNewline) { + KeyboardLayoutLoad(); + if (scancode >= 0x200) return nullptr; + if (scancode == ES_SCANCODE_ENTER || scancode == ES_SCANCODE_NUM_ENTER) return enableNewline ? "\n" : nullptr; + if (scancode == ES_SCANCODE_TAB) return enableTabs ? "\t" : nullptr; + if (scancode == ES_SCANCODE_BACKSPACE || scancode == ES_SCANCODE_DELETE) return nullptr; + uint16_t offset = api.keyboardLayout[scancode + (isShiftHeld ? 0x200 : 0) + (isAltGrHeld ? 0x400 : 0)]; + return offset ? ((char *) api.keyboardLayout + 0x1000 + offset) : nullptr; +} + +uint32_t ScancodeMapToLabel(uint32_t scancode) { + KeyboardLayoutLoad(); + const char *string = KeyboardLayoutLookup(scancode, false, false, false, false); + + if (string && string[0] && !string[1]) { + char c = string[0]; + if (c >= 'a' && c <= 'z') return ES_SCANCODE_A + c - 'a'; + if (c >= 'A' && c <= 'Z') return ES_SCANCODE_A + c - 'A'; + if (c >= '0' && c <= '9') return ES_SCANCODE_0 + c - '0'; + if (c == '/') return ES_SCANCODE_SLASH; + if (c == '[') return ES_SCANCODE_LEFT_BRACE; + if (c == ']') return ES_SCANCODE_RIGHT_BRACE; + if (c == '=') return ES_SCANCODE_EQUALS; + if (c == '-') return ES_SCANCODE_HYPHEN; + if (c == ',') return ES_SCANCODE_COMMA; + if (c == '.') return ES_SCANCODE_PERIOD; + if (c == '\\') return ES_SCANCODE_PUNCTUATION_1; + if (c == ';') return ES_SCANCODE_PUNCTUATION_3; + if (c == '\'') return ES_SCANCODE_PUNCTUATION_4; + if (c == '`') return ES_SCANCODE_PUNCTUATION_5; + } + + return scancode; +} + +bool ScancodeIsNonTypeable(uint32_t scancode) { + switch (scancode) { + case ES_SCANCODE_CAPS_LOCK: + case ES_SCANCODE_SCROLL_LOCK: + case ES_SCANCODE_NUM_LOCK: + case ES_SCANCODE_LEFT_SHIFT: + case ES_SCANCODE_LEFT_CTRL: + case ES_SCANCODE_LEFT_ALT: + case ES_SCANCODE_LEFT_FLAG: + case ES_SCANCODE_RIGHT_SHIFT: + case ES_SCANCODE_RIGHT_CTRL: + case ES_SCANCODE_RIGHT_ALT: + case ES_SCANCODE_PAUSE: + case ES_SCANCODE_CONTEXT_MENU: + case ES_SCANCODE_PRINT_SCREEN: + case ES_SCANCODE_F1: + case ES_SCANCODE_F2: + case ES_SCANCODE_F3: + case ES_SCANCODE_F4: + case ES_SCANCODE_F5: + case ES_SCANCODE_F6: + case ES_SCANCODE_F7: + case ES_SCANCODE_F8: + case ES_SCANCODE_F9: + case ES_SCANCODE_F10: + case ES_SCANCODE_F11: + case ES_SCANCODE_F12: + case ES_SCANCODE_ACPI_POWER: + case ES_SCANCODE_ACPI_SLEEP: + case ES_SCANCODE_ACPI_WAKE: + case ES_SCANCODE_MM_NEXT: + case ES_SCANCODE_MM_PREVIOUS: + case ES_SCANCODE_MM_STOP: + case ES_SCANCODE_MM_PAUSE: + case ES_SCANCODE_MM_MUTE: + case ES_SCANCODE_MM_QUIETER: + case ES_SCANCODE_MM_LOUDER: + case ES_SCANCODE_MM_SELECT: + case ES_SCANCODE_MM_EMAIL: + case ES_SCANCODE_MM_CALC: + case ES_SCANCODE_MM_FILES: + case ES_SCANCODE_WWW_SEARCH: + case ES_SCANCODE_WWW_HOME: + case ES_SCANCODE_WWW_BACK: + case ES_SCANCODE_WWW_FORWARD: + case ES_SCANCODE_WWW_STOP: + case ES_SCANCODE_WWW_REFRESH: + case ES_SCANCODE_WWW_STARRED: + return true; + + default: + return false; + } +} + +size_t EsMessageGetInputText(EsMessage *message, char *buffer) { + const char *string = KeyboardLayoutLookup(message->keyboard.scancode, + message->keyboard.modifiers & ES_MODIFIER_SHIFT, message->keyboard.modifiers & ES_MODIFIER_ALT_GR, + true, true); + size_t bytes = string ? EsCStringLength(string) : 0; + EsAssert(bytes < 64); + EsMemoryCopy(buffer, string, bytes); + return bytes; +} + +enum CharacterType { + CHARACTER_INVALID, + CHARACTER_IDENTIFIER, // A-Z, a-z, 0-9, _, >= 0x7F + CHARACTER_WHITESPACE, // space, tab, newline + CHARACTER_OTHER, +}; + +static CharacterType GetCharacterType(int character) { + if ((character >= '0' && character <= '9') + || (character >= 'a' && character <= 'z') + || (character >= 'A' && character <= 'Z') + || (character == '_') + || (character >= 0x80)) { + return CHARACTER_IDENTIFIER; + } + + if (character == '\n' || character == '\t' || character == ' ') { + return CHARACTER_WHITESPACE; + } + + return CHARACTER_OTHER; +} + +int TextboxCompareCarets(const TextboxCaret *left, const TextboxCaret *right) { + if (left->line < right->line) return -1; + if (left->line > right->line) return 1; + if (left->byte < right->byte) return -1; + if (left->byte > right->byte) return 1; + return 0; +} + +void TextboxSetActiveLine(EsTextbox *textbox, int lineIndex) { + if (textbox->activeLineIndex == lineIndex) { + return; + } + + if (lineIndex == -1) { + int32_t lineBytesDelta = textbox->activeLineBytes - textbox->activeLineOldBytes; + + // Step 1: Resize the data buffer to fit the new contents of the line. + + TextboxBufferResize((void **) &textbox->data, &textbox->dataAllocated, textbox->dataBytes + lineBytesDelta, 1); + + // Step 2: Move everything after the old end of the active line to its new position. + + EsMemoryMove(textbox->data + textbox->activeLineStart + textbox->activeLineOldBytes, + textbox->data + textbox->dataBytes, + lineBytesDelta, + false); + textbox->dataBytes += lineBytesDelta; + + // Step 3: Copy the active line back into the data buffer. + + EsMemoryCopy(textbox->data + textbox->activeLineStart, + textbox->activeLine, + textbox->activeLineBytes); + + // Step 4: Update the line byte offsets. + + for (uintptr_t i = textbox->activeLineIndex + 1; i < textbox->lines.Length(); i++) { + textbox->lines[i].offset += lineBytesDelta; + } + } else { + TextboxSetActiveLine(textbox, -1); + + DocumentLine *line = &textbox->lines[lineIndex]; + + TextboxBufferResize((void **) &textbox->activeLine, &textbox->activeLineAllocated, (textbox->activeLineBytes = line->lengthBytes), 1); + EsMemoryCopy(textbox->activeLine, textbox->data + line->offset, textbox->activeLineBytes); + + textbox->activeLineStart = line->offset; + textbox->activeLineOldBytes = textbox->activeLineBytes; + } + + textbox->activeLineIndex = lineIndex; +} + +void EsTextboxStartEdit(EsTextbox *textbox) { + textbox->state &= ~UI_STATE_LOST_STRONG_FOCUS; + + if ((textbox->flags & ES_TEXTBOX_EDIT_BASED) && !textbox->editing) { + EsMessage m = { ES_MSG_TEXTBOX_EDIT_START }; + + if (0 == EsMessageSend(textbox, &m)) { + EsTextboxSelectAll(textbox); + } + + if (textbox->state & UI_STATE_DESTROYING) { + return; + } + + textbox->editing = true; // Update this after sending the message so overlays can receive it. + TextboxSetActiveLine(textbox, -1); + textbox->editStartContent = (char *) EsHeapAllocate(textbox->dataBytes, false); + textbox->editStartContentBytes = textbox->dataBytes; + EsMemoryCopy(textbox->editStartContent, textbox->data, textbox->editStartContentBytes); + textbox->Repaint(true); + } +} + +void TextboxEndEdit(EsTextbox *textbox, bool reject) { + if ((textbox->flags & ES_TEXTBOX_EDIT_BASED) && textbox->editing) { + TextboxSetActiveLine(textbox, -1); + textbox->editing = false; + EsMessage m = { ES_MSG_TEXTBOX_EDIT_END }; + m.endEdit.rejected = reject; + m.endEdit.unchanged = textbox->dataBytes == textbox->editStartContentBytes + && 0 == EsMemoryCompare(textbox->data, textbox->editStartContent, textbox->dataBytes); + + if (reject || ES_REJECTED == EsMessageSend(textbox, &m)) { + EsTextboxSelectAll(textbox); + EsTextboxInsert(textbox, textbox->editStartContent, textbox->editStartContentBytes); + TextboxSetActiveLine(textbox, -1); + if (reject) EsMessageSend(textbox, &m); + } + + if (textbox->state & UI_STATE_DESTROYING) { + return; + } + + EsTextboxSetSelection(textbox, 0, 0, 0, 0); + EsHeapFree(textbox->editStartContent); + textbox->editStartContent = nullptr; + textbox->scroll.SetX(0); + textbox->Repaint(true); + } +} + +void TextboxUpdateCommands(EsTextbox *textbox, bool noClipboard) { + if (~textbox->state & UI_STATE_FOCUSED) { + return; + } + + EsCommand *command; + + bool selectionEmpty = !TextboxCompareCarets(textbox->carets + 0, textbox->carets + 1) && textbox->editing; + + command = EsCommandByID(textbox->instance, ES_COMMAND_DELETE); + command->data = textbox; + EsCommandSetDisabled(command, selectionEmpty); + + EsCommandSetCallback(command, [] (EsInstance *, EsElement *, EsCommand *command) { + EsTextbox *textbox = (EsTextbox *) command->data.p; + EsTextboxInsert(textbox, "", 0, true); + }); + + command = EsCommandByID(textbox->instance, ES_COMMAND_COPY); + command->data = textbox; + EsCommandSetDisabled(command, selectionEmpty); + + EsCommandSetCallback(command, [] (EsInstance *, EsElement *, EsCommand *command) { + EsTextbox *textbox = (EsTextbox *) command->data.p; + size_t textBytes; + char *text = EsTextboxGetContents(textbox, &textBytes, textbox->editing ? ES_TEXTBOX_GET_CONTENTS_SELECTED_ONLY : ES_FLAGS_DEFAULT); + EsError error = EsClipboardAddText(ES_CLIPBOARD_PRIMARY, text, textBytes); + EsHeapFree(text); + + EsRectangle bounds = EsElementGetWindowBounds(textbox); + int32_t x = (bounds.l + bounds.r) / 2; + int32_t y = (bounds.t + bounds.b) / 2; // TODO Position this in the middle of the selection. + + if (error == ES_SUCCESS) { + EsAnnouncementShow(textbox->window, ES_FLAGS_DEFAULT, x, y, INTERFACE_STRING(CommonAnnouncementTextCopied)); + } else if (error == ES_ERROR_INSUFFICIENT_RESOURCES || error == ES_ERROR_DRIVE_FULL) { + EsAnnouncementShow(textbox->window, ES_FLAGS_DEFAULT, x, y, INTERFACE_STRING(CommonAnnouncementCopyErrorResources)); + } else { + EsAnnouncementShow(textbox->window, ES_FLAGS_DEFAULT, x, y, INTERFACE_STRING(CommonAnnouncementCopyErrorOther)); + } + }); + + command = EsCommandByID(textbox->instance, ES_COMMAND_CUT); + command->data = textbox; + EsCommandSetDisabled(command, selectionEmpty); + + EsCommandSetCallback(command, [] (EsInstance *, EsElement *, EsCommand *command) { + EsTextbox *textbox = (EsTextbox *) command->data.p; + size_t textBytes; + char *text = EsTextboxGetContents(textbox, &textBytes, textbox->editing ? ES_TEXTBOX_GET_CONTENTS_SELECTED_ONLY : ES_FLAGS_DEFAULT); + EsClipboardAddText(ES_CLIPBOARD_PRIMARY, text, textBytes); + EsHeapFree(text); + EsTextboxStartEdit(textbox); + EsTextboxInsert(textbox, "", 0, true); + }); + + EsInstanceSetActiveUndoManager(textbox->instance, textbox->undo); + + command = EsCommandByID(textbox->instance, ES_COMMAND_SELECT_ALL); + command->data = textbox; + EsCommandSetDisabled(command, !(textbox->lines.Length() > 1 || textbox->lines[0].lengthBytes)); + + EsCommandSetCallback(command, [] (EsInstance *, EsElement *, EsCommand *command) { + EsTextboxSelectAll((EsTextbox *) command->data.p); + }); + + if (!noClipboard) { + command = EsCommandByID(textbox->instance, ES_COMMAND_PASTE); + command->data = textbox; + EsCommandSetDisabled(command, !EsClipboardHasText(ES_CLIPBOARD_PRIMARY)); + + EsCommandSetCallback(command, [] (EsInstance *, EsElement *, EsCommand *command) { + EsTextbox *textbox = (EsTextbox *) command->data.p; + + size_t textBytes = 0; + char *text = EsClipboardReadText(ES_CLIPBOARD_PRIMARY, &textBytes); + EsTextboxInsert(textbox, text, textBytes, true); + EsTextboxEnsureCaretVisible(textbox); + EsHeapFree(text); + }); + } +} + +char *TextboxGetDocumentLineBuffer(EsTextbox *textbox, DocumentLine *line) { + if (textbox->activeLineIndex == line - textbox->lines.array) { + return textbox->activeLine; + } else { + return textbox->data + line->offset; + } +} + +void TextboxFindLongestLine(EsTextbox *textbox) { + if (textbox->longestLine == -1) { + textbox->longestLine = 0; + textbox->longestLineWidth = textbox->lines[0].lengthWidth; + + for (uintptr_t i = 1; i < textbox->lines.Length(); i++) { + int32_t width = textbox->lines[i].lengthWidth; + + if (width > textbox->longestLineWidth) { + textbox->longestLine = i, textbox->longestLineWidth = width; + } + } + } +} + +TextboxVisibleLine *TextboxGetVisibleLine(EsTextbox *textbox, int32_t documentLineIndex) { + return textbox->firstVisibleLine > documentLineIndex + || textbox->firstVisibleLine + (int32_t) textbox->visibleLines.Length() <= documentLineIndex + ? nullptr : &textbox->visibleLines[documentLineIndex - textbox->firstVisibleLine]; +} + +void TextboxEnsureCaretVisibleActionCallback(EsElement *element, EsGeneric context) { + EsTextbox *textbox = (EsTextbox *) element; + bool verticallyCenter = context.u; + TextboxCaret caret = textbox->carets[1]; + + for (uintptr_t i = 0; i < 3; i++) { + // ScrollPane::SetY causes ES_MSG_SCROLL_Y to get sent to the textbox. + // This causes a TextboxRefreshVisibleLines, which may cause new lines to added. + // If these lines had not been previously horizontally measured, this will then occur. + // This then causes a ScrollPane::Refresh for the new horizontal width. + // If this causes the horizontal scroll bar to appear, then the caret may no longer be fully visible. + // Therefore, we repeat up to 3 times to ensure that the caret is definitely fully visible. + + EsRectangle bounds = textbox->GetBounds(); + DocumentLine *line = &textbox->lines[caret.line]; + int caretY = line->yPosition + textbox->insets.t; + + int scrollY = textbox->scroll.position[1]; + int viewportHeight = bounds.b; + caretY -= scrollY; + + if (viewportHeight > 0) { + if (verticallyCenter) { + scrollY += caretY - viewportHeight / 2; + } else { + if (caretY < textbox->insets.t) { + scrollY += caretY - textbox->insets.t; + } else if (caretY + line->height > viewportHeight - textbox->insets.b) { + scrollY += caretY + line->height - viewportHeight + textbox->insets.b; + } + } + + if (textbox->scroll.position[1] != scrollY) { + textbox->scroll.SetY(scrollY); + } else { + break; + } + } else { + break; + } + } + + TextboxVisibleLine *visibleLine = TextboxGetVisibleLine(textbox, caret.line); + + if (visibleLine) { + EsRectangle bounds = textbox->GetBounds(); + DocumentLine *line = &textbox->lines[caret.line]; + int scrollX = textbox->scroll.position[0]; + int viewportWidth = bounds.r; + int caretX = TextGetPartialStringWidth(textbox, &textbox->textStyle, + GET_BUFFER(line), line->lengthBytes, caret.byte) - scrollX + textbox->insets.l; + + if (caretX < textbox->insets.l) { + scrollX += caretX - textbox->insets.l; + } else if (caretX + 1 > viewportWidth - textbox->insets.r) { + scrollX += caretX + 1 - viewportWidth + textbox->insets.r; + } + + textbox->scroll.SetX(scrollX); + } + + UIQueueEnsureVisibleMessage(textbox, false); + textbox->ensureCaretVisibleQueued = false; +} + +void EsTextboxEnsureCaretVisible(EsTextbox *textbox, bool verticallyCenter) { + if (!textbox->ensureCaretVisibleQueued) { + UpdateAction action = {}; + action.element = textbox; + action.callback = TextboxEnsureCaretVisibleActionCallback; + action.context.u = verticallyCenter; + textbox->window->updateActions.Add(action); + textbox->ensureCaretVisibleQueued = true; + } +} + +bool TextboxMoveCaret(EsTextbox *textbox, TextboxCaret *caret, bool right, int moveType, bool strongWhitespace = false) { + TextboxCaret old = *caret; + EsDefer(TextboxUpdateCommands(textbox, true)); + + if (moveType == MOVE_CARET_LINE) { + caret->byte = right ? textbox->lines[caret->line].lengthBytes : 0; + } else if (moveType == MOVE_CARET_ALL) { + caret->line = right ? textbox->lines.Length() - 1 : 0; + caret->byte = right ? textbox->lines[caret->line].lengthBytes : 0; + } else if (moveType == MOVE_CARET_VERTICAL) { + if ((right && caret->line + 1 == (int32_t) textbox->lines.Length()) || (!right && !caret->line)) { + return false; + } + + if (textbox->verticalMotionHorizontalDepth == -1) { + textbox->verticalMotionHorizontalDepth = TextGetPartialStringWidth(textbox, &textbox->textStyle, + GET_BUFFER(&textbox->lines[caret->line]), textbox->lines[caret->line].lengthBytes, caret->byte); + } + + if (right) caret->line++; else caret->line--; + caret->byte = 0; + + DocumentLine *line = &textbox->lines[caret->line]; + int pointX = textbox->verticalMotionHorizontalDepth ? textbox->verticalMotionHorizontalDepth - 1 : 0; + ptrdiff_t result = TextGetCharacterAtPoint(textbox, &textbox->textStyle, + GET_BUFFER(line), line->lengthBytes, &pointX, ES_TEXT_GET_CHARACTER_AT_POINT_MIDDLE); + caret->byte = result == -1 ? line->lengthBytes : result; + } else { + CharacterType type = CHARACTER_INVALID; + char *currentLineBuffer = GET_BUFFER(&textbox->lines[caret->line]); + if (moveType == MOVE_CARET_WORD && right) goto checkCharacterType; + + while (true) { + if (!right) { + if (caret->byte || caret->line) { + if (caret->byte) { + caret->byte = utf8_retreat(currentLineBuffer + caret->byte) - currentLineBuffer; + } else { + caret->byte = textbox->lines[--caret->line].lengthBytes; + currentLineBuffer = GET_BUFFER(&textbox->lines[caret->line]); + } + } else { + break; // We cannot move any further left. + } + } else { + if (caret->line < (int32_t) textbox->lines.Length() - 1 || caret->byte < textbox->lines[caret->line].lengthBytes) { + if (caret->byte < textbox->lines[caret->line].lengthBytes) { + caret->byte = utf8_advance(currentLineBuffer + caret->byte) - currentLineBuffer; + } else { + caret->line++; + caret->byte = 0; + currentLineBuffer = GET_BUFFER(&textbox->lines[caret->line]); + } + } else { + break; // We cannot move any further right. + } + } + + if (moveType == MOVE_CARET_SINGLE) { + break; + } + + checkCharacterType:; + + int character; + + if (caret->byte == textbox->lines[caret->line].lengthBytes) { + character = '\n'; + } else { + character = utf8_value(currentLineBuffer + caret->byte); + } + + CharacterType newType = GetCharacterType(character); + + if (type == CHARACTER_INVALID) { + if (newType != CHARACTER_WHITESPACE || strongWhitespace) { + type = newType; + } + } else { + if (newType != type) { + if (!right) { + // We've gone too far. + TextboxMoveCaret(textbox, caret, true, MOVE_CARET_SINGLE); + } + + break; + } + } + } + } + + return caret->line != old.line; +} + +void EsTextboxMoveCaretRelative(EsTextbox *textbox, uint32_t flags) { + if (~flags & ES_TEXTBOX_MOVE_CARET_SECOND_ONLY) { + TextboxMoveCaret(textbox, &textbox->carets[0], ~flags & ES_TEXTBOX_MOVE_CARET_BACKWARDS, + flags & 0xFF, flags & ES_TEXTBOX_MOVE_CARET_STRONG_WHITESPACE); + } + + if (~flags & ES_TEXTBOX_MOVE_CARET_FIRST_ONLY) { + TextboxMoveCaret(textbox, &textbox->carets[1], ~flags & ES_TEXTBOX_MOVE_CARET_BACKWARDS, + flags & 0xFF, flags & ES_TEXTBOX_MOVE_CARET_STRONG_WHITESPACE); + } +} + +void TextboxRepaintLine(EsTextbox *textbox, int line) { + if (line == -1 || (~textbox->flags & ES_TEXTBOX_MULTILINE)) { + textbox->Repaint(true); + } else { + EsRectangle borders = textbox->borders; + int topInset = textbox->insets.t; + + TextboxVisibleLine *visibleLine = TextboxGetVisibleLine(textbox, line); + + if (visibleLine) { + EsRectangle bounds = textbox->GetBounds(); + EsRectangle lineBounds = ES_RECT_4(bounds.l + borders.l, bounds.r - borders.r, + visibleLine->yPosition + topInset - 1 - textbox->scroll.position[1], + visibleLine->yPosition + topInset + textbox->lines[line].height - textbox->scroll.position[1]); + // EsPrint("textbox bounds %R; line bounds %R\n", bounds); + textbox->Repaint(false, lineBounds); + } + } +} + +void TextboxSetHorizontalScroll(EsTextbox *textbox, int scroll) { + textbox->Repaint(true); + textbox->oldHorizontalScroll = scroll; +} + +void TextboxRefreshVisibleLines(EsTextbox *textbox, bool repaint = true) { + if (textbox->visibleLines.Length()) { + textbox->visibleLines.SetLength(0); + } + + int scrollX = textbox->scroll.position[0], scrollY = textbox->scroll.position[1]; + EsRectangle bounds = textbox->GetBounds(); + + int32_t low = 0, high = textbox->lines.Length() - 1, target = scrollY - textbox->insets.t; + + while (low != high) { + int32_t middle = (low + high) / 2; + int32_t position = textbox->lines[middle].yPosition; + + if (position < target && low != middle) low = middle; + else if (position > target && high != middle) high = middle; + else break; + } + + textbox->firstVisibleLine = (low + high) / 2; + if (textbox->firstVisibleLine) textbox->firstVisibleLine--; + + for (int32_t i = textbox->firstVisibleLine; i < (int32_t) textbox->lines.Length(); i++) { + TextboxVisibleLine line = {}; + line.yPosition = textbox->lines[i].yPosition; + textbox->visibleLines.Add(line); + + if (line.yPosition - scrollY > bounds.b) { + break; + } + } + + bool refreshXLimit = false; + + for (uintptr_t i = 0; i < textbox->visibleLines.Length(); i++) { + DocumentLine *line = &textbox->lines[textbox->firstVisibleLine + i]; + + if (line->lengthWidth != -1) { + continue; + } + + line->lengthWidth = TextGetStringWidth(textbox, &textbox->textStyle, + GET_BUFFER(line), line->lengthBytes); + + if (textbox->longestLine != -1 && line->lengthWidth > textbox->longestLineWidth) { + textbox->longestLine = textbox->firstVisibleLine + i; + textbox->longestLineWidth = line->lengthWidth; + refreshXLimit = true; + } + } + + if (refreshXLimit) { + textbox->scroll.Refresh(); + } + + textbox->scroll.SetX(scrollX); + + if (repaint) { + textbox->Repaint(true); + } +} + +void TextboxLineCountChangeCleanup(EsTextbox *textbox, int32_t offsetDelta, int32_t startLine) { + for (int32_t i = startLine; i < (int32_t) textbox->lines.Length(); i++) { + DocumentLine *line = &textbox->lines[i], *previous = &textbox->lines[i - 1]; + line->yPosition = previous->yPosition + previous->height; + line->offset += offsetDelta; + } + + TextboxRefreshVisibleLines(textbox); +} + +void EsTextboxMoveCaret(EsTextbox *textbox, int32_t line, int32_t byte) { + EsMessageMutexCheck(); + + textbox->carets[0].line = line; + textbox->carets[0].byte = byte; + textbox->carets[1].line = line; + textbox->carets[1].byte = byte; + textbox->Repaint(true); + TextboxUpdateCommands(textbox, true); +} + +void EsTextboxGetSelection(EsTextbox *textbox, int32_t *fromLine, int32_t *fromByte, int32_t *toLine, int32_t *toByte) { + EsMessageMutexCheck(); + + *fromLine = textbox->carets[0].line; + *fromByte = textbox->carets[0].byte; + *toLine = textbox->carets[1].line; + *toByte = textbox->carets[1].byte; +} + +void EsTextboxSetSelection(EsTextbox *textbox, int32_t fromLine, int32_t fromByte, int32_t toLine, int32_t toByte) { + EsMessageMutexCheck(); + + if (fromByte == -1) fromByte = textbox->lines[fromLine].lengthBytes; + if (toByte == -1) toByte = textbox->lines[toLine].lengthBytes; + if (fromByte < 0 || toByte < 0 || fromByte > textbox->lines[fromLine].lengthBytes || toByte > textbox->lines[toLine].lengthBytes) return; + textbox->carets[0].line = fromLine; + textbox->carets[0].byte = fromByte; + textbox->carets[1].line = toLine; + textbox->carets[1].byte = toByte; + textbox->Repaint(true); + TextboxUpdateCommands(textbox, true); + EsTextboxEnsureCaretVisible(textbox); +} + +void EsTextboxSelectAll(EsTextbox *textbox) { + EsMessageMutexCheck(); + + TextboxMoveCaret(textbox, &textbox->carets[0], false, MOVE_CARET_ALL); + TextboxMoveCaret(textbox, &textbox->carets[1], true, MOVE_CARET_ALL); + EsTextboxEnsureCaretVisible(textbox); + textbox->Repaint(true); +} + +void EsTextboxClear(EsTextbox *textbox, bool sendUpdatedMessage) { + EsMessageMutexCheck(); + + EsTextboxSelectAll(textbox); + EsTextboxInsert(textbox, "", 0, sendUpdatedMessage); +} + +size_t EsTextboxGetLineLength(EsTextbox *textbox, uintptr_t line) { + EsMessageMutexCheck(); + + return textbox->lines[line].lengthBytes; +} + +struct TextboxUndoItemHeader { + EsTextbox *textbox; + TextboxCaret caretsBefore[2]; + size_t insertBytes; + double timeStampMs; + // Followed by insert string. +}; + +void TextboxUndoItemCallback(const void *item, EsUndoManager *manager, EsMessage *message) { + if (message->type == ES_MSG_UNDO_INVOKE) { + TextboxUndoItemHeader *header = (TextboxUndoItemHeader *) item; + EsTextbox *textbox = header->textbox; + EsAssert(textbox->undo == manager); + TextboxRepaintLine(textbox, textbox->carets[0].line); + TextboxRepaintLine(textbox, textbox->carets[0].line); + textbox->carets[0] = header->caretsBefore[0]; + textbox->carets[1] = header->caretsBefore[1]; + EsTextboxInsert(textbox, (const char *) (header + 1), header->insertBytes, true); + EsTextboxEnsureCaretVisible(textbox); + } else if (message->type == ES_MSG_UNDO_CANCEL) { + // Nothing to do. + } +} + +void EsTextboxInsert(EsTextbox *textbox, const char *string, ptrdiff_t stringBytes, bool sendUpdatedMessage) { + EsMessageMutexCheck(); + + // EsPerformanceTimerPush(); + // double measureLineTime = 0; + + if (stringBytes == -1) { + stringBytes = EsCStringLength(string); + } + + TextboxUndoItemHeader *undoItem = nullptr; + size_t undoItemBytes = 0; + + textbox->wordSelectionAnchor = textbox->carets[0]; + textbox->wordSelectionAnchor2 = textbox->carets[1]; + + textbox->verticalMotionHorizontalDepth = -1; + + // ::: Delete the selected text. + + // Step 1: Get the range of text we're deleting. + + TextboxCaret deleteFrom, deleteTo; + int comparison = TextboxCompareCarets(textbox->carets + 0, textbox->carets + 1); + + if (comparison < 0) deleteFrom = textbox->carets[0], deleteTo = textbox->carets[1]; + else if (comparison > 0) deleteFrom = textbox->carets[1], deleteTo = textbox->carets[0]; + + if (comparison) { + textbox->carets[0] = textbox->carets[1] = deleteFrom; + + // Step 2: Calculate the number of bytes we are deleting. + + int32_t deltaBytes; + + if (deleteFrom.line == deleteTo.line) { + deltaBytes = deleteFrom.byte - deleteTo.byte; + } else { + TextboxSetActiveLine(textbox, -1); + + deltaBytes = deleteFrom.byte - deleteTo.byte; + + for (int32_t i = deleteFrom.line; i < deleteTo.line; i++) { + deltaBytes -= textbox->lines[i].lengthBytes; + } + } + + if (textbox->undo) { + // Step 3: Allocate space for an undo item. + + undoItemBytes = sizeof(TextboxUndoItemHeader) - deltaBytes + deleteTo.line - deleteFrom.line; + undoItem = (TextboxUndoItemHeader *) EsHeapAllocate(undoItemBytes, false); + EsMemoryZero(undoItem, sizeof(TextboxUndoItemHeader)); + undoItem->insertBytes = undoItemBytes - sizeof(TextboxUndoItemHeader); + } + + if (deleteFrom.line == deleteTo.line) { + EsAssert(deltaBytes < 0); // Expected deleteTo > deleteFrom. + DocumentLine *line = &textbox->lines[deleteFrom.line]; + TextboxSetActiveLine(textbox, deleteFrom.line); + + // Step 4: Update the width of the line and repaint it. + + line->lengthWidth = TextGetStringWidth(textbox, &textbox->textStyle, textbox->activeLine, textbox->activeLineBytes); + TextboxRepaintLine(textbox, deleteFrom.line); + + // Step 5: Update the active line buffer. + + if (undoItem) EsMemoryCopy(undoItem + 1, textbox->activeLine + deleteFrom.byte, -deltaBytes); + EsMemoryMove(textbox->activeLine + deleteTo.byte, textbox->activeLine + line->lengthBytes, deltaBytes, false); + textbox->activeLineBytes += deltaBytes; + line->lengthBytes += deltaBytes; + + // Step 6: Update the longest line. + + if (textbox->longestLine == deleteFrom.line && line->lengthWidth < textbox->longestLineWidth) { + textbox->longestLine = -1; + } + } else { + if (undoItem) { + // Step 4: Copy into the undo item. + + char *position = (char *) (undoItem + 1); + + for (int32_t i = deleteFrom.line; i <= deleteTo.line; i++) { + char *from = textbox->data + textbox->lines[i].offset; + char *to = textbox->data + textbox->lines[i].offset + textbox->lines[i].lengthBytes; + if (i == deleteFrom.line) from += deleteFrom.byte; + if (i == deleteTo.line) to += deleteTo.byte - textbox->lines[i].lengthBytes; + EsMemoryCopy(position, from, to - from); + position += to - from; + if (i != deleteTo.line) *position++ = '\n'; + } + } + + // Step 5: Remove the text from the buffer. + + EsMemoryMove(textbox->data + deleteTo.byte + textbox->lines[deleteTo.line].offset, textbox->data + textbox->dataBytes, deltaBytes, false); + textbox->dataBytes += deltaBytes; + + // Step 6: Merged the joined lines. + + DocumentLine *firstLine = &textbox->lines[deleteFrom.line]; + firstLine->lengthBytes = textbox->lines[deleteTo.line].lengthBytes - deleteTo.byte + deleteFrom.byte; + firstLine->lengthWidth = TextGetStringWidth(textbox, &textbox->textStyle, textbox->data + firstLine->offset, firstLine->lengthBytes); + + // Step 7: Remove the deleted lines and update the textbox. + + textbox->lines.DeleteMany(deleteFrom.line + 1, deleteTo.line - deleteFrom.line); + textbox->longestLine = -1; + TextboxLineCountChangeCleanup(textbox, deltaBytes, deleteFrom.line + 1); + } + } else { + if (textbox->undo) { + undoItemBytes = sizeof(TextboxUndoItemHeader); + undoItem = (TextboxUndoItemHeader *) EsHeapAllocate(undoItemBytes, false); + EsMemoryZero(undoItem, sizeof(TextboxUndoItemHeader)); + } + } + + if (undoItem) { + undoItem->caretsBefore[0] = undoItem->caretsBefore[1] = textbox->carets[0]; + } + + // ::: Insert the new text. + + if (!stringBytes) goto done; + + { + TextboxCaret insertionPoint = textbox->carets[0]; + + DocumentLine *line = &textbox->lines[insertionPoint.line]; + int32_t lineByteOffset = line->offset, + offsetIntoLine = insertionPoint.byte, + byteOffset = offsetIntoLine + lineByteOffset; + + // Step 1: Count the number of newlines in the input string. + + uintptr_t position = 0, + newlines = 0, + carriageReturns = 0; + + while (position < (size_t) stringBytes) { + int length; + UTF8_LENGTH_CHAR(string + position, length); + if (length == 0) length = 1; + + if (position + length > (size_t) stringBytes) { + break; + } else if (string[position] == '\n') { + newlines++; + } else if (string[position] == '\r' && position != (size_t) stringBytes - 1 && string[position + 1] == '\n') { + carriageReturns++; + } + + position += length; + } + + size_t bytesToInsert = stringBytes - newlines - carriageReturns; + + if (!newlines || (~textbox->flags & ES_TEXTBOX_MULTILINE)) { + // Step 2: Update the active line buffer. + + TextboxSetActiveLine(textbox, insertionPoint.line); + TextboxBufferResize((void **) &textbox->activeLine, &textbox->activeLineAllocated, (textbox->activeLineBytes += bytesToInsert), 1); + EsMemoryMove(textbox->activeLine + offsetIntoLine, textbox->activeLine + line->lengthBytes, bytesToInsert, false); + + const char *dataToInsert = string; + size_t added = 0; + + for (uintptr_t i = 0; i < newlines + 1; i++) { + const char *end = (const char *) EsCRTmemchr(dataToInsert, '\n', stringBytes - (dataToInsert - string)) ?: string + stringBytes; + bool carriageReturn = end != string && end[-1] == '\r'; + if (carriageReturn) end--; + EsMemoryCopy(textbox->activeLine + offsetIntoLine + added, dataToInsert, end - dataToInsert); + added += end - dataToInsert; + dataToInsert = end + (carriageReturn ? 2 : 1); + } + + EsAssert(added == bytesToInsert); // Added incorrect number of bytes in EsTextboxInsert. + + line->lengthBytes += bytesToInsert; + + // Step 3: Update the carets, line width, and repaint it. + + textbox->carets[0].byte += bytesToInsert; + textbox->carets[1].byte += bytesToInsert; + line->lengthWidth = TextGetStringWidth(textbox, &textbox->textStyle, textbox->activeLine, line->lengthBytes); + TextboxRepaintLine(textbox, insertionPoint.line); + + // Step 4: Update the longest line. + + if (textbox->longestLine != -1 && line->lengthWidth > textbox->longestLineWidth) { + textbox->longestLine = insertionPoint.line; + textbox->longestLineWidth = line->lengthWidth; + } + } else { + // Step 2: Make room in the buffer for the contents of the string. + + TextboxSetActiveLine(textbox, -1); + TextboxBufferResize((void **) &textbox->data, &textbox->dataAllocated, textbox->dataBytes + bytesToInsert, 1); + EsMemoryMove(textbox->data + byteOffset, textbox->data + textbox->dataBytes, bytesToInsert, false); + textbox->dataBytes += bytesToInsert; + + // Step 3: Truncate the insertion line. + + int32_t truncation = line->lengthBytes - insertionPoint.byte; + line->lengthBytes = insertionPoint.byte; + + // Step 4: Add the new lines. + + textbox->lines.InsertMany(insertionPoint.line + 1, newlines); + const char *dataToInsert = string; + uintptr_t insertedBytes = 0; + + for (uintptr_t i = 0; i < newlines + 1; i++) { + DocumentLine *line = &textbox->lines[insertionPoint.line + i], *previous = line - 1; + + // Step 4a: Initialise the line. + + if (i) { + EsMemoryZero(line, sizeof(*line)); + line->height = TextGetLineHeight(textbox, &textbox->textStyle); + line->yPosition = previous->yPosition + previous->height; + line->offset = lineByteOffset + insertedBytes; + } + + // Step 4b: Copy the string data into the line. + + const char *end = (const char *) EsCRTmemchr(dataToInsert, '\n', stringBytes - (dataToInsert - string)) ?: string + stringBytes; + bool carriageReturn = end != string && end[-1] == '\r'; + if (carriageReturn) end--; + EsMemoryCopy(textbox->data + line->offset + line->lengthBytes, dataToInsert, end - dataToInsert); + line->lengthBytes += end - dataToInsert; + insertedBytes += line->lengthBytes; + dataToInsert = end + (carriageReturn ? 2 : 1); + + if (i == newlines) { + line->lengthBytes += truncation; + } + + // Step 4c: Update the line's width. + + // EsPerformanceTimerPush(); +#if 0 + line->lengthWidth = EsTextGetPartialStringWidth(&textbox->textStyle, textbox->data + line->offset, line->lengthBytes, 0, line->lengthBytes); +#else + line->lengthWidth = -1; +#endif + // double time = EsPerformanceTimerPop(); + // measureLineTime += time; + // EsPrint("Measured the length of line %d in %Fms.\n", insertionPoint.line + i, time * 1000); + } + + // Step 5: Update the carets. + + textbox->carets[0].line = insertionPoint.line + newlines; + textbox->carets[1].line = insertionPoint.line + newlines; + textbox->carets[0].byte = textbox->lines[insertionPoint.line + newlines].lengthBytes - truncation; + textbox->carets[1].byte = textbox->lines[insertionPoint.line + newlines].lengthBytes - truncation; + + // Step 6: Update the textbox. + + textbox->longestLine = -1; + TextboxLineCountChangeCleanup(textbox, bytesToInsert, insertionPoint.line + 1 + newlines); + } + + if (undoItem) undoItem->caretsBefore[1] = textbox->carets[0]; + } + + done:; + + if (sendUpdatedMessage) { + EsMessage m = { ES_MSG_TEXTBOX_UPDATED }; + EsMessageSend(textbox, &m); + } else if (textbox->overlayCallback) { + EsMessage m = { ES_MSG_TEXTBOX_UPDATED }; + textbox->overlayCallback(textbox, &m); + } + + if (textbox->state & UI_STATE_DESTROYING) { + return; + } + + TextboxFindLongestLine(textbox); + InspectorNotifyElementContentChanged(textbox); + + if (undoItem && (undoItem->insertBytes || TextboxCompareCarets(undoItem->caretsBefore + 0, undoItem->caretsBefore + 1))) { + undoItem->timeStampMs = EsTimeStampMs(); + + EsUndoCallback previousCallback; + const void *previousItem; + + if (!EsUndoInUndo(textbox->undo) + && EsUndoPeek(textbox->undo, &previousCallback, &previousItem) + && previousCallback == TextboxUndoItemCallback) { + TextboxUndoItemHeader *header = (TextboxUndoItemHeader *) previousItem; + +#define TEXTBOX_UNDO_TIMEOUT (500) // TODO Make this configurable. + if (undoItem->timeStampMs - header->timeStampMs < TEXTBOX_UNDO_TIMEOUT) { + if (!undoItem->insertBytes && !header->insertBytes + && undoItem->caretsBefore[0].line == header->caretsBefore[1].line + && undoItem->caretsBefore[0].byte == header->caretsBefore[1].byte) { + // Merge the items. + undoItem->caretsBefore[0] = header->caretsBefore[0]; + EsUndoPop(textbox->undo); + } else { + // Add the new item to the same group as the previous. + EsUndoContinueGroup(textbox->undo); + } + } + } + + undoItem->textbox = textbox; + EsUndoPush(textbox->undo, TextboxUndoItemCallback, undoItem, undoItemBytes, false /* do not set instance's undo manager */); + } + + EsHeapFree(undoItem); + + // double time = EsPerformanceTimerPop(); + // EsPrint("EsTextboxInsert in %Fms (%Fms measuring new lines).\n", time * 1000, measureLineTime * 1000); + + textbox->scroll.Refresh(); + TextboxUpdateCommands(textbox, true); +} + +char *EsTextboxGetContents(EsTextbox *textbox, size_t *_bytes, uint32_t flags) { + EsMessageMutexCheck(); + + TextboxSetActiveLine(textbox, -1); + + bool includeNewline = textbox->flags & ES_TEXTBOX_MULTILINE; + size_t bytes = textbox->dataBytes + (includeNewline ? textbox->lines.Length() : 0); + char *buffer = (char *) EsHeapAllocate(bytes + 1, false); + buffer[bytes] = 0; + + uintptr_t position = 0; + uintptr_t lineFrom = 0, lineTo = textbox->lines.Length() - 1; + + if (flags & ES_TEXTBOX_GET_CONTENTS_SELECTED_ONLY) { + lineFrom = textbox->carets[0].line; + lineTo = textbox->carets[1].line; + + if (lineFrom > lineTo) { + uintptr_t swap = lineFrom; + lineFrom = lineTo, lineTo = swap; + } + } + + for (uintptr_t i = lineFrom; i <= lineTo; i++) { + DocumentLine *line = &textbox->lines[i]; + + uintptr_t offsetFrom = 0; + uintptr_t offsetTo = line->lengthBytes; + + if (flags & ES_TEXTBOX_GET_CONTENTS_SELECTED_ONLY) { + if (i == lineFrom) { + offsetFrom = TextboxCompareCarets(textbox->carets + 0, textbox->carets + 1) < 0 ? textbox->carets[0].byte : textbox->carets[1].byte; + } + + if (i == lineTo) { + offsetTo = TextboxCompareCarets(textbox->carets + 0, textbox->carets + 1) > 0 ? textbox->carets[0].byte : textbox->carets[1].byte; + } + } + + EsMemoryCopy(buffer + position, GET_BUFFER(line) + offsetFrom, offsetTo - offsetFrom); + position += offsetTo - offsetFrom; + + if (includeNewline && i != lineTo) { + buffer[position++] = '\n'; + } + } + + buffer[position] = 0; + EsAssert(position <= bytes); + if (_bytes) *_bytes = position; + + char *result = (char *) EsHeapReallocate(buffer, position + 1, false); + + if (!result) { + EsHeapFree(buffer); + } + + return result; +} + +double EsTextboxGetContentsAsDouble(EsTextbox *textbox, uint32_t flags) { + size_t bytes; + char *text = EsTextboxGetContents(textbox, &bytes, flags); + double result = EsDoubleParse(text, bytes, nullptr); + EsHeapFree(text); + return result; +} + +bool EsTextboxFind(EsTextbox *textbox, const char *needle, intptr_t _needleBytes, int32_t *_line, int32_t *_byte, uint32_t flags) { + EsMessageMutexCheck(); + + if (_needleBytes == 0) { + return false; + } + + uintptr_t needleBytes = _needleBytes == -1 ? EsCStringLength(needle) : _needleBytes; + uint32_t lineIndex = *_line, byteIndex = *_byte; + bool firstLoop = true; + + while (true) { + DocumentLine *line = &textbox->lines[lineIndex]; + const char *buffer = GET_BUFFER(line); + size_t bufferBytes = line->lengthBytes; + EsAssert(byteIndex <= bufferBytes); // Invalid find byte offset. + + // TODO Case-insensitive search. + // TODO Ignore quotation mark type. + + if (flags & ES_TEXTBOX_FIND_BACKWARDS) { + if (bufferBytes >= needleBytes) { + for (uintptr_t i = byteIndex; i >= needleBytes; i--) { + for (uintptr_t j = 0; j < needleBytes; j++) { + if (buffer[i - needleBytes + j] != needle[j]) { + goto previousPosition; + } + } + + *_line = lineIndex; + *_byte = i - needleBytes; + return true; + + previousPosition:; + } + } + + if ((int32_t) lineIndex <= *_line && !firstLoop) { + return false; + } + + if (lineIndex == 0) { + firstLoop = false; + lineIndex = textbox->lines.Length() - 1; + } else { + lineIndex--; + } + + byteIndex = textbox->lines[lineIndex].lengthBytes; + } else { + if (bufferBytes >= needleBytes) { + for (uintptr_t i = byteIndex; i <= bufferBytes - needleBytes; i++) { + for (uintptr_t j = 0; j < needleBytes; j++) { + if (buffer[i + j] != needle[j]) { + goto nextPosition; + } + } + + *_line = lineIndex; + *_byte = i; + return true; + + nextPosition:; + } + } + + lineIndex++; + + if ((int32_t) lineIndex > *_line && !firstLoop) { + return false; + } + + if (lineIndex == textbox->lines.Length()) { + firstLoop = false; + lineIndex = 0; + } + + byteIndex = 0; + } + } + + return false; +} + +bool TextboxFindCaret(EsTextbox *textbox, int positionX, int positionY, bool secondCaret, int clickChainCount) { + int startLine0 = textbox->carets[0].line, startLine1 = textbox->carets[1].line; + EsRectangle bounds = textbox->GetBounds(); + + if (positionX < 0) { + positionX = 0; + } else if (positionX >= bounds.r) { + positionX = bounds.r - 1; + } + + if (positionY < 0) { + positionY = 0; + } else if (positionY >= bounds.b) { + positionY = bounds.b - 1; + } + + if (clickChainCount >= 4) { + textbox->carets[0].line = 0; + textbox->carets[0].byte = 0; + textbox->carets[1].line = textbox->lines.Length() - 1; + textbox->carets[1].byte = textbox->lines[textbox->lines.Length() - 1].lengthBytes; + } else { + for (uintptr_t i = 0; i < textbox->visibleLines.Length(); i++) { + TextboxVisibleLine *visibleLine = &textbox->visibleLines[i]; + DocumentLine *line = &textbox->lines[textbox->firstVisibleLine + i]; + + EsRectangle lineBounds = ES_RECT_4(textbox->insets.l, bounds.r, + textbox->insets.t + visibleLine->yPosition, + textbox->insets.t + visibleLine->yPosition + line->height); + lineBounds.l -= textbox->scroll.position[0]; + lineBounds.t -= textbox->scroll.position[1]; + lineBounds.b -= textbox->scroll.position[1]; + + if (!((positionY >= lineBounds.t || i + textbox->firstVisibleLine == 0) && (positionY < lineBounds.b + || i + textbox->firstVisibleLine == textbox->lines.Length() - 1))) { + continue; + } + + if (!line->lengthBytes) { + textbox->carets[1].byte = 0; + } else { + DocumentLine *line = &textbox->lines[i + textbox->firstVisibleLine]; + int pointX = positionX + textbox->scroll.position[0] - textbox->insets.l; + if (pointX < 0) pointX = 0; + ptrdiff_t result = TextGetCharacterAtPoint(textbox, &textbox->textStyle, + GET_BUFFER(line), line->lengthBytes, + &pointX, ES_TEXT_GET_CHARACTER_AT_POINT_MIDDLE); + textbox->carets[1].byte = result == -1 ? line->lengthBytes : result; + } + + textbox->carets[1].line = i + textbox->firstVisibleLine; + + break; + } + + if (!secondCaret) { + textbox->carets[0] = textbox->carets[1]; + + if (clickChainCount == 2) { + TextboxMoveCaret(textbox, textbox->carets + 0, MOVE_CARET_BACKWARDS, MOVE_CARET_WORD, true); + TextboxMoveCaret(textbox, textbox->carets + 1, MOVE_CARET_FORWARDS, MOVE_CARET_WORD, true); + textbox->wordSelectionAnchor = textbox->carets[0]; + textbox->wordSelectionAnchor2 = textbox->carets[1]; + } else if (clickChainCount == 3) { + TextboxMoveCaret(textbox, textbox->carets + 0, MOVE_CARET_BACKWARDS, MOVE_CARET_LINE, true); + TextboxMoveCaret(textbox, textbox->carets + 1, MOVE_CARET_FORWARDS, MOVE_CARET_LINE, true); + textbox->wordSelectionAnchor = textbox->carets[0]; + textbox->wordSelectionAnchor2 = textbox->carets[1]; + } + } else { + if (clickChainCount == 2) { + if (TextboxCompareCarets(textbox->carets + 1, textbox->carets + 0) < 0) { + TextboxMoveCaret(textbox, textbox->carets + 1, MOVE_CARET_BACKWARDS, MOVE_CARET_WORD); + textbox->carets[0] = textbox->wordSelectionAnchor2; + } else { + TextboxMoveCaret(textbox, textbox->carets + 1, MOVE_CARET_FORWARDS, MOVE_CARET_WORD); + textbox->carets[0] = textbox->wordSelectionAnchor; + } + } else if (clickChainCount == 3) { + if (TextboxCompareCarets(textbox->carets + 1, textbox->carets + 0) < 0) { + TextboxMoveCaret(textbox, textbox->carets + 1, MOVE_CARET_BACKWARDS, MOVE_CARET_LINE); + textbox->carets[0] = textbox->wordSelectionAnchor2; + } else { + TextboxMoveCaret(textbox, textbox->carets + 1, MOVE_CARET_FORWARDS, MOVE_CARET_LINE); + textbox->carets[0] = textbox->wordSelectionAnchor; + } + } + } + } + + TextboxUpdateCommands(textbox, true); + return textbox->carets[0].line != startLine0 || textbox->carets[1].line != startLine1; +} + +void TextboxMoveCaretToCursor(EsTextbox *textbox, int x, int y, bool doNotMoveIfNoSelection) { + int oldCompare = TextboxCompareCarets(textbox->carets + 0, textbox->carets + 1); + bool hasSelection = oldCompare != 0; + TextboxCaret old[2] = { textbox->carets[0], textbox->carets[1] }; + bool lineChanged = TextboxFindCaret(textbox, x, y, gui.clickChainCount == 1, gui.clickChainCount); + + if (doNotMoveIfNoSelection && TextboxCompareCarets(&old[0], &old[1]) != 0) { + textbox->carets[0] = old[0]; + textbox->carets[1] = old[1]; + } else if (gui.clickChainCount == 1 && !EsKeyboardIsShiftHeld()) { + textbox->carets[0] = textbox->carets[1]; + } + + TextboxUpdateCommands(textbox, true); + textbox->verticalMotionHorizontalDepth = -1; + TextboxRepaintLine(textbox, lineChanged || hasSelection ? -1 : textbox->carets[0].line); + EsTextboxEnsureCaretVisible(textbox); +} + +int ProcessTextboxMarginMessage(EsElement *element, EsMessage *message) { + EsTextbox *textbox = (EsTextbox *) element->parent; + + if (message->type == ES_MSG_PAINT) { + EsPainter *painter = message->painter; + + for (int32_t i = 0; i < (int32_t) textbox->visibleLines.Length(); i++) { + TextboxVisibleLine *visibleLine = &textbox->visibleLines[i]; + DocumentLine *line = &textbox->lines[i + textbox->firstVisibleLine]; + + EsRectangle bounds; + bounds.l = painter->offsetX + element->style->insets.l; + bounds.r = painter->offsetX + painter->width - element->style->insets.r; + bounds.t = painter->offsetY + textbox->insets.t + visibleLine->yPosition - textbox->scroll.position[1]; + bounds.b = bounds.t + line->height; + + char label[64]; + EsTextRun textRun[2] = {}; + element->style->GetTextStyle(&textRun[0].style); + textRun[0].style.figures = ES_TEXT_FIGURE_TABULAR; + textRun[1].offset = EsStringFormat(label, sizeof(label), "%d", i + textbox->firstVisibleLine + 1); + EsTextPlanProperties properties = {}; + properties.flags = ES_TEXT_V_CENTER | ES_TEXT_H_RIGHT | ES_TEXT_ELLIPSIS | ES_TEXT_PLAN_SINGLE_USE; + EsTextPlan *plan = EsTextPlanCreate(element, &properties, bounds, label, textRun, 1); + if (plan) EsDrawText(painter, plan, bounds, nullptr, nullptr); + } + } else if (message->type == ES_MSG_MOUSE_LEFT_DOWN) { + return ES_HANDLED; + } + + return 0; +} + +void TextboxStyleChanged(EsTextbox *textbox) { + textbox->borders = textbox->style->borders; + textbox->insets = textbox->style->insets; + + if (textbox->flags & ES_TEXTBOX_MARGIN) { + int marginWidth = textbox->margin->style->preferredWidth; + textbox->borders.l += marginWidth; + textbox->insets.l += marginWidth + textbox->margin->style->gapMajor; + } + + int lineHeight = TextGetLineHeight(textbox, &textbox->textStyle); + + for (int32_t i = 0; i < (int32_t) textbox->lines.Length(); i++) { + DocumentLine *line = &textbox->lines[i]; + DocumentLine *previous = i ? (&textbox->lines[i - 1]) : nullptr; + line->height = lineHeight; + line->yPosition = previous ? (previous->yPosition + previous->height) : 0; + line->lengthWidth = -1; + textbox->longestLine = -1; + } + + TextboxRefreshVisibleLines(textbox); + TextboxFindLongestLine(textbox); + textbox->scroll.Refresh(); + EsElementRepaint(textbox); +} + +int ProcessTextboxMessage(EsElement *element, EsMessage *message) { + EsTextbox *textbox = (EsTextbox *) element; + + if (!textbox->editing && textbox->overlayCallback) { + int response = textbox->overlayCallback(element, message); + if (response != 0 && message->type != ES_MSG_DESTROY) return response; + } + + int response = textbox->scroll.ReceivedMessage(message); + if (response) return response; + response = ES_HANDLED; + + if (message->type == ES_MSG_PAINT) { + EsPainter *painter = message->painter; + + EsTextSelection selectionProperties = {}; + selectionProperties.hideCaret = (~textbox->state & UI_STATE_FOCUSED) || (textbox->flags & ES_ELEMENT_DISABLED) || !textbox->editing; + selectionProperties.snapCaretToInsets = true; + selectionProperties.background = textbox->style->metrics->selectedBackground; + selectionProperties.foreground = textbox->style->metrics->selectedText; + + EsRectangle clip; + EsRectangleClip(painter->clip, ES_RECT_4(painter->offsetX + textbox->borders.l, + painter->offsetX + painter->width - textbox->borders.r, + painter->offsetY + textbox->borders.t, + painter->offsetY + painter->height - textbox->borders.b), &clip); + + Array textRuns = {}; + + for (int32_t i = 0; i < (int32_t) textbox->visibleLines.Length(); i++) { + TextboxVisibleLine *visibleLine = &textbox->visibleLines[i]; + DocumentLine *line = &textbox->lines[i + textbox->firstVisibleLine]; + + EsRectangle lineBounds = ES_RECT_4(painter->offsetX + textbox->insets.l, + painter->offsetX + painter->width, + painter->offsetY + textbox->insets.t + visibleLine->yPosition, + painter->offsetY + textbox->insets.t + visibleLine->yPosition + line->height); + lineBounds.l -= textbox->scroll.position[0]; + lineBounds.t -= textbox->scroll.position[1]; + lineBounds.b -= textbox->scroll.position[1]; + + if (~textbox->flags & ES_TEXTBOX_MULTILINE) { + lineBounds.b = painter->offsetY + painter->height - textbox->insets.b; + } + + int32_t caret0 = textbox->carets[0].byte, caret1 = textbox->carets[1].byte; + if (textbox->carets[0].line < i + textbox->firstVisibleLine) caret0 = -2; + if (textbox->carets[0].line > i + textbox->firstVisibleLine) caret0 = line->lengthBytes + 2; + if (textbox->carets[1].line < i + textbox->firstVisibleLine) caret1 = -2; + if (textbox->carets[1].line > i + textbox->firstVisibleLine) caret1 = line->lengthBytes + 2; + + if (textbox->carets[1].line == i + textbox->firstVisibleLine && textbox->syntaxHighlightingLanguage) { + EsRectangle line = ES_RECT_4(painter->offsetX, painter->offsetX + painter->width, lineBounds.t, lineBounds.b); + EsDrawBlock(painter, line, textbox->syntaxHighlightingColors[0]); + } + + if (textbox->syntaxHighlightingLanguage && line->lengthBytes) { + if (textRuns.Length()) textRuns.SetLength(0); + textRuns = TextApplySyntaxHighlighting(&textbox->textStyle, textbox->syntaxHighlightingLanguage, + textbox->syntaxHighlightingColors, textRuns, GET_BUFFER(line), line->lengthBytes); + } else { + textRuns.SetLength(2); + textRuns[0].style = textbox->textStyle; + textRuns[0].offset = 0; + textRuns[1].offset = line->lengthBytes; + } + + EsTextPlanProperties properties = {}; + properties.flags = ES_TEXT_V_CENTER | ES_TEXT_H_LEFT | ES_TEXT_PLAN_SINGLE_USE; + selectionProperties.caret0 = caret0; + selectionProperties.caret1 = caret1; + EsTextPlan *plan; + + if (!textRuns.Length()) { + plan = nullptr; + } else if (textRuns[1].offset) { + plan = EsTextPlanCreate(element, &properties, lineBounds, GET_BUFFER(line), textRuns.array, textRuns.Length() - 1); + } else { + textRuns[1].offset = 1; // Make sure that the caret and selection is draw correctly, even on empty lines. + plan = EsTextPlanCreate(element, &properties, lineBounds, " ", textRuns.array, textRuns.Length() - 1); + } + + if (plan) { + EsDrawText(painter, plan, lineBounds, &clip, &selectionProperties); + } + } + + textRuns.Free(); + } else if (message->type == ES_MSG_LAYOUT) { + EsRectangle bounds = textbox->GetBounds(); + + if (textbox->margin) { + int marginWidth = textbox->margin->style->preferredWidth; + textbox->margin->InternalMove(marginWidth, Height(bounds), bounds.l, bounds.t); + } + + TextboxRefreshVisibleLines(textbox); + } else if (message->type == ES_MSG_DESTROY) { + textbox->visibleLines.Free(); + textbox->lines.Free(); + UndoManagerDestroy(&textbox->localUndo); + EsHeapFree(textbox->activeLine); + EsHeapFree(textbox->data); + EsHeapFree(textbox->editStartContent); + } else if (message->type == ES_MSG_KEY_TYPED && !ScancodeIsNonTypeable(message->keyboard.scancode)) { + bool verticalMotion = false; + bool ctrl = message->keyboard.modifiers & ES_MODIFIER_CTRL; + + if (message->keyboard.modifiers & ~(ES_MODIFIER_CTRL | ES_MODIFIER_ALT | ES_MODIFIER_SHIFT | ES_MODIFIER_ALT_GR)) { + // Unused modifier. + return 0; + } + + if (message->keyboard.scancode == ES_SCANCODE_LEFT_ARROW || message->keyboard.scancode == ES_SCANCODE_RIGHT_ARROW + || message->keyboard.scancode == ES_SCANCODE_HOME || message->keyboard.scancode == ES_SCANCODE_END + || message->keyboard.scancode == ES_SCANCODE_UP_ARROW || message->keyboard.scancode == ES_SCANCODE_DOWN_ARROW) { + bool direction = (message->keyboard.scancode == ES_SCANCODE_LEFT_ARROW || message->keyboard.scancode == ES_SCANCODE_HOME + || message->keyboard.scancode == ES_SCANCODE_UP_ARROW) + ? MOVE_CARET_BACKWARDS : MOVE_CARET_FORWARDS; + int moveType = (message->keyboard.scancode == ES_SCANCODE_HOME || message->keyboard.scancode == ES_SCANCODE_END) + ? (ctrl ? MOVE_CARET_ALL : MOVE_CARET_LINE) + : ((message->keyboard.scancode == ES_SCANCODE_UP_ARROW || message->keyboard.scancode == ES_SCANCODE_DOWN_ARROW) + ? MOVE_CARET_VERTICAL : (ctrl ? MOVE_CARET_WORD : MOVE_CARET_SINGLE)); + if (moveType == MOVE_CARET_VERTICAL) verticalMotion = true; + + int32_t lineFrom = textbox->carets[1].line; + + if (message->keyboard.modifiers & ES_MODIFIER_SHIFT) { + TextboxMoveCaret(textbox, &textbox->carets[1], direction, moveType); + } else { + int caretCompare = TextboxCompareCarets(textbox->carets + 1, textbox->carets + 0); + + if ((caretCompare < 0 && direction == MOVE_CARET_BACKWARDS) || (caretCompare > 0 && direction == MOVE_CARET_FORWARDS)) { + textbox->carets[0] = textbox->carets[1]; + TextboxUpdateCommands(textbox, true); + } else if ((caretCompare > 0 && direction == MOVE_CARET_BACKWARDS) || (caretCompare < 0 && direction == MOVE_CARET_FORWARDS)) { + textbox->carets[1] = textbox->carets[0]; + TextboxUpdateCommands(textbox, true); + } else { + TextboxMoveCaret(textbox, &textbox->carets[1], direction, moveType); + textbox->carets[0] = textbox->carets[1]; + TextboxUpdateCommands(textbox, true); + } + } + + int32_t lineTo = textbox->carets[1].line; + if (lineFrom > lineTo) { int32_t t = lineTo; lineTo = lineFrom; lineFrom = t; } + for (int32_t i = lineFrom; i <= lineTo; i++) TextboxRepaintLine(textbox, i); + } else if (message->keyboard.scancode == ES_SCANCODE_PAGE_UP || message->keyboard.scancode == ES_SCANCODE_PAGE_DOWN) { + for (uintptr_t i = 0; i < 10; i++) { + TextboxMoveCaret(textbox, textbox->carets + 1, + message->keyboard.scancode == ES_SCANCODE_PAGE_UP ? MOVE_CARET_BACKWARDS : MOVE_CARET_FORWARDS, + MOVE_CARET_VERTICAL); + } + + if (~message->keyboard.modifiers & ES_MODIFIER_SHIFT) { + textbox->carets[0] = textbox->carets[1]; + TextboxUpdateCommands(textbox, true); + } + + textbox->Repaint(true); + verticalMotion = true; + } else if (message->keyboard.scancode == ES_SCANCODE_BACKSPACE || message->keyboard.scancode == ES_SCANCODE_DELETE) { + if (!textbox->editing) { + EsTextboxStartEdit(textbox); + } + + if (!TextboxCompareCarets(textbox->carets + 0, textbox->carets + 1)) { + TextboxMoveCaret(textbox, textbox->carets + 1, message->keyboard.scancode == ES_SCANCODE_BACKSPACE ? MOVE_CARET_BACKWARDS : MOVE_CARET_FORWARDS, + ctrl ? MOVE_CARET_WORD : MOVE_CARET_SINGLE); + } + + EsTextboxInsert(textbox, EsLiteral("")); + } else if (message->keyboard.scancode == ES_SCANCODE_ENTER && (textbox->flags & ES_TEXTBOX_EDIT_BASED)) { + if (textbox->editing) { + TextboxEndEdit(textbox, false); + } else { + EsTextboxStartEdit(textbox); + } + } else if (message->keyboard.scancode == ES_SCANCODE_ESCAPE && (textbox->flags & ES_TEXTBOX_EDIT_BASED)) { + TextboxEndEdit(textbox, true); + } else if (message->keyboard.scancode == ES_SCANCODE_TAB && (~textbox->flags & ES_TEXTBOX_ALLOW_TABS)) { + response = 0; + } else { + if (!textbox->editing) { + EsTextboxStartEdit(textbox); + } + + const char *inputString = KeyboardLayoutLookup(message->keyboard.scancode, + message->keyboard.modifiers & ES_MODIFIER_SHIFT, message->keyboard.modifiers & ES_MODIFIER_ALT_GR, + true, textbox->flags & ES_TEXTBOX_MULTILINE); + + if (inputString && (message->keyboard.modifiers & ~(ES_MODIFIER_SHIFT | ES_MODIFIER_ALT_GR)) == 0) { + if (textbox->smartQuotes && api.global->useSmartQuotes) { + DocumentLine *currentLine = &textbox->lines[textbox->carets[0].line]; + const char *buffer = GET_BUFFER(currentLine); + bool left = !textbox->carets[0].byte || buffer[textbox->carets[0].byte - 1] == ' '; + + if (inputString[0] == '"' && inputString[1] == 0) { + inputString = left ? "\u201C" : "\u201D"; + } else if (inputString[0] == '\'' && inputString[1] == 0) { + inputString = left ? "\u2018" : "\u2019"; + } + } + + EsTextboxInsert(textbox, inputString, -1); + + if (inputString[0] == '\n' && inputString[1] == 0 && textbox->carets[0].line) { + // Copy the indentation from the previous line. + + DocumentLine *previousLine = &textbox->lines[textbox->carets[0].line - 1]; + const char *buffer = GET_BUFFER(previousLine); + int32_t i = 0; + + for (; i < previousLine->lengthBytes; i++) { + if (buffer[i] != '\t') { + break; + } + } + + EsTextboxInsert(textbox, buffer, i); + } + } else { + response = 0; + } + } + + if (!verticalMotion) { + textbox->verticalMotionHorizontalDepth = -1; + } + + if (response != 0 && (~textbox->state & UI_STATE_DESTROYING)) { + TextboxFindLongestLine(textbox); + textbox->scroll.Refresh(); + EsTextboxEnsureCaretVisible(textbox); + } + } else if (message->type == ES_MSG_MOUSE_LEFT_DOWN || message->type == ES_MSG_MOUSE_RIGHT_DOWN) { + TextboxMoveCaretToCursor(textbox, message->mouseDown.positionX, message->mouseDown.positionY, message->type == ES_MSG_MOUSE_RIGHT_DOWN); + } else if (message->type == ES_MSG_MOUSE_LEFT_CLICK) { + EsTextboxStartEdit(textbox); + } else if (message->type == ES_MSG_FOCUSED_START || message->type == ES_MSG_PRIMARY_CLIPBOARD_UPDATED) { + TextboxUpdateCommands(textbox, false); + EsInstanceSetActiveUndoManager(textbox->instance, textbox->undo); + textbox->Repaint(true); + } else if (message->type == ES_MSG_FOCUSED_END) { + EsCommandSetCallback(EsCommandByID(textbox->instance, ES_COMMAND_SELECT_ALL), nullptr); + EsCommandSetCallback(EsCommandByID(textbox->instance, ES_COMMAND_DELETE), nullptr); + EsCommandSetCallback(EsCommandByID(textbox->instance, ES_COMMAND_COPY), nullptr); + EsCommandSetCallback(EsCommandByID(textbox->instance, ES_COMMAND_CUT), nullptr); + EsCommandSetCallback(EsCommandByID(textbox->instance, ES_COMMAND_PASTE), nullptr); + EsInstanceSetActiveUndoManager(textbox->instance, textbox->instance->undoManager); + textbox->Repaint(true); + } else if (message->type == ES_MSG_STRONG_FOCUS_END) { + TextboxEndEdit(textbox, textbox->flags & ES_TEXTBOX_REJECT_EDIT_IF_LOST_FOCUS); + } else if (message->type == ES_MSG_MOUSE_LEFT_DRAG || message->type == ES_MSG_MOUSE_RIGHT_DRAG || (message->type == ES_MSG_ANIMATE && textbox->scroll.dragScrolling)) { + int32_t lineFrom = textbox->carets[1].line; + + if (gui.lastClickButton == ES_MSG_MOUSE_RIGHT_DOWN && !textbox->inRightClickDrag) { + TextboxMoveCaretToCursor(textbox, message->mouseDragged.originalPositionX, message->mouseDragged.originalPositionY, false); + textbox->inRightClickDrag = true; + } + + EsPoint position = EsMouseGetPosition(textbox); + TextboxFindCaret(textbox, position.x, position.y, true, gui.clickChainCount); + + int32_t lineTo = textbox->carets[1].line; + if (lineFrom > lineTo) { int32_t t = lineTo; lineTo = lineFrom; lineFrom = t; } + for (int32_t i = lineFrom; i <= lineTo; i++) TextboxRepaintLine(textbox, i); + } else if (message->type == ES_MSG_GET_CURSOR) { + if (!textbox->editing || (textbox->flags & ES_ELEMENT_DISABLED)) { + message->cursorStyle = ES_CURSOR_NORMAL; + } else { + return 0; + } + } else if (message->type == ES_MSG_MOUSE_RIGHT_UP) { + textbox->inRightClickDrag = false; + EsMenu *menu = EsMenuCreate(textbox, ES_MENU_AT_CURSOR); + if (!menu) return ES_HANDLED; + + // TODO User customisation of menus. + + if (textbox->editing) { + EsMenuAddCommand(menu, 0, INTERFACE_STRING(CommonUndo), EsCommandByID(textbox->instance, ES_COMMAND_UNDO)); + EsMenuAddSeparator(menu); + } + + EsMenuAddCommand(menu, 0, INTERFACE_STRING(CommonClipboardCut), EsCommandByID(textbox->instance, ES_COMMAND_CUT)); + EsMenuAddCommand(menu, 0, INTERFACE_STRING(CommonClipboardCopy), EsCommandByID(textbox->instance, ES_COMMAND_COPY)); + EsMenuAddCommand(menu, 0, INTERFACE_STRING(CommonClipboardPaste), EsCommandByID(textbox->instance, ES_COMMAND_PASTE)); + + if (textbox->editing) { + EsMenuAddSeparator(menu); + EsMenuAddCommand(menu, 0, INTERFACE_STRING(CommonSelectionSelectAll), EsCommandByID(textbox->instance, ES_COMMAND_SELECT_ALL)); + EsMenuAddCommand(menu, 0, INTERFACE_STRING(CommonSelectionDelete), EsCommandByID(textbox->instance, ES_COMMAND_DELETE)); + + // Add the smart context menu, if necessary. + + if ((~textbox->flags & ES_TEXTBOX_NO_SMART_CONTEXT_MENUS) && textbox->carets[0].line == textbox->carets[1].line) { + int32_t selectionFrom = textbox->carets[0].byte, selectionTo = textbox->carets[1].byte; + + if (selectionTo < selectionFrom) { + int32_t temporary = selectionFrom; + selectionFrom = selectionTo; + selectionTo = temporary; + } + + if (selectionTo - selectionFrom == 7) { + char buffer[7]; + EsMemoryCopy(buffer, GET_BUFFER(&textbox->lines[textbox->carets[0].line]) + selectionFrom, 7); + + if (buffer[0] == '#' && EsCRTisxdigit(buffer[1]) && EsCRTisxdigit(buffer[2]) && EsCRTisxdigit(buffer[3]) + && EsCRTisxdigit(buffer[4]) && EsCRTisxdigit(buffer[5]) && EsCRTisxdigit(buffer[6])) { + // It's a color hex-code! + // TODO Versions with alpha. + EsMenuNextColumn(menu); + ColorPickerCreate(menu, { textbox }, EsColorParse(buffer, 7), false); + + textbox->colorUppercase = true; + + for (uintptr_t i = 1; i <= 6; i++) { + if (buffer[i] >= 'a' && buffer[i] <= 'f') { + textbox->colorUppercase = false; + break; + } + } + } + } + } + } + + EsMenuShow(menu); + } else if (message->type == ES_MSG_COLOR_CHANGED) { + EsAssert(~textbox->flags & ES_TEXTBOX_NO_SMART_CONTEXT_MENUS); // Textbox sent color changed message, but it cannot have smart context menus? + uint32_t color = message->colorChanged.newColor; + + if (message->colorChanged.pickerClosed) { + int32_t selectionFrom = textbox->carets[0].byte, selectionTo = textbox->carets[1].byte; + + if (textbox->carets[0].line == textbox->carets[1].line && AbsoluteInteger(selectionFrom - selectionTo) == 7) { + char buffer[7]; + const char *hexChars = textbox->colorUppercase ? "0123456789ABCDEF" : "0123456789abcedf"; + size_t length = EsStringFormat(buffer, 7, "#%c%c%c%c%c%c", + hexChars[(color >> 20) & 0xF], hexChars[(color >> 16) & 0xF], hexChars[(color >> 12) & 0xF], + hexChars[(color >> 8) & 0xF], hexChars[(color >> 4) & 0xF], hexChars[(color >> 0) & 0xF]); + EsTextboxInsert(textbox, buffer, length, true); + EsTextboxSetSelection(textbox, textbox->carets[1].line, textbox->carets[1].byte - 7, + textbox->carets[1].line, textbox->carets[1].byte); + } + } + } else if (message->type == ES_MSG_GET_WIDTH) { + message->measure.width = textbox->longestLineWidth + textbox->insets.l + textbox->insets.r; + } else if (message->type == ES_MSG_GET_HEIGHT) { + DocumentLine *lastLine = &textbox->lines.Last(); + message->measure.height = lastLine->yPosition + lastLine->height + textbox->insets.t + textbox->insets.b; + } else if (message->type == ES_MSG_SCROLL_X) { + TextboxSetHorizontalScroll(textbox, message->scroll.scroll); + } else if (message->type == ES_MSG_SCROLL_Y) { + TextboxRefreshVisibleLines(textbox, false); + EsElementRepaintForScroll(textbox, message, EsRectangleAdd(element->GetInternalOffset(), element->style->borders)); + } else if (message->type == ES_MSG_GET_INSPECTOR_INFORMATION) { + DocumentLine *firstLine = &textbox->lines.First(); + EsBufferFormat(message->getContent.buffer, "'%s'", firstLine->lengthBytes, GET_BUFFER(firstLine)); + } else if (message->type == ES_MSG_UI_SCALE_CHANGED) { + if (textbox->margin) { + // Force the margin to update its style now, so that its width can be read correctly by TextboxStyleChanged. + textbox->margin->RefreshStyle(nullptr, false, true); + } + + textbox->style->GetTextStyle(&textbox->textStyle); + + if (textbox->overrideTextSize) { + textbox->textStyle.size = textbox->overrideTextSize; + } + + if (textbox->overrideFont.family) { + textbox->textStyle.font = textbox->overrideFont; + } + + TextboxStyleChanged(textbox); + } else { + response = 0; + } + + return response; +} + +EsTextbox *EsTextboxCreate(EsElement *parent, uint64_t flags, const EsStyle *style) { + EsTextbox *textbox = (EsTextbox *) EsHeapAllocate(sizeof(EsTextbox), true); + if (!textbox) return nullptr; + + if (!style) { + if (flags & ES_TEXTBOX_MULTILINE) { + style = ES_STYLE_TEXTBOX_BORDERED_MULTILINE; + } else { + style = ES_STYLE_TEXTBOX_BORDERED_SINGLE; + } + } + + textbox->Initialise(parent, ES_ELEMENT_FOCUSABLE | flags, ProcessTextboxMessage, style); + textbox->cName = "textbox"; + + textbox->scroll.Setup(textbox, + (flags & ES_TEXTBOX_MULTILINE) ? ES_SCROLL_MODE_AUTO : ES_SCROLL_MODE_HIDDEN, + (flags & ES_TEXTBOX_MULTILINE) ? ES_SCROLL_MODE_AUTO : ES_SCROLL_MODE_NONE, + ES_SCROLL_X_DRAG | ES_SCROLL_Y_DRAG); + + textbox->undo = &textbox->localUndo; + textbox->undo->instance = textbox->instance; + + textbox->borders = textbox->style->borders; + textbox->insets = textbox->style->insets; + + textbox->style->GetTextStyle(&textbox->textStyle); + + textbox->smartQuotes = true; + + DocumentLine firstLine = {}; + firstLine.height = TextGetLineHeight(textbox, &textbox->textStyle); + textbox->lines.Add(firstLine); + + TextboxVisibleLine firstVisibleLine = {}; + textbox->visibleLines.Add(firstVisibleLine); + + textbox->activeLineIndex = textbox->verticalMotionHorizontalDepth = textbox->longestLine = -1; + + if (~flags & ES_TEXTBOX_EDIT_BASED) { + textbox->editing = true; + } + + if (textbox->flags & ES_TEXTBOX_MARGIN) { + textbox->margin = EsCustomElementCreate(textbox, ES_CELL_FILL, ES_STYLE_TEXTBOX_MARGIN); + textbox->margin->cName = "margin"; + textbox->margin->messageUser = ProcessTextboxMarginMessage; + + int marginWidth = textbox->margin->style->preferredWidth; + textbox->borders.l += marginWidth; + textbox->insets.l += marginWidth + textbox->margin->style->gapMajor; + } + + return textbox; +} + +void EsTextboxUseNumberOverlay(EsTextbox *textbox, bool defaultBehaviour) { + EsMessageMutexCheck(); + + EsAssert(textbox->flags & ES_TEXTBOX_EDIT_BASED); // Using textbox overlay without edit based mode. + EsAssert(~textbox->flags & ES_TEXTBOX_MULTILINE); // Using number overlay with multiline mode. + + textbox->overlayData = defaultBehaviour; + + textbox->overlayCallback = [] (EsElement *element, EsMessage *message) { + EsTextbox *textbox = (EsTextbox *) element; + bool defaultBehaviour = textbox->overlayData.u; + + if (message->type == ES_MSG_MOUSE_LEFT_DRAG) { + if (!gui.draggingStarted) { + EsMessage m = { ES_MSG_TEXTBOX_NUMBER_DRAG_START }; + EsMessageSend(textbox, &m); + } + + TextboxFindCaret(textbox, message->mouseDragged.originalPositionX, message->mouseDragged.originalPositionY, false, 1); + + EsMessage m = { ES_MSG_TEXTBOX_NUMBER_DRAG_DELTA }; + m.numberDragDelta.delta = message->mouseDragged.originalPositionY - message->mouseDragged.newPositionY; + m.numberDragDelta.fast = EsKeyboardIsShiftHeld(); + m.numberDragDelta.hoverCharacter = textbox->carets[1].byte; + EsMessageSend(textbox, &m); + + EsMouseSetPosition(textbox->window, gui.lastClickX, gui.lastClickY); + } else if (message->type == ES_MSG_KEY_TYPED && message->keyboard.scancode == ES_SCANCODE_UP_ARROW) { + EsMessage m = { ES_MSG_TEXTBOX_NUMBER_DRAG_DELTA }; + m.numberDragDelta.delta = 1; + m.numberDragDelta.fast = EsKeyboardIsShiftHeld(); + m.numberDragDelta.hoverCharacter = 0; + EsMessageSend(textbox, &m); + } else if (message->type == ES_MSG_KEY_TYPED && message->keyboard.scancode == ES_SCANCODE_DOWN_ARROW) { + EsMessage m = { ES_MSG_TEXTBOX_NUMBER_DRAG_DELTA }; + m.numberDragDelta.delta = -1; + m.numberDragDelta.fast = EsKeyboardIsShiftHeld(); + m.numberDragDelta.hoverCharacter = 0; + EsMessageSend(textbox, &m); + } else if (message->type == ES_MSG_MOUSE_LEFT_UP) { + if (gui.draggingStarted) { + EsMessage m = { ES_MSG_TEXTBOX_NUMBER_DRAG_END }; + EsMessageSend(textbox, &m); + } + } else if (message->type == ES_MSG_GET_CURSOR) { + if (gui.draggingStarted) { + message->cursorStyle = ES_CURSOR_BLANK; + } else if (~textbox->flags & ES_ELEMENT_DISABLED) { + message->cursorStyle = ES_CURSOR_RESIZE_VERTICAL; + } else { + message->cursorStyle = ES_CURSOR_NORMAL; + } + } else if (message->type == ES_MSG_TEXTBOX_EDIT_END && defaultBehaviour) { + double oldValue = EsDoubleParse(textbox->editStartContent, textbox->editStartContentBytes, nullptr); + + char *expression = EsTextboxGetContents(textbox); + EsCalculationValue value = EsCalculateFromUserExpression(expression); + EsHeapFree(expression); + + if (value.error) { + return ES_REJECTED; + } else { + EsMessage m = { ES_MSG_TEXTBOX_NUMBER_UPDATED }; + m.numberUpdated.delta = value.number - oldValue; + m.numberUpdated.newValue = value.number; + EsMessageSend(textbox, &m); + + char result[64]; + size_t resultBytes = EsStringFormat(result, sizeof(result), "%F", (double) m.numberUpdated.newValue); + EsTextboxSelectAll(textbox); + EsTextboxInsert(textbox, result, resultBytes); + } + } else if (message->type == ES_MSG_TEXTBOX_NUMBER_DRAG_DELTA && defaultBehaviour) { + TextboxSetActiveLine(textbox, -1); + double oldValue = EsDoubleParse(textbox->data, textbox->lines[0].lengthBytes, nullptr); + double newValue = oldValue + message->numberDragDelta.delta * (message->numberDragDelta.fast ? 10 : 1); + + EsMessage m = { ES_MSG_TEXTBOX_NUMBER_UPDATED }; + m.numberUpdated.delta = newValue - oldValue; + m.numberUpdated.newValue = newValue; + EsMessageSend(textbox, &m); + + char result[64]; + size_t resultBytes = EsStringFormat(result, sizeof(result), "%F", m.numberUpdated.newValue); + EsTextboxSelectAll(textbox); + EsTextboxInsert(textbox, result, resultBytes); + } else { + return 0; + } + + return ES_HANDLED; + }; +} + +void TextboxBreadcrumbOverlayRecreate(EsTextbox *textbox) { + if (textbox->overlayData.p) { + // Remove the old breadcrumb panel. + ((EsElement *) textbox->overlayData.p)->Destroy(); + } + + EsPanel *panel = EsPanelCreate(textbox, ES_PANEL_HORIZONTAL | ES_CELL_FILL | ES_ELEMENT_NO_HOVER, ES_STYLE_BREADCRUMB_BAR_PANEL); + textbox->overlayData = panel; + + if (!panel) { + return; + } + + uint8_t _buffer[256]; + EsBuffer buffer = { .out = _buffer, .bytes = sizeof(_buffer) }; + EsMessage m = { ES_MSG_TEXTBOX_GET_BREADCRUMB }; + m.getBreadcrumb.buffer = &buffer; + + while (true) { + buffer.position = 0, m.getBreadcrumb.icon = 0; + int response = EsMessageSend(textbox, &m); + EsAssert(response != 0); // Must handle ES_MSG_TEXTBOX_GET_BREADCRUMB message for breadcrumb overlay. + if (response == ES_REJECTED) break; + + EsButton *crumb = EsButtonCreate(panel, ES_BUTTON_NOT_FOCUSABLE | ES_BUTTON_COMPACT | ES_CELL_V_FILL, + ES_STYLE_BREADCRUMB_BAR_CRUMB, (char *) buffer.out, buffer.position); + + if (crumb) { + EsButtonSetIcon(crumb, m.getBreadcrumb.icon); + + crumb->userData = m.getBreadcrumb.index; + + crumb->messageUser = [] (EsElement *element, EsMessage *message) { + if (message->type == ES_MSG_MOUSE_LEFT_CLICK) { + EsMessage m = { ES_MSG_TEXTBOX_ACTIVATE_BREADCRUMB }; + m.activateBreadcrumb = element->userData.u; + EsMessageSend(element->parent->parent, &m); + } else { + return 0; + } + + return ES_HANDLED; + }; + } + + m.getBreadcrumb.index++; + } +} + +void EsTextboxUseBreadcrumbOverlay(EsTextbox *textbox) { + EsMessageMutexCheck(); + + EsAssert(textbox->flags & ES_TEXTBOX_EDIT_BASED); // Using textbox overlay without edit based mode. + + // Use this to store the panel containing the breadcrumb buttons. + textbox->overlayData = nullptr; + + textbox->overlayCallback = [] (EsElement *element, EsMessage *message) { + EsTextbox *textbox = (EsTextbox *) element; + + if (message->type == ES_MSG_TEXTBOX_UPDATED) { + TextboxBreadcrumbOverlayRecreate(textbox); + } else if (message->type == ES_MSG_TEXTBOX_EDIT_START) { + ((EsElement *) textbox->overlayData.p)->Destroy(); + textbox->overlayData.p = nullptr; + } else if (message->type == ES_MSG_TEXTBOX_EDIT_END) { + TextboxBreadcrumbOverlayRecreate(textbox); + } else if (message->type == ES_MSG_LAYOUT) { + EsRectangle bounds = textbox->GetBounds(); + ((EsElement *) textbox->overlayData.p)->InternalMove(bounds.r, bounds.b, 0, 0); + } else if (message->type == ES_MSG_PAINT) { + return ES_HANDLED; + } + + return 0; + }; + + TextboxBreadcrumbOverlayRecreate(textbox); +} + +void EsTextboxSetUndoManager(EsTextbox *textbox, EsUndoManager *undoManager) { + EsMessageMutexCheck(); + EsAssert(~textbox->state & UI_STATE_FOCUSED); // Can't change undo manager if the textbox is focused. + EsAssert(textbox->undo == &textbox->localUndo); // This can only be set once. + textbox->undo = undoManager; +} + +void EsTextboxSetTextSize(EsTextbox *textbox, uint16_t size) { + textbox->overrideTextSize = size; + textbox->textStyle.size = size; + TextboxStyleChanged(textbox); +} + +void EsTextboxSetFont(EsTextbox *textbox, EsFont font) { + textbox->overrideFont = font; + textbox->textStyle.font = font; + TextboxStyleChanged(textbox); +} + +void EsTextboxSetupSyntaxHighlighting(EsTextbox *textbox, uint32_t language, uint32_t *customColors, size_t customColorCount) { + textbox->syntaxHighlightingLanguage = language; + + // TODO Load these from the theme file. + textbox->syntaxHighlightingColors[0] = 0x04000000; // Highlighted line. + textbox->syntaxHighlightingColors[1] = 0xFF000000; // Default. + textbox->syntaxHighlightingColors[2] = 0xFFA11F20; // Comment. + textbox->syntaxHighlightingColors[3] = 0xFF037E01; // String. + textbox->syntaxHighlightingColors[4] = 0xFF213EF1; // Number. + textbox->syntaxHighlightingColors[5] = 0xFF7F0480; // Operator. + textbox->syntaxHighlightingColors[6] = 0xFF545D70; // Preprocessor. + textbox->syntaxHighlightingColors[7] = 0xFF17546D; // Keyword. + + if (customColorCount > sizeof(textbox->syntaxHighlightingColors) / sizeof(uint32_t)) { + customColorCount = sizeof(textbox->syntaxHighlightingColors) / sizeof(uint32_t); + } + + EsMemoryCopy(textbox->syntaxHighlightingColors, customColors, customColorCount * sizeof(uint32_t)); + + textbox->Repaint(true); +} + +void EsTextboxEnableSmartQuotes(EsTextbox *textbox, bool enabled) { + textbox->smartQuotes = enabled; +} + +#undef GET_BUFFER diff --git a/desktop/theme.cpp b/desktop/theme.cpp index f798750..1aef088 100644 --- a/desktop/theme.cpp +++ b/desktop/theme.cpp @@ -248,29 +248,6 @@ typedef struct ThemeHeader { // Followed by array of ThemeStyles and then an array of ThemeConstants. } ThemeHeader; -typedef struct BasicFontKerningEntry { - uint16_t leftGlyphIndex, rightGlyphIndex; - int16_t xAdvance; -} BasicFontKerningEntry; - -typedef struct BasicFontGlyph { - uint32_t codepoint; - int16_t xAdvance, xOffset, yOffset; - uint16_t width, height; - uint16_t pointCount; - uint32_t offsetToPoints; // Cubic bezier points. Contains 3*pointCount-2 of (x,y) float pairs. -} BasicFontGlyph; - -typedef struct BasicFontHeader { -#define BASIC_FONT_SIGNATURE (0x83259919) - uint32_t signature; - int32_t ascender, descender; - uint16_t glyphCount; - uint16_t kerningEntries; - // Followed by array of BasicFontGlyph. - // Followed by array of BasicFontKerningEntry. -} BasicFontHeader; - ////////////////////////////////////////// #define THEME_RECT_WIDTH(_r) ((_r).r - (_r).l) diff --git a/res/Fonts/Bitmap Sans Regular 9.font b/res/Fonts/Bitmap Sans Regular 9.font index 5e209210b0f6704d0fa6c0c31f0f0b613440a960..6d037861a2228ce9d5e270379d92e615480ec59c 100644 GIT binary patch literal 2872 zcmZA3UuYaf90%~PSpx!=sr z{PwrIStQk`$fM69qP1i;AC180b6BY`UD+=ZM+~(AaWUYskX?*DLMhW%v!aM7hzM5orYcR z*lVy&j=cff?AY6|Esnhhn|AC&*jC3rf?eU*C$MdfeF3{tTY!~v9fb51Y`fk!=21jv zU^6-{M1;xdx3C@9?{)*5XG!0~u5#jjgw1OEKYzc%uC|8DuWRTJ*qmekz^<`Y<=Kyk zb~mAz!>sT|jrf56t2FG^7_E@Xtya#rpV|!uq z*04-uSw`1SH_G?r2UQOsb*gZDR+^dRyg5B%H z{SG@|&EzGZKVkQ2;~aCX3Hl3mzm2P&2j^i2wFS1uL;4r?fQ>Wb#S~$Yi%wh*>_I24 zA9l!z8-g9SaT3|{k*#^yL#yHxEx;aj#ybFeL|bIz=q=dej=c+e!Wut^CC>3R?D&fD zb2p^3uqU-e_C14l3hb#>}kh-gFR!-+(#jugPm~V&cjZwGDNQ-dUnOQ?n6pp zr?ge?yo3f|&%wG~V*>A^HtcyPZW8u_w#e3+M;l>7YZ*;niK{*ZyV$YKurX~y_vNSW zG;AHL+vRHE_ZF_6&>IPUS$Dg*34>r{WVX?mjboWMgS4-&naXyvne#rtGFyYO-YOPSzT{LCGZ z8st_KzMY7`j^|0KlwVy9Unb}<3r{nQ+d?GIBClf(BF8LP;b~sr5J`>nSXDh#*L}%e zZ+-||c>PW1EUkvk@n59g@ZXi00w0-lnIG`IwZmIaFQ1sNW}ccU31NrJdi08_Nw~$x zPe7goKLxl=gca)9v=Etoenmx@v6Hi78#VGCj_r$L@)-!31|! Jk~EvX{~uBp$cg{} literal 2770 zcmZA3O>Epm6bJA(_Sha@is|GNGUD-gp@Q8Xcr3Qqv5jD=(I>Etu5EyJ0@t_%yV@C#h5cXH=)QRukEj}{hs{}>pQb+P0=5)+dcaec89Zi%zwcSc=k8!PG_hh%Zdi5+~us! zOOg^D^sEWH+gW{$D`59{aV6|tFK#33KF>D84h>CUohzD!EqMLygWW%j<8`3uAnXBW z8Ojv42z$_rI|_TqvlFm~Jv$3~#F%n>HKR9Rhh3cAtBT%+J?h1M0DH`t%}Ycd!4{2i zj(M#K`WW`Oi>sdp7hz8ri`*KI=`+}qF3$Fs(pRu0FYX)I5ijn0*i&BIPq3$5oJ96Q zWa|>_=&CqH|G=K{`U{ZUXN@IpuPVx5$DG-DWYmT|=i1s_rF13ixHG%YQ<{K1Z_MuB z1hbxoz2MBwH>Rzy6Q1pYom^##_QFniwjXxd89#?5u5kwT;?VfH8`BExC1Z(u&)}T` zdwErVF}(vjU_>$-9p7e@bm*D0oPd{i+FuWRECYB5ymNc4k+kWd&akK65U3lh166P`r|H}qdgvW zZ?vf^9J+~1;!l*W6f=R?T~)mq{D@XnmDs5zcH}bDX%vQ0s@>$vxw)mKg@t~<+Auyo zIk~0ka-*v1nu{T*(ZbneYucQrJP(oc6pe*bObBCosE_;1e~_%Fx~fzNEZ><{?f+2>=;TD~yfO`Y2z3E{dc)|eU9 zgYXw4KdE>Syj!`2JC^D G82Jy2)VPrV diff --git a/shared/strings.cpp b/shared/strings.cpp index 5c8255f..be8a2f9 100644 --- a/shared/strings.cpp +++ b/shared/strings.cpp @@ -7,10 +7,11 @@ #define DEFINE_INTERFACE_STRING(name, text) static const char *interfaceString_ ## name = text; #define INTERFACE_STRING(name) interfaceString_ ## name, -1 -#define ELLIPSIS "…" -#define HYPHENATION_POINT "‧" +#define ELLIPSIS "\u2026" +#define HYPHENATION_POINT "\u2027" #define OPEN_SPEECH "\u201C" #define CLOSE_SPEECH "\u201D" + #define SYSTEM_BRAND_SHORT "Essence" // Common. diff --git a/util/build_common.h b/util/build_common.h index fbda9b6..d986e1a 100644 --- a/util/build_common.h +++ b/util/build_common.h @@ -299,8 +299,7 @@ Option options[] = { { "Dependency.stb_image", OPTION_TYPE_BOOL, { .b = true } }, { "Dependency.stb_image_write", OPTION_TYPE_BOOL, { .b = true } }, { "Dependency.stb_sprintf", OPTION_TYPE_BOOL, { .b = true } }, - { "Dependency.HarfBuzz", OPTION_TYPE_BOOL, { .b = true } }, - { "Dependency.FreeType", OPTION_TYPE_BOOL, { .b = true } }, + { "Dependency.FreeTypeAndHarfBuzz", OPTION_TYPE_BOOL, { .b = true } }, { "Emulator.AHCI", OPTION_TYPE_BOOL, { .b = true } }, { "Emulator.ATA", OPTION_TYPE_BOOL, { .b = false } }, { "Emulator.NVMe", OPTION_TYPE_BOOL, { .b = false }, .warning = "Recent versions of Qemu have trouble booting from NVMe drives." }, diff --git a/util/build_core.c b/util/build_core.c index b05bf43..1997048 100644 --- a/util/build_core.c +++ b/util/build_core.c @@ -160,7 +160,6 @@ bool verbose; bool useColoredOutput; bool forEmulator, bootUseVBE, noImportPOSIX; bool systemBuild; -bool convertFonts = true; bool hasNativeToolchain; EsINIState *fontLines; EsINIState *generalOptions; @@ -891,18 +890,7 @@ void OutputSystemConfiguration() { char buffer[4096]; if (fontLines[i].key[0] == '.') { - if (convertFonts) { -#ifdef OS_ESSENCE - // TODO. -#else - snprintf(buffer, sizeof(buffer), "bin/designer --make-font \"res/Fonts/%s\" \"bin/%.*s.dat\"", - fontLines[i].value, (int) fontLines[i].valueBytes - 4, fontLines[i].value); - system(buffer); - FilePrintFormat(file, "%s=|Fonts:/%.*s.dat\n", fontLines[i].key, (int) fontLines[i].valueBytes - 4, fontLines[i].value); -#endif - } else { - FilePrintFormat(file, "%s=:%s\n", fontLines[i].key, fontLines[i].value); - } + FilePrintFormat(file, "%s=:%s\n", fontLines[i].key, fontLines[i].value); } else { size_t bytes = EsINIFormat(fontLines + i, buffer, sizeof(buffer)); FileWrite(file, bytes, buffer); @@ -1223,22 +1211,6 @@ void Install(const char *driveFile, uint64_t partitionSize, const char *partitio ImportNode root = {}; CreateImportNode("root", &root); - // TODO Update this. -#if 0 - if (convertFonts) { - ImportNode *fontsFolder = ImportNodeMakeDirectory(ImportNodeFindChild(&root, SYSTEM_FOLDER_NAME), "Fonts"); - - for (uintptr_t i = 0; i < arrlenu(fontLines); i++) { - if (fontLines[i].key[0] == '.') { - char source[4096], destination[4096]; - snprintf(source, sizeof(source), "bin/%.*s.dat", (int) fontLines[i].valueBytes - 4, fontLines[i].value); - snprintf(destination, sizeof(destination), "%.*s.dat", (int) fontLines[i].valueBytes - 4, fontLines[i].value); - ImportNodeAddFile(fontsFolder, strdup(destination), strdup(source)); - } - } - } -#endif - MountVolume(); Import(root, superblock.root); UnmountVolume(); @@ -1343,13 +1315,9 @@ int main(int argc, char **argv) { strcat(commonCompileFlags, " -DUSE_STB_IMAGE_WRITE "); } else if (0 == strcmp(s.key, "Dependency.stb_sprintf") && atoi(s.value)) { strcat(commonCompileFlags, " -DUSE_STB_SPRINTF "); - } else if (0 == strcmp(s.key, "Dependency.HarfBuzz") && atoi(s.value)) { - strcat(apiLinkFlags2, " -lharfbuzz "); - strcat(commonCompileFlags, " -DUSE_HARFBUZZ "); - } else if (0 == strcmp(s.key, "Dependency.FreeType") && atoi(s.value)) { - strcat(apiLinkFlags2, " -lfreetype "); - strcat(commonCompileFlags, " -DUSE_FREETYPE "); - convertFonts = false; + } else if (0 == strcmp(s.key, "Dependency.FreeTypeAndHarfBuzz") && atoi(s.value)) { + strcat(apiLinkFlags2, " -lharfbuzz -lfreetype "); + strcat(commonCompileFlags, " -DUSE_FREETYPE_AND_HARFBUZZ "); } else if (0 == strcmp(s.key, "Flag._ALWAYS_USE_VBE")) { bootUseVBE = !!atoi(s.value); } else if (0 == strcmp(s.key, "Flag.COM_OUTPUT") && atoi(s.value)) { diff --git a/util/font_editor.c b/util/font_editor.c index 43d0f21..12c2c70 100644 --- a/util/font_editor.c +++ b/util/font_editor.c @@ -1,4 +1,3 @@ -// TODO Required: final file format, line metrics, horizontally scrolling kerning editor. // TODO Extensions: binary search, shifting glyphs in editor, undo/redo. #define UI_IMPLEMENTATION @@ -14,6 +13,7 @@ typedef struct FileHeader { uint16_t glyphCount; uint8_t headerBytes, glyphHeaderBytes; + uint16_t yAscent, yDescent; // Followed by glyphCount copies of FileGlyphHeader, sorted by codepoint. } FileHeader; @@ -49,12 +49,14 @@ UIWindow *window; UITabPane *tabPane; UIElement *editor; UIElement *kerning; -UITextbox *previewText; +UITextbox *previewText, *yAscentTextbox, *yDescentTextbox; +UIScrollBar *kerningHScroll, *kerningVScroll; Glyph *glyphsArray; size_t glyphCount; intptr_t selectedGlyph = -1; int selectedPixelX, selectedPixelY; int selectedPairX, selectedPairY, selectedPairI, selectedPairJ; +int yAscent, yDescent; char *path; void Save(void *cp) { @@ -65,6 +67,8 @@ void Save(void *cp) { .glyphCount = glyphCount, .headerBytes = sizeof(FileHeader), .glyphHeaderBytes = sizeof(FileGlyphHeader), + .yAscent = yAscent, + .yDescent = yDescent, }; fwrite(&header, 1, sizeof(header), f); @@ -122,9 +126,12 @@ void Load() { FILE *f = fopen(path, "rb"); if (f) { - FileHeader header; - fread(&header, 1, sizeof(header), f); + FileHeader header = { 0 }; + fread(&header, 1, 4, f); if (ferror(f)) goto end; + fseek(f, 0, SEEK_SET); + fread(&header, 1, header.headerBytes > sizeof(FileHeader) ? sizeof(FileHeader) : header.headerBytes, f); + fseek(f, header.headerBytes, SEEK_SET); glyphCount = header.glyphCount; glyphsArray = (Glyph *) calloc(glyphCount, sizeof(Glyph)); if (!glyphsArray) goto end; @@ -172,6 +179,12 @@ void Load() { free(bits); fseek(f, position, SEEK_SET); } + + char buffer[32]; + snprintf(buffer, sizeof(buffer), "%d", header.yAscent); + UITextboxReplace(yAscentTextbox, buffer, -1, true); + snprintf(buffer, sizeof(buffer), "%d", header.yDescent); + UITextboxReplace(yDescentTextbox, buffer, -1, true); end:; @@ -207,9 +220,9 @@ int CompareKernings(const void *_a, const void *_b) { void AddGlyph(void *cp) { char *number = NULL; - UIDialogShow(window, 0, "Enter the glyph number:\n%t\n%f%b", &number, "Add"); + UIDialogShow(window, 0, "Enter the glyph number (base 16):\n%t\n%f%b", &number, "Add"); Glyph g = { 0 }; - g.number = atoi(number); + g.number = strtol(number, NULL, 16); free(number); glyphsTable->itemCount = ++glyphCount; glyphsArray = realloc(glyphsArray, sizeof(Glyph) * glyphCount); @@ -240,9 +253,13 @@ int GlyphsTableMessage(UIElement *element, UIMessage message, int di, void *dp) m->isSelected = selectedGlyph == m->index; if (m->column == 0) { - return snprintf(m->buffer, m->bufferBytes, "%c", glyphsArray[m->index].number); + if (glyphsArray[m->index].number < 256) { + return snprintf(m->buffer, m->bufferBytes, "%c", glyphsArray[m->index].number); + } else { + return 0; + } } else if (m->column == 1) { - return snprintf(m->buffer, m->bufferBytes, "%d", glyphsArray[m->index].number); + return snprintf(m->buffer, m->bufferBytes, "U+%.4X", glyphsArray[m->index].number); } } else if (message == UI_MSG_LEFT_DOWN || message == UI_MSG_MOUSE_DRAG) { int index = UITableHitTest((UITable *) element, element->window->cursorX, element->window->cursorY); @@ -301,10 +318,13 @@ int GetAdvance(int leftGlyph, int rightGlyph, bool *hasKerningEntry) { } void DrawPreviewText(UIPainter *painter, UIElement *element, Glyph *g) { - UIDrawBlock(painter, UI_RECT_4(element->clip.r - 100, element->clip.r, element->clip.t, element->clip.t + 50), 0xFFFFFFFF); + UIDrawBlock(painter, UI_RECT_4(element->bounds.r - 100, element->bounds.r, element->bounds.t, element->bounds.t + 50), 0xFFFFFFFF); + UIDrawBlock(painter, UI_RECT_4(element->bounds.r - 100, element->bounds.r, element->bounds.t + 25 - yAscent, element->bounds.t + 26 - yAscent), 0xFF88FF88); + UIDrawBlock(painter, UI_RECT_4(element->bounds.r - 100, element->bounds.r, element->bounds.t + 25, element->bounds.t + 26), 0xFF88FF88); + UIDrawBlock(painter, UI_RECT_4(element->bounds.r - 100, element->bounds.r, element->bounds.t + 25 + yDescent, element->bounds.t + 26 + yDescent), 0xFF88FF88); if (previewText->bytes == 0 && g) { - DrawGlyph(painter, g, element->clip.r - 100 + 5, element->clip.t + 25); + DrawGlyph(painter, g, element->bounds.r - 100 + 5, element->bounds.t + 25); return; } @@ -317,7 +337,7 @@ void DrawPreviewText(UIPainter *painter, UIElement *element, Glyph *g) { for (uintptr_t j = 0; j < glyphCount; j++) { if (glyphsArray[j].number == previewText->string[i]) { if (previous != -1) px += GetAdvance(previous, j, NULL); - DrawGlyph(painter, &glyphsArray[j], element->clip.r - 100 + 5 + px, element->clip.t + 25); + DrawGlyph(painter, &glyphsArray[j], element->bounds.r - 100 + 5 + px, element->bounds.t + 25); previous = j; break; } @@ -418,7 +438,7 @@ int KerningEditorMessage(UIElement *element, UIMessage message, int di, void *dp UIPainter *painter = (UIPainter *) dp; UIDrawBlock(painter, element->bounds, 0xD0D1D4); - int x = element->bounds.l + 20, y = element->bounds.t + 20; + int x = element->bounds.l + 20 - kerningHScroll->position, y = element->bounds.t + 20 - kerningVScroll->position; selectedPairI = -1, selectedPairJ = -1; @@ -432,7 +452,7 @@ int KerningEditorMessage(UIElement *element, UIMessage message, int di, void *dp UIRectangle border = UI_RECT_4(x - 5, x + 20, y - 15, y + 5); if (hasKerningEntry) { - UIDrawBorder(painter, border, 0xFF0099FF, UI_RECT_1(1)); + UIDrawBorder(painter, border, 0xFF0099FF, UI_RECT_1(2)); } if (selectedPairX == (x - 20 - element->bounds.l) / 25 && selectedPairY == (y - 20 - element->bounds.t) / 20) { @@ -443,13 +463,29 @@ int KerningEditorMessage(UIElement *element, UIMessage message, int di, void *dp x += 25; } - x = element->bounds.l + 20; + x = element->bounds.l + 20 - kerningHScroll->position; y += 20; } DrawPreviewText(painter, element, NULL); - } else if (message == UI_MSG_GET_HEIGHT) { - return 20 * glyphCount + 40; + } else if (message == UI_MSG_LAYOUT) { + { + kerningHScroll->maximum = 25 * glyphCount + 40; + kerningHScroll->page = UI_RECT_WIDTH(element->bounds); + UIRectangle scrollBarBounds = element->bounds; + scrollBarBounds.r = scrollBarBounds.r - UI_SIZE_SCROLL_BAR * element->window->scale; + scrollBarBounds.t = scrollBarBounds.b - UI_SIZE_SCROLL_BAR * element->window->scale; + UIElementMove(&kerningHScroll->e, scrollBarBounds, true); + } + + { + kerningVScroll->maximum = 20 * glyphCount + 40; + kerningVScroll->page = UI_RECT_HEIGHT(element->bounds); + UIRectangle scrollBarBounds = element->bounds; + scrollBarBounds.b = scrollBarBounds.b - UI_SIZE_SCROLL_BAR * element->window->scale; + scrollBarBounds.l = scrollBarBounds.r - UI_SIZE_SCROLL_BAR * element->window->scale; + UIElementMove(&kerningVScroll->e, scrollBarBounds, true); + } } else if (message == UI_MSG_LEFT_DOWN || message == UI_MSG_RIGHT_DOWN) { int delta = message == UI_MSG_LEFT_DOWN ? 1 : -1; @@ -491,6 +527,10 @@ int KerningEditorMessage(UIElement *element, UIMessage message, int di, void *dp selectedPairY = pairY; UIElementRepaint(element, NULL); } + } else if (message == UI_MSG_SCROLLED) { + UIElementRepaint(element, NULL); + } else if (message == UI_MSG_MOUSE_WHEEL) { + return UIElementMessage(&kerningVScroll->e, message, di, dp); } return 0; @@ -505,6 +545,17 @@ int PreviewTextMessage(UIElement *element, UIMessage message, int di, void *dp) return 0; } +int NumberTextboxMessage(UIElement *element, UIMessage message, int di, void *dp) { + if (message == UI_MSG_VALUE_CHANGED) { + UITextbox *textbox = (UITextbox *) element; + char buffer[32]; + snprintf(buffer, sizeof(buffer), "%.*s", (int) textbox->bytes, textbox->string); + *(int *) element->cp = atoi(buffer); + } + + return 0; +} + int main(int argc, char **argv) { if (argc != 2) { fprintf(stderr, "Usage: %s \n", argv[0]); @@ -514,6 +565,7 @@ int main(int argc, char **argv) { path = argv[1]; UIInitialise(); + ui.theme = _uiThemeClassic; window = UIWindowCreate(0, UI_ELEMENT_PARENT_PUSH, "Font Editor", 1024, 768); UIPanelCreate(0, UI_ELEMENT_PARENT_PUSH | UI_PANEL_EXPAND); @@ -527,11 +579,27 @@ int main(int argc, char **argv) { previewText->e.messageUser = PreviewTextMessage; UIParentPop(); - tabPane = UITabPaneCreate(0, UI_ELEMENT_PARENT_PUSH | UI_ELEMENT_V_FILL, "Select glyph\tEdit\tKerning"); + tabPane = UITabPaneCreate(0, UI_ELEMENT_PARENT_PUSH | UI_ELEMENT_V_FILL, "Glyphs\tEdit\tKerning\tGeneral"); glyphsTable = UITableCreate(0, 0, "ASCII\tNumber"); glyphsTable->e.messageUser = GlyphsTableMessage; editor = UIElementCreate(sizeof(UIElement), 0, 0, GlyphEditorMessage, "Glyph editor"); - kerning = UIElementCreate(sizeof(UIElement), &UIPanelCreate(0, UI_PANEL_SCROLL)->e, UI_ELEMENT_H_FILL, KerningEditorMessage, "Kerning editor"); + kerning = UIElementCreate(sizeof(UIElement), 0, 0, KerningEditorMessage, "Kerning editor"); + kerningHScroll = UIScrollBarCreate(kerning, UI_SCROLL_BAR_HORIZONTAL); + kerningVScroll = UIScrollBarCreate(kerning, 0); + + UIPanelCreate(0, UI_ELEMENT_PARENT_PUSH | UI_PANEL_GRAY | UI_PANEL_MEDIUM_SPACING | UI_PANEL_SCROLL); + UIPanelCreate(0, UI_ELEMENT_PARENT_PUSH | UI_PANEL_EXPAND | UI_PANEL_MEDIUM_SPACING); + UILabelCreate(0, 0, "Y ascent:", -1); + yAscentTextbox = UITextboxCreate(&UIPanelCreate(0, UI_PANEL_HORIZONTAL)->e, 0); + yAscentTextbox->e.cp = &yAscent; + yAscentTextbox->e.messageUser = NumberTextboxMessage; + UILabelCreate(0, 0, "Y descent:", -1); + yDescentTextbox = UITextboxCreate(&UIPanelCreate(0, UI_PANEL_HORIZONTAL)->e, 0); + yDescentTextbox->e.cp = &yDescent; + yDescentTextbox->e.messageUser = NumberTextboxMessage; + UILabelCreate(0, 0, "The sum of the ascent and descent determine the line height.", -1); + UIParentPop(); + UIParentPop(); UIWindowRegisterShortcut(window, (UIShortcut) { .code = UI_KEYCODE_LETTER('S'), .ctrl = true, .invoke = Save });