// This file is part of the Essence operating system. // It is released under the terms of the MIT license -- see LICENSE.md. // Written by: nakst. #define ES_INSTANCE_TYPE Instance #include #include // TODO Document save/load model, then merge into API. // TODO Replace toolbar, then merge into API. // TODO Merge Format menu into API. // TODO Word wrap (textbox feature). // TODO Possible extension features: // - Block selection // - Folding // - Tab settings and auto-indent // - Macros // - Status bar // - Goto line // - Find in files // - Convert case // - Sort lines // - Trim trailing space // - Indent/comment/join/split shortcuts #define SETTINGS_FILE "|Settings:/Default.ini" const EsInstanceClassEditorSettings editorSettings = { INTERFACE_STRING(TextEditorNewFileName), INTERFACE_STRING(TextEditorNewDocument), ES_ICON_TEXT, }; const EsStyle styleFormatPopupColumn = { .metrics = { .mask = ES_THEME_METRICS_GAP_MAJOR, .gapMajor = 5, }, }; struct Instance : EsInstance { EsTextbox *textboxDocument, *textboxSearch; EsElement *toolbarMain, *toolbarSearch; EsTextDisplay *displaySearch; EsButton *buttonFormat; EsCommand commandFindNext, commandFindPrevious, commandFind, commandFormat, commandZoomIn, commandZoomOut; uint32_t syntaxHighlightingLanguage; int32_t textSize; int32_t scrollCumulative; }; const int presetTextSizes[] = { 8, 9, 10, 11, 12, 13, 14, 16, 18, 24, 30, 36, 48, 60, 72, 96, 120, 144, }; int32_t globalTextSize = 10; void Find(Instance *instance, bool backwards) { EsWindowSwitchToolbar(instance->window, instance->toolbarSearch, ES_TRANSITION_SLIDE_UP); size_t needleBytes; char *needle = EsTextboxGetContents(instance->textboxSearch, &needleBytes); int32_t line0, byte0, line1, byte1; EsTextboxGetSelection(instance->textboxDocument, &line0, &byte0, &line1, &byte1); if (backwards) { if (line1 < line0) { line0 = line1; byte0 = byte1; } else if (line1 == line0 && byte1 < byte0) { byte0 = byte1; } } else { if (line1 > line0) { line0 = line1; byte0 = byte1; } else if (line1 == line0 && byte1 > byte0) { byte0 = byte1; } } bool found = EsTextboxFind(instance->textboxDocument, needle, needleBytes, &line0, &byte0, backwards ? ES_TEXTBOX_FIND_BACKWARDS : ES_FLAGS_DEFAULT); if (found) { EsTextDisplaySetContents(instance->displaySearch, ""); EsTextboxSetSelection(instance->textboxDocument, line0, byte0, line0, byte0 + needleBytes); EsTextboxEnsureCaretVisible(instance->textboxDocument, true); EsElementFocus(instance->textboxDocument); } else if (!needleBytes) { EsTextDisplaySetContents(instance->displaySearch, INTERFACE_STRING(CommonSearchPrompt2)); EsElementFocus(instance->textboxSearch); } else { EsTextDisplaySetContents(instance->displaySearch, INTERFACE_STRING(CommonSearchNoMatches)); EsElementFocus(instance->textboxSearch); } EsHeapFree(needle); } void SetLanguage(Instance *instance, uint32_t newLanguage) { EsFont font = {}; font.family = newLanguage ? ES_FONT_MONOSPACED : ES_FONT_SANS; font.weight = ES_FONT_REGULAR; EsTextboxSetFont(instance->textboxDocument, font); instance->syntaxHighlightingLanguage = newLanguage; EsTextboxSetupSyntaxHighlighting(instance->textboxDocument, newLanguage); EsTextboxEnableSmartReplacement(instance->textboxDocument, !newLanguage); } void FormatPopupCreate(Instance *instance) { EsMenu *menu = EsMenuCreate(instance->buttonFormat, ES_FLAGS_DEFAULT); EsPanel *panel = EsPanelCreate(menu, ES_PANEL_HORIZONTAL, ES_STYLE_PANEL_POPUP); { EsPanel *column = EsPanelCreate(panel, ES_FLAGS_DEFAULT, EsStyleIntern(&styleFormatPopupColumn)); EsTextDisplayCreate(column, ES_CELL_H_EXPAND, ES_STYLE_TEXT_LABEL, INTERFACE_STRING(CommonFormatSize)); EsListView *list = EsListViewCreate(column, ES_LIST_VIEW_CHOICE_SELECT | ES_LIST_VIEW_FIXED_ITEMS, ES_STYLE_LIST_CHOICE_BORDERED); size_t presetSizeCount = sizeof(presetTextSizes) / sizeof(presetTextSizes[0]); int currentSize = instance->textSize; char buffer[64]; if (currentSize < presetTextSizes[0]) { // The current size is not in the list; add it. EsListViewFixedItemSetString(list, EsListViewFixedItemInsert(list, currentSize), 0, buffer, EsStringFormat(buffer, sizeof(buffer), "%d pt", currentSize)); } for (uintptr_t i = 0; i < presetSizeCount; i++) { EsListViewFixedItemSetString(list, EsListViewFixedItemInsert(list, presetTextSizes[i]), 0, buffer, EsStringFormat(buffer, sizeof(buffer), "%d pt", presetTextSizes[i])); if (currentSize > presetTextSizes[i] && (i == presetSizeCount - 1 || (i != presetSizeCount - 1 && currentSize < presetTextSizes[i + 1]))) { // The current size is not in the list; add it. EsListViewFixedItemSetString(list, EsListViewFixedItemInsert(list, currentSize), 0, buffer, EsStringFormat(buffer, sizeof(buffer), "%d pt", currentSize)); } } EsListViewFixedItemSelect(list, currentSize); list->messageUser = [] (EsElement *element, EsMessage *message) { if (message->type == ES_MSG_LIST_VIEW_SELECT) { Instance *instance = element->instance; EsGeneric newSize; if (EsListViewFixedItemGetSelected(((EsListView *) element), &newSize)) { globalTextSize = instance->textSize = newSize.u; EsTextboxSetTextSize(instance->textboxDocument, instance->textSize); } } return 0; }; } { EsPanel *column = EsPanelCreate(panel, ES_FLAGS_DEFAULT, EsStyleIntern(&styleFormatPopupColumn)); EsTextDisplayCreate(column, ES_CELL_H_EXPAND, ES_STYLE_TEXT_LABEL, INTERFACE_STRING(CommonFormatLanguage)); EsListView *list = EsListViewCreate(column, ES_LIST_VIEW_CHOICE_SELECT | ES_LIST_VIEW_FIXED_ITEMS, ES_STYLE_LIST_CHOICE_BORDERED); EsListViewFixedItemSetString(list, EsListViewFixedItemInsert(list, 0), 0, INTERFACE_STRING(CommonFormatPlainText)); EsListViewFixedItemSetString(list, EsListViewFixedItemInsert(list, ES_SYNTAX_HIGHLIGHTING_LANGUAGE_C), 0, "C/C++", -1); EsListViewFixedItemSetString(list, EsListViewFixedItemInsert(list, ES_SYNTAX_HIGHLIGHTING_LANGUAGE_INI), 0, "Ini", -1); EsListViewFixedItemSelect(list, instance->syntaxHighlightingLanguage); list->messageUser = [] (EsElement *element, EsMessage *message) { if (message->type == ES_MSG_LIST_VIEW_SELECT) { Instance *instance = element->instance; EsGeneric newLanguage; if (EsListViewFixedItemGetSelected(((EsListView *) element), &newLanguage)) { SetLanguage(instance, newLanguage.u); } } return 0; }; } EsMenuShow(menu); } void CommandZoom(Instance *instance, EsElement *, EsCommand *command) { int32_t delta = instance->scrollCumulative > 0 ? instance->scrollCumulative / ES_SCROLL_WHEEL_NOTCH : -(-instance->scrollCumulative / ES_SCROLL_WHEEL_NOTCH); instance->scrollCumulative -= delta * ES_SCROLL_WHEEL_NOTCH; if (command) delta += command->data.i; intptr_t presetSizeCount = sizeof(presetTextSizes) / sizeof(presetTextSizes[0]); int32_t newIndex = delta; for (intptr_t i = 0; i <= presetSizeCount; i++) { if (i == presetSizeCount || presetTextSizes[i] > instance->textSize) { newIndex = i - 1 + delta; break; } } if (newIndex < 0) newIndex = 0; if (newIndex > presetSizeCount - 1) newIndex = presetSizeCount - 1; if (instance->textSize != presetTextSizes[newIndex]) { globalTextSize = instance->textSize = presetTextSizes[newIndex]; EsTextboxSetTextSize(instance->textboxDocument, instance->textSize); } } int TextboxDocumentMessage(EsElement *element, EsMessage *message) { if (message->type == ES_MSG_SCROLL_WHEEL && EsKeyboardIsCtrlHeld()) { // TODO Maybe detecting the input needed to do this (Ctrl+Scroll) should be dealt with in the API, // so that the user could potentially customize it. Instance *instance = element->instance; instance->scrollCumulative += message->scrollWheel.dy; CommandZoom(instance, element, nullptr); return ES_HANDLED; } else { return 0; } } bool StringEndsWith(const char *string, size_t stringBytes, const char *prefix, size_t prefixBytes, bool caseInsensitive) { string += stringBytes - 1; prefix += prefixBytes - 1; while (true) { if (!prefixBytes) return true; if (!stringBytes) return false; char c1 = *string; char c2 = *prefix; if (caseInsensitive) { if (c1 >= 'a' && c1 <= 'z') c1 = c1 - 'a' + 'A'; if (c2 >= 'a' && c2 <= 'z') c2 = c2 - 'a' + 'A'; } if (c1 != c2) return false; stringBytes--; prefixBytes--; string--; prefix--; } } int InstanceCallback(Instance *instance, EsMessage *message) { if (message->type == ES_MSG_INSTANCE_SAVE) { size_t byteCount; char *contents = EsTextboxGetContents(instance->textboxDocument, &byteCount); EsFileStoreWriteAll(message->instanceSave.file, contents, byteCount); bool fileNameContainsExtension = false; for (uintptr_t i = 0; i < (uintptr_t) message->instanceSave.nameOrPathBytes && i < 5; i++) { if (message->instanceSave.nameOrPath[message->instanceSave.nameOrPathBytes - i - 1] == '.') { fileNameContainsExtension = true; } } if (fileNameContainsExtension) { // Don't set a content type, it should be matched from the file extension. } else { EsUniqueIdentifier plainText = (EsUniqueIdentifier) {{ 0x25, 0x65, 0x4C, 0x89, 0xE7, 0x29, 0xEA, 0x9E, 0xB5, 0xBE, 0xB5, 0xCA, 0xA7, 0x60, 0xBD, 0x3D }}; EsFileStoreSetContentType(message->instanceSave.file, plainText); } EsHeapFree(contents); EsInstanceSaveComplete(instance, message->instanceSave.file, true); } else if (message->type == ES_MSG_INSTANCE_OPEN) { size_t fileSize; char *file = (char *) EsFileStoreReadAll(message->instanceOpen.file, &fileSize); if (!file) { EsInstanceOpenComplete(instance, message->instanceOpen.file, false); } else if (!EsUTF8IsValid(file, fileSize)) { EsInstanceOpenComplete(instance, message->instanceOpen.file, false); } else { EsTextboxSelectAll(instance->textboxDocument); EsTextboxInsert(instance->textboxDocument, file, fileSize); EsTextboxSetSelection(instance->textboxDocument, 0, 0, 0, 0); EsElementRelayout(instance->textboxDocument); if (StringEndsWith(message->instanceOpen.nameOrPath, message->instanceOpen.nameOrPathBytes, EsLiteral(".c"), true) || StringEndsWith(message->instanceOpen.nameOrPath, message->instanceOpen.nameOrPathBytes, EsLiteral(".cpp"), true) || StringEndsWith(message->instanceOpen.nameOrPath, message->instanceOpen.nameOrPathBytes, EsLiteral(".h"), true)) { SetLanguage(instance, ES_SYNTAX_HIGHLIGHTING_LANGUAGE_C); } else if (StringEndsWith(message->instanceOpen.nameOrPath, message->instanceOpen.nameOrPathBytes, EsLiteral(".ini"), true)) { SetLanguage(instance, ES_SYNTAX_HIGHLIGHTING_LANGUAGE_INI); } else { SetLanguage(instance, 0); } EsInstanceOpenComplete(instance, message->instanceOpen.file, true); } EsHeapFree(file); } else { return 0; } return ES_HANDLED; } void ProcessApplicationMessage(EsMessage *message) { if (message->type == ES_MSG_INSTANCE_CREATE) { Instance *instance = EsInstanceCreate(message, INTERFACE_STRING(TextEditorTitle)); instance->callback = InstanceCallback; EsInstanceSetClassEditor(instance, &editorSettings); EsWindow *window = instance->window; EsWindowSetIcon(window, ES_ICON_ACCESSORIES_TEXT_EDITOR); EsButton *button; // Commands: uint32_t stableID = 1; EsCommandRegister(&instance->commandFindNext, instance, INTERFACE_STRING(CommonSearchNext), [] (Instance *instance, EsElement *, EsCommand *) { Find(instance, false); }, stableID++, "F3", true); EsCommandRegister(&instance->commandFindPrevious, instance, INTERFACE_STRING(CommonSearchPrevious), [] (Instance *instance, EsElement *, EsCommand *) { Find(instance, true); }, stableID++, "Shift+F3", true); EsCommandRegister(&instance->commandFind, instance, INTERFACE_STRING(CommonSearchOpen), [] (Instance *instance, EsElement *, EsCommand *) { EsWindowSwitchToolbar(instance->window, instance->toolbarSearch, ES_TRANSITION_ZOOM_OUT); EsElementFocus(instance->textboxSearch); }, stableID++, "Ctrl+F", true); EsCommandRegister(&instance->commandFormat, instance, INTERFACE_STRING(CommonFormatPopup), [] (Instance *instance, EsElement *, EsCommand *) { FormatPopupCreate(instance); }, stableID++, "Ctrl+Alt+T", true); EsCommandRegister(&instance->commandZoomIn, instance, INTERFACE_STRING(CommonZoomIn), CommandZoom, stableID++, "Ctrl+=", true)->data.i = 1; EsCommandRegister(&instance->commandZoomOut, instance, INTERFACE_STRING(CommonZoomOut), CommandZoom, stableID++, "Ctrl+-", true)->data.i = -1; // Content: EsPanel *panel = EsPanelCreate(window, ES_CELL_FILL, ES_STYLE_PANEL_WINDOW_DIVIDER); uint64_t documentFlags = ES_CELL_FILL | ES_TEXTBOX_MULTILINE | ES_TEXTBOX_ALLOW_TABS | ES_TEXTBOX_MARGIN; instance->textboxDocument = EsTextboxCreate(panel, documentFlags, ES_STYLE_TEXTBOX_NO_BORDER); instance->textboxDocument->cName = "document"; instance->textboxDocument->messageUser = TextboxDocumentMessage; instance->textSize = globalTextSize; EsTextboxSetTextSize(instance->textboxDocument, globalTextSize); EsTextboxSetUndoManager(instance->textboxDocument, instance->undoManager); EsElementFocus(instance->textboxDocument); // Main toolbar: EsElement *toolbarMain = instance->toolbarMain = EsWindowGetToolbar(window, true); EsFileMenuAddToToolbar(toolbarMain); button = EsButtonCreate(toolbarMain, ES_FLAGS_DEFAULT, {}, INTERFACE_STRING(CommonSearchOpen)); button->accessKey = 'S'; EsButtonSetIcon(button, ES_ICON_EDIT_FIND_SYMBOLIC); EsButtonOnCommand(button, [] (Instance *instance, EsElement *, EsCommand *) { EsWindowSwitchToolbar(instance->window, instance->toolbarSearch, ES_TRANSITION_SLIDE_UP); EsElementFocus(instance->textboxSearch); }); EsSpacerCreate(toolbarMain, ES_CELL_H_FILL); button = EsButtonCreate(toolbarMain, ES_BUTTON_DROPDOWN, {}, INTERFACE_STRING(CommonFormatPopup)); button->accessKey = 'M'; EsButtonSetIcon(button, ES_ICON_FORMAT_TEXT_LARGER_SYMBOLIC); EsCommandAddButton(&instance->commandFormat, button); instance->buttonFormat = button; EsWindowSwitchToolbar(window, toolbarMain, ES_TRANSITION_NONE); // Search toolbar: EsElement *toolbarSearch = instance->toolbarSearch = EsWindowGetToolbar(window, true); button = EsButtonCreate(toolbarSearch, ES_FLAGS_DEFAULT, 0); button->cName = "go back", button->accessKey = 'X'; EsButtonSetIcon(button, ES_ICON_GO_FIRST_SYMBOLIC); EsButtonOnCommand(button, [] (Instance *instance, EsElement *, EsCommand *) { EsWindowSwitchToolbar(instance->window, instance->toolbarMain, ES_TRANSITION_SLIDE_DOWN); }); EsSpacerCreate(toolbarSearch, ES_FLAGS_DEFAULT, 0, 14, 0); EsTextDisplayCreate(toolbarSearch, ES_FLAGS_DEFAULT, 0, INTERFACE_STRING(CommonSearchPrompt)); instance->textboxSearch = EsTextboxCreate(toolbarSearch, ES_FLAGS_DEFAULT, {}); instance->textboxSearch->cName = "search textbox"; instance->textboxSearch->accessKey = 'S'; instance->textboxSearch->messageUser = [] (EsElement *element, EsMessage *message) { Instance *instance = element->instance; if (message->type == ES_MSG_KEY_DOWN && message->keyboard.scancode == ES_SCANCODE_ENTER) { EsCommand *command = (message->keyboard.modifiers & ES_MODIFIER_SHIFT) ? &instance->commandFindPrevious : &instance->commandFindNext; command->callback(instance, element, command); return ES_HANDLED; } else if (message->type == ES_MSG_KEY_DOWN && message->keyboard.scancode == ES_SCANCODE_ESCAPE) { EsWindowSwitchToolbar(instance->window, instance->toolbarMain, ES_TRANSITION_SLIDE_DOWN); EsElementFocus(instance->textboxDocument); return ES_HANDLED; } else if (message->type == ES_MSG_FOCUSED_START) { EsTextboxSelectAll(instance->textboxSearch); } return 0; }; EsSpacerCreate(toolbarSearch, ES_FLAGS_DEFAULT, 0, 7, 0); instance->displaySearch = EsTextDisplayCreate(toolbarSearch, ES_CELL_H_FILL, {}, ""); EsPanel *buttonGroup = EsPanelCreate(toolbarSearch, ES_PANEL_HORIZONTAL | ES_ELEMENT_AUTO_GROUP); button = EsButtonCreate(buttonGroup, ES_FLAGS_DEFAULT, {}, INTERFACE_STRING(CommonSearchNext)); button->accessKey = 'N'; EsCommandAddButton(&instance->commandFindNext, button); EsSpacerCreate(buttonGroup, ES_CELL_V_FILL, ES_STYLE_TOOLBAR_BUTTON_GROUP_SEPARATOR); button = EsButtonCreate(buttonGroup, ES_FLAGS_DEFAULT, {}, INTERFACE_STRING(CommonSearchPrevious)); button->accessKey = 'P'; EsCommandAddButton(&instance->commandFindPrevious, button); } else if (message->type == ES_MSG_APPLICATION_EXIT) { EsBuffer buffer = {}; buffer.canGrow = true; EsBufferFormat(&buffer, "[general]\ntext_size=%d\n", globalTextSize); EsFileWriteAll(EsLiteral(SETTINGS_FILE), buffer.out, buffer.position); EsHeapFree(buffer.out); } } void _start() { _init(); size_t settingsFileBytes = 0; char *settingsFile = (char *) EsFileReadAll(EsLiteral(SETTINGS_FILE), &settingsFileBytes); EsINIState state = { .buffer = settingsFile, .bytes = settingsFileBytes }; while (EsINIParse(&state)) { if (0 == EsStringCompareRaw(state.section, state.sectionBytes, EsLiteral("general"))) { if (0 == EsStringCompareRaw(state.key, state.keyBytes, EsLiteral("text_size"))) { globalTextSize = EsIntegerParse(state.value, state.valueBytes); } } } EsHeapFree(settingsFile); while (true) { ProcessApplicationMessage(EsMessageReceive()); } }