rxi / microui

A tiny immediate-mode UI library
MIT License
3.29k stars 239 forks source link

Textbox improvements! #57

Open ryuukk opened 2 years ago

ryuukk commented 2 years ago

Hi, thanks for this awesome lib!

One thing that i felt was missing was proper textbox navigation, being able to correct a character without having to delete the entire thing

So i spent some time and managed to come up with something:

https://user-images.githubusercontent.com/44361234/147188029-caad8964-5768-4e6c-869c-dee355282a0b.mp4

Please let me know if you want to see more improvement to the code, so i can submit a PR!

Changes:

struct mu_Context {
  //. ..
  /* input state */
  // ...
  char input_text[32];
+  int input_text_written;
};
 /* reset input state */
  ctx->key_pressed = 0;
  ctx->input_text[0] = '\0';
+  ctx->input_text_written = 0;
void mu_input_text(mu_Context *ctx, const char *text) {
  int len = strlen(ctx->input_text);
  int size = strlen(text) + 1;
  expect(len + size <= (int) sizeof(ctx->input_text));
  memcpy(ctx->input_text + len, text, size);
+  ctx.input_text_written += (size - 1);
}

And here is how the new function looks like:

We basically introduce a new argument to the textbox_raw function A pointer to an int so we can track the index, and user can specific where the index should start

int mu_textbox_raw(mu_Context *ctx, char *buf, int bufsz, int *index, mu_Id id, mu_Rect r,
  int opt)
{
  int res = 0;
  mu_update_control(ctx, id, r, opt | MU_OPT_HOLDFOCUS);

  if (ctx->focus == id) {
    /* handle text input */
    int len = strlen(buf);
    int n = mu_min(bufsz - len - 1, (int) strlen(ctx->input_text));
    int i = *index;
    int needMove = len - i;
    if (n > 0) {
      if (needMove > 0) {
        memmove(buf + i + n, buf + i, needMove);
      }
      memcpy(buf + i, ctx->input_text, n);
      len += n;
      buf[len] = '\0';
      res |= MU_RES_CHANGE;
    }
    /* handle backspace */
    if (ctx->key_pressed & MU_KEY_BACKSPACE && len > 0) {
      /* skip utf-8 continuation bytes */
      while ((buf[--len] & 0xc0) == 0x80 && len > 0);

      (*index)--;
      memmove(buf + (*index), buf + (*index) + 1, len - (*index) + 1);
      buf[len] = '\0';
      res |= MU_RES_CHANGE;
    }
    /* handle return */
    if (ctx->key_pressed & MU_KEY_RETURN) {
      mu_set_focus(ctx, 0);
      res |= MU_RES_SUBMIT;
    }
    if (ctx.key_pressed & KEY_LEFT)
    {
      (*index)--;
    }
    else if (ctx.key_pressed & KEY_RIGHT)
    {
      (*index)++;
    }
    if ((*index) < 0) (*index) = 0;
    if (*index >= len) (*index) = len;
  } else {
    int len = strlen(buf);
    (*index) = len;
  }

  /* draw */
  mu_draw_control_frame(ctx, id, r, MU_COLOR_BASE, opt);
  if (ctx->focus == id) {
    mu_Color color = ctx->style->colors[MU_COLOR_TEXT];
    mu_Font font = ctx->style->font;
    int textw = ctx->text_width(font, buf, (*index));
    int texth = ctx->text_height(font);
    int ofx = r.w - ctx->style->padding - textw - 1;
    int textx = r.x + mu_min(ofx, ctx->style->padding);
    int texty = r.y + (r.h - texth) / 2;
    mu_push_clip_rect(ctx, r);
    mu_draw_text(ctx, font, buf, -1, mu_vec2(textx, texty), color);
    mu_draw_rect(ctx, mu_rect(textx + textw, texty, 1, texth), color);
    mu_pop_clip_rect(ctx);
  } else {
    mu_draw_control_text(ctx, buf, r, MU_COLOR_TEXT, opt);
  }

  (*index)+=ctx->input_text_written;
  return res;
}
ryuukk commented 2 years ago

I have a version without changing the function arguments, isntead i just have a new field in the Context I also have some basic mouse functionality

That's in D, it's a WIP, i'll port it to C when the PR is ready:

https://user-images.githubusercontent.com/44361234/147287932-3f979d80-4a30-4031-bda6-bfe818667288.mp4

TODO:

BONUS:

