diff --git a/demo/src/common.c b/demo/src/common.c index 78071e5..2437030 100644 --- a/demo/src/common.c +++ b/demo/src/common.c @@ -75,10 +75,8 @@ void commonDraw() { if (mouseWentDown("left")) soundPlay(beep, FALSE); - if (mouseWentDown("middle")) { - char text[1000]; - askTextEx("Test?", text, sizeof(text), TRUE, FALSE); - } + if (mouseWentDown("middle")) + askTextEx("Test\ntext input", buffer, sizeof(buffer), TRUE, FALSE); if (mouseWentDown("right")) messageBox("Test message box\nwith test message."); diff --git a/demo/src/main.c b/demo/src/main.c index 0480bd8..c8a5dd3 100644 --- a/demo/src/main.c +++ b/demo/src/main.c @@ -30,6 +30,7 @@ void draw() { int main() { worldSetHeight(512); worldSetWidth(1024); + worldSetResizable(TRUE); worldSetInit(&init); worldSetDraw(&draw); worldRun(); diff --git a/src/drawing.c b/src/drawing.c index 62e0113..ee93d82 100644 --- a/src/drawing.c +++ b/src/drawing.c @@ -499,10 +499,6 @@ void heliDrawingClearFrame() { void heliDrawingPrepareFrame() { resetPath(); - if (statesStackIndex > 0) { - //fprintf(stderr, "helianthus: drawing stack was not empty at end of frame\n"); - while(statesStackIndex > 0) popDrawingState(); - } heliDrawingClearFrame(); glDisable(GL_DEPTH_TEST); diff --git a/src/font.c b/src/font.c index 2b8bd87..2764026 100644 --- a/src/font.c +++ b/src/font.c @@ -307,6 +307,8 @@ void textSize(double size) void text(const char *text, double x, double y) { resetPath(); + if (!text || !text[0]) return; + HeliDrawingState *drawingState = heliDrawingGetState(); double fontScale = drawingState->fontSize/FONT_BASE_SIZE; @@ -403,14 +405,17 @@ void text(const char *text, double x, double y) { } } + int charsCount = lines[linesCount-1].end; + if (charsCount <= 0) return; + // bounds - double l = lines->l; - double t = lines->t; - double r = lines->r; - double b = lines->b; - for(int i = 1; i < linesCount; ++i) { + double l = 0; + double t = -lines->height; + double r = 0; + double b = 0; + for(int i = 0; i < linesCount; ++i) { HeliLineDesc *line = &lines[i]; - line->y = (line-1)->y + line->height; + if (i > 0) line->y = (line-1)->y + line->height; if (l > line->l) l = line->l; if (r < line->r) r = line->r; if (t > line->t + line->y) t = line->t + line->y; @@ -435,7 +440,6 @@ void text(const char *text, double x, double y) { } // sort by textures - int charsCount = lines[linesCount-1].end; for(int i = 0; i < charsCount-1; ++i) chars[i].next = &chars[i+1]; HeliCharDesc *first = NULL, *last = NULL, *current = chars; diff --git a/src/private.h b/src/private.h index 8fd29c6..c6944ab 100644 --- a/src/private.h +++ b/src/private.h @@ -1,3 +1,5 @@ +#ifndef HELI_PRIVATE_H +#define HELI_PRIVATE_H #include #include @@ -163,7 +165,29 @@ void heliSoundUpdate(); void heliSoundFinish(); +// world + +typedef struct _HeliDialog { + int shown; + const char *question; + char *answer; + int maxAnswerSize; + int multiline; + int password; + int pos; + int selPos; + double scrollX; + double scrollY; + int success; + char newText[4096]; +} HeliDialog; + +void heliDialogDraw(HeliDialog *dialog); + + // test void heliDoTests(); + +#endif diff --git a/src/world.c b/src/world.c index b2a886e..d4081ef 100644 --- a/src/world.c +++ b/src/world.c @@ -19,10 +19,15 @@ static unsigned long long elapsedTimeUs; static unsigned long long elapsedTimeSinceLastFrameUs; static unsigned int prevFrameTimeMs; +HeliDialog dialog; + static Callback initCallback; static Callback drawCallback; static Callback deinitCallback; +static const int minWidth = 200; +static const int minHeight = 200; + static int width = 512; static int height = 512; static int resizable; @@ -94,8 +99,8 @@ int mousePressedOver(Sprite sprite) { return keyEventGetCount(KEYEVENT_MOUSE_DOWN) && mouseIsOver(sprite); } static void resize(int w, int h) { - w = w > 200 ? w : 100; - h = h > 200 ? h : 200; + w = w > minWidth ? w : minWidth; + h = h > minHeight ? h : minHeight; if (width != w || height != h) { width = w; height = h; @@ -196,6 +201,19 @@ void messageBox(const char *message) { title, message, window ); + prevFrameTimeMs = SDL_GetTicks(); +} + +void askText(const char *question, char *answer, int maxAnswerSize) + { askTextEx(question, answer, maxAnswerSize, FALSE, FALSE); } + + +static void resetEvents() { + dialog.newText[0] = 0; + heliArrayClear(&keyEvents[KEYEVENT_KEY_WENTDOWN]); + heliArrayClear(&keyEvents[KEYEVENT_KEY_WENTUP]); + heliArrayClear(&keyEvents[KEYEVENT_MOUSE_WENTDOWN]); + heliArrayClear(&keyEvents[KEYEVENT_MOUSE_WENTUP]); } @@ -204,17 +222,7 @@ static void draw() { unsigned long long deltaUs = frameCount == 0 ? 0 : (currentFrameTimeMs - prevFrameTimeMs)*1000ull; prevFrameTimeMs = currentFrameTimeMs; - double dt = 1/frameRate; - unsigned long long timeStepUs = (unsigned long long)round(dt*1e6); - elapsedTimeSinceLastFrameUs += deltaUs; - if (elapsedTimeSinceLastFrameUs > 2000000) - elapsedTimeSinceLastFrameUs = 2000000; - if (frameCount == 0 || elapsedTimeSinceLastFrameUs >= timeStepUs) { - elapsedTimeUs += timeStepUs; - if (frameCount != 0) elapsedTimeSinceLastFrameUs -= timeStepUs; - ++frameCount; - - heliSpriteUpdate(dt); + if (dialog.shown) { heliSoundUpdate(); glViewport(0, 0, width, height); @@ -224,21 +232,44 @@ static void draw() { glMatrixMode(GL_MODELVIEW); heliDrawingPrepareFrame(); - if (drawCallback) drawCallback(); - - heliArrayClear(&keyEvents[KEYEVENT_KEY_WENTDOWN]); - heliArrayClear(&keyEvents[KEYEVENT_KEY_WENTUP]); - heliArrayClear(&keyEvents[KEYEVENT_MOUSE_WENTDOWN]); - heliArrayClear(&keyEvents[KEYEVENT_MOUSE_WENTUP]); + heliDialogDraw(&dialog); + resetEvents(); SDL_GL_SwapWindow(window); - } - - unsigned long long addUs = elapsedTimeSinceLastFrameUs + (SDL_GetTicks() - prevFrameTimeMs)*1000ull; - if (addUs < timeStepUs) { - unsigned long long waitUs = timeStepUs - addUs; - if (waitUs > 2000) - SDL_Delay( (unsigned int)((waitUs + 500)/1000ull) ); + SDL_Delay(20); + } else { + double dt = 1/frameRate; + unsigned long long timeStepUs = (unsigned long long)round(dt*1e6); + elapsedTimeSinceLastFrameUs += deltaUs; + if (elapsedTimeSinceLastFrameUs > 2000000) + elapsedTimeSinceLastFrameUs = 2000000; + if (frameCount == 0 || elapsedTimeSinceLastFrameUs >= timeStepUs) { + elapsedTimeUs += timeStepUs; + if (frameCount != 0) elapsedTimeSinceLastFrameUs -= timeStepUs; + ++frameCount; + + heliSpriteUpdate(dt); + heliSoundUpdate(); + + glViewport(0, 0, width, height); + glMatrixMode(GL_PROJECTION); + glLoadIdentity(); + glOrtho(0, width, height, 0, -1.0, 1.0); + glMatrixMode(GL_MODELVIEW); + + heliDrawingPrepareFrame(); + if (drawCallback) drawCallback(); + + resetEvents(); + SDL_GL_SwapWindow(window); + } + + unsigned long long addUs = elapsedTimeSinceLastFrameUs + (SDL_GetTicks() - prevFrameTimeMs)*1000ull; + if (addUs < timeStepUs) { + unsigned long long waitUs = timeStepUs - addUs; + if (waitUs > 2000) + SDL_Delay( (unsigned int)((waitUs + 500)/1000ull) ); + } } } @@ -289,6 +320,8 @@ static int init() { return FALSE; } + SDL_SetWindowMinimumSize(window, minWidth, minHeight); + context = SDL_GL_CreateContext(window); if (!context) { fprintf(stderr, "helianthus: cannot create OpenGL context: %s\n", SDL_GetError()); @@ -363,8 +396,55 @@ static void handleEvent(SDL_Event *e) { if (e->type == SDL_MOUSEMOTION) { _mouseX = e->motion.x; _mouseY = e->motion.y; + } else + if (e->type == SDL_TEXTINPUT) { + if (dialog.shown) { + int len = strlen(dialog.newText); + int newlen = strlen(e->text.text); + int dl = len + newlen + 1 - sizeof(dialog.newText); + if (dl > 0) newlen -= dl; + if (newlen > 0) { + memcpy(dialog.newText + len, e->text.text, newlen); + dialog.newText[len + newlen] = 0; + } + } + } +} + + +void askTextEx(const char *question, char *answer, int maxAnswerSize, int multiline, int password) { + if (maxAnswerSize < 0 || !answer) maxAnswerSize = 0; + + memset(&dialog, 0, sizeof(dialog)); + dialog.shown = TRUE; + dialog.question = question ? question : ""; + dialog.answer = calloc(1, maxAnswerSize + 1); + dialog.maxAnswerSize = maxAnswerSize - 1; + if (maxAnswerSize > 0) memcpy(dialog.answer, answer, maxAnswerSize); + dialog.multiline = multiline != 0; + dialog.password = password != 0; + dialog.pos = dialog.selPos = strlen(dialog.answer); + dialog.success = FALSE; + + SDL_StartTextInput(); + while(dialog.shown && !stopped) { + SDL_Event event; + while (SDL_PollEvent(&event)) + handleEvent(&event); + draw(); } + SDL_StopTextInput(); + + if (dialog.success && maxAnswerSize > 0) strcpy(answer, dialog.answer); + free(dialog.answer); + memset(&dialog, 0, sizeof(dialog)); + + resetEvents(); + heliArrayClear(&keyEvents[KEYEVENT_KEY_DOWN]); + heliArrayClear(&keyEvents[KEYEVENT_MOUSE_DOWN]); + prevFrameTimeMs = SDL_GetTicks(); + heliDrawingPrepareFrame(); } diff --git a/src/worldui.c b/src/worldui.c index 2876efa..34c6579 100644 --- a/src/worldui.c +++ b/src/worldui.c @@ -3,13 +3,256 @@ #include "private.h" +#include "drawing.h" +#include "font.h" #include "world.h" -void askTextEx(const char *question, char *answer, int maxAnswerSize, int multiline, int password) { - // TODO:bw +static int utf8charlen(const char *c) { + if (!*c) return 0; + if ( (c[0] & 0x80) == 0x00 ) + return 1; + if ( (c[0] & 0xE0) == 0xC0 + && (c[1] & 0xC0) == 0x80 ) + return 2; + if ( (c[0] & 0xF0) == 0xE0 + && (c[1] & 0xC0) == 0x80 + && (c[2] & 0xC0) == 0x80 ) + return 3; + if ( (c[0] & 0xF8) == 0xF0 + && (c[1] & 0xC0) == 0x80 + && (c[2] & 0xC0) == 0x80 + && (c[3] & 0xC0) == 0x80 ) + return 4; + return -1; } -void askText(const char *question, char *answer, int maxAnswerSize) - { askTextEx(question, answer, maxAnswerSize, FALSE, FALSE); } +static int utf8shift(const char *s, int pos, int offset) { + int len = strlen(s); + if (pos > len) { offset += pos - len; pos = len; } + if (pos < 0) { offset += pos; pos = 0; } + + while(offset > 0 && pos < len) { + int l = utf8charlen(s + pos); + if (l > 0) { + pos += l; + } else { + while(pos < len && utf8charlen(s + pos) <= 0) ++pos; + } + --offset; + } + + while(offset < 0 && pos > 0) { + do --pos; while(pos > 0 && utf8charlen(s + pos) <= 0); + ++offset; + } + + return pos; +} + + +static void fixPos(HeliDialog *dialog) { + int answerLen = strlen(dialog->answer); + if (dialog->pos < 0) dialog->pos = 0; + if (dialog->pos > answerLen) dialog->pos = answerLen; + if (dialog->selPos < 0) dialog->selPos = 0; + if (dialog->selPos > answerLen) dialog->selPos = answerLen; +} + + +static void insert(HeliDialog *dialog, const char *text) { + int answerLen = strlen(dialog->answer); + fixPos(dialog); + + int pos0 = dialog->pos < dialog->selPos ? dialog->pos : dialog->selPos; + int pos1 = dialog->pos < dialog->selPos ? dialog->selPos : dialog->pos; + int selLen = pos1 - pos0; + + int textLen = strlen(text); + int dl = answerLen - selLen + textLen - dialog->maxAnswerSize; + if (dl > 0) textLen -= dl; + if (textLen < 0) textLen = 0; + + int tailPos = pos0 + selLen; + int offset = textLen - selLen; + int tailLen = answerLen - tailPos; + if (offset && tailLen > 0) + memmove( + dialog->answer + tailPos + offset, + dialog->answer + tailPos, + tailLen); + tailPos += offset; + dialog->answer[tailPos + tailLen] = 0; + + if (textLen > 0) + memcpy(dialog->answer + pos0, text, textLen); + + dialog->pos = dialog->selPos = tailPos; +} + + +static void copy(HeliDialog *dialog) { + if (dialog->pos != dialog->selPos) { + int pos0 = dialog->pos < dialog->selPos ? dialog->pos : dialog->selPos; + int pos1 = dialog->pos < dialog->selPos ? dialog->selPos : dialog->pos; + char c = dialog->answer[pos1]; + dialog->answer[pos1] = 0; + SDL_SetClipboardText(dialog->answer + pos0); + dialog->answer[pos1] = c; + } +} + + +static void paste(HeliDialog *dialog) { + const char *text = SDL_GetClipboardText(); + if (text && text[0]) insert(dialog, text); +} + + +static void draw(HeliDialog *dialog) { + pushDrawingState(); + + const double w = worldGetWidth(); + const double h = worldGetHeight(); + const double border = 16; + const double scroll = 16; + const double title = 64; + const double buttons = 32; + const double l = border; + const double t = border + title; + const double r = w - border - scroll; + const double b = h - border - scroll - buttons; + + const double bt = h - buttons - border + 8; + const double bb = h - border; + const double bh = bb - bt; + double bw = (w - 2*border)/4; + if (bw < bh) bw = bh; + const double bl0 = border - 2; + const double br0 = bl0 + bw; + const double br1 = w - border + 2; + const double bl1 = br1 - bw; + + const char *strokeColor = "white"; + const char *fillColor = "0.3 0.3 0.3"; + strokeWeight(1); + textFontDefault(); + textSize(16); + + noStroke(); + fill(fillColor); + rect(0, 0, w, h); + + noFill(); + stroke(strokeColor); + textAlign(HALIGN_LEFT, VALIGN_TOP); + text(dialog->answer, l + dialog->scrollX, t + dialog->scrollY); + + noStroke(); + fill(fillColor); + rect(0, 0, l, h); + rect(r, 0, w - r, h); + rect(0, 0, w, t); + rect(0, b, w, h - b); + + noFill(); + stroke(strokeColor); + rect(l - 2, t - 2, r - l + 4, b - t + 4); + + textAlign(HALIGN_CENTER, VALIGN_CENTER); + text(dialog->question, w/2, title/2); + + rect(bl0, bt, bw, bh); + rect(bl1, bt, bw, bh); + text("<<", (bl0 + br0)/2, (bt + bb)/2); + text("\u23CE", (bl1 + br1)/2, (bt + bb)/2); + + if (mouseWentDown("left")) { + double x = mouseX(); + double y = mouseY(); + if (y >= bt && y <= bb) { + if (x >= bl0 && x <= br0) dialog->shown = FALSE; + if (x >= bl1 && x <= br1) { dialog->success = TRUE; dialog->shown = FALSE; } + } + } + + popDrawingState(); +} + + +void heliDialogDraw(HeliDialog *dialog) { + if (dialog->newText[0]) insert(dialog, dialog->newText); + + int shift = keyDown("left shift") || keyDown("right shift"); + int ctrl = keyDown("left ctrl") || keyDown("right ctrl"); + + if (keyWentDown("backspace")) { + if (dialog->pos == dialog->selPos) { + dialog->selPos = dialog->pos; + dialog->pos = utf8shift(dialog->answer, dialog->pos, -1); + } + insert(dialog, ""); + } + + if (keyWentDown("delete")) { + if (dialog->pos == dialog->selPos) { + dialog->selPos = dialog->pos; + dialog->pos = utf8shift(dialog->answer, dialog->pos, 1); + } else + if (shift) { + copy(dialog); + } + insert(dialog, ""); + } + + if (keyWentDown("left")) { + dialog->pos = utf8shift(dialog->answer, dialog->pos, -1); + if (!shift) dialog->selPos = dialog->pos; + } + + if (keyWentDown("right")) { + dialog->pos = utf8shift(dialog->answer, dialog->pos, 1); + if (!shift) dialog->selPos = dialog->pos; + } + + if (ctrl && keyWentDown("home")) { + dialog->pos = 0; + if (!shift) dialog->selPos = dialog->pos; + } + + if (ctrl && keyWentDown("end")) { + dialog->pos = strlen(dialog->answer); + if (!shift) dialog->selPos = dialog->pos; + } + + if (keyWentDown("return")) { + if (!dialog->multiline || ctrl) { + dialog->success = TRUE; + dialog->shown = FALSE; + } else { + insert(dialog, "\n"); + } + } + + if (ctrl && keyWentDown("c")) + copy(dialog); + if (ctrl && keyWentDown("insert")) + copy(dialog); + if (ctrl && keyWentDown("v")) + paste(dialog); + if (shift && keyWentDown("insert")) + paste(dialog); + if (ctrl && keyWentDown("x")) + { copy(dialog); insert(dialog, ""); } + + if (keyWentDown("escape")) dialog->shown = FALSE; + + if (!dialog->multiline) { + char *c = strpbrk(dialog->answer, "\n\r"); + if (c) *c = 0; + fixPos(dialog); + } + + draw(dialog); +}