From: Simon Glass <simon.glass@canonical.com> Add support for Ctrl-P and Ctrl-N to navigate between visual lines in multi-line text input. The navigation maintains the same horizontal pixel-position where possible, using text measurement to find the closest character position on the target line. For multi-line textedit objects: - Add scene_txtin_line_nav() callback that uses the visual line info attached to the text object - Set multiline=true and line_nav callback in scene_txtin_open() Also add multiline support to CLI line editing: - Add multiline bool and line_nav callback to struct cli_line_state - Handle Ctrl-P/N for multiline mode in cread_line_process_ch() Check the context positions as well. Co-developed-by: Claude Opus 4.5 <noreply@anthropic.com> Signed-off-by: Simon Glass <simon.glass@canonical.com> --- boot/scene_txtin.c | 96 ++++++++++++++++++++++++++++++++++++++++++++++ test/boot/expo.c | 27 ++++++++++++- 2 files changed, 122 insertions(+), 1 deletion(-) diff --git a/boot/scene_txtin.c b/boot/scene_txtin.c index 72552222fe1..cde9fdb8ccf 100644 --- a/boot/scene_txtin.c +++ b/boot/scene_txtin.c @@ -164,6 +164,98 @@ void scene_txtin_close(struct scene *scn, struct scene_txtin *tin) vidconsole_readline_end(scn->expo->cons, tin->ctx); } +/** + * scene_txtin_line_nav() - Navigate to previous/next line in multi-line input + * + * Moves the cursor to the previous or next line, trying to maintain the same + * horizontal pixel position. Uses the text measurement info attached to the + * edit text object. + * + * @cls: CLI line state + * @up: true to move to previous line, false for next line + * Return: New cursor position, or -ve if at boundary + */ +static int scene_txtin_line_nav(struct cli_line_state *cls, bool up) +{ + struct scene_txtin *tin = container_of(cls, struct scene_txtin, cls); + struct scene *scn = cls->priv; + struct scene_obj_txt *txt; + const struct vidconsole_mline *mline; + const struct vidconsole_mline *target; + struct vidconsole_bbox bbox; + uint pos = cls->num; + int cur_line, target_line; + int target_x, best_pos, best_diff; + int i, ret; + + txt = scene_obj_find(scn, tin->edit_id, SCENEOBJT_NONE); + if (!txt || !txt->gen.lines.count) + return -ENOENT; + + /* find which line the cursor is on */ + cur_line = -1; + for (i = 0; i < txt->gen.lines.count; i++) { + mline = alist_get(&txt->gen.lines, i, struct vidconsole_mline); + if (pos >= mline->start && pos <= mline->start + mline->len) { + cur_line = i; + break; + } + } + if (cur_line < 0) + return -EINVAL; + + /* find target line */ + target_line = up ? cur_line - 1 : cur_line + 1; + if (target_line < 0 || target_line >= txt->gen.lines.count) + return -EINVAL; + + /* measure text from line start to cursor to get x position */ + ret = vidconsole_measure(scn->expo->cons, txt->gen.font_name, + txt->gen.font_size, cls->buf + mline->start, + pos - mline->start, -1, &bbox, NULL); + if (ret) + return ret; + target_x = bbox.x1; + + /* find character position on target line closest to target_x */ + target = alist_get(&txt->gen.lines, target_line, struct vidconsole_mline); + best_pos = target->start; + best_diff = target_x; /* diff from position 0 */ + + for (i = 1; i <= target->len; i++) { + int diff; + + ret = vidconsole_measure(scn->expo->cons, txt->gen.font_name, + txt->gen.font_size, + cls->buf + target->start, i, -1, + &bbox, NULL); + if (ret) + break; + diff = abs(bbox.x1 - target_x); + if (diff < best_diff) { + best_diff = diff; + best_pos = target->start + i; + } + /* stop if we've gone past the target */ + if (bbox.x1 > target_x) + break; + } + + /* measure text to best_pos to get x coordinate for cursor */ + ret = vidconsole_measure(scn->expo->cons, txt->gen.font_name, + txt->gen.font_size, cls->buf + target->start, + best_pos - target->start, -1, &bbox, NULL); + if (ret) + return ret; + + /* set cursor position: text object position + line offset + char offset */ + vidconsole_set_cursor_pos(scn->expo->cons, tin->ctx, + txt->obj.bbox.x0 + bbox.x1, + txt->obj.bbox.y0 + target->bbox.y0); + + return best_pos; +} + int scene_txtin_open(struct scene *scn, struct scene_obj *obj, struct scene_txtin *tin) { @@ -196,6 +288,10 @@ int scene_txtin_open(struct scene *scn, struct scene_obj *obj, cls->insert = true; cls->putch = scene_txtin_putch; cls->priv = scn; + if (obj->type == SCENEOBJT_TEXTEDIT) { + cls->multiline = true; + cls->line_nav = scene_txtin_line_nav; + } cli_cread_add_initial(cls); /* make sure the cursor is visible */ diff --git a/test/boot/expo.c b/test/boot/expo.c index 8598f32d341..8006044c9c0 100644 --- a/test/boot/expo.c +++ b/test/boot/expo.c @@ -11,6 +11,7 @@ #include <membuf.h> #include <menu.h> #include <video.h> +#include <video_console.h> #include <linux/input.h> #include <test/cedit-test.h> #include <test/ut.h> @@ -1541,6 +1542,7 @@ static int expo_render_textedit(struct unit_test_state *uts) { struct scene_obj_txtedit *ted; struct scene_obj_menu *menu; + struct vidconsole_ctx *ctx; struct abuf buf, logo_copy; struct expo_action act; struct scene *scn; @@ -1590,15 +1592,20 @@ static int expo_render_textedit(struct unit_test_state *uts) /* the cursor should be at the end */ ut_asserteq(100, ted->tin.cls.num); ut_asserteq(100, ted->tin.cls.eol_num); + ctx = ted->tin.ctx; + ut_asserteq(343, VID_TO_PIXEL(ctx->xcur_frac)); + ut_asserteq(260, ctx->ycur); ut_asserteq(21526, video_compress_fb(uts, dev, false)); /* send a keypress to add a character */ ut_assertok(expo_send_key(exp, 'X')); ut_asserteq(101, ted->tin.cls.num); ut_asserteq(101, ted->tin.cls.eol_num); + ut_asserteq(353, VID_TO_PIXEL(ctx->xcur_frac)); + ut_asserteq(260, ctx->ycur); ut_assertok(scene_arrange(scn)); ut_assertok(expo_render(exp)); - ut_asserteq(21607, video_compress_fb(uts, dev, false)); + ut_asserteq(21612, video_compress_fb(uts, dev, false)); ut_assertok(expo_send_key(exp, CTL_CH('b'))); ut_assertok(expo_send_key(exp, CTL_CH('b'))); @@ -1609,6 +1616,9 @@ static int expo_render_textedit(struct unit_test_state *uts) ut_asserteq(101, ted->tin.cls.eol_num); ut_assertok(scene_arrange(scn)); ut_assertok(expo_render(exp)); + /* check cursor position after render (render_deps corrects it) */ + ut_asserteq(329, VID_TO_PIXEL(ctx->xcur_frac)); + ut_asserteq(260, ctx->ycur); ut_asserteq(21623, video_compress_fb(uts, dev, false)); /* delete a character at the cursor (removes 'e') */ @@ -1619,8 +1629,23 @@ static int expo_render_textedit(struct unit_test_state *uts) ut_asserteq(100, ted->tin.cls.eol_num); ut_assertok(scene_arrange(scn)); ut_assertok(expo_render(exp)); + /* check cursor position after render (render_deps corrects it) */ + ut_asserteq(329, VID_TO_PIXEL(ctx->xcur_frac)); + ut_asserteq(260, ctx->ycur); ut_asserteq(21541, video_compress_fb(uts, dev, false)); + /* move cursor to previous visual line at same x position */ + ut_assertok(expo_send_key(exp, CTL_CH('p'))); + ut_asserteq(67, ted->tin.cls.num); + ut_asserteq(100, ted->tin.cls.eol_num); + ut_asserteq(328, VID_TO_PIXEL(ctx->xcur_frac)); + ut_asserteq(240, ctx->ycur); + ut_assertok(scene_arrange(scn)); + ut_assertok(expo_render(exp)); + ut_asserteq(327, VID_TO_PIXEL(ctx->xcur_frac)); + ut_asserteq(240, ctx->ycur); + ut_asserteq(21538, video_compress_fb(uts, dev, false)); + /* close the textedit with Enter (BKEY_SELECT) */ ut_assertok(expo_send_key(exp, BKEY_SELECT)); ut_assertok(expo_action_get(exp, &act)); -- 2.43.0