diff --git a/apps/recorder/keyboard.c b/apps/recorder/keyboard.c index 249e4daf03..ee47470bb1 100644 --- a/apps/recorder/keyboard.c +++ b/apps/recorder/keyboard.c @@ -32,6 +32,7 @@ #include "icon.h" #include "pcmbuf.h" #include "lang.h" +#include "bidi.h" #include "keyboard.h" #include "viewport.h" #include "file.h" @@ -209,6 +210,11 @@ static int kbd_create_viewports(struct keyboard_parameters * kbd_param) { struct viewport *vp = &kbd_param[l].kbd_viewports[i]; viewport_set_defaults(vp, l); + /* The keyboard lays out its own cells/grid by absolute x, so the + * automatic RTL right-alignment (which mirrors every putsxy) would + * reverse the picker grid and the edit line. Clear it here and do + * RTL handling explicitly in kbd_draw_edit_line instead. */ + vp->flags &= ~VP_FLAG_ALIGNMENT_MASK; vp->font = FONT_UI; } } @@ -726,11 +732,15 @@ int kbd_input(char* text, int buflen, ucschar_t *kbd) break; case ACTION_KBD_CURSOR_RIGHT: - kbd_move_cursor(&state, 1); + /* move the caret to the visual right: one position forward in + * LTR text, one position back in RTL text */ + kbd_move_cursor(&state, + text_is_rtl((const unsigned char *)state.text) ? -1 : 1); break; case ACTION_KBD_CURSOR_LEFT: - kbd_move_cursor(&state, -1); + kbd_move_cursor(&state, + text_is_rtl((const unsigned char *)state.text) ? 1 : -1); break; case ACTION_NONE: @@ -1087,12 +1097,81 @@ static void kbd_draw_picker(struct keyboard_parameters *pm, sc->set_viewport(last); } +/* Pixel width of the single UTF-8 character that starts at `p`. */ +static int kbd_char_width(struct screen *sc, const unsigned char *p) +{ + ucschar_t c; + unsigned char tmp[8]; + unsigned char *e; + int w; + utf8decode(p, &c); + e = utf8encode(c, tmp); + *e = '\0'; + sc->getstringsize((char *)tmp, &w, NULL); + return w; +} + +/* Visual x-offset (pixels, from the start of the drawn line) of the caret that + * sits at logical character index `cpos` of the visible string `vis`. It mirrors + * how the text engine lays the line out: bidi with LTR base orientation, where + * non-RTL runs keep their order while Hebrew/Arabic runs are reversed in place. + * The string is scanned once, run by run, accumulating a running offset and + * stopping at the run that holds the caret, so no per-character buffer is kept. + * (Like the rest of the edit line, it does not handle diacritics that combine + * several codepoints into a single displayed glyph.) */ +static int kbd_caret_visual_x(struct screen *sc, const char *vis, int cpos) +{ + const unsigned char *p = (const unsigned char *)vis; + int x = 0; /* offset contributed by runs to the left of the caret */ + int pos = 0; /* logical index of the next character */ + + while (*p) + { + ucschar_t c; + utf8decode(p, &c); + bool heb = is_rtl_char(c); + int run_start = pos; + int w_run = 0; /* total width of this run */ + int w_before = 0; /* width of this run's chars before the caret */ + + while (*p) + { + const unsigned char *next = utf8decode(p, &c); + /* Spaces/tabs are direction-neutral: they stay inside the current + * run so a multi-word Hebrew string is one reversed run (matching + * the bidi engine) instead of splitting at every space, which would + * misplace the caret. */ + if ((bool)is_rtl_char(c) != heb && c != ' ' && c != '\t') + break; /* end of run */ + int w = kbd_char_width(sc, p); + if (pos < cpos) + w_before += w; + w_run += w; + pos++; + if (next == p) /* guard against a malformed byte */ + break; + p = next; + } + + if (cpos <= run_start) /* caret is before this run */ + break; + if (cpos >= pos) /* caret is past this whole run */ + { + x += w_run; + continue; + } + /* caret is inside this run */ + x += heb ? (w_run - w_before) /* reversed: later chars sit to the left */ + : w_before; /* in order: earlier chars sit to the left */ + break; + } + return x; +} + static void kbd_draw_edit_line(struct keyboard_parameters *pm, struct screen *sc, struct edit_state *state) { - char outline[8]; - unsigned char *utf8; - int i = 0, j = 0, w; + int w; int icon_w, icon_y; struct viewport *last; struct viewport *vp = &pm->kbd_viewports[eKBD_VP_TEXT]; @@ -1103,77 +1182,131 @@ static void kbd_draw_edit_line(struct keyboard_parameters *pm, int sc_w = vp->width; int y = (vp->height - pm->font_h) / 2; - - int text_margin = (sc_w - pm->text_w * pm->max_chars_text) / 2; - -#if 0 - /* Clear text area one pixel above separator line so any overdraw - doesn't collide */ - screen_clear_area(sc, 0, y - 1, sc_w, pm->font_h + 6); - - sc->hline(0, sc_w - 1, y); -#endif - /* write out the text */ sc->setfont(pm->curfont); - pm->leftpos = MAX(0, MIN(state->len_utf8, state->editpos + 2) - - pm->max_chars_text); - - pm->curpos = state->editpos - pm->leftpos; - utf8 = state->text + utf8seek(state->text, pm->leftpos); - - while (*utf8 && i < pm->max_chars_text) - { - j = utf8seek(utf8, 1); - strmemccpy(outline, utf8, j+1); - sc->getstringsize(outline, &w, NULL); - sc->putsxy(text_margin + i*pm->text_w + (pm->text_w-w)/2, - y, outline); - utf8 += j; - i++; - } + /* Base direction follows the text itself (first strong character): a + * Hebrew/Arabic name is laid out right-to-left while a Latin name stays + * readable left-to-right, even under an RTL UI language. */ + bool rtl = text_is_rtl((const unsigned char *)state->text); icon_w = get_icon_width(sc->screen_type); icon_y = (vp->height - get_icon_height(sc->screen_type)) / 2; + + /* Decide how much of the text to show. If the whole name fits we show it + * all using (almost) the full width; only when it is genuinely too long do + * we scroll and reserve a slot on each side for the scroll indicators. + * Widths are measured per glyph (not from a fixed representative width), so + * every pixel is used and a name that fits is never shown as "scrolled". */ + int total_w; + sc->getstringsize(state->text, &total_w, NULL); + + int pad = 2; + bool overflow = total_w > sc_w - 2 * pad; + int margin = overflow ? (icon_w + 2) : pad; + int area_l = margin; + int area_r = sc_w - margin; + int avail = area_r - area_l; + if (avail < 1) + avail = sc_w; + + int leftpos, endchar; + if (!overflow) + { + leftpos = 0; + endchar = state->len_utf8; + } + else + { + /* keep the caret in view: take the characters just before it until the + * line is full, then extend forward as far as still fits */ + int acc = 0; + leftpos = state->editpos; + while (leftpos > 0) + { + int cw = kbd_char_width(sc, + state->text + utf8seek(state->text, leftpos - 1)); + if (acc + cw > avail) + break; + acc += cw; + leftpos--; + } + endchar = state->editpos; + while (endchar < state->len_utf8) + { + int cw = kbd_char_width(sc, + state->text + utf8seek(state->text, endchar)); + if (acc + cw > avail) + break; + acc += cw; + endchar++; + } + } + pm->leftpos = leftpos; + pm->curpos = state->editpos - leftpos; + + /* The whole visible window is drawn as ONE string so the bidi engine lays + * out mixed Hebrew/Latin runs correctly (splitting it at the caret would + * break a run and garble the text). The caret is placed from the width of + * the logical text before it: exact for uniform-direction text, and a + * sensible approximation inside mixed runs. */ + int startbyte = utf8seek(state->text, leftpos); + int endbyte = utf8seek(state->text, endchar); + + char linebuf[256]; + int vlen = endbyte - startbyte; + if (vlen < 0) vlen = 0; + if (vlen >= (int)sizeof(linebuf)) vlen = sizeof(linebuf) - 1; + memcpy(linebuf, state->text + startbyte, vlen); + linebuf[vlen] = '\0'; + + int vis_w = 0; + sc->getstringsize(linebuf, &vis_w, NULL); + + /* Place the line: right-aligned for an RTL name, left-aligned otherwise. */ + int textx = rtl ? (area_r - vis_w) : area_l; + if (textx < area_l) + textx = area_l; + sc->putsxy(textx, y, linebuf); + + /* Caret = line origin + the visual offset of the logical caret position, + * computed the same way bidi lays the line out (so it is correct for mixed + * Hebrew/Latin too). */ + int cursor_x = textx + kbd_caret_visual_x(sc, linebuf, pm->curpos); + + /* indicator: earlier text scrolled out of view (opposite side for RTL) */ if (pm->leftpos > 0) { - /* Draw nicer bitmap arrow if room, else settle for "<". */ - if (text_margin >= icon_w) - { + if (margin >= icon_w) screen_put_icon_with_offset(sc, 0, 0, - (text_margin - icon_w) / 2, - icon_y, Icon_Reverse_Cursor); - } + rtl ? (sc_w - icon_w) : 0, icon_y, + rtl ? Icon_Cursor : Icon_Reverse_Cursor); else { - sc->getstringsize("<", &w, NULL); - sc->putsxy(text_margin - w, y, "<"); + sc->getstringsize(rtl ? ">" : "<", &w, NULL); + sc->putsxy(rtl ? (sc_w - w) : 0, y, rtl ? ">" : "<"); } } - if (state->len_utf8 - pm->leftpos > pm->max_chars_text) + /* indicator: later text scrolled out of view */ + if (endchar < state->len_utf8) { - /* Draw nicer bitmap arrow if room, else settle for ">". */ - if (text_margin >= icon_w) - { + if (margin >= icon_w) screen_put_icon_with_offset(sc, 0, 0, - sc_w - (text_margin + icon_w) / 2, - icon_y, Icon_Cursor); - } + rtl ? 0 : (sc_w - icon_w), icon_y, + rtl ? Icon_Reverse_Cursor : Icon_Cursor); else { - sc->putsxy(sc_w - text_margin, y, ">"); + sc->getstringsize(rtl ? "<" : ">", &w, NULL); + sc->putsxy(rtl ? 0 : (sc_w - w), y, rtl ? "<" : ">"); } } - /* cursor */ - i = text_margin + pm->curpos * pm->text_w; - + /* caret */ if (state->cur_blink) - sc->vline(i, y, y + pm->font_h - 1); + sc->vline(cursor_x, y, y + pm->font_h - 1); if (state->hangul) /* draw underbar */ - sc->hline(i - pm->text_w, i, y + pm->font_h - 1); + sc->hline(cursor_x - pm->text_w, cursor_x, y + pm->font_h - 1); if (pm->line_edit) { diff --git a/firmware/bidi.c b/firmware/bidi.c index e9f7d53e3b..558ca8831f 100644 --- a/firmware/bidi.c +++ b/firmware/bidi.c @@ -36,13 +36,31 @@ #define _HEB_ORIENTATION_LTR 1 #define _HEB_ORIENTATION_RTL 0 -#define ischar(c) ((c > 0x0589 && c < 0x0700) || \ - (c >= 0xfb50 && c <= 0xfefc) ? 1 : 0) +/* ischar() now lives in bidi.h as the shared is_rtl_char() macro */ #define _isblank(c) ((c==' ' || c=='\t') ? 1 : 0) #define _isnewline(c) ((c=='\n' || c=='\r') ? 1 : 0) #define XOR(a,b) ((a||b) && !(a&&b)) #ifndef BOOTLOADER +/* True if the first strongly-directional character of the string is RTL, i.e. + * the string should be laid out right-to-left. */ +bool text_is_rtl(const unsigned char *str) +{ + while (*str) + { + ucschar_t c; + const unsigned char *next = utf8decode(str, &c); + if (is_rtl_char(c)) + return true; + if ((c >= 'A' && c <= 'Z') || (c >= 'a' && c <= 'z')) + return false; + if (next == str) + break; + str = next; + } + return false; +} + static const arab_t * arab_lookup(ucschar_t uchar) { if (uchar >= 0x621 && uchar <= 0x63a) @@ -178,13 +196,13 @@ ucschar_t *bidi_l2v(const unsigned char *str, int orientation) target--; } - if (ischar(*tmp)) + if (is_rtl_char(*tmp)) block_type = _HEB_BLOCK_TYPE_HEB; else block_type = _HEB_BLOCK_TYPE_ENG; do { - while((XOR(ischar(*(tmp+1)),block_type) + while((XOR(is_rtl_char(*(tmp+1)),block_type) || _isblank(*(tmp+1)) || ispunct((int)*(tmp+1)) || *(tmp+1)=='\n') && block_end < length-1) { diff --git a/firmware/export/bidi.h b/firmware/export/bidi.h index 104cccd0e0..93a4bc1b8e 100644 --- a/firmware/export/bidi.h +++ b/firmware/export/bidi.h @@ -20,6 +20,18 @@ #ifndef BIDI_H #define BIDI_H +#include + +/* True for code points that belong to a right-to-left script (Hebrew, Arabic + * and the Arabic presentation forms). Shared by the bidi engine and by callers + * that need to know a string's direction. */ +#define is_rtl_char(c) (((c) > 0x0589 && (c) < 0x0700) || \ + ((c) >= 0xfb50 && (c) <= 0xfefc)) + +/* True if the string's first strongly-directional character is RTL, i.e. the + * string should be laid out right-to-left. */ +bool text_is_rtl(const unsigned char *str); + ucschar_t *bidi_l2v(const unsigned char *str, int orientation); #endif /* BIDI_H */