int textbox_raw(Context *ctx, char *buf, int bufsz, Id id, Rect r, int opt)
{
    Id lastFocus = ctx.focus;

    int res = 0;
    update_control(ctx, id, r, OPT_HOLDFOCUS);

    if (ctx.focus != lastFocus && ctx.focus == id)
    {
        int len = cast(int) str_len(buf);
        ctx.textbox_index = len;
        ctx.textbox_select_min = 0;
        ctx.textbox_select_max = 0;
    }

    if (ctx.focus == id)
    {
        scope(exit) (ctx.textbox_index)+=ctx.input_text_written;

        /* handle text input */
        int len = cast(int) str_len(buf);
        int n = min(bufsz - len - 1, cast(int) str_len(ctx.input_text.ptr));
        int i = ctx.textbox_index;
        int needMove = len - i;

        if (n > 0)
        {
            if (needMove > 0)
            {
                memmove(buf + i + n, buf + i, needMove);
            }
            memcpy(buf + i, ctx.input_text.ptr, n);
            len += n;
            buf[len] = '\0';
            res |= RES_CHANGE;
        }
        /* handle backspace */
        if ((ctx.key_pressed & KEY_BACKSPACE ||  ctx.key_repeated & KEY_BACKSPACE) && len > 0)
        {
            /* skip utf-8 continuation bytes */
            while ((buf[--len] & 0xc0) == 0x80 && len > 0)
            {
            }

            if (i > 0)
            {
                ctx.textbox_index --;
                memmove(buf + (ctx.textbox_index), buf + (ctx.textbox_index) + 1, len - (ctx.textbox_index) + 1);
                buf[len] = '\0';
                res |= RES_CHANGE;
            }
        }
        /* handle backspace */
        if ((ctx.key_pressed & KEY_DELETE ||  ctx.key_repeated & KEY_DELETE) && len > 0)
        {
            if (len == 0 || i == (len+1)) goto skip;

            memmove(buf + (ctx.textbox_index), buf + (ctx.textbox_index) + 1, len - (ctx.textbox_index) + 1);
            buf[len] = '\0';
            res |= RES_CHANGE;
        }

        skip:{}

        if (ctx.mouse_pressed & MOUSE_LEFT && len > 0)
        {
            Font font = ctx.style.font;
            int offsetX = ctx.mouse_pos.x - r.x;
            int textw = ctx.text_width(font, buf, len);
            int textwIndex = ctx.text_width(font, &buf[ctx.textbox_index], len);
            int w = max(textw, textwIndex);

            // click too far, set index to len
            if (offsetX > textw)
            {
                LINFO("sup {} {} {}", offsetX, w, len);
                ctx.textbox_index = len;
            }
            else
            {
                // TODO: doesn't work when text has scrolled..
                // need to figure out a better way to handle this
                // for now i guess that'll do...

                //   aaaaa[aaaaaaaaaaa]aaaaaa
                int num = cast(int) (offsetX / cast(float) w * len);

                // LINFO("les {} {} {}", offsetX, textw, num);
                ctx.textbox_index = num;
            }
        }

        /* handle return */
        if (ctx.key_pressed & KEY_RETURN)
        {
            set_focus(ctx, 0);
            res |= RES_SUBMIT;
        }
        else if (ctx.key_pressed & KEY_LEFT)
        {
            ctx.textbox_index--;
        }
        else if (ctx.key_pressed & KEY_RIGHT)
        {
            ctx.textbox_index ++;
        }
        if ((ctx.textbox_index) < 0) ctx.textbox_index = 0;
        if (ctx.textbox_index >= len) ctx.textbox_index = len;
    }

    /* draw */
    draw_control_frame(ctx, id, r, COLOR_BASE, 0);
    if (ctx.focus == id)
    {
        Color color = ctx.style.colors[COLOR_TEXT];
        Font font = ctx.style.font;
        int textw = ctx.text_width(font, buf, ctx.textbox_index);
        int texth = ctx.text_height(font);
        int ofx = r.w - ctx.style.padding - textw - 1;
        int textx = r.x + min(ofx, ctx.style.padding);
        int texty = r.y + (r.h - texth) / 2;
        push_clip_rect(ctx, r);

        // text
        draw_text(ctx, font, buf, -1, vec2(textx, texty), color);

        // cursor
        draw_rect(ctx, rect(textx + textw, texty, 1, texth), color);

        // draw selection
        if (ctx.textbox_select_min != ctx.textbox_select_max)
        {

        }

        pop_clip_rect(ctx);
    }
    else
    {
        draw_control_text(ctx, buf, r, COLOR_TEXT, 0);
    }

    return res;
}
abnercoimbre commented 2 years ago

Hi @ryuukk did you ever finish this work to submit a PR? It would be very useful to have a standard solution for all microui users.

ericoporto commented 2 years ago

@ryuukk , I am curious about this as well, since I could then port these changes to my port :P